Skip to content

Commit

Permalink
Matter virtual lights (#19511)
Browse files Browse the repository at this point in the history
  • Loading branch information
s-hadinger authored Sep 13, 2023
1 parent 156f198 commit 00bd97f
Show file tree
Hide file tree
Showing 44 changed files with 10,239 additions and 8,858 deletions.
3 changes: 2 additions & 1 deletion lib/libesp32/berry_matter/src/be_matter_module.c
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ extern const bclass be_class_Matter_TLV; // need to declare it upfront because
#include "solidify/solidified_Matter_Plugin_9_Virt_Light1.h"
#include "solidify/solidified_Matter_Plugin_4_Light2.h"
#include "solidify/solidified_Matter_Plugin_9_Virt_Light2.h"
#include "solidify/solidified_Matter_Plugin_3_Light3.h"
#include "solidify/solidified_Matter_Plugin_4_Light3.h"
#include "solidify/solidified_Matter_Plugin_9_Virt_Light3.h"
#include "solidify/solidified_Matter_Plugin_2_Shutter.h"
#include "solidify/solidified_Matter_Plugin_3_ShutterTilt.h"
Expand Down Expand Up @@ -296,6 +296,7 @@ module matter (scope: global, strings: weak) {
jitter, closure(matter_jitter_closure)
inspect, closure(matter_inspect_closure)
consolidate_clusters, closure(matter_consolidate_clusters_closure)
UC_LIST, closure(matter_UC_LIST_closure)
Profiler, class(be_class_Matter_Profiler)
// Status codes
Expand Down
11 changes: 11 additions & 0 deletions lib/libesp32/berry_matter/src/embedded/Matter_0_Inspect.be
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,14 @@ def consolidate_clusters(cl, m)
return ret
end
matter.consolidate_clusters = consolidate_clusters

#############################################################
# consolidate_update_commands_list
#
# Build a consolidated list and remove duplicates
#@ solidify:matter.UC_LIST,weak
def UC_LIST(cl, *l)
var uc_parent = super(cl).UPDATE_COMMANDS
return uc_parent + l
end
matter.UC_LIST = UC_LIST
123 changes: 94 additions & 29 deletions lib/libesp32/berry_matter/src/embedded/Matter_Device.be
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,7 @@ class Matter_Device
def register_commands()
tasmota.add_cmd("MtrJoin", /cmd_found, idx, payload, payload_json -> self.MtrJoin(cmd_found, idx, payload, payload_json))
tasmota.add_cmd("MtrUpdate", /cmd_found, idx, payload, payload_json -> self.MtrUpdate(cmd_found, idx, payload, payload_json))
tasmota.add_cmd("MtrInfo", /cmd_found, idx, payload, payload_json -> self.MtrInfo(cmd_found, idx, payload, payload_json))
end

#####################################################################
Expand All @@ -1553,47 +1554,111 @@ class Matter_Device
# `MtrUpdate`
#
# MtrUpdate {"ep":1, "Power":1}
# MtrUpdate {"Name":"ep1", "Power":1}
# MtrUpdate {"Name":"My_virtual_light", "Power":1}
# MtrUpdate {"name":"ep1", "power":1}
# MtrUpdate {"Name":"Light0", "Power":0}
# MtrUpdate {"Name":"Light0", "Power":1}
# MtrUpdate {"Name":"Light1", "Power":0}
# MtrUpdate {"Name":"Light1", "Power":1,"Bri":55}
# MtrUpdate {"Name":"Light2", "Power":0}
# MtrUpdate {"Name":"Light2", "Power":1, "CT":400, "Bri":20}
# MtrUpdate {"Name":"Light3", "Power":0}
# MtrUpdate {"Name":"Light3", "Power":1, "Bri":20, "Hue":85, "Sat":200}
#
def MtrUpdate(cmd_found, idx, payload, payload_json)
if payload_json == nil return tasmota.resp_cmnd("Invalid JSON") end
if payload_json == nil return tasmota.resp_cmnd_str("Invalid JSON") end

var key_ep = tasmota.find_key_i(payload_json, 'Ep')
var key_name = tasmota.find_key_i(payload_json, 'Name')
if key_ep || key_name
var pl = nil # plugin instance

if key_ep
var ep = int(payload_json[key_ep])
if ep <= 0 return tasmota.resp_cmnd_str("Invalid 'Ep' attribute") end
pl = self.find_plugin_by_endpoint(ep)
payload_json.remove(key_ep)
end

if key_name
if pl == nil
pl = self.find_plugin_by_friendly_name(payload_json[key_name])
end
payload_json.remove(key_name)
end

var key_i
if (key_i := tasmota.find_key_i(payload_json, 'Device')) != nil
var pl = self.find_plugin_by_name_or_ep(payload[key_i])
if (pl == nil) return tasmota.resp_cmnd("Invalid Device") end
if (!pl.virtual) return tasmota.resp_cmnd("Device is not virtual") end
# find endpoint (plugin) by name
# can be:
# - integer: endpoint number
# - "ep<n>": endpoint number
# - "<name>": friendly name for endpoint
if (pl == nil) return tasmota.resp_cmnd_str("Invalid Device") end
if (!pl.virtual) return tasmota.resp_cmnd_str("Device is not virtual") end
# filter parameter accedpted by plugin, and rename with canonical
# Ex: {"power":1,"HUE":2} becomes {"Power":1,"Hue":2}
var uc = pl.consolidate_update_commands()
# check that all commands are in the list of supported commands
var cmd_cleaned = {}
for k: payload_json.keys()
var cleaned_command_idx = tasmota.find_list_i(uc, k)
if (cleaned_command_idx == nil)
tasmota.resp_cmnd_str(f"Invalid command '{payload_json[k]}'")
return
end
cmd_cleaned[uc[cleaned_command_idx]] = payload_json[k]
end
# call plug-in
pl.update_virtual(cmd_cleaned)
var state_json = pl.state_json()
if state_json
var cmnd_status = f'{{"{cmd_found}":{state_json}}}'
return tasmota.resp_cmnd(cmnd_status)
else
return tasmota.resp_cmnd_done()
end
end

tasmota.resp_cmnd_done()
tasmota.resp_cmnd_str("Missing 'Device' attribute")
end

#####################################################################
# find_plugin_by_name_or_ep
# `MtrInfo`
#
# `name`can be:
# - integer: endpoint number
# - "ep<n>": endpoint number
# - "<name>": friendly name for endpoint
def find_plugin_by_name_or_ep(name)
if type(name) == 'int'
if (name > 0) return self.find_plugin_by_endpoint(name) end
elif type(name) == 'string'
if name[0..1] == "ep"
var ep_num = int(name[2..])
if ep_num > 0 return self.find_plugin_by_endpoint(ep_num) end
else
return self.find_plugin_by_friendly_name(name)
# MtrInfo 9
def MtrInfo(cmd_found, idx, payload, payload_json)
if payload == ""
# dump all devices
end

if payload == ""
# dump all
for pl: self.plugins
self.MtrInfo_one(pl.endpoint)
end

elif type(payload_json) == 'int'
# try ep number
self.MtrInfo_one(payload_json)

else
# try by name
var pl = self.find_plugin_by_friendly_name(payload)
if pl != nil
self.MtrInfo_one(pl.endpoint)
end
end
return nil # invalid type

tasmota.resp_cmnd_done()
end

# output for a single endpoint
def MtrInfo_one(ep)
var pl = self.find_plugin_by_endpoint(ep)
if pl == nil return end # abort

var state_json = pl.state_json()
if state_json
var mtr_info = f'{{"' 'MtrInfo"' ':{state_json}}}'
# publish
# tasmota.publish_rule(mtr_info)
tasmota.publish_result(mtr_info, "")
end
end

end
matter.Device = Matter_Device

Expand Down
47 changes: 47 additions & 0 deletions lib/libesp32/berry_matter/src/embedded/Matter_Plugin_0.be
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,14 @@ class Matter_Plugin
0x001D: [0,1,2,3,0xFFFC,0xFFFD], # Descriptor Cluster 9.5 p.453
0x0039: [0x11], # Bridged Device Basic Information 9.13 p.485
}
# Accepted Update commands for virtual devices
static var UPDATE_COMMANDS = []
var device # reference to the `device` global object
var endpoint # current endpoint
var clusters # map from cluster to list of attributes, typically constructed from CLUSTERS hierachy
var tick # tick value when it was last updated
var node_label # name of the endpoint, used only in bridge mode, "" if none
var virtual # (bool) is the device pure virtual (i.e. not related to a device implementation by Tasmota)

#############################################################
# MVC Model
Expand All @@ -63,6 +66,7 @@ class Matter_Plugin
self.clusters = self.consolidate_clusters()
self.parse_configuration(config)
self.node_label = config.find("name", "")
self.virtual = false
end

# proxy for the same method in IM
Expand Down Expand Up @@ -149,6 +153,14 @@ class Matter_Plugin
# return ret
end

#############################################################
# consolidate_update_commands
#
# Return consolidated "update commands" for this class
def consolidate_update_commands()
return self.UPDATE_COMMANDS
end

#############################################################
# Publish to MQTT a command received from controller
#
Expand Down Expand Up @@ -370,6 +382,41 @@ class Matter_Plugin
return conf
end

#############################################################
# append_state_json
#
# Output the current state in JSON
# Takes the JSON string prefix
# New values need to be appended with `,"key":value` (including prefix comma)
def append_state_json()
return ""
end

# This is to be called by matter_device to get the full state JSON
# including "Ep":<ep>,"Name"="<friendly_name"
def state_json()
import json
var ep_name = self.node_label ? f',"Name":{json.dump(self.node_label)}' : ""
var state = self.append_state_json()
if state
var ret = f'{{"Ep":{self.endpoint:i}{ep_name}{state}}}'
return ret
else
return nil
end
end

#############################################################
# update_virtual
#
# Update internal state for virtual devices
# The map is pre-cleaned and contains only keys declared in
# `self.UPDATE_COMMANDS` with the adequate case
# (no need to handle case-insensitive)
def update_virtual(payload_json)
# pass
end

end

matter.Plugin = Matter_Plugin
17 changes: 1 addition & 16 deletions lib/libesp32/berry_matter/src/embedded/Matter_Plugin_1_Device.be
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,7 @@ class Matter_Plugin_Device : Matter_Plugin
# var clusters # map from cluster to list of attributes, typically constructed from CLUSTERS hierachy
# var tick # tick value when it was last updated
# var node_label # name of the endpoint, used only in bridge mode, "" if none
var virtual # (bool) is the device pure virtual (i.e. not related to a device implementation by Tasmota)

#############################################################
# Constructor
def init(device, endpoint, config)
self.virtual = config.find("virtual", false)
super(self).init(device, endpoint, config)
end
# var virtual # (bool) is the device pure virtual (i.e. not related to a device implementation by Tasmota)

#############################################################
# read an attribute
Expand Down Expand Up @@ -179,13 +172,5 @@ class Matter_Plugin_Device : Matter_Plugin
end
end

#############################################################
# update_virtual
#
# Update internal state for virtual devices
def update_virtual(payload_json)
# pass
end

end
matter.Plugin_Device = Matter_Plugin_Device
12 changes: 11 additions & 1 deletion lib/libesp32/berry_matter/src/embedded/Matter_Plugin_2_Light0.be
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Matter_Plugin_Light0 : Matter_Plugin_Device
# 0x0005: inherited # Scenes 1.4 p.30 - no writable
0x0006: [0,0xFFFC,0xFFFD], # On/Off 1.5 p.48
})
static var UPDATE_COMMANDS = matter.UC_LIST(_class, "Power")
static var TYPES = { 0x0100: 2 } # OnOff Light, but not actually used because Relay is managed by OnOff

# Inherited
Expand Down Expand Up @@ -145,12 +146,21 @@ class Matter_Plugin_Light0 : Matter_Plugin_Device
return m.find(key_i)
end

#############################################################
# append_state_json
#
# Output the current state in JSON
# New values need to be appended with `,"key":value` (including prefix comma)
def append_state_json()
return f',"Power":{int(self.shadow_onoff)}'
end

#############################################################
# update_virtual
#
# Update internal state for virtual devices
def update_virtual(payload_json)
var val_onoff = self.find_val_i(payload_json, 'Power')
var val_onoff = payload_json.find("Power")
if val_onoff != nil
self.set_onoff(bool(val_onoff))
end
Expand Down
23 changes: 23 additions & 0 deletions lib/libesp32/berry_matter/src/embedded/Matter_Plugin_2_OnOff.be
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class Matter_Plugin_OnOff : Matter_Plugin_Device
# 0x0005: inherited # Scenes 1.4 p.30 - no writable
0x0006: [0,0xFFFC,0xFFFD], # On/Off 1.5 p.48
})
static var UPDATE_COMMANDS = matter.UC_LIST(_class, "Power")
static var TYPES = { 0x010A: 2 } # On/Off Plug-in Unit

# Inherited
Expand Down Expand Up @@ -149,5 +150,27 @@ class Matter_Plugin_OnOff : Matter_Plugin_Device

end

#############################################################
# append_state_json
#
# Output the current state in JSON
# Takes the JSON string prefix
# New values need to be appended with `,"key":value` (including prefix comma)
def append_state_json()
return f',"Power":{int(self.shadow_onoff)}'
end

#############################################################
# update_virtual
#
# Update internal state for virtual devices
def update_virtual(payload_json)
var val_onoff = payload_json.find("Power")
if val_onoff != nil
self.set_onoff(bool(val_onoff))
end
super(self).update_virtual(payload_json)
end

end
matter.Plugin_OnOff = Matter_Plugin_OnOff
Original file line number Diff line number Diff line change
Expand Up @@ -225,5 +225,14 @@ class Matter_Plugin_Shutter : Matter_Plugin_Device
end
end

#############################################################
# append_state_json
#
# Output the current state in JSON
# New values need to be appended with `,"key":value` (including prefix comma)
def append_state_json(payload_str)
return f',"ShutterPos":{self.shadow_shutter_pos},"ShutterTarget":{self.shadow_shutter_target}'
end

end
matter.Plugin_Shutter = Matter_Plugin_Shutter
Loading

0 comments on commit 00bd97f

Please sign in to comment.