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

568 lines
21 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#
# Matter_Plugin_0.be - generic superclass for all Matter plugins, used to define specific behaviors (light, switch, media...)
#
# Copyright (C) 2023 Stephan Hadinger & Theo Arends
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import matter
# Matter modules for extensibility
# template but not actually used
#@ solidify:Matter_Plugin,weak
class Matter_Plugin
# Global type system for plugins
static var TYPE = "" # name of the plug-in in json
static var DISPLAY_NAME = "" # display name of the plug-in
static var ARG = "" # additional argument name (or empty if none)
static var ARG_TYPE = / x -> str(x) # function to convert argument to the right type
static var ARG_HINT = "_Not used_" # Hint for entering the Argument (inside 'placeholder')
# Behavior of the plugin, frequency at which `update_shadow()` is called
static var UPDATE_TIME = 5000 # default is every 5 seconds
static var VIRTUAL = false # set to true only for virtual devices
static var BRIDGE = false # set to true only for bridged devices (ESP8266 or OpenBK)
static var ZIGBEE = false # set to true only when mapped to a zigbee device
var update_next # next timestamp for update
# Configuration of the plugin: clusters and type
static var CLUSTERS = matter.consolidate_clusters(_class, {
0x001D: [0,1,2,3], # Descriptor Cluster 9.5 p.453
})
static var COMMANDS = {
0x001D: nil, # Descriptor Cluster 9.5 p.453
}
# static var TYPES = { <device_type>: <revision> } # needs to be defined for each endpoint
# `FEATURE_MAPS` contains any non-zero value per cluster, if not present default to `0`
static var FEATURE_MAPS = { # feature map per cluster
0x0031: 0x04, # Put Eth for now which should work for any on-network
0x0102: 1 + 4, # Lift + PA_LF
0x0202: 2, # Fan: Auto
}
# `CLUSTER_REVISIONS` contains revision numbers for each cluster, or `1` if not present
static var CLUSTER_REVISIONS = {
0x0003: 4, # New data model format and notation
0x0004: 4, # New data model format and notation
0x0005: 5, # "new data model format and notation"
0x0006: 5, # Addition of Dead Front behavior and associated FeatureMap entry
0x0008: 5, # "new data model format and notation"
0x001D: 2, # Semantic tag list; TagList feature
# 0x001F: 1, # Initial Release
0x0028: 2, # Added ProductAppearance attribute
# 0x002A: 1, # Initial Release
# 0x002B: 1, # Initial Release
# 0x002C: 1, # Initial Release
# 0x0030: 1, # Initial Release
# 0x0031: 1, # Initial Release
# 0x0032: 1, # Initial Release
# 0x0033: 1, # Initial Release
# 0x0034: 1, # Initial Release
0x0038: 2, #
# 0x003B: 1, # Initial Release
# 0x003C: 1, # Initial Release
# 0x003E: 1, # Initial Release
0x003F: 2, # Clarify KeySetWrite validation and behavior on invalid epoch key lengths
# 0x0040: 1, # Initial Release
# 0x0041: 1, # Initial Release
# 0x0042: 1, # Initial Release
# 0x005B: 1, # Initial Release
# 0x005C: 1, # Initial Release
0x0101: 7, # Added support for European door locks (unbolt feature)
0x0102: 5, # New data model format and notation
0x0200: 4, # Added feature map
0x0201: 6, # Introduced the LTNE feature and adapted text (spec issue #5778)
0x0202: 4, # Change conformance for FanModeSe­ quenceEnum
0x0204: 2, # New data model format and notation, added "Conversion of Temperature Values for Display" section
0x0300: 6, # Added clarifications to Scenes support for Matter
0x0301: 4, # New data model format and notation
0x0400: 3, # New data model format and notation
0x0402: 4, # New data model format and notation
0x0403: 3, # New data model format and notation
0x0404: 3, # New data model format and notation
0x0405: 3, # New data model format and notation
0x0406: 3, # New data model format and notation
0x0407: 3, # New data model format and notation
0x0408: 3, # New data model format and notation
}
# Accepted Update commands for virtual devices
static var UPDATE_COMMANDS = []
var device # reference to the `device` global object
var endpoint # current endpoint
var tick # tick value when it was last updated
var node_label # name of the endpoint, used only in bridge mode, "" if none
#############################################################
# MVC Model
#
# Model linking the plugin to the Tasmota behavior
#############################################################
#############################################################
# Constructor
#
# device: contains the root device object so the plugin can "call home"
# endpoint: (int) the endpoint number (16 bits)
# arguments: (map) the map for all complementary arguments that are plugin specific
def init(device, endpoint, config)
self.device = device
self.endpoint = endpoint
self.parse_configuration(config)
self.node_label = config.find("name", "")
end
# proxy for the same method in IM
def ack_request(ctx)
var msg = ctx.msg
if msg != nil
self.device.message_handler.im.send_ack_now(msg)
end
ctx.msg = nil
end
#############################################################
# parse_configuration
#
# Parse configuration map
# TO BE OVERRIDEN
def parse_configuration(config)
end
#############################################################
# is_local_device
#
# Returns true if it's a local device, or false for a
# remotely device controlled via HTTP
def is_local_device()
return !(self.BRIDGE)
end
#############################################################
# Stub for updating shadow values (local copies of what we published to the Matter gateway)
#
# This method should collect the data from the local or remote device
# and call `parse_status(<data>)` when data is available.
#
# TO BE OVERRIDDEN
# This call is synnchronous and blocking.
def update_shadow()
self.tick = self.device.tick
end
#############################################################
# Stub for updating shadow values (local copies of what we published to the Matter gateway)
#
# This call is synnchronous and blocking.
def update_shadow_lazy()
if self.tick != self.device.tick
self.update_shadow()
self.tick = self.device.tick
end
end
#############################################################
# signal that an attribute has been changed
#
# If `endpoint` is `nil`, send to all endpoints
def attribute_updated(cluster, attribute, fabric_specific)
self.device.attribute_updated(self.endpoint, cluster, attribute, fabric_specific)
end
#############################################################
# generate a new event
#
def publish_event(cluster, event_id, priority, data0, data1, data2)
self.device.events.publish_event(self.endpoint, cluster, event_id, true #-urgent-#, priority, data0, data1, data2)
end
# def publish_event_non_urgent(cluster, event, priority, data0, data1, data2)
# self.device.events.publish_event(self.endpoint, cluster, event, false #-non_urgent-#, priority, data0, data1, data2)
# end
#- testing
var root = matter_device.plugins[0]
var tlv_solo = matter.TLV.Matter_TLV_item()
tlv_solo.set(matter.TLV.U4, 42)
root.publish_event(0x001D, 0, matter.EVENT_CRITICAL, tlv_solo)
matter_device.events.dump()
-#
# elements are made of `Matter_EventDataIB`
# var path #
# var node # u64 as bytes
# var endpoint # u16
# var cluster # u32
# var event # u32
# var is_urgent # bool
# var event_number # u64 as bytes
# var priority # u8
# # one of
# var epoch_timestamp # u64
# var system_timestamp # u64
# var delta_epoch_timestamp # u64
# var delta_system_timestamp # u64
# # data
# var data # any TLV
# EVENT_DEBUG=0
# EVENT_INFO=1
# EVENT_CRITICAL=2
#############################################################
# consolidate_clusters
#
# Build a consolidated map of all the `CLUSTERS` static vars
# from the inheritance hierarchy
def get_clusters()
return self.CLUSTERS
# def real_super(o) return super(o) end # enclose `super()` in a static function to disable special behavior for super in instances
# var ret = {}
# var o = self # start with self
# while o != nil # when we rich highest class, `super()` returns `nil`
# var CL = o.CLUSTERS
# for k: CL.keys()
# # check if key already exists
# if !ret.contains(k) ret[k] = [] end
# for attr: CL[k] # iterate on values
# if ret[k].find(attr) == nil
# ret[k].push(attr)
# end
# end
# end
# o = real_super(o)
# end
# 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
#
# we limit to 3 commands (to we need more?)
def publish_command(key1, value1, key2, value2, key3, value3)
# if zigbee, decompose simple commands
if self.ZIGBEE && self.zigbee_mapper && self.zigbee_mapper.resolve_zb_device()
self.zigbee_mapper.zb_single_command(key1, value1)
self.zigbee_mapper.zb_single_command(key2, value2)
self.zigbee_mapper.zb_single_command(key3, value3)
end
import json
var payload = f"{json.dump(key1)}:{json.dump(value1)}"
if key2 != nil
payload = f"{payload},{json.dump(key2)}:{json.dump(value2)}"
end
if key3 != nil
payload = f"{payload},{json.dump(key3)}:{json.dump(value3)}"
end
matter.publish_command('MtrReceived', self.endpoint, self.node_label, payload)
end
#############################################################
# Which endpoints does it handle (list of numbers)
def get_endpoint()
return self.endpoint
end
def get_cluster_list_sorted()
return self.device.k2l(self.CLUSTERS)
end
def contains_cluster(cluster)
return self.CLUSTERS.contains(cluster)
end
# def get_attribute_list(cluster)
# return self.clusters.find(cluster, [])
# end
# returns as a constant bytes of 16-bit ints, big endian
def get_attribute_list_bytes(cluster)
return self.CLUSTERS.find(cluster, nil)
end
def contains_attribute(cluster, attribute)
var attr_list = self.CLUSTERS.find(cluster)
# log(f"MTR: contains_attribute {cluster=} {attribute=} {attr_list=}")
if attr_list != nil
var idx = 0
var attr_sz = size(attr_list) / 2 # group of 16-bit integers, big endian
while idx < attr_sz
if attr_list.get(idx * 2, -2) == attribute
return true
end
idx += 1
end
end
return false
end
def set_name(n)
if n != self.node_label
self.attribute_updated(0x0039, 0x0005)
end
self.node_label = n
end
def get_name() return self.node_label end
#############################################################
# MVC Model
#
# View reading attributes
#############################################################
#############################################################
# read attribute
#
# Arg:
# `tlv_solo` contains an instance of `Matter_TLV_item` to avoid allocating a new object with TLV.create_TLV
def read_attribute(session, ctx, tlv_solo)
var TLV = matter.TLV
var cluster = ctx.cluster
var attribute = ctx.attribute
if cluster == 0x001D # ========== Descriptor Cluster 9.5 p.453 ==========
if attribute == 0x0000 # ---------- DeviceTypeList / list[DeviceTypeStruct] ----------
var dtl = TLV.Matter_TLV_array()
var types = self.TYPES
for dt: types.keys()
var d1 = dtl.add_struct()
d1.add_TLV(0, TLV.U2, dt) # DeviceType
d1.add_TLV(1, TLV.U2, types[dt]) # Revision
end
return dtl
elif attribute == 0x0001 # ---------- ServerList / list[cluster-id] ----------
var sl = TLV.Matter_TLV_array()
for cl: self.get_cluster_list_sorted()
sl.add_TLV(nil, TLV.U4, cl)
end
return sl
elif attribute == 0x0002 # ---------- ClientList / list[cluster-id] ----------
var cl = TLV.Matter_TLV_array()
return cl
elif attribute == 0x0003 # ---------- PartsList / list[endpoint-no]----------
var pl = TLV.Matter_TLV_array()
return pl
elif attribute == 0xFFFC # ---------- FeatureMap / map32 ----------
return tlv_solo.set(TLV.U4, 0) #
elif attribute == 0xFFFD # ---------- ClusterRevision / u2 ----------
return tlv_solo.set(TLV.U4, 1) # "Initial Release"
end
end
# Handle attributes 0xFFF8 - 0xFFFD for all clusters
if attribute == 0xFFF8 # GeneratedCommandList
var gcl = TLV.Matter_TLV_array()
return gcl # return empty list
elif attribute == 0xFFFB # AttributeList
var acli = TLV.Matter_TLV_array()
var attr_list_bytes = self.get_attribute_list_bytes(cluster)
var attr_list_bytes_sz = (attr_list_bytes != nil) ? size(attr_list_bytes) : 0
var idx = 0
while idx < attr_list_bytes_sz
acli.add_TLV(nil, TLV.U2, attr_list_bytes.get(idx * 2, -2))
idx += 1
end
return acli # TODO, empty list for now
elif attribute == 0xFFFA # EventList
var el = TLV.Matter_TLV_array()
return el # return empty list
elif attribute == 0xFFF9 # AcceptedCommandList
var al = TLV.Matter_TLV_array()
return al # TODO
elif attribute == 0xFFFC # FeatureMap
var featuremap = self.FEATURE_MAPS.find(cluster, 0)
return tlv_solo.set(TLV.U4, featuremap)
elif attribute == 0xFFFD # ClusterRevision
var clusterrevision = self.CLUSTER_REVISIONS.find(cluster, 1)
return tlv_solo.set(TLV.U4, clusterrevision)
end
# no handler found, return nil
return nil
end
#############################################################
# read event
# TODO
def read_event(session, endpoint, cluster, eventid)
return nil
end
#############################################################
# subscribe attribute
# TODO
def subscribe_attribute(session, endpoint, cluster, attribute)
return nil
end
#############################################################
# subscribe event
# TODO
def subscribe_event(session, endpoint, cluster, eventid)
return nil
end
#############################################################
# MVC Model
#
# Controller write attributes
#############################################################
#############################################################
# write attribute
def write_attribute(session, ctx, write_data)
return nil
end
#############################################################
# MVC Model
#
# Controller invoke request
#############################################################
#############################################################
# invoke command
def invoke_request(session, val, ctx)
return nil
end
#############################################################
# timed request
# TODO - should we even support this?
def timed_request(session, val, ctx)
return nil
end
#############################################################
# parse sensor
#
# The device calls regularly `tasmota.read_sensors()` and converts
# it to json.
def parse_sensors(payload)
end
#############################################################
# every_250ms
#
# check if the timer expired and update_shadow() needs to be called
def every_250ms()
if self.update_next == nil
self.update_next = matter.jitter(self.UPDATE_TIME)
else
if tasmota.time_reached(self.update_next)
if self.tick != self.device.tick
self.update_shadow() # call update_shadow if not already called
end
self.update_next = tasmota.millis(self.UPDATE_TIME) # rearm timer
end
end
end
#############################################################
# UI Methods
#############################################################
# ui_conf_to_string
#
# Convert the current plugin parameters to a single string
static def ui_conf_to_string(cl, conf)
var arg_name = cl.ARG
var arg = arg_name ? str(conf.find(arg_name, '')) : ''
# print("MTR: ui_conf_to_string", conf, cl, arg_name, arg)
return arg
end
#############################################################
# ui_string_to_conf
#
# Convert the string in UI to actual parameters added to the map
static def ui_string_to_conf(cl, conf, arg)
var arg_name = cl.ARG
var arg_type = cl.ARG_TYPE
if arg && arg_name
conf[arg_name] = arg_type(arg)
end
# print("ui_string_to_conf", conf, arg)
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)
# pass
end
#######################################################################
# _parse_update_virtual: parse a single value out of MtrUpdate JSON
#
# Used internally by `update_virtual`
#
# Args
# payload: the native payload (converted from JSON) from MtrUpdate
# key: key name in the JSON payload to read from, do nothing if key does not exist or content is `null`
# old_val: previous value, used to detect a change or return the value unchanged
# type_func: type enforcer for value, typically `int`, `bool`, `str`, `number`, `real`
# cluster/attribute: in case the value has change, publish a change to cluster/attribute. Don't publish if they are `nil`
#
# Returns:
# `old_val` if key does not exist, JSON value is `null`, or value is unchanged
# or new value from JSON (which is the new shadow value)
#
def _parse_update_virtual(payload, key, old_val, type_func, cluster, attribute)
var val = payload.find(key)
if (val != nil)
val = type_func(val)
if (val != old_val) && (cluster != nil) && (attribute != nil)
self.attribute_updated(cluster, attribute)
end
return val
end
return old_val
end
end
matter.Plugin = Matter_Plugin