Skip to content

Commit 00bd97f

Browse files
authored
Matter virtual lights (#19511)
1 parent 156f198 commit 00bd97f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+10239
-8858
lines changed

lib/libesp32/berry_matter/src/be_matter_module.c

+2-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ extern const bclass be_class_Matter_TLV; // need to declare it upfront because
220220
#include "solidify/solidified_Matter_Plugin_9_Virt_Light1.h"
221221
#include "solidify/solidified_Matter_Plugin_4_Light2.h"
222222
#include "solidify/solidified_Matter_Plugin_9_Virt_Light2.h"
223-
#include "solidify/solidified_Matter_Plugin_3_Light3.h"
223+
#include "solidify/solidified_Matter_Plugin_4_Light3.h"
224224
#include "solidify/solidified_Matter_Plugin_9_Virt_Light3.h"
225225
#include "solidify/solidified_Matter_Plugin_2_Shutter.h"
226226
#include "solidify/solidified_Matter_Plugin_3_ShutterTilt.h"
@@ -296,6 +296,7 @@ module matter (scope: global, strings: weak) {
296296
jitter, closure(matter_jitter_closure)
297297
inspect, closure(matter_inspect_closure)
298298
consolidate_clusters, closure(matter_consolidate_clusters_closure)
299+
UC_LIST, closure(matter_UC_LIST_closure)
299300
Profiler, class(be_class_Matter_Profiler)
300301
301302
// Status codes

lib/libesp32/berry_matter/src/embedded/Matter_0_Inspect.be

+11
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,14 @@ def consolidate_clusters(cl, m)
102102
return ret
103103
end
104104
matter.consolidate_clusters = consolidate_clusters
105+
106+
#############################################################
107+
# consolidate_update_commands_list
108+
#
109+
# Build a consolidated list and remove duplicates
110+
#@ solidify:matter.UC_LIST,weak
111+
def UC_LIST(cl, *l)
112+
var uc_parent = super(cl).UPDATE_COMMANDS
113+
return uc_parent + l
114+
end
115+
matter.UC_LIST = UC_LIST

lib/libesp32/berry_matter/src/embedded/Matter_Device.be

+94-29
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,7 @@ class Matter_Device
15321532
def register_commands()
15331533
tasmota.add_cmd("MtrJoin", /cmd_found, idx, payload, payload_json -> self.MtrJoin(cmd_found, idx, payload, payload_json))
15341534
tasmota.add_cmd("MtrUpdate", /cmd_found, idx, payload, payload_json -> self.MtrUpdate(cmd_found, idx, payload, payload_json))
1535+
tasmota.add_cmd("MtrInfo", /cmd_found, idx, payload, payload_json -> self.MtrInfo(cmd_found, idx, payload, payload_json))
15351536
end
15361537

15371538
#####################################################################
@@ -1553,47 +1554,111 @@ class Matter_Device
15531554
# `MtrUpdate`
15541555
#
15551556
# MtrUpdate {"ep":1, "Power":1}
1556-
# MtrUpdate {"Name":"ep1", "Power":1}
1557-
# MtrUpdate {"Name":"My_virtual_light", "Power":1}
1557+
# MtrUpdate {"name":"ep1", "power":1}
1558+
# MtrUpdate {"Name":"Light0", "Power":0}
1559+
# MtrUpdate {"Name":"Light0", "Power":1}
1560+
# MtrUpdate {"Name":"Light1", "Power":0}
1561+
# MtrUpdate {"Name":"Light1", "Power":1,"Bri":55}
1562+
# MtrUpdate {"Name":"Light2", "Power":0}
1563+
# MtrUpdate {"Name":"Light2", "Power":1, "CT":400, "Bri":20}
1564+
# MtrUpdate {"Name":"Light3", "Power":0}
1565+
# MtrUpdate {"Name":"Light3", "Power":1, "Bri":20, "Hue":85, "Sat":200}
15581566
#
15591567
def MtrUpdate(cmd_found, idx, payload, payload_json)
1560-
if payload_json == nil return tasmota.resp_cmnd("Invalid JSON") end
1568+
if payload_json == nil return tasmota.resp_cmnd_str("Invalid JSON") end
1569+
1570+
var key_ep = tasmota.find_key_i(payload_json, 'Ep')
1571+
var key_name = tasmota.find_key_i(payload_json, 'Name')
1572+
if key_ep || key_name
1573+
var pl = nil # plugin instance
1574+
1575+
if key_ep
1576+
var ep = int(payload_json[key_ep])
1577+
if ep <= 0 return tasmota.resp_cmnd_str("Invalid 'Ep' attribute") end
1578+
pl = self.find_plugin_by_endpoint(ep)
1579+
payload_json.remove(key_ep)
1580+
end
1581+
1582+
if key_name
1583+
if pl == nil
1584+
pl = self.find_plugin_by_friendly_name(payload_json[key_name])
1585+
end
1586+
payload_json.remove(key_name)
1587+
end
15611588

1562-
var key_i
1563-
if (key_i := tasmota.find_key_i(payload_json, 'Device')) != nil
1564-
var pl = self.find_plugin_by_name_or_ep(payload[key_i])
1565-
if (pl == nil) return tasmota.resp_cmnd("Invalid Device") end
1566-
if (!pl.virtual) return tasmota.resp_cmnd("Device is not virtual") end
1567-
# find endpoint (plugin) by name
1568-
# can be:
1569-
# - integer: endpoint number
1570-
# - "ep<n>": endpoint number
1571-
# - "<name>": friendly name for endpoint
1589+
if (pl == nil) return tasmota.resp_cmnd_str("Invalid Device") end
1590+
if (!pl.virtual) return tasmota.resp_cmnd_str("Device is not virtual") end
1591+
# filter parameter accedpted by plugin, and rename with canonical
1592+
# Ex: {"power":1,"HUE":2} becomes {"Power":1,"Hue":2}
1593+
var uc = pl.consolidate_update_commands()
1594+
# check that all commands are in the list of supported commands
1595+
var cmd_cleaned = {}
1596+
for k: payload_json.keys()
1597+
var cleaned_command_idx = tasmota.find_list_i(uc, k)
1598+
if (cleaned_command_idx == nil)
1599+
tasmota.resp_cmnd_str(f"Invalid command '{payload_json[k]}'")
1600+
return
1601+
end
1602+
cmd_cleaned[uc[cleaned_command_idx]] = payload_json[k]
1603+
end
1604+
# call plug-in
1605+
pl.update_virtual(cmd_cleaned)
1606+
var state_json = pl.state_json()
1607+
if state_json
1608+
var cmnd_status = f'{{"{cmd_found}":{state_json}}}'
1609+
return tasmota.resp_cmnd(cmnd_status)
1610+
else
1611+
return tasmota.resp_cmnd_done()
1612+
end
15721613
end
15731614

1574-
tasmota.resp_cmnd_done()
1615+
tasmota.resp_cmnd_str("Missing 'Device' attribute")
15751616
end
15761617

15771618
#####################################################################
1578-
# find_plugin_by_name_or_ep
1619+
# `MtrInfo`
15791620
#
1580-
# `name`can be:
1581-
# - integer: endpoint number
1582-
# - "ep<n>": endpoint number
1583-
# - "<name>": friendly name for endpoint
1584-
def find_plugin_by_name_or_ep(name)
1585-
if type(name) == 'int'
1586-
if (name > 0) return self.find_plugin_by_endpoint(name) end
1587-
elif type(name) == 'string'
1588-
if name[0..1] == "ep"
1589-
var ep_num = int(name[2..])
1590-
if ep_num > 0 return self.find_plugin_by_endpoint(ep_num) end
1591-
else
1592-
return self.find_plugin_by_friendly_name(name)
1621+
# MtrInfo 9
1622+
def MtrInfo(cmd_found, idx, payload, payload_json)
1623+
if payload == ""
1624+
# dump all devices
1625+
end
1626+
1627+
if payload == ""
1628+
# dump all
1629+
for pl: self.plugins
1630+
self.MtrInfo_one(pl.endpoint)
1631+
end
1632+
1633+
elif type(payload_json) == 'int'
1634+
# try ep number
1635+
self.MtrInfo_one(payload_json)
1636+
1637+
else
1638+
# try by name
1639+
var pl = self.find_plugin_by_friendly_name(payload)
1640+
if pl != nil
1641+
self.MtrInfo_one(pl.endpoint)
15931642
end
15941643
end
1595-
return nil # invalid type
1644+
1645+
tasmota.resp_cmnd_done()
15961646
end
1647+
1648+
# output for a single endpoint
1649+
def MtrInfo_one(ep)
1650+
var pl = self.find_plugin_by_endpoint(ep)
1651+
if pl == nil return end # abort
1652+
1653+
var state_json = pl.state_json()
1654+
if state_json
1655+
var mtr_info = f'{{"' 'MtrInfo"' ':{state_json}}}'
1656+
# publish
1657+
# tasmota.publish_rule(mtr_info)
1658+
tasmota.publish_result(mtr_info, "")
1659+
end
1660+
end
1661+
15971662
end
15981663
matter.Device = Matter_Device
15991664

lib/libesp32/berry_matter/src/embedded/Matter_Plugin_0.be

+47
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,14 @@ class Matter_Plugin
3939
0x001D: [0,1,2,3,0xFFFC,0xFFFD], # Descriptor Cluster 9.5 p.453
4040
0x0039: [0x11], # Bridged Device Basic Information 9.13 p.485
4141
}
42+
# Accepted Update commands for virtual devices
43+
static var UPDATE_COMMANDS = []
4244
var device # reference to the `device` global object
4345
var endpoint # current endpoint
4446
var clusters # map from cluster to list of attributes, typically constructed from CLUSTERS hierachy
4547
var tick # tick value when it was last updated
4648
var node_label # name of the endpoint, used only in bridge mode, "" if none
49+
var virtual # (bool) is the device pure virtual (i.e. not related to a device implementation by Tasmota)
4750

4851
#############################################################
4952
# MVC Model
@@ -63,6 +66,7 @@ class Matter_Plugin
6366
self.clusters = self.consolidate_clusters()
6467
self.parse_configuration(config)
6568
self.node_label = config.find("name", "")
69+
self.virtual = false
6670
end
6771

6872
# proxy for the same method in IM
@@ -149,6 +153,14 @@ class Matter_Plugin
149153
# return ret
150154
end
151155

156+
#############################################################
157+
# consolidate_update_commands
158+
#
159+
# Return consolidated "update commands" for this class
160+
def consolidate_update_commands()
161+
return self.UPDATE_COMMANDS
162+
end
163+
152164
#############################################################
153165
# Publish to MQTT a command received from controller
154166
#
@@ -370,6 +382,41 @@ class Matter_Plugin
370382
return conf
371383
end
372384

385+
#############################################################
386+
# append_state_json
387+
#
388+
# Output the current state in JSON
389+
# Takes the JSON string prefix
390+
# New values need to be appended with `,"key":value` (including prefix comma)
391+
def append_state_json()
392+
return ""
393+
end
394+
395+
# This is to be called by matter_device to get the full state JSON
396+
# including "Ep":<ep>,"Name"="<friendly_name"
397+
def state_json()
398+
import json
399+
var ep_name = self.node_label ? f',"Name":{json.dump(self.node_label)}' : ""
400+
var state = self.append_state_json()
401+
if state
402+
var ret = f'{{"Ep":{self.endpoint:i}{ep_name}{state}}}'
403+
return ret
404+
else
405+
return nil
406+
end
407+
end
408+
409+
#############################################################
410+
# update_virtual
411+
#
412+
# Update internal state for virtual devices
413+
# The map is pre-cleaned and contains only keys declared in
414+
# `self.UPDATE_COMMANDS` with the adequate case
415+
# (no need to handle case-insensitive)
416+
def update_virtual(payload_json)
417+
# pass
418+
end
419+
373420
end
374421

375422
matter.Plugin = Matter_Plugin

lib/libesp32/berry_matter/src/embedded/Matter_Plugin_1_Device.be

+1-16
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,7 @@ class Matter_Plugin_Device : Matter_Plugin
3737
# var clusters # map from cluster to list of attributes, typically constructed from CLUSTERS hierachy
3838
# var tick # tick value when it was last updated
3939
# var node_label # name of the endpoint, used only in bridge mode, "" if none
40-
var virtual # (bool) is the device pure virtual (i.e. not related to a device implementation by Tasmota)
41-
42-
#############################################################
43-
# Constructor
44-
def init(device, endpoint, config)
45-
self.virtual = config.find("virtual", false)
46-
super(self).init(device, endpoint, config)
47-
end
40+
# var virtual # (bool) is the device pure virtual (i.e. not related to a device implementation by Tasmota)
4841

4942
#############################################################
5043
# read an attribute
@@ -179,13 +172,5 @@ class Matter_Plugin_Device : Matter_Plugin
179172
end
180173
end
181174

182-
#############################################################
183-
# update_virtual
184-
#
185-
# Update internal state for virtual devices
186-
def update_virtual(payload_json)
187-
# pass
188-
end
189-
190175
end
191176
matter.Plugin_Device = Matter_Plugin_Device

lib/libesp32/berry_matter/src/embedded/Matter_Plugin_2_Light0.be

+11-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class Matter_Plugin_Light0 : Matter_Plugin_Device
3535
# 0x0005: inherited # Scenes 1.4 p.30 - no writable
3636
0x0006: [0,0xFFFC,0xFFFD], # On/Off 1.5 p.48
3737
})
38+
static var UPDATE_COMMANDS = matter.UC_LIST(_class, "Power")
3839
static var TYPES = { 0x0100: 2 } # OnOff Light, but not actually used because Relay is managed by OnOff
3940

4041
# Inherited
@@ -145,12 +146,21 @@ class Matter_Plugin_Light0 : Matter_Plugin_Device
145146
return m.find(key_i)
146147
end
147148

149+
#############################################################
150+
# append_state_json
151+
#
152+
# Output the current state in JSON
153+
# New values need to be appended with `,"key":value` (including prefix comma)
154+
def append_state_json()
155+
return f',"Power":{int(self.shadow_onoff)}'
156+
end
157+
148158
#############################################################
149159
# update_virtual
150160
#
151161
# Update internal state for virtual devices
152162
def update_virtual(payload_json)
153-
var val_onoff = self.find_val_i(payload_json, 'Power')
163+
var val_onoff = payload_json.find("Power")
154164
if val_onoff != nil
155165
self.set_onoff(bool(val_onoff))
156166
end

lib/libesp32/berry_matter/src/embedded/Matter_Plugin_2_OnOff.be

+23
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class Matter_Plugin_OnOff : Matter_Plugin_Device
3737
# 0x0005: inherited # Scenes 1.4 p.30 - no writable
3838
0x0006: [0,0xFFFC,0xFFFD], # On/Off 1.5 p.48
3939
})
40+
static var UPDATE_COMMANDS = matter.UC_LIST(_class, "Power")
4041
static var TYPES = { 0x010A: 2 } # On/Off Plug-in Unit
4142

4243
# Inherited
@@ -149,5 +150,27 @@ class Matter_Plugin_OnOff : Matter_Plugin_Device
149150

150151
end
151152

153+
#############################################################
154+
# append_state_json
155+
#
156+
# Output the current state in JSON
157+
# Takes the JSON string prefix
158+
# New values need to be appended with `,"key":value` (including prefix comma)
159+
def append_state_json()
160+
return f',"Power":{int(self.shadow_onoff)}'
161+
end
162+
163+
#############################################################
164+
# update_virtual
165+
#
166+
# Update internal state for virtual devices
167+
def update_virtual(payload_json)
168+
var val_onoff = payload_json.find("Power")
169+
if val_onoff != nil
170+
self.set_onoff(bool(val_onoff))
171+
end
172+
super(self).update_virtual(payload_json)
173+
end
174+
152175
end
153176
matter.Plugin_OnOff = Matter_Plugin_OnOff

lib/libesp32/berry_matter/src/embedded/Matter_Plugin_2_Shutter.be

+9
Original file line numberDiff line numberDiff line change
@@ -225,5 +225,14 @@ class Matter_Plugin_Shutter : Matter_Plugin_Device
225225
end
226226
end
227227

228+
#############################################################
229+
# append_state_json
230+
#
231+
# Output the current state in JSON
232+
# New values need to be appended with `,"key":value` (including prefix comma)
233+
def append_state_json(payload_str)
234+
return f',"ShutterPos":{self.shadow_shutter_pos},"ShutterTarget":{self.shadow_shutter_target}'
235+
end
236+
228237
end
229238
matter.Plugin_Shutter = Matter_Plugin_Shutter

0 commit comments

Comments
 (0)