mirror of
https://github.com/arendst/Tasmota.git
synced 2025-04-25 07:17:16 +00:00
678 lines
35 KiB
Plaintext
678 lines
35 KiB
Plaintext
#
|
||
# Matter_Plugin_Root.be - implements the core features that a Matter device must implemment
|
||
#
|
||
# 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/>.
|
||
#
|
||
|
||
# Matter plug-in for root behavior
|
||
|
||
# dummy declaration for solidification
|
||
class Matter_Plugin end
|
||
|
||
#@ solidify:Matter_Plugin_Root,weak
|
||
|
||
class Matter_Plugin_Root : Matter_Plugin
|
||
static var TYPE = "root" # name of the plug-in in json
|
||
static var NAME = "Root node" # display name of the plug-in
|
||
static var CLUSTERS = {
|
||
# 0x001D: inherited # Descriptor Cluster 9.5 p.453
|
||
0x001F: [0,2,3,4], # Access Control Cluster, p.461
|
||
0x0028: [0,1,2,3,4,5,6,7,8,9,0x0A,0x0F,0x12,0x13],# Basic Information Cluster cluster 11.1 p.565
|
||
# 0x002A: [0,1,2,3], # OTA Software Update Requestor Cluster Definition 11.19.7 p.762
|
||
0x002B: [0,1], # Localization Configuration Cluster 11.3 p.580
|
||
0x002C: [0,1,2], # Time Format Localization Cluster 11.4 p.581
|
||
0x0030: [0,1,2,3,4], # GeneralCommissioning cluster 11.9 p.627
|
||
0x0031: [3,4,0xFFFC], # Network Commissioning Cluster cluster 11.8 p.606
|
||
0x0032: [], # Diagnostic Logs Cluster 11.10 p.637
|
||
0x0033: [0,1,2,8], # General Diagnostics Cluster 11.11 p.642
|
||
0x0034: [], # Software Diagnostics Cluster 11.12 p.654
|
||
0x0038: [0,1,7], # Time Synchronization 11.16 p.689
|
||
0x003C: [0,1,2], # Administrator Commissioning Cluster 11.18 p.725
|
||
0x003E: [0,1,2,3,4,5], # Node Operational Credentials Cluster 11.17 p.704
|
||
0x003F: [] # Group Key Management Cluster 11.2 p.572
|
||
}
|
||
static var TYPES = { 0x0016: 1 } # Root node
|
||
|
||
#############################################################
|
||
# Constructor
|
||
def init(device, endpoint, arguments)
|
||
super(self).init(device, endpoint, arguments)
|
||
end
|
||
|
||
#############################################################
|
||
# read an attribute
|
||
#
|
||
def read_attribute(session, ctx)
|
||
import string
|
||
var TLV = matter.TLV
|
||
var cluster = ctx.cluster
|
||
var attribute = ctx.attribute
|
||
|
||
if cluster == 0x0030 # ========== GeneralCommissioning cluster 11.9 p.627 ==========
|
||
|
||
if attribute == 0x0000 # ---------- Breadcrumb ----------
|
||
return TLV.create_TLV(TLV.U8, session._breadcrumb)
|
||
elif attribute == 0x0001 # ---------- BasicCommissioningInfo / BasicCommissioningInfo----------
|
||
var bci = TLV.Matter_TLV_struct()
|
||
bci.add_TLV(0, TLV.U2, 60) # FailSafeExpiryLengthSeconds
|
||
bci.add_TLV(1, TLV.U2, 900) # MaxCumulativeFailsafeSeconds
|
||
return bci
|
||
elif attribute == 0x0002 # ---------- RegulatoryConfig / RegulatoryLocationType ----------
|
||
return TLV.create_TLV(TLV.U1, 2) # 2 = IndoorOutdoor | esp-matter = 0
|
||
elif attribute == 0x0003 # ---------- LocationCapability / RegulatoryLocationType----------
|
||
return TLV.create_TLV(TLV.U1, 2) # 2 = IndoorOutdoor
|
||
elif attribute == 0x0004 # ---------- SupportsConcurrentConnection / bool ----------
|
||
return TLV.create_TLV(TLV.BOOL, false) # false - maybe can set to true
|
||
end
|
||
# ====================================================================================================
|
||
elif cluster == 0x0032 # ========== Diagnostic Logs Cluster 11.10 p.637 ==========
|
||
# no attributes
|
||
|
||
# ====================================================================================================
|
||
elif cluster == 0x0033 # ========== General Diagnostics Cluster 11.11 p.642 ==========
|
||
|
||
if attribute == 0x0000 # ---------- NetworkInterfaces ----------
|
||
var nwi = TLV.Matter_TLV_array() # list network interfaces, empty list for now, p.647
|
||
|
||
var tas_eth = tasmota.eth()
|
||
if (tas_eth['up'])
|
||
var eth = nwi.add_struct(nil)
|
||
eth.add_TLV(0, TLV.UTF1, 'ethernet') # Name
|
||
eth.add_TLV(1, TLV.BOOL, 1) # IsOperational
|
||
eth.add_TLV(2, TLV.BOOL, 1) # OffPremiseServicesReachableIPv4
|
||
eth.add_TLV(3, TLV.NULL, nil) # OffPremiseServicesReachableIPv6
|
||
var mac = bytes().fromhex(string.replace(tas_eth.find("mac", ""), ":", ""))
|
||
eth.add_TLV(4, TLV.B1, mac) # HardwareAddress
|
||
var ip4 = eth.add_array(5) # IPv4Addresses
|
||
ip4.add_TLV(nil, TLV.B1, matter.get_ip_bytes(tas_eth.find("ip", "")))
|
||
var ip6 = eth.add_array(6) # IPv6Addresses
|
||
ip6.add_TLV(nil, TLV.B1, matter.get_ip_bytes(tas_eth.find("ip6local", "")))
|
||
ip6.add_TLV(nil, TLV.B1, matter.get_ip_bytes(tas_eth.find("ip6", "")))
|
||
eth.add_TLV(7, TLV.U1, 2) # InterfaceType, p646
|
||
end
|
||
|
||
var tas_wif = tasmota.wifi()
|
||
if (tas_wif['up'])
|
||
var wif = nwi.add_struct(nil)
|
||
wif.add_TLV(0, TLV.UTF1, 'wifi') # Name
|
||
wif.add_TLV(1, TLV.BOOL, 1) # IsOperational
|
||
wif.add_TLV(2, TLV.BOOL, 1) # OffPremiseServicesReachableIPv4
|
||
wif.add_TLV(3, TLV.NULL, nil) # OffPremiseServicesReachableIPv6
|
||
var mac = bytes().fromhex(string.replace(tas_wif.find("mac", ""), ":", ""))
|
||
wif.add_TLV(4, TLV.B1, mac) # HardwareAddress
|
||
var ip4 = wif.add_array(5) # IPv4Addresses
|
||
ip4.add_TLV(nil, TLV.B1, matter.get_ip_bytes(tas_wif.find("ip", "")))
|
||
var ip6 = wif.add_array(6) # IPv6Addresses
|
||
ip6.add_TLV(nil, TLV.B1, matter.get_ip_bytes(tas_wif.find("ip6local", "")))
|
||
ip6.add_TLV(nil, TLV.B1, matter.get_ip_bytes(tas_wif.find("ip6", "")))
|
||
wif.add_TLV(7, TLV.U1, 1) # InterfaceType, p646
|
||
end
|
||
return nwi
|
||
elif attribute == 0x0001 # ---------- RebootCount u16 ----------
|
||
return TLV.create_TLV(TLV.U2, tasmota.cmd("Status 1", true)['StatusPRM']['BootCount'])
|
||
elif attribute == 0x0002 # ---------- UpTime u16 ----------
|
||
return TLV.create_TLV(TLV.U4, tasmota.cmd("Status 11", true)['StatusSTS']['UptimeSec'])
|
||
# TODO add later other attributes
|
||
elif attribute == 0x0008 # ---------- TestEventTriggersEnabled bool ----------
|
||
return TLV.create_TLV(TLV.BOOL, false) # false - maybe can set to true
|
||
end
|
||
|
||
# ====================================================================================================
|
||
elif cluster == 0x0034 # ========== Software Diagnostics Cluster 11.12 p.654 ==========
|
||
# no mandatory attributes - to be added later (maybe)
|
||
|
||
# ====================================================================================================
|
||
elif cluster == 0x0038 # ========== Time Synchronization 11.16 p.689 ==========
|
||
if attribute == 0x0000 # ---------- UTCTime / epoch_us ----------
|
||
var epoch_us = int64(tasmota.rtc()['utc']) * int64(1000000)
|
||
return TLV.create_TLV(TLV.U8, epoch_us) # TODO test the conversion of int64()
|
||
elif attribute == 0x0001 # ---------- Granularity / enum ----------
|
||
return TLV.create_TLV(TLV.U1, 3) # MillisecondsGranularity (NTP every hour, i.e. 36ms max drift)
|
||
# TODO add some missing args
|
||
elif attribute == 0x0007 # ---------- LocalTime / epoch_us ----------
|
||
var epoch_us = int64(tasmota.rtc()['local']) * int64(1000000)
|
||
return TLV.create_TLV(TLV.U8, epoch_us) # TODO test the conversion of int64()
|
||
end
|
||
|
||
# ====================================================================================================
|
||
elif cluster == 0x003E # ========== Node Operational Credentials Cluster 11.17 p.704 ==========
|
||
|
||
if attribute == 0x0000 # ---------- NOCs / list[NOCStruct] ----------
|
||
var nocl = TLV.Matter_TLV_array() # NOCs, p.711
|
||
for loc_fabric: self.device.sessions.active_fabrics()
|
||
var nocs = nocl.add_struct(nil)
|
||
nocs.add_TLV(1, TLV.B2, loc_fabric.get_noc()) # NOC
|
||
nocs.add_TLV(2, TLV.B2, loc_fabric.get_icac()) # ICAC
|
||
nocs.add_TLV(0xFE, TLV.U2, loc_fabric.get_fabric_index()) # Label
|
||
end
|
||
return nocl
|
||
elif attribute == 0x0001 # ---------- Fabrics / list[FabricDescriptorStruct] ----------
|
||
var fabrics = TLV.Matter_TLV_array() # Fabrics, p.711
|
||
for loc_fabric: self.device.sessions.active_fabrics()
|
||
var root_ca_tlv = TLV.parse(loc_fabric.get_ca())
|
||
var fab = fabrics.add_struct(nil) # encoding see p.303
|
||
fab.add_TLV(1, TLV.B2, root_ca_tlv.findsubval(9)) # RootPublicKey
|
||
fab.add_TLV(2, TLV.U2, loc_fabric.get_admin_vendor()) # VendorID
|
||
fab.add_TLV(3, TLV.U8, loc_fabric.get_fabric_id()) # FabricID
|
||
fab.add_TLV(4, TLV.U8, loc_fabric.get_device_id()) # NodeID
|
||
fab.add_TLV(5, TLV.UTF1, loc_fabric.get_fabric_label()) # Label
|
||
fab.add_TLV(0xFE, TLV.U2, loc_fabric.get_fabric_index()) # idx
|
||
end
|
||
return fabrics
|
||
elif attribute == 0x0002 # ---------- SupportedFabrics / u1 ----------
|
||
return TLV.create_TLV(TLV.U1, matter.Fabric._MAX_CASE) # Max 5 fabrics
|
||
elif attribute == 0x0003 # ---------- CommissionedFabrics / u1 ----------
|
||
var fabric_actice = self.device.sessions.count_active_fabrics()
|
||
return TLV.create_TLV(TLV.U1, fabric_actice) # number of active fabrics
|
||
elif attribute == 0x0004 # ---------- TrustedRootCertificates / list[octstr] ----------
|
||
# TODO
|
||
elif attribute == 0x0005 # ---------- Current FabricIndex / u1 ----------
|
||
var fab_index = session._fabric.get_fabric_index()
|
||
if fab_index == nil fab_index = 0 end # if PASE session, then the fabric index should be zero
|
||
return TLV.create_TLV(TLV.U1, fab_index) # number of active sessions
|
||
end
|
||
|
||
# ====================================================================================================
|
||
elif cluster == 0x003C # ========== Administrator Commissioning Cluster 11.18 p.725 ==========
|
||
if attribute == 0x0000 # ---------- WindowStatus / u8 ----------
|
||
var commissioning_open = self.device.is_commissioning_open()
|
||
var basic_commissioning = self.device.is_root_commissioning_open()
|
||
var val = commissioning_open ? (basic_commissioning ? 2 #-BasicWindowOpen-# : 1 #-EnhancedWindowOpen-#) : 0 #-WindowNotOpen-#
|
||
return TLV.create_TLV(TLV.U1, val)
|
||
elif attribute == 0x0001 # ---------- AdminFabricIndex / u16 ----------
|
||
var admin_fabric = self.device.commissioning_admin_fabric
|
||
if admin_fabric != nil
|
||
return TLV.create_TLV(TLV.U2, admin_fabric.get_fabric_index())
|
||
else
|
||
return TLV.create_TLV(TLV.NULL, nil)
|
||
end
|
||
elif attribute == 0x0002 # ---------- AdminVendorId / u16 ----------
|
||
var admin_fabric = self.device.commissioning_admin_fabric
|
||
if admin_fabric != nil
|
||
return TLV.create_TLV(TLV.U2, admin_fabric.get_admin_vendor())
|
||
else
|
||
return TLV.create_TLV(TLV.NULL, nil)
|
||
end
|
||
end
|
||
|
||
# ====================================================================================================
|
||
elif cluster == 0x0028 # ========== Basic Information Cluster cluster 11.1 p.565 ==========
|
||
|
||
if attribute == 0x0000 # ---------- DataModelRevision / CommissioningWindowStatus ----------
|
||
return TLV.create_TLV(TLV.U2, 1)
|
||
elif attribute == 0x0001 # ---------- VendorName / string ----------
|
||
return TLV.create_TLV(TLV.UTF1, "Tasmota")
|
||
elif attribute == 0x0002 # ---------- VendorID / vendor-id ----------
|
||
return TLV.create_TLV(TLV.U2, self.device.vendorid) # Vendor ID reserved for development
|
||
elif attribute == 0x0003 # ---------- ProductName / string ----------
|
||
return TLV.create_TLV(TLV.UTF1, tasmota.cmd("DeviceName", true)['DeviceName'])
|
||
elif attribute == 0x0004 # ---------- ProductID / u16 (opt) ----------
|
||
return TLV.create_TLV(TLV.U2, 32768) # taken from esp-matter example
|
||
elif attribute == 0x0005 # ---------- NodeLabel / string ----------
|
||
return TLV.create_TLV(TLV.UTF1, tasmota.cmd("FriendlyName", true)['FriendlyName1'])
|
||
elif attribute == 0x0006 # ---------- Location / string ----------
|
||
return TLV.create_TLV(TLV.UTF1, "XX") # no location
|
||
elif attribute == 0x0007 # ---------- HardwareVersion / u16 ----------
|
||
return TLV.create_TLV(TLV.U2, 0)
|
||
elif attribute == 0x0008 # ---------- HardwareVersionString / string ----------
|
||
return TLV.create_TLV(TLV.UTF1, tasmota.cmd("Status 2", true)['StatusFWR']['Hardware'])
|
||
elif attribute == 0x0009 # ---------- SoftwareVersion / u32 ----------
|
||
return TLV.create_TLV(TLV.U2, 1)
|
||
elif attribute == 0x000A # ---------- SoftwareVersionString / string ----------
|
||
var version_full = tasmota.cmd("Status 2", true)['StatusFWR']['Version']
|
||
var version_end = string.find(version_full, '(')
|
||
if version_end > 0 version_full = version_full[0..version_end - 1] end
|
||
return TLV.create_TLV(TLV.UTF1, version_full)
|
||
elif attribute == 0x000F # ---------- SerialNumber / string ----------
|
||
return TLV.create_TLV(TLV.UTF1, tasmota.wifi().find("mac", ""))
|
||
elif attribute == 0x0012 # ---------- UniqueID / string 32 max ----------
|
||
return TLV.create_TLV(TLV.UTF1, tasmota.wifi().find("mac", ""))
|
||
elif attribute == 0x0013 # ---------- CapabilityMinima / CapabilityMinimaStruct ----------
|
||
var cps = TLV.Matter_TLV_struct()
|
||
cps.add_TLV(0, TLV.U2, 3) # CaseSessionsPerFabric = 3
|
||
cps.add_TLV(1, TLV.U2, 3) # SubscriptionsPerFabric = 5
|
||
return cps
|
||
end
|
||
|
||
# ====================================================================================================
|
||
elif cluster == 0x003F # ========== Group Key Management Cluster 11.2 p.572 ==========
|
||
# TODO
|
||
|
||
# ====================================================================================================
|
||
elif cluster == 0x002A # ========== OTA Software Update Requestor Cluster Definition 11.19.7 p.762 ==========
|
||
|
||
if attribute == 0x0000 # ---------- DefaultOTAProviders / list[ProviderLocationStruct] ----------
|
||
return TLV.Matter_TLV_array() # empty list for now TODO
|
||
elif attribute == 0x0001 # ---------- UpdatePossible / bool ----------
|
||
return TLV.create_TLV(TLV.BOOL, 0) # we claim that update is not possible, would require to go to Tasmota UI
|
||
elif attribute == 0x0002 # ---------- UpdateState / UpdateStateEnum ----------
|
||
return TLV.create_TLV(TLV.U1, 1) # Idle
|
||
elif attribute == 0x0003 # ---------- UpdateStateProgress / uint8 ----------
|
||
return TLV.create_TLV(TLV.NULL, nil) # null, nothing in process
|
||
end
|
||
|
||
# ====================================================================================================
|
||
elif cluster == 0x002B # ========== Localization Configuration Cluster 11.3 p.580 ==========
|
||
|
||
if attribute == 0x0000 # ---------- ActiveLocale / string ----------
|
||
return TLV.create_TLV(TLV.UTF1, tasmota.locale())
|
||
elif attribute == 0x0001 # ---------- SupportedLocales / list[string] ----------
|
||
var locl = TLV.Matter_TLV_array()
|
||
locl.add_TLV(nil, TLV.UTF1, tasmota.locale())
|
||
return locl
|
||
end
|
||
|
||
# ====================================================================================================
|
||
elif cluster == 0x002C # ========== Time Format Localization Cluster 11.4 p.581 ==========
|
||
|
||
if attribute == 0x0000 # ---------- HourFormat / HourFormat ----------
|
||
return TLV.create_TLV(TLV.U1, 1) # 1 = 24hr
|
||
elif attribute == 0x0001 # ---------- ActiveCalendarType / CalendarType ----------
|
||
return TLV.create_TLV(TLV.U1, 4) # 4 = Gregorian
|
||
elif attribute == 0x0002 # ---------- SupportedCalendarTypes / list[CalendarType] ----------
|
||
var callist = TLV.Matter_TLV_array()
|
||
callist.add_TLV(nil, TLV.create_TLV(TLV.U1, 4))
|
||
return callist
|
||
end
|
||
|
||
# ====================================================================================================
|
||
elif cluster == 0x0031 # ========== Network Commissioning Cluster cluster 11.8 p.606 ==========
|
||
if attribute == 0x0003 # ---------- ConnectMaxTimeSeconds / uint8 ----------
|
||
return TLV.create_TLV(TLV.U1, 30) # 30 - value taking from example in esp-matter
|
||
elif attribute == 0xFFFC # ---------- FeatureMap / map32 ----------
|
||
return TLV.create_TLV(TLV.U4, 0x04) # Put Eth for now which should work for any on-network
|
||
end
|
||
|
||
elif cluster == 0x001D # ========== Descriptor Cluster 9.5 p.453 ==========
|
||
|
||
# overwrite PartsList
|
||
if attribute == 0x0003 # ---------- PartsList / list[endpoint-no]----------
|
||
var pl = TLV.Matter_TLV_array()
|
||
var eps = self.device.get_active_endpoints(true)
|
||
for ep: eps
|
||
pl.add_TLV(nil, TLV.U2, ep) # add each endpoint
|
||
end
|
||
return pl
|
||
else
|
||
return super(self).read_attribute(session, ctx)
|
||
end
|
||
|
||
else
|
||
return super(self).read_attribute(session, ctx)
|
||
|
||
end
|
||
# no match found, return that the attribute is unsupported
|
||
end
|
||
|
||
#############################################################
|
||
# Invoke a command
|
||
#
|
||
# returns a TLV object if successful, contains the response
|
||
# or an `int` to indicate a status
|
||
def invoke_request(session, val, ctx)
|
||
import crypto
|
||
import string
|
||
var TLV = matter.TLV
|
||
var cluster = ctx.cluster
|
||
var command = ctx.command
|
||
if cluster == 0x0030 # ========== GeneralCommissioning cluster 11.9 p.627 ==========
|
||
|
||
if command == 0x0000 # ---------- ArmFailSafe ----------
|
||
# create ArmFailSafeResponse
|
||
# ID=1
|
||
# 0=ErrorCode (OK=0)
|
||
# 1=DebugText
|
||
var ExpiryLengthSeconds = val.findsubval(0, 900)
|
||
var Breadcrumb = val.findsubval(1, 0)
|
||
session._breadcrumb = Breadcrumb
|
||
|
||
var afsr = TLV.Matter_TLV_struct()
|
||
afsr.add_TLV(0, TLV.U1, 0) # ErrorCode = OK
|
||
afsr.add_TLV(1, TLV.UTF1, "") # DebugText = ""
|
||
ctx.command = 0x01 # ArmFailSafeResponse
|
||
return afsr
|
||
|
||
elif command == 0x0002 # ---------- SetRegulatoryConfig ----------
|
||
var NewRegulatoryConfig = val.findsubval(0) # RegulatoryLocationType Enum
|
||
var CountryCode = val.findsubval(1, "XX")
|
||
var Breadcrumb = val.findsubval(2, 0)
|
||
session._breadcrumb = Breadcrumb
|
||
# create SetRegulatoryConfigResponse
|
||
# ID=1
|
||
# 0=ErrorCode (OK=0)
|
||
# 1=DebugText
|
||
var srcr = TLV.Matter_TLV_struct()
|
||
srcr.add_TLV(0, TLV.U1, 0) # ErrorCode = OK
|
||
srcr.add_TLV(1, TLV.UTF1, "") # DebugText = ""
|
||
ctx.command = 0x03 # SetRegulatoryConfigResponse
|
||
return srcr
|
||
|
||
elif command == 0x0004 # ---------- CommissioningComplete p.636 ----------
|
||
# no data
|
||
session._breadcrumb = 0 # clear breadcrumb
|
||
session.fabric_completed() # fabric information is complete, persist
|
||
session.set_no_expiration()
|
||
session.save()
|
||
|
||
# create CommissioningCompleteResponse
|
||
# ID=1
|
||
# 0=ErrorCode (OK=0)
|
||
# 1=DebugText
|
||
var ccr = TLV.Matter_TLV_struct()
|
||
ccr.add_TLV(0, TLV.U1, 0) # ErrorCode = OK
|
||
ccr.add_TLV(1, TLV.UTF1, "") # DebugText = ""
|
||
ctx.command = 0x05 # CommissioningCompleteResponse
|
||
|
||
self.device.start_commissioning_complete_deferred(session)
|
||
return ccr
|
||
end
|
||
|
||
elif cluster == 0x003E # ========== Node Operational Credentials Cluster 11.17 p.704 ==========
|
||
|
||
if command == 0x0002 # ---------- CertificateChainRequest ----------
|
||
var CertificateType = val.findsubval(0) # CertificateChainType Enum 1=DACCertificate 2=PAICertificate
|
||
if CertificateType != 1 && CertificateType != 2
|
||
ctx.status = matter.UNSUPPORTED_COMMAND
|
||
return nil
|
||
end
|
||
# create CertificateChainResponse
|
||
# ID=1
|
||
# 0=Certificate (octstr)
|
||
var ccr = TLV.Matter_TLV_struct()
|
||
ccr.add_TLV(0, TLV.B2, CertificateType == 1 ? matter.DAC_Cert_FFF1_8000() : matter.PAI_Cert_FFF1()) # send DAC_Cert_FFF1_8000 or PAI_Cert_FFF1
|
||
ctx.command = 0x03 # CertificateChainResponse
|
||
return ccr
|
||
|
||
elif command == 0x0000 # ---------- AttestationRequest ----------
|
||
var AttestationNonce = val.findsubval(0) # octstr
|
||
if size(AttestationNonce) != 32 return nil end # check size on nonce
|
||
ctx.command = 0x01 # AttestationResponse
|
||
|
||
# build Attestation Elements 11.17.5.4 p.707
|
||
var att_elts = TLV.Matter_TLV_struct()
|
||
att_elts.add_TLV(1, TLV.B2, matter.CD_FFF1_8000()) # certification_declaration
|
||
att_elts.add_TLV(2, TLV.B1, AttestationNonce) # attestation_nonce
|
||
att_elts.add_TLV(3, TLV.U4, tasmota.rtc()['utc']) # timestamp in epoch-s
|
||
var attestation_message = att_elts.tlv2raw()
|
||
|
||
var ac = session.get_ac()
|
||
var attestation_tbs = attestation_message + ac
|
||
tasmota.log("MTR: attestation_tbs=" + attestation_tbs.tohex(), 3)
|
||
|
||
var attestation_signature = crypto.EC_P256().ecdsa_sign_sha256(matter.DAC_Priv_FFF1_8000(), attestation_tbs)
|
||
|
||
# create AttestationResponse
|
||
# 0=AttestationElements (octstr max 900 bytes)
|
||
# 1=AttestationSignature (octstr 64)
|
||
var ar = TLV.Matter_TLV_struct()
|
||
ar.add_TLV(0, TLV.B2, attestation_message) # AttestationElements
|
||
ar.add_TLV(1, TLV.B1, attestation_signature) # AttestationElements
|
||
ctx.command = 0x01 # AttestationResponse
|
||
return ar
|
||
|
||
elif command == 0x0004 # ---------- CSRRequest ----------
|
||
var CSRNonce = val.findsubval(0) # octstr 32
|
||
if size(CSRNonce) != 32 return nil end # check size on nonce
|
||
var IsForUpdateNOC = val.findsubval(1, false) # bool
|
||
|
||
var csr = session.gen_CSR()
|
||
|
||
var nocsr_elements = TLV.Matter_TLV_struct()
|
||
nocsr_elements.add_TLV(1, TLV.B2, csr)
|
||
nocsr_elements.add_TLV(2, TLV.B1, CSRNonce)
|
||
var nocsr_elements_message = nocsr_elements.tlv2raw()
|
||
# sign with attestation challenge
|
||
var nocsr_tbs = nocsr_elements_message + session.get_ac()
|
||
tasmota.log("MTR: nocsr_tbs=" + nocsr_tbs.tohex(), 3)
|
||
var attestation_signature = crypto.EC_P256().ecdsa_sign_sha256(matter.DAC_Priv_FFF1_8000(), nocsr_tbs)
|
||
|
||
# create CSRResponse
|
||
# 0=NOCSRElements (octstr max 900 bytes)
|
||
# 1=AttestationSignature (octstr 64)
|
||
var csrr = TLV.Matter_TLV_struct()
|
||
csrr.add_TLV(0, TLV.B2, nocsr_elements_message) # AttestationElements
|
||
csrr.add_TLV(1, TLV.B1, attestation_signature) # AttestationElements
|
||
ctx.command = 0x05 # CSRResponse
|
||
return csrr
|
||
|
||
elif command == 0x000B # ---------- AddTrustedRootCertificate ----------
|
||
var RootCACertificate = val.findsubval(0) # octstr 400 max
|
||
session.set_ca(RootCACertificate)
|
||
tasmota.log("MTR: received ca_root="+RootCACertificate.tohex(), 3)
|
||
ctx.status = matter.SUCCESS # OK
|
||
return nil # trigger a standalone ack
|
||
|
||
elif command == 0x0006 # ---------- AddNOC ----------
|
||
var NOCValue = val.findsubval(0) # octstr max 400
|
||
var ICACValue = val.findsubval(1) # octstr max 400
|
||
# Apple sends an empty ICAC instead of a missing attribute, fix this
|
||
if size(ICACValue) == 0 ICACValue = nil end
|
||
var IpkValue = val.findsubval(2) # octstr max 16
|
||
var CaseAdminSubject = val.findsubval(3)
|
||
var AdminVendorId = val.findsubval(4)
|
||
|
||
if session.get_ca() == nil
|
||
tasmota.log("MTR: Error: AdNOC without CA", 2)
|
||
return nil
|
||
end
|
||
|
||
session.set_noc(NOCValue, ICACValue)
|
||
session.set_ipk_epoch_key(IpkValue)
|
||
session.set_admin_subject_vendor(CaseAdminSubject, AdminVendorId)
|
||
|
||
# extract important information from NOC
|
||
var noc_cert = matter.TLV.parse(NOCValue)
|
||
var dnlist = noc_cert.findsub(6)
|
||
var fabric_id = dnlist.findsubval(21)
|
||
var deviceid = dnlist.findsubval(17)
|
||
if !fabric_id || !deviceid
|
||
tasmota.log("MTR: Error: no fabricid nor deviceid in NOC certificate", 2)
|
||
return false
|
||
end
|
||
# convert fo bytes(8)
|
||
if type(fabric_id) == 'int' fabric_id = int64.fromu32(fabric_id).tobytes() else fabric_id = fabric_id.tobytes() end
|
||
if type(deviceid) == 'int' deviceid = int64.fromu32(deviceid).tobytes() else deviceid = deviceid.tobytes() end
|
||
|
||
var root_ca = matter.TLV.parse(session.get_ca()).findsubval(9) # extract public key from ca
|
||
root_ca = root_ca[1..] # remove first byte as per Matter specification
|
||
var info = bytes().fromstring("CompressedFabric") # as per spec, 4.3.2.2 p.99
|
||
var hk = crypto.HKDF_SHA256()
|
||
var fabric_rev = fabric_id.copy().reverse()
|
||
var k_fabric = hk.derive(root_ca, fabric_rev, info, 8)
|
||
session.set_fabric_device(fabric_id, deviceid, k_fabric, self.device.commissioning_admin_fabric)
|
||
|
||
# We have a candidate fabric, add it as expirable for 2 minutes
|
||
session.persist_to_fabric() # fabric object is completed, persist it
|
||
session.fabric_candidate()
|
||
|
||
# move to next step
|
||
self.device.start_operational_discovery_deferred(session)
|
||
# session.fabric_completed()
|
||
tasmota.log("MTR: ------------------------------------------", 3)
|
||
tasmota.log("MTR: fabric=" + matter.inspect(session._fabric), 3)
|
||
tasmota.log("MTR: ------------------------------------------", 3)
|
||
session._fabric.log_new_fabric() # log that we registered a new fabric
|
||
# create NOCResponse
|
||
# 0=StatusCode
|
||
# 1=FabricIndex (1-254) (opt)
|
||
# 2=DebugText (opt)
|
||
var nocr = TLV.Matter_TLV_struct()
|
||
nocr.add_TLV(0, TLV.U1, matter.SUCCESS) # Status
|
||
nocr.add_TLV(1, TLV.U1, 1) # fabric-index
|
||
ctx.command = 0x08 # NOCResponse
|
||
return nocr
|
||
|
||
elif command == 0x0009 # ---------- UpdateFabricLabel ----------
|
||
var label = val.findsubval(0) # Label string max 32
|
||
session.set_fabric_label(label)
|
||
tasmota.log(string.format("MTR: . Update fabric '%s' label='%s'", session._fabric.get_fabric_id().copy().reverse().tohex(), str(label)), 2)
|
||
ctx.status = matter.SUCCESS # OK
|
||
return nil # trigger a standalone ack
|
||
|
||
elif command == 0x000A # ---------- RemoveFabric ----------
|
||
var index = val.findsubval(0) # FabricIndex
|
||
ctx.log = "fabric_index:"+str(index)
|
||
|
||
for fab: self.device.sessions.active_fabrics()
|
||
if fab.get_fabric_index() == index
|
||
tasmota.log("MTR: removing fabric " + fab.get_fabric_id().copy().reverse().tohex(), 2)
|
||
# defer actual removal to send a response
|
||
tasmota.set_timer(2000, def () self.device.remove_fabric(fab) end)
|
||
return true # Ok
|
||
end
|
||
end
|
||
tasmota.log("MTR: RemoveFabric fabric("+str(index)+") not found", 2)
|
||
ctx.status = matter.INVALID_ACTION
|
||
return nil # trigger a standalone ack
|
||
|
||
end
|
||
|
||
# ====================================================================================================
|
||
elif cluster == 0x003C # ========== Administrator Commissioning Cluster 11.18 p.725 ==========
|
||
|
||
if command == 0x0000 # ---------- OpenCommissioningWindow ----------
|
||
var timeout = val.findsubval(0) # CommissioningTimeout u2
|
||
var passcode_verifier = val.findsubval(1) # PAKEPasscodeVerifier octstr
|
||
var discriminator = val.findsubval(2) # Discriminator u2
|
||
var iterations = val.findsubval(3) # Iterations u4
|
||
var salt = val.findsubval(4) # Salt octstr
|
||
|
||
tasmota.log(string.format("MTR: OpenCommissioningWindow(timeout=%i, passcode=%s, discriminator=%i, iterations=%i, salt=%s)",
|
||
timeout, passcode_verifier.tohex(), discriminator, iterations, salt.tohex()), 2)
|
||
|
||
# check values
|
||
if timeout == nil || passcode_verifier == nil || discriminator == nil || iterations == nil || salt == nil
|
||
ctx.status = matter.INVALID_DATA_TYPE
|
||
return nil # trigger a standalone ack
|
||
end
|
||
if size(passcode_verifier) != 32+65 || size(salt) < 16 || size(salt) > 32
|
||
tasmota.log("MTR: wrong size for PAKE parameters")
|
||
ctx.status = matter.CONSTRAINT_ERROR
|
||
return nil # trigger a standalone ack
|
||
end
|
||
|
||
var w0 = passcode_verifier[0..31]
|
||
var L = passcode_verifier[32..]
|
||
|
||
self.device.start_basic_commissioning(timeout, iterations, discriminator, salt, w0, #-w1,-# L, session.get_fabric())
|
||
# TODO announce in MDNS
|
||
return true # OK
|
||
elif command == 0x0001 # ---------- OpenBasicCommissioningWindow ----------
|
||
var commissioning_timeout = val.findsubval(0) # CommissioningTimeout
|
||
tasmota.log("MTR: OpenBasicCommissioningWindow commissioning_timeout="+str(commissioning_timeout), 2)
|
||
self.device.start_root_basic_commissioning(commissioning_timeout)
|
||
return true
|
||
elif command == 0x0002 # ---------- RevokeCommissioning ----------
|
||
# TODO add checks that the commissioning window was opened by the same fabric
|
||
self.device.stop_basic_commissioning()
|
||
return true
|
||
end
|
||
|
||
# ====================================================================================================
|
||
elif cluster == 0x002A # ========== OTA Software Update Requestor Cluster Definition 11.19.7 p.762 ==========
|
||
|
||
if command == 0x0000 # ---------- DefaultOTAProviders ----------
|
||
return true # OK
|
||
end
|
||
|
||
else
|
||
return super(self).invoke_request(session, val, ctx)
|
||
end
|
||
|
||
end
|
||
|
||
#############################################################
|
||
# write an attribute
|
||
#
|
||
def write_attribute(session, ctx, write_data)
|
||
import string
|
||
var TLV = matter.TLV
|
||
var cluster = ctx.cluster
|
||
var attribute = ctx.attribute
|
||
|
||
# 0x001D no writable attributes
|
||
# 0x0032 no attributes
|
||
# 0x0033 no writable attributes
|
||
# 0x0034 no writable attributes
|
||
# 0x0038 no mandatory writable attributes
|
||
# 0x003C no writable attributes
|
||
# 0x003E no writable attributes
|
||
|
||
if cluster == 0x0030 # ========== GeneralCommissioning cluster 11.9 p.627 ==========
|
||
|
||
if attribute == 0x0000 # ---------- Breadcrumb ----------
|
||
if type(write_data) == 'int' || isinstance(write_data, int64)
|
||
session._breadcrumb = write_data
|
||
self.attribute_updated(ctx.cluster, ctx.attribute) # TODO should we have a more generalized way each time a write_attribute is triggered, declare the attribute as changed?
|
||
return true
|
||
else
|
||
ctx.status = matter.CONSTRAINT_ERROR
|
||
return false
|
||
end
|
||
end
|
||
|
||
# ====================================================================================================
|
||
elif cluster == 0x001F # ========== Access Control Cluster 9.10 p.461 ==========
|
||
if attribute == 0x0000 # ACL - list[AccessControlEntryStruct]
|
||
return true
|
||
end
|
||
|
||
# ====================================================================================================
|
||
elif cluster == 0x0028 # ========== Basic Information Cluster cluster 11.1 p.565 ==========
|
||
|
||
if attribute == 0x0005 # ---------- NodeLabel / string ----------
|
||
# TODO
|
||
return true
|
||
elif attribute == 0x0006 # ---------- Location / string ----------
|
||
# TODO
|
||
return true
|
||
end
|
||
# ====================================================================================================
|
||
elif cluster == 0x002A # ========== OTA Software Update Requestor Cluster Definition 11.19.7 p.762 ==========
|
||
|
||
if attribute == 0x0000 # ---------- DefaultOTAProviders / list[ProviderLocationStruct] ----------
|
||
return true # silently ignore
|
||
end
|
||
# ====================================================================================================
|
||
elif cluster == 0x002B # ========== Localization Configuration Cluster 11.3 p.580 ==========
|
||
|
||
if attribute == 0x0000 # ---------- ActiveLocale / string ----------
|
||
ctx.status = matter.CONSTRAINT_ERROR # changing locale is not possible
|
||
return false
|
||
end
|
||
# ====================================================================================================
|
||
elif cluster == 0x002C # ========== Time Format Localization Cluster 11.4 p.581 ==========
|
||
|
||
if attribute == 0x0000 # ---------- HourFormat / HourFormat ----------
|
||
# TODO
|
||
return true
|
||
elif attribute == 0x0001 # ---------- ActiveCalendarType / CalendarType ----------
|
||
# TODO
|
||
return true
|
||
end
|
||
# ====================================================================================================
|
||
elif cluster == 0x0031 # ========== Network Commissioning Cluster cluster 11.8 p.606 ==========
|
||
if attribute == 0x0004 # ---------- InterfaceEnabled / bool ----------
|
||
ctx.status = matter.INVALID_ACTION
|
||
return false
|
||
end
|
||
|
||
|
||
end
|
||
end
|
||
end
|
||
matter.Plugin_Root = Matter_Plugin_Root
|
||
|