My take on Scenes, using JRuby

I’ve designed scene handling this way, hope it can be usefull for someone.

Prerequisite : using the semantic model, having your items are on equipments , located in locations

The scene will group actions on items in a given location.

Scenes are identified using scene metadata held by items:

Example items file:

Switch    Tapo01_Output           "Sonos R Output switch"       <PowerOutlet>    (gSonosOneR)               ["Control", "Power"]    {channel="tapocontrol:P115:1:1:actuator#output",
                                                                                                                                     scene="OFF=OFF, TV=OFF, MUSIC=ON, CINEMA=ON, IDLE=OFF"}

Switch    Tapo05_Output           "Sonos L Output switch"       <PowerOutlet>    (gSonosOneL)               ["Control", "Power"]    {channel="tapocontrol:P115:1:5:actuator#output",
                                                                                                                                     scene="OFF=OFF, TV=OFF, MUSIC=ON, CINEMA=ON, IDLE=OFF"}

Switch    Tapo03_Output           "Sonos Five Output switch"    <PowerOutlet>    (gSonosFive)               ["Control", "Power"]    {channel="tapocontrol:P115:1:3:actuator#output"
                                                                                                                                     scene="OFF=OFF, MUSIC=ON"}

Switch    Multiprise01_Output1    "Switch Lave-Linge"           <PowerOutlet>    (gMultipriseBuanderie)     ["Control", "Power"]    {channel="tapocontrol:P300:1:1:actuator#output1",
                                                                                                                                     scene="OFF=OFF, IDLE=ON"}
Switch    Multiprise01_Output2    "Switch Sèche-Linge"          <PowerOutlet>    (gMultipriseBuanderie)     ["Control", "Power"]    {channel="tapocontrol:P300:1:1:actuator#output2",
                                                                                                                                     scene="OFF=OFF, IDLE=ON"}
Switch    Multiprise01_Output3    "Switch Pompe"                <PowerOutlet>    (gMultipriseBuanderie)     ["Control", "Power"]    {channel="tapocontrol:P300:1:1:actuator#output3",
                                                                                                                                     scene="OFF=OFF, IDLE=ON"}

Switch    Multiprise02_Output1    "Switch Télévision"           <PowerOutlet>    (gMultipriseTelevision)    ["Control", "Power"]    {channel="tapocontrol:P300:1:2:actuator#output1",
                                                                                                                                     scene="OFF=OFF, TV=ON, MUSIC=OFF, CINEMA=ON, IDLE=ON"}
Switch    Multiprise02_Output2    "Switch Arc"                  <PowerOutlet>    (gMultipriseTelevision)    ["Control", "Power"]    {channel="tapocontrol:P300:1:2:actuator#output2",
                                                                                                                                     scene="OFF=OFF, TV=ON, MUSIC=ON, CINEMA=ON, IDLE=OFF"}
Switch    Multiprise02_Output3    "Switch Sub"                  <PowerOutlet>    (gMultipriseTelevision)    ["Control", "Power"]    {channel="tapocontrol:P300:1:2:actuator#output3",
                                                                                                                                    scene="OFF=OFF, TV=OFF, MUSIC=ON, CINEMA=ON, IDLE=OFF"}

The following rule will:

  • Identify all items holding the scene metadata, group them by location
  • Create a scene action item in the location with appropriate description
  • The script makes the assomption that location group name is named gSomeWhere and will name the scene trigger SomeWhere_SCN
  • Associate a nice name with the scene raw name (TV => Télévision) for the state description and commands on the scene action item
  • Create a rule that will trigger whenever a scene receives a command and send according commands to items
  • Monitor scene participants items to identify the appropriate scene whenever one changes.

scenes.rb


# frozen_string_literal: true

require 'json'

LIBELLE = { 'ON' => 'Allumé', 'OFF' => 'Eteint', 'IDLE' => 'En attente', 'CINEMA' => 'Cinéma',
            'MUSIC' => 'Musique', 'TV' => 'Télévision' }.freeze

def options_to_hash(options)
  options = "{\"#{options}\"}".gsub('=', '":"').gsub(',', '","')
  result = {}
  JSON.parse(options).each { |k, v| result[k.strip] = v.strip }
  result
end

# Get all items holding the 'scene' metadata
SCENE_PARTICIPANTS = items.sort_by(&:name).reject do |item|
  item.metadata[:scene].nil?
end
SCENE_PARTICIPANTS = SCENE_PARTICIPANTS.map do |item|
  {item: item, location: item.location.name, options: options_to_hash(item.metadata[:scene].value)}
end

logger.debug "SCENE_PARTICIPANTS: #{SCENE_PARTICIPANTS}"

SCENES = Hash.new { |hash, key| hash[key] = {} }
SCENE_PARTICIPANTS.each do |participant|
  location = participant[:location]
  participant[:options].each do |scene_name, state|
    SCENES[location][scene_name] = {} unless SCENES[location].key?(scene_name)
    SCENES[location][scene_name][participant[:item]] = state
  end
end

# SCENES: {"gSalon"=>{"OFF"=>{"LG_TV_Power"=>"OFF", "Multiprise02_Output1"=>"OFF", "Multiprise02_Output2"=>"OFF", "Multiprise02_Output3"=>"OFF", "Tapo01_Output"=>"OFF", "Tapo05_Output"=>"OFF"},
#                      "TV"=>{"LG_TV_Power"=>"ON", "Multiprise02_Output1"=>"ON", "Multiprise02_Output2"=>"ON", "Multiprise02_Output3"=>"OFF", "Tapo01_Output"=>"OFF", "Tapo05_Output"=>"OFF"},
#                      "MUSIC"=>{"LG_TV_Power"=>"OFF", "Multiprise02_Output1"=>"OFF", "Multiprise02_Output2"=>"ON", "Multiprise02_Output3"=>"ON", "Tapo01_Output"=>"ON", "Tapo05_Output"=>"ON"},
#                      "CINEMA"=>{"LG_TV_Power"=>"ON", "Multiprise02_Output1"=>"ON", "Multiprise02_Output2"=>"ON", "Multiprise02_Output3"=>"ON", "Tapo01_Output"=>"ON", "Tapo05_Output"=>"ON"},
#                      "IDLE"=>{"Multiprise02_Output1"=>"ON", "Multiprise02_Output2"=>"OFF", "Multiprise02_Output3"=>"OFF", "Tapo01_Output"=>"OFF", "Tapo05_Output"=>"OFF"}},
#           "gAtelier"=>{"OFF"=>{"MagicLum1_Output"=>"OFF", "MagicLum2_Output"=>"OFF"},
#                         "ON"=>{"MagicLum1_Output"=>"ON", "MagicLum2_Output"=>"ON"},
#                         "MIXTE"=>{"MagicLum1_Output"=>"ON", "MagicLum2_Output"=>"OFF"},
#                         "MIXTE2"=>{"MagicLum1_Output"=>"OFF", "MagicLum2_Output"=>"ON"}},

logger.debug "SCENES: #{SCENES}"

def array_to_options(arr)
  options = ''
  arr.each do |elmt|
    trad = LIBELLE[elmt]
    options += "#{elmt}=#{trad.nil? ? elmt : trad},"
  end
  options.chop
end

def create_scene_item(place, scenes)
  scene_item = items.build do
    string_item "#{place.name[1..]}_SCN", "🎉 #{place.label}",
                tags: ['Scene', Semantics::Status],
                group: place.name,
                icon: 'housemode'
  end
  scene_item.metadata[:stateDescription] = ' ', { options: array_to_options(scenes) }
  scene_item.metadata[:autoupdate] = false
  place.metadata[:linked_scene] = scene_item.name
  scene_item
end

ALL_SCENES = []
SCENES.each do |location, scenes|
  ALL_SCENES << create_scene_item(items[location], scenes.keys)
end

rule 'A scene was triggered' do
  received_command(*ALL_SCENES)
  run do |event|
    location = event.item.location.name
    scene_name = event.command.to_s
    participants = SCENES[location][scene_name]

    if participants.nil?
      logger.warn "No actions #{scene_name} for #{location} found"
    else
      participants.each do |k, v|
        logger.debug "'#{k.name}' will be positionned to '#{v}'"
        k.ensure << v
        sleep 0.2
      end
    end
  end
end

SCENES.each do |location, loc_detail|
  rule "Monitor scene participants for #{location}", id: "scene_monitoring_#{location}" do
    changed(*loc_detail.values[0].keys, attach: location)
    debounce_for(2.seconds)
    run do |event|
      location = event.attachment
      scene_item = items[items[location].metadata[:linked_scene].value]
      logger.debug "A scene participant of #{location} state's has changed"

      result = SCENES[location].find do |_scene, composition|
        composition.all? do |member, state|
          member.state.to_s == state
        end
      end

      break if result.nil? || result.empty?

      logger.debug "Target scene found: #{result[0]}"
      scene_item.update result[0] if scene_item.state.to_s != result[0]
    end
  end
end

Example of scene item created by the script:
image

I’m still not a ruby guru but making progresses I guess.

1 Like

As a Ruby enthusiast, this gives me ideas.
I’ve been wanting to overhaul my scenes.

Merci Gaël!

1 Like

I’ll gladly see what you’ll improve. My next step is to push common scene of locations to their containing location (e.g. OFF to trigger at basement OFF of all rooms)