369 lines
12 KiB
Plaintext
369 lines
12 KiB
Plaintext
#
|
|
# Matter_IM_Message.be - suppport for Matter Interaction Model messages responses
|
|
#
|
|
# 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_Path,weak
|
|
#@ solidify:Matter_IM_Message,weak
|
|
#@ solidify:Matter_IM_Status,weak
|
|
#@ solidify:Matter_IM_InvokeResponse,weak
|
|
#@ solidify:Matter_IM_WriteResponse,weak
|
|
#@ solidify:Matter_IM_ReportData,weak
|
|
#@ solidify:Matter_IM_ReportDataSubscribed,weak
|
|
#@ solidify:Matter_IM_SubscribeResponse,weak
|
|
|
|
#################################################################################
|
|
# Matter_Path
|
|
#
|
|
# Used to store all the elements of the reponse to an attribute or command
|
|
#################################################################################
|
|
class Matter_Path
|
|
var endpoint # endpoint or `nil` if expansion
|
|
var cluster # cluster or `nil` if expansion
|
|
var attribute # attribute or `nil` if expansion
|
|
var command # command
|
|
var status # status to be returned (matter.SUCCESS or matter.<ERROR>)
|
|
|
|
def tostring()
|
|
try
|
|
import string
|
|
var s = ""
|
|
s += (self.endpoint != nil ? string.format("[%02X]", self.endpoint) : "[**]")
|
|
s += (self.cluster != nil ? string.format("%04X/", self.cluster) : "****/")
|
|
s += (self.attribute != nil ? string.format("%04X", self.attribute) : "")
|
|
s += (self.command != nil ? string.format("%04X", self.command) : "")
|
|
if self.attribute == nil && self.command == nil s += "****" end
|
|
return s
|
|
except .. as e, m
|
|
return "Exception> " + str(e) + ", " + str(m)
|
|
end
|
|
end
|
|
|
|
end
|
|
matter.Path = Matter_Path
|
|
|
|
#################################################################################
|
|
# Matter_IM_Message
|
|
#
|
|
# Superclass for all IM responses
|
|
#################################################################################
|
|
class Matter_IM_Message
|
|
static var MSG_TIMEOUT = 10000 # 10s
|
|
var expiration # expiration time for the reporting
|
|
var resp # response Frame object
|
|
var ready # bool: ready to send (true) or wait (false)
|
|
var data # TLV data of the response (if any)
|
|
|
|
# build a response message stub
|
|
def init(msg, opcode, reliable)
|
|
self.resp = msg.build_response(opcode, reliable)
|
|
self.ready = true
|
|
self.expiration = tasmota.millis() + self.MSG_TIMEOUT
|
|
end
|
|
|
|
# the message is being removed due to expiration
|
|
def reached_timeout()
|
|
end
|
|
|
|
# ack received for previous message, proceed to next (if any)
|
|
# return true if we manage the ack ourselves, false if it needs to be done upper
|
|
def ack_received(msg)
|
|
self.expiration = tasmota.millis() + self.MSG_TIMEOUT # give more time
|
|
return false
|
|
end
|
|
|
|
# Status Report OK received for previous message, proceed to next (if any)
|
|
# return true if we manage the ack ourselves, false if it needs to be done upper
|
|
def status_ok_received(msg)
|
|
self.expiration = tasmota.millis() + self.MSG_TIMEOUT # give more time
|
|
if msg
|
|
self.resp = msg.build_response(self.resp.opcode, self.resp.x_flag_r, self.resp) # update packet
|
|
end
|
|
self.ready = true
|
|
return true
|
|
end
|
|
|
|
# we received an ACK error, do any necessary cleaning
|
|
def status_error_received(msg)
|
|
end
|
|
|
|
# get the exchange-id for this message
|
|
def get_exchangeid()
|
|
return self.resp.exchange_id
|
|
end
|
|
|
|
# return true if transaction is complete (remove object from queue)
|
|
# default responder for data
|
|
def send(responder)
|
|
var resp = self.resp
|
|
resp.encode(self.data.to_TLV().encode()) # payload in cleartext
|
|
resp.encrypt()
|
|
responder.send_response(resp.raw, resp.remote_ip, resp.remote_port, resp.message_counter)
|
|
return true
|
|
end
|
|
|
|
end
|
|
matter.IM_Message = Matter_IM_Message
|
|
|
|
#################################################################################
|
|
# Matter_IM_Status
|
|
#
|
|
# Send a simple Status Message
|
|
#################################################################################
|
|
class Matter_IM_Status : Matter_IM_Message
|
|
|
|
def init(msg, status)
|
|
super(self).init(msg, 0x01 #-Status Response-#, true #-reliable-#)
|
|
var sr = matter.StatusResponseMessage()
|
|
sr.status = status
|
|
self.data = sr
|
|
self.ready = true # send immediately
|
|
end
|
|
end
|
|
matter.IM_Status = Matter_IM_Status
|
|
|
|
#################################################################################
|
|
# Matter_IM_InvokeResponse
|
|
#
|
|
# Send a simple Status Message
|
|
#################################################################################
|
|
class Matter_IM_InvokeResponse : Matter_IM_Message
|
|
|
|
def init(msg, data)
|
|
super(self).init(msg, 0x09 #-Invoke Response-#, true)
|
|
self.data = data
|
|
self.ready = true # send immediately
|
|
end
|
|
end
|
|
matter.IM_InvokeResponse = Matter_IM_InvokeResponse
|
|
|
|
#################################################################################
|
|
# Matter_IM_WriteResponse
|
|
#
|
|
# Send a simple Status Message
|
|
#################################################################################
|
|
class Matter_IM_WriteResponse : Matter_IM_Message
|
|
|
|
def init(msg, data)
|
|
super(self).init(msg, 0x07 #-Write Response-#, true)
|
|
self.data = data
|
|
self.ready = true # send immediately
|
|
end
|
|
|
|
end
|
|
matter.IM_WriteResponse = Matter_IM_WriteResponse
|
|
|
|
#################################################################################
|
|
# Matter_IM_ReportData
|
|
#
|
|
# Report Data for a Read Request
|
|
#################################################################################
|
|
class Matter_IM_ReportData : Matter_IM_Message
|
|
static var MAX_MESSAGE = 1200 # max bytes size for a single TLV worklaod
|
|
|
|
def init(msg, data)
|
|
super(self).init(msg, 0x05 #-Report Data-#, true)
|
|
self.data = data
|
|
self.ready = true # send immediately
|
|
end
|
|
|
|
# return true if transaction is complete (remove object from queue)
|
|
# default responder for data
|
|
def send(responder)
|
|
import string
|
|
var resp = self.resp
|
|
var ret = self.data
|
|
var was_chunked = ret.more_chunked_messages # is this following a chunked packet?
|
|
|
|
# compute the acceptable size
|
|
var msg_sz = 0
|
|
var elements = 0
|
|
if size(ret.attribute_reports) > 0
|
|
msg_sz = ret.attribute_reports[0].to_TLV().encode_len()
|
|
elements = 1
|
|
end
|
|
while msg_sz < self.MAX_MESSAGE && elements < size(ret.attribute_reports)
|
|
var next_sz = ret.attribute_reports[elements].to_TLV().encode_len()
|
|
if msg_sz + next_sz < self.MAX_MESSAGE
|
|
msg_sz += next_sz
|
|
elements += 1
|
|
else
|
|
break
|
|
end
|
|
end
|
|
|
|
tasmota.log(string.format("MTR: elements=%i msg_sz=%i total=%i", elements, msg_sz, size(ret.attribute_reports)), 4)
|
|
var next_elemnts = ret.attribute_reports[elements .. ]
|
|
ret.attribute_reports = ret.attribute_reports[0 .. elements - 1]
|
|
ret.more_chunked_messages = (size(next_elemnts) > 0)
|
|
|
|
if was_chunked
|
|
tasmota.log(string.format("MTR: Read_Attr next_chunk exch=%i", self.get_exchangeid()), 3)
|
|
end
|
|
if ret.more_chunked_messages
|
|
if !was_chunked
|
|
tasmota.log(string.format("MTR: Read_Attr first_chunk exch=%i", self.get_exchangeid()), 3)
|
|
end
|
|
tasmota.log("MTR: sending TLV" + str(ret), 3)
|
|
end
|
|
|
|
resp.encode(self.data.to_TLV().encode()) # payload in cleartext
|
|
resp.encrypt()
|
|
responder.send_response(resp.raw, resp.remote_ip, resp.remote_port, resp.message_counter)
|
|
|
|
if size(next_elemnts) > 0
|
|
ret.attribute_reports = next_elemnts
|
|
tasmota.log("MTR: to_be_sent_later TLV" + str(ret), 3)
|
|
return false # keep alive
|
|
else
|
|
return true # finished, remove
|
|
end
|
|
end
|
|
|
|
end
|
|
matter.IM_ReportData = Matter_IM_ReportData
|
|
|
|
|
|
#################################################################################
|
|
# Matter_IM_ReportDataSubscribed
|
|
#
|
|
# Main difference is that we are the spontaneous inititor
|
|
#################################################################################
|
|
class Matter_IM_ReportDataSubscribed : Matter_IM_ReportData
|
|
var sub # subscription object
|
|
var report_data_phase # true during reportdata
|
|
|
|
def init(message_handler, session, data, sub)
|
|
self.resp = matter.Frame.initiate_response(message_handler, session, 0x05 #-Report Data-#, true)
|
|
self.data = data
|
|
self.ready = true # send immediately
|
|
self.expiration = tasmota.millis() + self.MSG_TIMEOUT
|
|
#
|
|
self.sub = sub
|
|
self.report_data_phase = true
|
|
end
|
|
|
|
def reached_timeout()
|
|
self.sub.remove_self()
|
|
end
|
|
|
|
# ack received, confirm the heartbeat
|
|
def ack_received(msg)
|
|
super(self).ack_received(msg)
|
|
if !self.report_data_phase
|
|
# if ack is received while all data is sent, means that it finished without error
|
|
self.ready = true
|
|
return true # proceed to calling send() which removes the message
|
|
else
|
|
return false # do nothing
|
|
end
|
|
end
|
|
|
|
# we received an ACK error, remove subscription
|
|
def status_error_received(msg)
|
|
self.sub.remove_self()
|
|
end
|
|
|
|
# ack received for previous message, proceed to next (if any)
|
|
# return true if we manage the ack ourselves, false if it needs to be done upper
|
|
def status_ok_received(msg)
|
|
if self.report_data_phase
|
|
return super(self).status_ok_received(msg)
|
|
else
|
|
super(self).status_ok_received(nil)
|
|
return false # let the caller to the ack
|
|
end
|
|
end
|
|
|
|
# returns true if transaction is complete (remove object from queue)
|
|
# default responder for data
|
|
def send(responder)
|
|
if size(self.data.attribute_reports) > 0
|
|
if self.report_data_phase
|
|
var ret = super(self).send(responder)
|
|
if !ret return false end # ReportData needs to continue
|
|
# ReportData is finished
|
|
self.report_data_phase = false
|
|
return false
|
|
else
|
|
# send a simple ACK
|
|
var resp = self.resp.build_standalone_ack()
|
|
resp.encode()
|
|
resp.encrypt()
|
|
responder.send_response(resp.raw, resp.remote_ip, resp.remote_port, resp.message_counter)
|
|
return true # we received a ack(), just finish
|
|
end
|
|
|
|
else
|
|
# simple heartbeat ReportData
|
|
if self.report_data_phase
|
|
super(self).send(responder)
|
|
self.report_data_phase = false
|
|
return false # don't expect any response
|
|
else
|
|
return true # we're done, remove message
|
|
end
|
|
end
|
|
end
|
|
end
|
|
matter.IM_ReportDataSubscribed = Matter_IM_ReportDataSubscribed
|
|
|
|
#################################################################################
|
|
# Matter_IM_SubscribeResponse
|
|
#
|
|
# Report Data for a Read Request
|
|
#################################################################################
|
|
class Matter_IM_SubscribeResponse : Matter_IM_ReportData
|
|
var sub # subscription object
|
|
var report_data_phase # true during reportdata
|
|
|
|
def init(msg, data, sub)
|
|
super(self).init(msg, data)
|
|
self.sub = sub
|
|
self.report_data_phase = true
|
|
end
|
|
|
|
# return true if transaction is complete (remove object from queue)
|
|
# default responder for data
|
|
def send(responder)
|
|
if self.report_data_phase
|
|
var ret = super(self).send(responder)
|
|
if !ret return false end # ReportData needs to continue
|
|
# ReportData is finished
|
|
self.report_data_phase = false
|
|
return false
|
|
|
|
else
|
|
# send the final SubscribeReponse
|
|
var resp = self.resp
|
|
var sr = matter.SubscribeResponseMessage()
|
|
sr.subscription_id = self.sub.subscription_id
|
|
sr.max_interval = self.sub.max_interval
|
|
|
|
self.resp.opcode = 0x04 #- Subscribe Response -#
|
|
resp.encode(sr.to_TLV().encode()) # payload in cleartext
|
|
resp.encrypt()
|
|
responder.send_response(resp.raw, resp.remote_ip, resp.remote_port, resp.message_counter)
|
|
return true
|
|
end
|
|
end
|
|
|
|
end
|
|
matter.IM_SubscribeResponse = Matter_IM_SubscribeResponse
|