mirror of https://github.com/arendst/Tasmota.git
Matter refactor commissioning (#21870)
This commit is contained in:
parent
0c52b40f49
commit
eb71fcfd2e
|
@ -222,7 +222,7 @@ extern const bclass be_class_Matter_TLV; // need to declare it upfront because
|
|||
#include "solidify/solidified_Matter_Session.h"
|
||||
#include "solidify/solidified_Matter_Session_Store.h"
|
||||
#include "solidify/solidified_Matter_Commissioning_Data.h"
|
||||
#include "solidify/solidified_Matter_Commissioning.h"
|
||||
#include "solidify/solidified_Matter_Commissioning_Context.h"
|
||||
#include "solidify/solidified_Matter_Message.h"
|
||||
#include "solidify/solidified_Matter_MessageHandler.h"
|
||||
#include "solidify/solidified_Matter_IM_Message.h"
|
||||
|
@ -231,6 +231,7 @@ extern const bclass be_class_Matter_TLV; // need to declare it upfront because
|
|||
#include "solidify/solidified_Matter_EventHandler.h"
|
||||
#include "solidify/solidified_Matter_Control_Message.h"
|
||||
#include "solidify/solidified_Matter_Plugin_0.h"
|
||||
#include "solidify/solidified_Matter_z_Commissioning.h"
|
||||
#include "solidify/solidified_Matter_Base38.h"
|
||||
#include "solidify/solidified_Matter_UI.h"
|
||||
#include "solidify/solidified_Matter_Profiler.h"
|
||||
|
@ -470,6 +471,9 @@ module matter (scope: global, strings: weak) {
|
|||
Control_Message, class(be_class_Matter_Control_Message)
|
||||
UI, class(be_class_Matter_UI)
|
||||
|
||||
// Commissioning
|
||||
Commissioning, class(be_class_Matter_Commissioning)
|
||||
|
||||
// QR Code
|
||||
QRCode, class(be_class_Matter_QRCode)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Matter_Commissioning.be - suppport for Matter Commissioning process PASE and CASE
|
||||
# Matter_Commissioning_Context.be - suppport for Matter Commissioning process PASE and CASE
|
||||
#
|
||||
# Copyright (C) 2023 Stephan Hadinger & Theo Arends
|
||||
#
|
||||
|
@ -53,7 +53,7 @@ class Matter_Commisioning_Context
|
|||
|
||||
def process_incoming(msg)
|
||||
#
|
||||
if !self.device.is_commissioning_open() && msg.opcode >= 0x20 && msg.opcode <= 0x24
|
||||
if !self.device.commissioning.is_commissioning_open() && msg.opcode >= 0x20 && msg.opcode <= 0x24
|
||||
log("MTR: commissioning not open", 2)
|
||||
return false
|
||||
end
|
||||
|
@ -139,8 +139,8 @@ class Matter_Commisioning_Context
|
|||
# generate 32 bytes random
|
||||
pbkdfparamresp.responderRandom = crypto.random(32)
|
||||
pbkdfparamresp.responderSessionId = session.__future_local_session_id
|
||||
pbkdfparamresp.pbkdf_parameters_salt = self.device.commissioning_salt
|
||||
pbkdfparamresp.pbkdf_parameters_iterations = self.device.commissioning_iterations
|
||||
pbkdfparamresp.pbkdf_parameters_salt = self.device.commissioning.commissioning_salt
|
||||
pbkdfparamresp.pbkdf_parameters_iterations = self.device.commissioning.commissioning_iterations
|
||||
# log("MTR: pbkdfparamresp: " + str(matter.inspect(pbkdfparamresp)), 4)
|
||||
var pbkdfparamresp_raw = pbkdfparamresp.tlv2raw()
|
||||
# log("MTR: pbkdfparamresp_raw: " + pbkdfparamresp_raw.tohex(), 4)
|
||||
|
@ -172,7 +172,7 @@ class Matter_Commisioning_Context
|
|||
|
||||
# instanciate SPAKE
|
||||
# for testing purpose, we don't send `w1` to make sure
|
||||
var spake = crypto.SPAKE2P_Matter(self.device.commissioning_w0, nil, self.device.commissioning_L)
|
||||
var spake = crypto.SPAKE2P_Matter(self.device.commissioning.commissioning_w0, nil, self.device.commissioning.commissioning_L)
|
||||
|
||||
# generate `y` nonce (not persisted)
|
||||
var y = crypto.random(32) # 32 bytes random known only by verifier
|
|
@ -196,17 +196,17 @@ class Matter_Plugin_Root : Matter_Plugin
|
|||
# ====================================================================================================
|
||||
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 commissioning_open = self.device.commissioning.is_commissioning_open()
|
||||
var basic_commissioning = self.device.commissioning.is_root_commissioning_open()
|
||||
var val = commissioning_open ? (basic_commissioning ? 2 #-BasicWindowOpen-# : 1 #-EnhancedWindowOpen-#) : 0 #-WindowNotOpen-#
|
||||
return tlv_solo.set(TLV.U1, val)
|
||||
elif attribute == 0x0001 # ---------- AdminFabricIndex / u16 ----------
|
||||
var admin_fabric = self.device.commissioning_admin_fabric
|
||||
var admin_fabric = self.device.commissioning.commissioning_admin_fabric
|
||||
if admin_fabric != nil
|
||||
return tlv_solo.set_or_nil(TLV.U2, admin_fabric.get_fabric_index())
|
||||
end
|
||||
elif attribute == 0x0002 # ---------- AdminVendorId / u16 ----------
|
||||
var admin_fabric = self.device.commissioning_admin_fabric
|
||||
var admin_fabric = self.device.commissioning.commissioning_admin_fabric
|
||||
if admin_fabric != nil
|
||||
return tlv_solo.set_or_nil(TLV.U2, admin_fabric.get_admin_vendor())
|
||||
end
|
||||
|
@ -387,7 +387,7 @@ class Matter_Plugin_Root : Matter_Plugin
|
|||
ccr.add_TLV(1, TLV.UTF1, "") # DebugText = ""
|
||||
ctx.command = 0x05 # CommissioningCompleteResponse
|
||||
|
||||
self.device.start_commissioning_complete_deferred(session)
|
||||
self.device.commissioning.start_commissioning_complete_deferred(session)
|
||||
return ccr
|
||||
else
|
||||
raise "context_error", "CommissioningComplete: no fabric attached"
|
||||
|
@ -526,7 +526,7 @@ class Matter_Plugin_Root : Matter_Plugin
|
|||
var hk = crypto.HKDF_SHA256()
|
||||
var fabric_rev = fabric_id.copy().reverse()
|
||||
var k_fabric = hk.derive(root_ca_pub, fabric_rev, info, 8)
|
||||
var parent_fabric = session._fabric ? session._fabric : self.device.commissioning_admin_fabric # get parent fabric whether CASE or PASE
|
||||
var parent_fabric = session._fabric ? session._fabric : self.device.commissioning.commissioning_admin_fabric # get parent fabric whether CASE or PASE
|
||||
new_fabric.set_fabric_device(fabric_id, deviceid, k_fabric, parent_fabric)
|
||||
|
||||
# log("MTR: AddNoc k_fabric=" + str(k_fabric), 3)
|
||||
|
@ -534,7 +534,7 @@ class Matter_Plugin_Root : Matter_Plugin
|
|||
new_fabric.fabric_candidate()
|
||||
|
||||
# move to next step
|
||||
self.device.start_operational_discovery_deferred(new_fabric)
|
||||
self.device.commissioning.start_operational_discovery_deferred(new_fabric)
|
||||
|
||||
# we keep the PASE session for 1 minute
|
||||
if session.is_PASE()
|
||||
|
@ -627,7 +627,7 @@ class Matter_Plugin_Root : Matter_Plugin
|
|||
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())
|
||||
self.device.commissioning.start_basic_commissioning(timeout, iterations, discriminator, salt, w0, #-w1,-# L, session.get_fabric())
|
||||
# TODO announce in MDNS
|
||||
return true # OK
|
||||
elif command == 0x0001 # ---------- OpenBasicCommissioningWindow ----------
|
||||
|
@ -637,7 +637,7 @@ class Matter_Plugin_Root : Matter_Plugin
|
|||
return true
|
||||
elif command == 0x0002 # ---------- RevokeCommissioning ----------
|
||||
# TODO add checks that the commissioning window was opened by the same fabric
|
||||
self.device.stop_basic_commissioning()
|
||||
self.device.commissioning.stop_basic_commissioning()
|
||||
return true
|
||||
end
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ class Matter_UI
|
|||
|
||||
if self.matter_enabled()
|
||||
# checkbox for Matter commissioning
|
||||
var commissioning_open_checked = self.device.commissioning_open != nil ? "checked" : ""
|
||||
var commissioning_open_checked = self.device.commissioning.commissioning_open != nil ? "checked" : ""
|
||||
webserver.content_send(f"<p><input id='comm' type='checkbox' name='comm' {commissioning_open_checked}>")
|
||||
webserver.content_send("<label for='comm'><b>Commissioning open</b></label></p>")
|
||||
var disable_bridge_mode_checked = self.device.disable_bridge_mode ? " checked" : ""
|
||||
|
@ -173,17 +173,17 @@ class Matter_UI
|
|||
def show_commissioning_info()
|
||||
import webserver
|
||||
|
||||
var seconds_left = (self.device.commissioning_open - tasmota.millis()) / 1000
|
||||
var seconds_left = (self.device.commissioning.commissioning_open - tasmota.millis()) / 1000
|
||||
if seconds_left < 0 seconds_left = 0 end
|
||||
var min_left = (seconds_left + 30) / 60
|
||||
|
||||
webserver.content_send(f"<fieldset><legend><b> Commissioning open for {min_left:i} min </b></legend><p></p>")
|
||||
|
||||
var pairing_code = self.device.compute_manual_pairing_code()
|
||||
var pairing_code = self.device.commissioning.compute_manual_pairing_code()
|
||||
webserver.content_send(f"<p>Manual pairing code:<br><b>{pairing_code[0..3]}-{pairing_code[4..6]}-{pairing_code[7..]}</b></p><hr>")
|
||||
|
||||
webserver.content_send("<div><center>")
|
||||
var qr_text = self.device.compute_qrcode_content()
|
||||
var qr_text = self.device.commissioning.compute_qrcode_content()
|
||||
self.show_qrcode(qr_text)
|
||||
webserver.content_send(f"<p> {qr_text}</p>")
|
||||
webserver.content_send("</div><p></p></fieldset><p></p>")
|
||||
|
@ -794,11 +794,11 @@ class Matter_UI
|
|||
end
|
||||
#- and force restart -#
|
||||
webserver.redirect("/?rst=")
|
||||
elif matter_commissioning_requested != (self.device.commissioning_open != nil)
|
||||
elif matter_commissioning_requested != (self.device.commissioning.commissioning_open != nil)
|
||||
if matter_commissioning_requested
|
||||
self.device.start_root_basic_commissioning()
|
||||
else
|
||||
self.device.stop_basic_commissioning()
|
||||
self.device.commissioning.stop_basic_commissioning()
|
||||
end
|
||||
|
||||
#- and force restart -#
|
||||
|
@ -1085,7 +1085,7 @@ class Matter_UI
|
|||
|
||||
self.show_bridge_status()
|
||||
|
||||
if self.device.is_root_commissioning_open()
|
||||
if self.device.commissioning.is_root_commissioning_open()
|
||||
self.show_commissioning_info()
|
||||
end
|
||||
|
||||
|
@ -1095,7 +1095,7 @@ class Matter_UI
|
|||
def web_get_arg()
|
||||
import webserver
|
||||
if webserver.has_arg("mtc0") # Close Commissioning
|
||||
self.device.stop_basic_commissioning()
|
||||
self.device.commissioning.stop_basic_commissioning()
|
||||
elif webserver.has_arg("mtc1") # Open Commissioning
|
||||
self.device.start_root_basic_commissioning()
|
||||
end
|
||||
|
|
|
@ -0,0 +1,483 @@
|
|||
#
|
||||
# Matter_z_Commissioning.be - implements the commissioning and MDNS logic
|
||||
#
|
||||
# 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
|
||||
|
||||
#@ solidify:Matter_Commissioning,weak
|
||||
|
||||
class Matter_Commissioning
|
||||
var device # reference to the main Device instance
|
||||
|
||||
static var PBKDF_ITERATIONS = 1000 # I don't see any reason to choose a different number
|
||||
static var PASE_TIMEOUT = 10*60 # default open commissioning window (10 minutes)
|
||||
# Commissioning open
|
||||
var commissioning_open # timestamp for timeout of commissioning (millis()) or `nil` if closed
|
||||
var commissioning_iterations # current PBKDF number of iterations
|
||||
var commissioning_discriminator # commissioning_discriminator
|
||||
var commissioning_salt # current salt
|
||||
var commissioning_w0 # current w0 (SPAKE2+)
|
||||
var commissioning_L # current L (SPAKE2+)
|
||||
var commissioning_admin_fabric # the fabric that opened the currint commissioning window, or `nil` for default
|
||||
# information about the device
|
||||
var commissioning_instance_wifi # random instance name for commissioning (mDNS)
|
||||
var commissioning_instance_eth # random instance name for commissioning (mDNS)
|
||||
var hostname_wifi # MAC-derived hostname for commissioning
|
||||
var hostname_eth # MAC-derived hostname for commissioning
|
||||
# mDNS active announces
|
||||
var mdns_pase_eth # do we have an active PASE mDNS announce for eth
|
||||
var mdns_pase_wifi # do we have an active PASE mDNS announce for wifi
|
||||
|
||||
|
||||
#############################################################
|
||||
def init(device)
|
||||
self.device = device
|
||||
self._init_basic_commissioning()
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start Basic Commissioning Window if needed at startup
|
||||
def _init_basic_commissioning()
|
||||
# if no fabric is configured, automatically open commissioning at restart
|
||||
if self.device.sessions.count_active_fabrics() == 0
|
||||
self.start_root_basic_commissioning()
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# dispatch every second click to sub-objects that need it
|
||||
def every_second()
|
||||
if self.commissioning_open != nil && tasmota.time_reached(self.commissioning_open) # timeout reached, close provisioning
|
||||
self.commissioning_open = nil
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start Basic Commissioning with root parameters
|
||||
#
|
||||
# Open window for `timeout_s` (default 10 minutes)
|
||||
def start_root_basic_commissioning(timeout_s)
|
||||
if timeout_s == nil timeout_s = self.PASE_TIMEOUT end
|
||||
|
||||
# show Manual pairing code in logs
|
||||
var pairing_code = self.compute_manual_pairing_code()
|
||||
log(format("MTR: Manual pairing code: %s", pairing_code), 2)
|
||||
|
||||
# output MQTT
|
||||
var qr_code = self.compute_qrcode_content()
|
||||
tasmota.publish_result(format('{"Matter":{"Commissioning":1,"PairingCode":"%s","QRCode":"%s"}}', pairing_code, qr_code), 'Matter')
|
||||
|
||||
# compute PBKDF
|
||||
import crypto
|
||||
var root_salt = crypto.random(16)
|
||||
|
||||
# Compute the PBKDF parameters for SPAKE2+ from root parameters
|
||||
var passcode = bytes().add(self.device.root_passcode, 4)
|
||||
|
||||
var tv = crypto.PBKDF2_HMAC_SHA256().derive(passcode, root_salt, self.PBKDF_ITERATIONS, 80)
|
||||
var w0s = tv[0..39]
|
||||
var w1s = tv[40..79]
|
||||
|
||||
var root_w0 = crypto.EC_P256().mod(w0s)
|
||||
var w1 = crypto.EC_P256().mod(w1s) # w1 is temporarily computed then discarded
|
||||
# self.root_w1 = crypto.EC_P256().mod(w1s)
|
||||
var root_L = crypto.EC_P256().public_key(w1)
|
||||
self.start_basic_commissioning(timeout_s, self.PBKDF_ITERATIONS, self.device.root_discriminator, root_salt, root_w0, #-self.root_w1,-# root_L, nil)
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start Basic Commissioning Window with custom parameters
|
||||
def start_basic_commissioning(timeout_s, iterations, discriminator, salt, w0, L, admin_fabric)
|
||||
self.commissioning_open = tasmota.millis() + timeout_s * 1000
|
||||
self.commissioning_iterations = iterations
|
||||
self.commissioning_discriminator = discriminator
|
||||
self.commissioning_salt = salt
|
||||
self.commissioning_w0 = w0
|
||||
self.commissioning_L = L
|
||||
self.commissioning_admin_fabric = admin_fabric
|
||||
|
||||
if tasmota.wifi()['up'] || tasmota.eth()['up']
|
||||
self.mdns_announce_PASE()
|
||||
else
|
||||
tasmota.add_rule("Wifi#Connected", def ()
|
||||
self.mdns_announce_PASE()
|
||||
tasmota.remove_rule("Wifi#Connected", "mdns_announce_PASE")
|
||||
end, "mdns_announce_PASE")
|
||||
tasmota.add_rule("Eth#Connected", def ()
|
||||
self.mdns_announce_PASE()
|
||||
tasmota.remove_rule("Eth#Connected", "mdns_announce_PASE")
|
||||
end, "mdns_announce_PASE")
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Is root commissioning currently open. Mostly for UI to know if QRCode needs to be shown.
|
||||
def is_root_commissioning_open()
|
||||
return self.commissioning_open != nil && self.commissioning_admin_fabric == nil
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Stop PASE commissioning, mostly called when CASE is about to start
|
||||
def stop_basic_commissioning()
|
||||
var n = nil
|
||||
if self.is_root_commissioning_open()
|
||||
tasmota.publish_result('{"Matter":{"Commissioning":0}}', 'Matter')
|
||||
end
|
||||
self.commissioning_open = n
|
||||
|
||||
self.mdns_remove_PASE()
|
||||
|
||||
# clear any PBKDF information to free memory
|
||||
self.commissioning_iterations = n
|
||||
self.commissioning_discriminator = n
|
||||
self.commissioning_salt = n
|
||||
self.commissioning_w0 = n
|
||||
# self.commissioning_w1 = nil
|
||||
self.commissioning_L = n
|
||||
self.commissioning_admin_fabric = n
|
||||
end
|
||||
def is_commissioning_open()
|
||||
return self.commissioning_open != nil
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Compute QR Code content - can be done only for root PASE
|
||||
def compute_qrcode_content()
|
||||
var raw = bytes().resize(11) # we don't use TLV Data so it's only 88 bits or 11 bytes
|
||||
# version is `000` dont touch
|
||||
raw.setbits(3, 16, self.device.VENDOR_ID)
|
||||
raw.setbits(19, 16, self.device.PRODUCT_ID)
|
||||
# custom flow = 0 (offset=35, len=2)
|
||||
raw.setbits(37, 8, 0x04) # already on IP network
|
||||
raw.setbits(45, 12, self.device.root_discriminator & 0xFFF)
|
||||
raw.setbits(57, 27, self.device.root_passcode & 0x7FFFFFF)
|
||||
# padding (offset=84 len=4)
|
||||
return "MT:" + matter.Base38.encode(raw)
|
||||
end
|
||||
|
||||
|
||||
#############################################################
|
||||
# Compute the 11 digits manual pairing code (wihout vendorid nor productid) p.223
|
||||
# <BR>
|
||||
# can be done only for root PASE (we need the passcode, but we don't get it with OpenCommissioningWindow command)
|
||||
def compute_manual_pairing_code()
|
||||
var digit_1 = (self.device.root_discriminator & 0x0FFF) >> 10
|
||||
var digit_2_6 = ((self.device.root_discriminator & 0x0300) << 6) | (self.device.root_passcode & 0x3FFF)
|
||||
var digit_7_10 = (self.device.root_passcode >> 14)
|
||||
|
||||
var ret = format("%1i%05i%04i", digit_1, digit_2_6, digit_7_10)
|
||||
ret += matter.Verhoeff.checksum(ret)
|
||||
return ret
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start Operational Discovery for this session
|
||||
#
|
||||
# Deferred until next tick.
|
||||
def start_operational_discovery_deferred(fabric)
|
||||
# defer to next click
|
||||
tasmota.set_timer(0, /-> self.start_operational_discovery(fabric))
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start Commissioning Complete for this session
|
||||
#
|
||||
# Deferred until next tick.
|
||||
def start_commissioning_complete_deferred(session)
|
||||
# defer to next click
|
||||
tasmota.set_timer(0, /-> self.start_commissioning_complete(session))
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start Operational Discovery for this session
|
||||
#
|
||||
# Stop Basic Commissioning and clean PASE specific values (to save memory).
|
||||
# Announce fabric entry in mDNS.
|
||||
def start_operational_discovery(fabric)
|
||||
import crypto
|
||||
import mdns
|
||||
|
||||
self.stop_basic_commissioning() # close all PASE commissioning information
|
||||
|
||||
self.mdns_announce_op_discovery(fabric)
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Commissioning Complete
|
||||
#
|
||||
# Stop basic commissioning.
|
||||
def start_commissioning_complete(session)
|
||||
var fabric = session.get_fabric()
|
||||
var fabric_id = fabric.get_fabric_id().copy().reverse().tohex()
|
||||
var vendor_name = fabric.get_admin_vendor_name()
|
||||
log(f"MTR: --- Commissioning complete for Fabric '{fabric_id}' (Vendor {vendor_name}) ---", 2)
|
||||
self.stop_basic_commissioning() # by default close commissioning when it's complete
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# mDNS Configuration
|
||||
#############################################################
|
||||
# Start mDNS and announce hostnames for Wifi and ETH from MAC
|
||||
#
|
||||
# When the announce is active, `hostname_wifi` and `hostname_eth`
|
||||
# are defined
|
||||
def start_mdns_announce_hostnames()
|
||||
if tasmota.wifi()['up']
|
||||
self._mdns_announce_hostname(false)
|
||||
else
|
||||
tasmota.add_rule("Wifi#Connected", def ()
|
||||
self._mdns_announce_hostname(false)
|
||||
tasmota.remove_rule("Wifi#Connected", "matter_mdns_host")
|
||||
end, "matter_mdns_host")
|
||||
end
|
||||
|
||||
if tasmota.eth()['up']
|
||||
self._mdns_announce_hostname(true)
|
||||
else
|
||||
tasmota.add_rule("Eth#Connected", def ()
|
||||
self._mdns_announce_hostname(true)
|
||||
tasmota.remove_rule("Eth#Connected", "matter_mdns_host")
|
||||
end, "matter_mdns_host")
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start UDP mDNS announcements hostname
|
||||
# This announcement is independant from commissioning windows
|
||||
#
|
||||
# eth is `true` if ethernet turned up, `false` is wifi turned up
|
||||
def _mdns_announce_hostname(is_eth)
|
||||
import mdns
|
||||
import string
|
||||
|
||||
mdns.start()
|
||||
|
||||
try
|
||||
if is_eth
|
||||
# Add Hostname (based on MAC) with IPv4/IPv6 addresses
|
||||
var eth = tasmota.eth()
|
||||
self.hostname_eth = string.replace(eth.find("mac"), ':', '')
|
||||
if !self.device.ipv4only || !eth.contains('ip6local')
|
||||
# log(format("MTR: calling mdns.add_hostname(%s, %s, %s)", self.hostname_eth, eth.find('ip6local',''), eth.find('ip','')), 4)
|
||||
mdns.add_hostname(self.hostname_eth, eth.find('ip6local',''), eth.find('ip',''), eth.find('ip6',''))
|
||||
else
|
||||
log(format("MTR: calling mdns.add_hostname(%s, %s)", self.hostname_eth, eth.find('ip','')), 3)
|
||||
mdns.add_hostname(self.hostname_eth, eth.find('ip',''))
|
||||
end
|
||||
else
|
||||
var wifi = tasmota.wifi()
|
||||
self.hostname_wifi = string.replace(wifi.find("mac"), ':', '')
|
||||
if !self.device.ipv4only || !wifi.contains('ip6local')
|
||||
# log(format("MTR: calling mdns.add_hostname(%s, %s, %s)", self.hostname_wifi, wifi.find('ip6local',''), wifi.find('ip','')), 4)
|
||||
mdns.add_hostname(self.hostname_wifi, wifi.find('ip6local',''), wifi.find('ip',''), wifi.find('ip6',''))
|
||||
else
|
||||
log(format("MTR: calling mdns.add_hostname(%s, %s)", self.hostname_wifi, wifi.find('ip','')), 3)
|
||||
mdns.add_hostname(self.hostname_wifi, wifi.find('ip',''))
|
||||
end
|
||||
end
|
||||
log(format("MTR: start mDNS on %s host '%s.local'", is_eth ? "eth" : "wifi", is_eth ? self.hostname_eth : self.hostname_wifi), 3)
|
||||
except .. as e, m
|
||||
log("MTR: Exception" + str(e) + "|" + str(m), 2)
|
||||
end
|
||||
|
||||
self.mdns_announce_op_discovery_all_fabrics()
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Announce MDNS for PASE commissioning
|
||||
def mdns_announce_PASE()
|
||||
import mdns
|
||||
import crypto
|
||||
|
||||
var services = {
|
||||
"VP": f"{self.device.VENDOR_ID}+{self.device.PRODUCT_ID}",
|
||||
"D": self.commissioning_discriminator,
|
||||
"CM":1, # requires passcode
|
||||
"T":0, # no support for TCP
|
||||
"SII":5000, "SAI":300
|
||||
}
|
||||
|
||||
self.commissioning_instance_wifi = crypto.random(8).tohex() # 16 characters random hostname
|
||||
self.commissioning_instance_eth = crypto.random(8).tohex() # 16 characters random hostname
|
||||
|
||||
try
|
||||
if self.hostname_eth
|
||||
# Add Matter `_matterc._udp` service
|
||||
# log(format("MTR: calling mdns.add_service(%s, %s, %i, %s, %s, %s)", "_matterc", "_udp", 5540, str(services), self.commissioning_instance_eth, self.hostname_eth), 4)
|
||||
mdns.add_service("_matterc", "_udp", 5540, services, self.commissioning_instance_eth, self.hostname_eth)
|
||||
self.mdns_pase_eth = true
|
||||
|
||||
log(format("MTR: announce mDNS on %s '%s' ptr to `%s.local`", "eth", self.commissioning_instance_eth, self.hostname_eth), 2)
|
||||
|
||||
# `mdns.add_subtype(service:string, proto:string, instance:string, hostname:string, subtype:string) -> nil`
|
||||
var subtype = "_L" + str(self.commissioning_discriminator & 0xFFF)
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_eth, self.hostname_eth, subtype)
|
||||
subtype = "_S" + str((self.commissioning_discriminator & 0xF00) >> 8)
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_eth, self.hostname_eth, subtype)
|
||||
subtype = "_V" + str(self.device.VENDOR_ID)
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_eth, self.hostname_eth, subtype)
|
||||
subtype = "_CM1"
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_eth, self.hostname_eth, subtype)
|
||||
end
|
||||
if self.hostname_wifi
|
||||
|
||||
# log(format("MTR: calling mdns.add_service(%s, %s, %i, %s, %s, %s)", "_matterc", "_udp", 5540, str(services), self.commissioning_instance_wifi, self.hostname_wifi), 4)
|
||||
mdns.add_service("_matterc", "_udp", 5540, services, self.commissioning_instance_wifi, self.hostname_wifi)
|
||||
self.mdns_pase_wifi = true
|
||||
|
||||
log(format("MTR: starting mDNS on %s '%s' ptr to `%s.local`", "wifi", self.commissioning_instance_wifi, self.hostname_wifi), 3)
|
||||
|
||||
# `mdns.add_subtype(service:string, proto:string, instance:string, hostname:string, subtype:string) -> nil`
|
||||
var subtype = "_L" + str(self.commissioning_discriminator & 0xFFF)
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_wifi, self.hostname_wifi, subtype)
|
||||
subtype = "_S" + str((self.commissioning_discriminator & 0xF00) >> 8)
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_wifi, self.hostname_wifi, subtype)
|
||||
subtype = "_V" + str(self.device.VENDOR_ID)
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_wifi, self.hostname_wifi, subtype)
|
||||
subtype = "_CM1"
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_wifi, self.hostname_wifi, subtype)
|
||||
end
|
||||
except .. as e, m
|
||||
log("MTR: Exception" + str(e) + "|" + str(m), 2)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# MDNS remove any PASE announce
|
||||
def mdns_remove_PASE()
|
||||
import mdns
|
||||
|
||||
try
|
||||
if self.mdns_pase_eth
|
||||
log(format("MTR: calling mdns.remove_service(%s, %s, %s, %s)", "_matterc", "_udp", self.commissioning_instance_eth, self.hostname_eth), 3)
|
||||
log(format("MTR: remove mDNS on %s '%s'", "eth", self.commissioning_instance_eth), 3)
|
||||
self.mdns_pase_eth = false
|
||||
mdns.remove_service("_matterc", "_udp", self.commissioning_instance_eth, self.hostname_eth)
|
||||
end
|
||||
if self.mdns_pase_wifi
|
||||
log(format("MTR: calling mdns.remove_service(%s, %s, %s, %s)", "_matterc", "_udp", self.commissioning_instance_wifi, self.hostname_wifi), 3)
|
||||
log(format("MTR: remove mDNS on %s '%s'", "wifi", self.commissioning_instance_wifi), 3)
|
||||
self.mdns_pase_wifi = false
|
||||
mdns.remove_service("_matterc", "_udp", self.commissioning_instance_wifi, self.hostname_wifi)
|
||||
end
|
||||
except .. as e, m
|
||||
log("MTR: Exception" + str(e) + "|" + str(m), 2)
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start UDP mDNS announcements for commissioning for all persisted sessions
|
||||
def mdns_announce_op_discovery_all_fabrics()
|
||||
for fabric: self.device.sessions.active_fabrics()
|
||||
if fabric.get_device_id() && fabric.get_fabric_id()
|
||||
self.mdns_announce_op_discovery(fabric)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start UDP mDNS announcements for commissioning
|
||||
def mdns_announce_op_discovery(fabric)
|
||||
import mdns
|
||||
try
|
||||
var device_id = fabric.get_device_id().copy().reverse()
|
||||
var k_fabric = fabric.get_fabric_compressed()
|
||||
var op_node = k_fabric.tohex() + "-" + device_id.tohex()
|
||||
log("MTR: Operational Discovery node = " + op_node, 3)
|
||||
|
||||
# mdns
|
||||
if (tasmota.eth().find("up"))
|
||||
log(format("MTR: adding mDNS on %s '%s' ptr to `%s.local`", "eth", op_node, self.hostname_eth), 3)
|
||||
mdns.add_service("_matter","_tcp", 5540, nil, op_node, self.hostname_eth)
|
||||
var subtype = "_I" + k_fabric.tohex()
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matter", "_tcp", op_node, self.hostname_eth, subtype)
|
||||
end
|
||||
if (tasmota.wifi().find("up"))
|
||||
log(format("MTR: adding mDNS on %s '%s' ptr to `%s.local`", "wifi", op_node, self.hostname_wifi), 3)
|
||||
mdns.add_service("_matter","_tcp", 5540, nil, op_node, self.hostname_wifi)
|
||||
var subtype = "_I" + k_fabric.tohex()
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matter", "_tcp", op_node, self.hostname_wifi, subtype)
|
||||
end
|
||||
except .. as e, m
|
||||
log("MTR: Exception" + str(e) + "|" + str(m), 2)
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Remove all mDNS announces for all fabrics
|
||||
def mdns_remove_op_discovery_all_fabrics()
|
||||
for fabric: self.device.sessions.active_fabrics()
|
||||
if fabric.get_device_id() && fabric.get_fabric_id()
|
||||
self.mdns_remove_op_discovery(fabric)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Remove mDNS announce for fabric
|
||||
def mdns_remove_op_discovery(fabric)
|
||||
import mdns
|
||||
try
|
||||
var device_id = fabric.get_device_id().copy().reverse()
|
||||
var k_fabric = fabric.get_fabric_compressed()
|
||||
var op_node = k_fabric.tohex() + "-" + device_id.tohex()
|
||||
|
||||
# mdns
|
||||
if (tasmota.eth().find("up"))
|
||||
log(format("MTR: remove mDNS on %s '%s'", "eth", op_node), 3)
|
||||
mdns.remove_service("_matter", "_tcp", op_node, self.hostname_eth)
|
||||
end
|
||||
if (tasmota.wifi().find("up"))
|
||||
log(format("MTR: remove mDNS on %s '%s'", "wifi", op_node), 3)
|
||||
mdns.remove_service("_matter", "_tcp", op_node, self.hostname_wifi)
|
||||
end
|
||||
except .. as e, m
|
||||
log("MTR: Exception" + str(e) + "|" + str(m), 2)
|
||||
end
|
||||
end
|
||||
|
||||
#####################################################################
|
||||
# Generate random passcode
|
||||
#####################################################################
|
||||
static var PASSCODE_INVALID = [ 0, 11111111, 22222222, 33333333, 44444444, 55555555, 66666666, 77777777, 88888888, 99999999, 12345678, 87654321]
|
||||
def generate_random_passcode()
|
||||
import crypto
|
||||
var passcode
|
||||
while true
|
||||
passcode = crypto.random(4).get(0, 4) & 0x7FFFFFF
|
||||
if passcode > 0x5F5E0FE continue end # larger than allowed
|
||||
for inv: self.PASSCODE_INVALID
|
||||
if passcode == inv passcode = nil end
|
||||
end
|
||||
if passcode != nil return passcode end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
matter.Commissioning = Matter_Commissioning
|
|
@ -23,11 +23,9 @@ import matter
|
|||
|
||||
class Matter_Device
|
||||
static var UDP_PORT = 5540 # this is the default port for group multicast, we also use it for unicast
|
||||
static var PBKDF_ITERATIONS = 1000 # I don't see any reason to choose a different number
|
||||
static var VENDOR_ID = 0xFFF1
|
||||
static var PRODUCT_ID = 0x8000
|
||||
static var FILENAME = "_matter_device.json"
|
||||
static var PASE_TIMEOUT = 10*60 # default open commissioning window (10 minutes)
|
||||
var started # is the Matter Device started (configured, mDNS and UDPServer started)
|
||||
var plugins # list of plugins instances
|
||||
var plugins_persist # true if plugins configuration needs to be saved
|
||||
|
@ -37,27 +35,12 @@ class Matter_Device
|
|||
var udp_server # `matter.UDPServer()` object
|
||||
var profiler
|
||||
var message_handler # `matter.MessageHandler()` object
|
||||
var commissioning # `matter.Commissioning()` object
|
||||
var sessions # `matter.Session_Store()` objet
|
||||
var ui
|
||||
var tick # increment at each tick, avoids to repeat too frequently some actions
|
||||
# Events
|
||||
var events # Event handler
|
||||
# Commissioning open
|
||||
var commissioning_open # timestamp for timeout of commissioning (millis()) or `nil` if closed
|
||||
var commissioning_iterations # current PBKDF number of iterations
|
||||
var commissioning_discriminator # commissioning_discriminator
|
||||
var commissioning_salt # current salt
|
||||
var commissioning_w0 # current w0 (SPAKE2+)
|
||||
var commissioning_L # current L (SPAKE2+)
|
||||
var commissioning_admin_fabric # the fabric that opened the currint commissioning window, or `nil` for default
|
||||
# information about the device
|
||||
var commissioning_instance_wifi # random instance name for commissioning (mDNS)
|
||||
var commissioning_instance_eth # random instance name for commissioning (mDNS)
|
||||
var hostname_wifi # MAC-derived hostname for commissioning
|
||||
var hostname_eth # MAC-derived hostname for commissioning
|
||||
# mDNS active announces
|
||||
var mdns_pase_eth # do we have an active PASE mDNS announce for eth
|
||||
var mdns_pase_wifi # do we have an active PASE mDNS announce for wifi
|
||||
# for brige mode, list of HTTP_remote objects (only one instance per remote object)
|
||||
var http_remotes # map of 'domain:port' to `Matter_HTTP_remote` instance or `nil` if no bridges
|
||||
# saved in parameters
|
||||
|
@ -114,7 +97,7 @@ class Matter_Device
|
|||
end, "matter_start")
|
||||
end
|
||||
|
||||
self._init_basic_commissioning()
|
||||
self.commissioning = matter.Commissioning(self)
|
||||
|
||||
tasmota.add_driver(self)
|
||||
|
||||
|
@ -131,60 +114,18 @@ class Matter_Device
|
|||
|
||||
self._start_udp(self.UDP_PORT)
|
||||
|
||||
self.start_mdns_announce_hostnames()
|
||||
self.commissioning.start_mdns_announce_hostnames()
|
||||
|
||||
self.started = true
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start Basic Commissioning Window if needed at startup
|
||||
def _init_basic_commissioning()
|
||||
# if no fabric is configured, automatically open commissioning at restart
|
||||
if self.sessions.count_active_fabrics() == 0
|
||||
self.start_root_basic_commissioning()
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start Basic Commissioning with root parameters
|
||||
#
|
||||
# Open window for `timeout_s` (default 10 minutes)
|
||||
def start_root_basic_commissioning(timeout_s)
|
||||
if timeout_s == nil timeout_s = self.PASE_TIMEOUT end
|
||||
|
||||
# show Manual pairing code in logs
|
||||
var pairing_code = self.compute_manual_pairing_code()
|
||||
log(format("MTR: Manual pairing code: %s", pairing_code), 2)
|
||||
|
||||
# output MQTT
|
||||
var qr_code = self.compute_qrcode_content()
|
||||
tasmota.publish_result(format('{"Matter":{"Commissioning":1,"PairingCode":"%s","QRCode":"%s"}}', pairing_code, qr_code), 'Matter')
|
||||
|
||||
# compute PBKDF
|
||||
import crypto
|
||||
var root_salt = crypto.random(16)
|
||||
|
||||
# Compute the PBKDF parameters for SPAKE2+ from root parameters
|
||||
var passcode = bytes().add(self.root_passcode, 4)
|
||||
|
||||
var tv = crypto.PBKDF2_HMAC_SHA256().derive(passcode, root_salt, self.PBKDF_ITERATIONS, 80)
|
||||
var w0s = tv[0..39]
|
||||
var w1s = tv[40..79]
|
||||
|
||||
var root_w0 = crypto.EC_P256().mod(w0s)
|
||||
var w1 = crypto.EC_P256().mod(w1s) # w1 is temporarily computed then discarded
|
||||
# self.root_w1 = crypto.EC_P256().mod(w1s)
|
||||
var root_L = crypto.EC_P256().public_key(w1)
|
||||
self.start_basic_commissioning(timeout_s, self.PBKDF_ITERATIONS, self.root_discriminator, root_salt, root_w0, #-self.root_w1,-# root_L, nil)
|
||||
end
|
||||
|
||||
#####################################################################
|
||||
# Remove a fabric and clean all corresponding values and mDNS entries
|
||||
def remove_fabric(fabric)
|
||||
if fabric != nil
|
||||
log("MTR: removing fabric " + fabric.get_fabric_id().copy().reverse().tohex(), 2)
|
||||
self.message_handler.im.subs_shop.remove_by_fabric(fabric)
|
||||
self.mdns_remove_op_discovery(fabric)
|
||||
self.commissioning.mdns_remove_op_discovery(fabric)
|
||||
self.sessions.remove_fabric(fabric)
|
||||
end
|
||||
# var sub_fabrics = self.sessions.find_children_fabrics(fabric_parent.get_fabric_index())
|
||||
|
@ -201,91 +142,6 @@ class Matter_Device
|
|||
self.sessions.save_fabrics()
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start Basic Commissioning Window with custom parameters
|
||||
def start_basic_commissioning(timeout_s, iterations, discriminator, salt, w0, L, admin_fabric)
|
||||
self.commissioning_open = tasmota.millis() + timeout_s * 1000
|
||||
self.commissioning_iterations = iterations
|
||||
self.commissioning_discriminator = discriminator
|
||||
self.commissioning_salt = salt
|
||||
self.commissioning_w0 = w0
|
||||
self.commissioning_L = L
|
||||
self.commissioning_admin_fabric = admin_fabric
|
||||
|
||||
if tasmota.wifi()['up'] || tasmota.eth()['up']
|
||||
self.mdns_announce_PASE()
|
||||
else
|
||||
tasmota.add_rule("Wifi#Connected", def ()
|
||||
self.mdns_announce_PASE()
|
||||
tasmota.remove_rule("Wifi#Connected", "mdns_announce_PASE")
|
||||
end, "mdns_announce_PASE")
|
||||
tasmota.add_rule("Eth#Connected", def ()
|
||||
self.mdns_announce_PASE()
|
||||
tasmota.remove_rule("Eth#Connected", "mdns_announce_PASE")
|
||||
end, "mdns_announce_PASE")
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Is root commissioning currently open. Mostly for UI to know if QRCode needs to be shown.
|
||||
def is_root_commissioning_open()
|
||||
return self.commissioning_open != nil && self.commissioning_admin_fabric == nil
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Stop PASE commissioning, mostly called when CASE is about to start
|
||||
def stop_basic_commissioning()
|
||||
var n = nil
|
||||
if self.is_root_commissioning_open()
|
||||
tasmota.publish_result('{"Matter":{"Commissioning":0}}', 'Matter')
|
||||
end
|
||||
self.commissioning_open = n
|
||||
|
||||
self.mdns_remove_PASE()
|
||||
|
||||
# clear any PBKDF information to free memory
|
||||
self.commissioning_iterations = n
|
||||
self.commissioning_discriminator = n
|
||||
self.commissioning_salt = n
|
||||
self.commissioning_w0 = n
|
||||
# self.commissioning_w1 = nil
|
||||
self.commissioning_L = n
|
||||
self.commissioning_admin_fabric = n
|
||||
end
|
||||
def is_commissioning_open()
|
||||
return self.commissioning_open != nil
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Compute QR Code content - can be done only for root PASE
|
||||
def compute_qrcode_content()
|
||||
var raw = bytes().resize(11) # we don't use TLV Data so it's only 88 bits or 11 bytes
|
||||
# version is `000` dont touch
|
||||
raw.setbits(3, 16, self.VENDOR_ID)
|
||||
raw.setbits(19, 16, self.PRODUCT_ID)
|
||||
# custom flow = 0 (offset=35, len=2)
|
||||
raw.setbits(37, 8, 0x04) # already on IP network
|
||||
raw.setbits(45, 12, self.root_discriminator & 0xFFF)
|
||||
raw.setbits(57, 27, self.root_passcode & 0x7FFFFFF)
|
||||
# padding (offset=84 len=4)
|
||||
return "MT:" + matter.Base38.encode(raw)
|
||||
end
|
||||
|
||||
|
||||
#############################################################
|
||||
# Compute the 11 digits manual pairing code (wihout vendorid nor productid) p.223
|
||||
# <BR>
|
||||
# can be done only for root PASE (we need the passcode, but we don't get it with OpenCommissioningWindow command)
|
||||
def compute_manual_pairing_code()
|
||||
var digit_1 = (self.root_discriminator & 0x0FFF) >> 10
|
||||
var digit_2_6 = ((self.root_discriminator & 0x0300) << 6) | (self.root_passcode & 0x3FFF)
|
||||
var digit_7_10 = (self.root_passcode >> 14)
|
||||
|
||||
var ret = format("%1i%05i%04i", digit_1, digit_2_6, digit_7_10)
|
||||
ret += matter.Verhoeff.checksum(ret)
|
||||
return ret
|
||||
end
|
||||
|
||||
#####################################################################
|
||||
# Driver handling of buttons
|
||||
#####################################################################
|
||||
|
@ -330,9 +186,7 @@ class Matter_Device
|
|||
self.sessions.every_second()
|
||||
self.message_handler.every_second()
|
||||
self.events.every_second() # periodically remove bytes() representation of events
|
||||
if self.commissioning_open != nil && tasmota.time_reached(self.commissioning_open) # timeout reached, close provisioning
|
||||
self.commissioning_open = nil
|
||||
end
|
||||
self.commissioning.every_second()
|
||||
end
|
||||
|
||||
#############################################################
|
||||
|
@ -440,50 +294,6 @@ class Matter_Device
|
|||
self.udp_server.start(/ raw, addr, port -> self.msg_received(raw, addr, port))
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start Operational Discovery for this session
|
||||
#
|
||||
# Deferred until next tick.
|
||||
def start_operational_discovery_deferred(fabric)
|
||||
# defer to next click
|
||||
tasmota.set_timer(0, /-> self.start_operational_discovery(fabric))
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start Commissioning Complete for this session
|
||||
#
|
||||
# Deferred until next tick.
|
||||
def start_commissioning_complete_deferred(session)
|
||||
# defer to next click
|
||||
tasmota.set_timer(0, /-> self.start_commissioning_complete(session))
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start Operational Discovery for this session
|
||||
#
|
||||
# Stop Basic Commissioning and clean PASE specific values (to save memory).
|
||||
# Announce fabric entry in mDNS.
|
||||
def start_operational_discovery(fabric)
|
||||
import crypto
|
||||
import mdns
|
||||
|
||||
self.stop_basic_commissioning() # close all PASE commissioning information
|
||||
|
||||
self.mdns_announce_op_discovery(fabric)
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Commissioning Complete
|
||||
#
|
||||
# Stop basic commissioning.
|
||||
def start_commissioning_complete(session)
|
||||
var fabric = session.get_fabric()
|
||||
var fabric_id = fabric.get_fabric_id().copy().reverse().tohex()
|
||||
var vendor_name = fabric.get_admin_vendor_name()
|
||||
log(f"MTR: --- Commissioning complete for Fabric '{fabric_id}' (Vendor {vendor_name}) ---", 2)
|
||||
self.stop_basic_commissioning() # by default close commissioning when it's complete
|
||||
end
|
||||
|
||||
|
||||
#################################################################################
|
||||
# Simple insertion sort - sorts the list in place, and returns the list
|
||||
|
@ -734,7 +544,7 @@ class Matter_Device
|
|||
dirty = true
|
||||
end
|
||||
if self.root_passcode == nil
|
||||
self.root_passcode = self.generate_random_passcode()
|
||||
self.root_passcode = self.commissioning.generate_random_passcode()
|
||||
dirty = true
|
||||
end
|
||||
if dirty self.save_param() end
|
||||
|
@ -814,239 +624,6 @@ class Matter_Device
|
|||
ctx.status = matter.UNSUPPORTED_ENDPOINT
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# mDNS Configuration
|
||||
#############################################################
|
||||
# Start mDNS and announce hostnames for Wifi and ETH from MAC
|
||||
#
|
||||
# When the announce is active, `hostname_wifi` and `hostname_eth`
|
||||
# are defined
|
||||
def start_mdns_announce_hostnames()
|
||||
if tasmota.wifi()['up']
|
||||
self._mdns_announce_hostname(false)
|
||||
else
|
||||
tasmota.add_rule("Wifi#Connected", def ()
|
||||
self._mdns_announce_hostname(false)
|
||||
tasmota.remove_rule("Wifi#Connected", "matter_mdns_host")
|
||||
end, "matter_mdns_host")
|
||||
end
|
||||
|
||||
if tasmota.eth()['up']
|
||||
self._mdns_announce_hostname(true)
|
||||
else
|
||||
tasmota.add_rule("Eth#Connected", def ()
|
||||
self._mdns_announce_hostname(true)
|
||||
tasmota.remove_rule("Eth#Connected", "matter_mdns_host")
|
||||
end, "matter_mdns_host")
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start UDP mDNS announcements hostname
|
||||
# This announcement is independant from commissioning windows
|
||||
#
|
||||
# eth is `true` if ethernet turned up, `false` is wifi turned up
|
||||
def _mdns_announce_hostname(is_eth)
|
||||
import mdns
|
||||
import string
|
||||
|
||||
mdns.start()
|
||||
|
||||
try
|
||||
if is_eth
|
||||
# Add Hostname (based on MAC) with IPv4/IPv6 addresses
|
||||
var eth = tasmota.eth()
|
||||
self.hostname_eth = string.replace(eth.find("mac"), ':', '')
|
||||
if !self.ipv4only || !eth.contains('ip6local')
|
||||
# log(format("MTR: calling mdns.add_hostname(%s, %s, %s)", self.hostname_eth, eth.find('ip6local',''), eth.find('ip','')), 4)
|
||||
mdns.add_hostname(self.hostname_eth, eth.find('ip6local',''), eth.find('ip',''), eth.find('ip6',''))
|
||||
else
|
||||
log(format("MTR: calling mdns.add_hostname(%s, %s)", self.hostname_eth, eth.find('ip','')), 3)
|
||||
mdns.add_hostname(self.hostname_eth, eth.find('ip',''))
|
||||
end
|
||||
else
|
||||
var wifi = tasmota.wifi()
|
||||
self.hostname_wifi = string.replace(wifi.find("mac"), ':', '')
|
||||
if !self.ipv4only || !wifi.contains('ip6local')
|
||||
# log(format("MTR: calling mdns.add_hostname(%s, %s, %s)", self.hostname_wifi, wifi.find('ip6local',''), wifi.find('ip','')), 4)
|
||||
mdns.add_hostname(self.hostname_wifi, wifi.find('ip6local',''), wifi.find('ip',''), wifi.find('ip6',''))
|
||||
else
|
||||
log(format("MTR: calling mdns.add_hostname(%s, %s)", self.hostname_wifi, wifi.find('ip','')), 3)
|
||||
mdns.add_hostname(self.hostname_wifi, wifi.find('ip',''))
|
||||
end
|
||||
end
|
||||
log(format("MTR: start mDNS on %s host '%s.local'", is_eth ? "eth" : "wifi", is_eth ? self.hostname_eth : self.hostname_wifi), 3)
|
||||
except .. as e, m
|
||||
log("MTR: Exception" + str(e) + "|" + str(m), 2)
|
||||
end
|
||||
|
||||
self.mdns_announce_op_discovery_all_fabrics()
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Announce MDNS for PASE commissioning
|
||||
def mdns_announce_PASE()
|
||||
import mdns
|
||||
import crypto
|
||||
|
||||
var services = {
|
||||
"VP": f"{self.VENDOR_ID}+{self.PRODUCT_ID}",
|
||||
"D": self.commissioning_discriminator,
|
||||
"CM":1, # requires passcode
|
||||
"T":0, # no support for TCP
|
||||
"SII":5000, "SAI":300
|
||||
}
|
||||
|
||||
self.commissioning_instance_wifi = crypto.random(8).tohex() # 16 characters random hostname
|
||||
self.commissioning_instance_eth = crypto.random(8).tohex() # 16 characters random hostname
|
||||
|
||||
try
|
||||
if self.hostname_eth
|
||||
# Add Matter `_matterc._udp` service
|
||||
# log(format("MTR: calling mdns.add_service(%s, %s, %i, %s, %s, %s)", "_matterc", "_udp", 5540, str(services), self.commissioning_instance_eth, self.hostname_eth), 4)
|
||||
mdns.add_service("_matterc", "_udp", 5540, services, self.commissioning_instance_eth, self.hostname_eth)
|
||||
self.mdns_pase_eth = true
|
||||
|
||||
log(format("MTR: announce mDNS on %s '%s' ptr to `%s.local`", "eth", self.commissioning_instance_eth, self.hostname_eth), 2)
|
||||
|
||||
# `mdns.add_subtype(service:string, proto:string, instance:string, hostname:string, subtype:string) -> nil`
|
||||
var subtype = "_L" + str(self.commissioning_discriminator & 0xFFF)
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_eth, self.hostname_eth, subtype)
|
||||
subtype = "_S" + str((self.commissioning_discriminator & 0xF00) >> 8)
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_eth, self.hostname_eth, subtype)
|
||||
subtype = "_V" + str(self.VENDOR_ID)
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_eth, self.hostname_eth, subtype)
|
||||
subtype = "_CM1"
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_eth, self.hostname_eth, subtype)
|
||||
end
|
||||
if self.hostname_wifi
|
||||
|
||||
# log(format("MTR: calling mdns.add_service(%s, %s, %i, %s, %s, %s)", "_matterc", "_udp", 5540, str(services), self.commissioning_instance_wifi, self.hostname_wifi), 4)
|
||||
mdns.add_service("_matterc", "_udp", 5540, services, self.commissioning_instance_wifi, self.hostname_wifi)
|
||||
self.mdns_pase_wifi = true
|
||||
|
||||
log(format("MTR: starting mDNS on %s '%s' ptr to `%s.local`", "wifi", self.commissioning_instance_wifi, self.hostname_wifi), 3)
|
||||
|
||||
# `mdns.add_subtype(service:string, proto:string, instance:string, hostname:string, subtype:string) -> nil`
|
||||
var subtype = "_L" + str(self.commissioning_discriminator & 0xFFF)
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_wifi, self.hostname_wifi, subtype)
|
||||
subtype = "_S" + str((self.commissioning_discriminator & 0xF00) >> 8)
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_wifi, self.hostname_wifi, subtype)
|
||||
subtype = "_V" + str(self.VENDOR_ID)
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_wifi, self.hostname_wifi, subtype)
|
||||
subtype = "_CM1"
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matterc", "_udp", self.commissioning_instance_wifi, self.hostname_wifi, subtype)
|
||||
end
|
||||
except .. as e, m
|
||||
log("MTR: Exception" + str(e) + "|" + str(m), 2)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# MDNS remove any PASE announce
|
||||
def mdns_remove_PASE()
|
||||
import mdns
|
||||
|
||||
try
|
||||
if self.mdns_pase_eth
|
||||
log(format("MTR: calling mdns.remove_service(%s, %s, %s, %s)", "_matterc", "_udp", self.commissioning_instance_eth, self.hostname_eth), 3)
|
||||
log(format("MTR: remove mDNS on %s '%s'", "eth", self.commissioning_instance_eth), 3)
|
||||
self.mdns_pase_eth = false
|
||||
mdns.remove_service("_matterc", "_udp", self.commissioning_instance_eth, self.hostname_eth)
|
||||
end
|
||||
if self.mdns_pase_wifi
|
||||
log(format("MTR: calling mdns.remove_service(%s, %s, %s, %s)", "_matterc", "_udp", self.commissioning_instance_wifi, self.hostname_wifi), 3)
|
||||
log(format("MTR: remove mDNS on %s '%s'", "wifi", self.commissioning_instance_wifi), 3)
|
||||
self.mdns_pase_wifi = false
|
||||
mdns.remove_service("_matterc", "_udp", self.commissioning_instance_wifi, self.hostname_wifi)
|
||||
end
|
||||
except .. as e, m
|
||||
log("MTR: Exception" + str(e) + "|" + str(m), 2)
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start UDP mDNS announcements for commissioning for all persisted sessions
|
||||
def mdns_announce_op_discovery_all_fabrics()
|
||||
for fabric: self.sessions.active_fabrics()
|
||||
if fabric.get_device_id() && fabric.get_fabric_id()
|
||||
self.mdns_announce_op_discovery(fabric)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Start UDP mDNS announcements for commissioning
|
||||
def mdns_announce_op_discovery(fabric)
|
||||
import mdns
|
||||
try
|
||||
var device_id = fabric.get_device_id().copy().reverse()
|
||||
var k_fabric = fabric.get_fabric_compressed()
|
||||
var op_node = k_fabric.tohex() + "-" + device_id.tohex()
|
||||
log("MTR: Operational Discovery node = " + op_node, 3)
|
||||
|
||||
# mdns
|
||||
if (tasmota.eth().find("up"))
|
||||
log(format("MTR: adding mDNS on %s '%s' ptr to `%s.local`", "eth", op_node, self.hostname_eth), 3)
|
||||
mdns.add_service("_matter","_tcp", 5540, nil, op_node, self.hostname_eth)
|
||||
var subtype = "_I" + k_fabric.tohex()
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matter", "_tcp", op_node, self.hostname_eth, subtype)
|
||||
end
|
||||
if (tasmota.wifi().find("up"))
|
||||
log(format("MTR: adding mDNS on %s '%s' ptr to `%s.local`", "wifi", op_node, self.hostname_wifi), 3)
|
||||
mdns.add_service("_matter","_tcp", 5540, nil, op_node, self.hostname_wifi)
|
||||
var subtype = "_I" + k_fabric.tohex()
|
||||
log("MTR: adding subtype: "+subtype, 3)
|
||||
mdns.add_subtype("_matter", "_tcp", op_node, self.hostname_wifi, subtype)
|
||||
end
|
||||
except .. as e, m
|
||||
log("MTR: Exception" + str(e) + "|" + str(m), 2)
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Remove all mDNS announces for all fabrics
|
||||
def mdns_remove_op_discovery_all_fabrics()
|
||||
for fabric: self.sessions.active_fabrics()
|
||||
if fabric.get_device_id() && fabric.get_fabric_id()
|
||||
self.mdns_remove_op_discovery(fabric)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Remove mDNS announce for fabric
|
||||
def mdns_remove_op_discovery(fabric)
|
||||
import mdns
|
||||
try
|
||||
var device_id = fabric.get_device_id().copy().reverse()
|
||||
var k_fabric = fabric.get_fabric_compressed()
|
||||
var op_node = k_fabric.tohex() + "-" + device_id.tohex()
|
||||
|
||||
# mdns
|
||||
if (tasmota.eth().find("up"))
|
||||
log(format("MTR: remove mDNS on %s '%s'", "eth", op_node), 3)
|
||||
mdns.remove_service("_matter", "_tcp", op_node, self.hostname_eth)
|
||||
end
|
||||
if (tasmota.wifi().find("up"))
|
||||
log(format("MTR: remove mDNS on %s '%s'", "wifi", op_node), 3)
|
||||
mdns.remove_service("_matter", "_tcp", op_node, self.hostname_wifi)
|
||||
end
|
||||
except .. as e, m
|
||||
log("MTR: Exception" + str(e) + "|" + str(m), 2)
|
||||
end
|
||||
end
|
||||
|
||||
#############################################################
|
||||
# Try to clean MDNS entries before restart.
|
||||
#
|
||||
|
@ -1408,23 +985,6 @@ class Matter_Device
|
|||
end
|
||||
end
|
||||
|
||||
#####################################################################
|
||||
# Generate random passcode
|
||||
#####################################################################
|
||||
static var PASSCODE_INVALID = [ 0, 11111111, 22222222, 33333333, 44444444, 55555555, 66666666, 77777777, 88888888, 99999999, 12345678, 87654321]
|
||||
def generate_random_passcode()
|
||||
import crypto
|
||||
var passcode
|
||||
while true
|
||||
passcode = crypto.random(4).get(0, 4) & 0x7FFFFFF
|
||||
if passcode > 0x5F5E0FE continue end # larger than allowed
|
||||
for inv: self.PASSCODE_INVALID
|
||||
if passcode == inv passcode = nil end
|
||||
end
|
||||
if passcode != nil return passcode end
|
||||
end
|
||||
end
|
||||
|
||||
#####################################################################
|
||||
# Manager HTTP remotes
|
||||
#####################################################################
|
||||
|
@ -1523,7 +1083,7 @@ class Matter_Device
|
|||
def MtrJoin(cmd_found, idx, payload, payload_json)
|
||||
var payload_int = int(payload)
|
||||
if payload_int
|
||||
self.start_root_basic_commissioning()
|
||||
self.commissioning.start_root_basic_commissioning()
|
||||
else
|
||||
self.stop_basic_commissioning()
|
||||
end
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue