Skip to content

Commit

Permalink
Matter POC for remote Relay (#18575)
Browse files Browse the repository at this point in the history
  • Loading branch information
s-hadinger committed May 3, 2023
1 parent 9097f50 commit c26ec44
Show file tree
Hide file tree
Showing 27 changed files with 2,760 additions and 1,086 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
- Matter support for Shutters (without Tilt) (#18509)
- Support for TC74 temperature sensor by Michael Loftis (#18042)
- Matter support for Shutters with Tilt
- Matter POC for remote Relay

### Breaking Changed

Expand Down
4 changes: 4 additions & 0 deletions lib/libesp32/berry_matter/src/be_matter_module.c
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ extern const bclass be_class_Matter_TLV; // need to declare it upfront because
#include "solidify/solidified_Matter_Plugin_Sensor_Temp.h"
#include "solidify/solidified_Matter_Plugin_Sensor_Illuminance.h"
#include "solidify/solidified_Matter_Plugin_Sensor_Humidity.h"
#include "solidify/solidified_Matter_Plugin_Bridge_HTTP.h"
#include "solidify/solidified_Matter_Plugin_Bridge_OnOff.h"

/*********************************************************************************************\
* Get a bytes() object of the certificate DAC/PAI_Cert
Expand Down Expand Up @@ -346,6 +348,8 @@ module matter (scope: global, strings: weak) {
Plugin_Sensor_Temp, class(be_class_Matter_Plugin_Sensor_Temp) // Temperature Sensor
Plugin_Sensor_Illuminance, class(be_class_Matter_Plugin_Sensor_Illuminance) // Illuminance Sensor
Plugin_Sensor_Humidity, class(be_class_Matter_Plugin_Sensor_Humidity) // Humidity Sensor
Plugin_Bridge_HTTP, class(be_class_Matter_Plugin_Bridge_HTTP) // HTTP bridge superclass
Plugin_Bridge_OnOff, class(be_class_Matter_Plugin_Bridge_OnOff) // HTTP Relay/Light behavior (OnOff)
}
@const_object_info_end */
Expand Down
16 changes: 8 additions & 8 deletions lib/libesp32/berry_matter/src/embedded/Matter_Device.be
Original file line number Diff line number Diff line change
Expand Up @@ -297,10 +297,16 @@ class Matter_Device
if self.commissioning_open != nil && tasmota.time_reached(self.commissioning_open) # timeout reached, close provisioning
self.commissioning_open = nil
end
# call all plugins
end

#############################################################
# dispatch every 250ms to all plugins
def every_250ms()
self.message_handler.every_250ms()
# call all plugins, use a manual loop to avoid creating a new object
var idx = 0
while idx < size(self.plugins)
self.plugins[idx].every_second()
self.plugins[idx].every_250ms()
idx += 1
end
end
Expand Down Expand Up @@ -334,12 +340,6 @@ class Matter_Device
self.tick += 1
end

#############################################################
# dispatch every 250ms click to sub-objects that need it
def every_250ms()
self.message_handler.every_250ms()
end

#############################################################
def stop()
tasmota.remove_driver(self)
Expand Down
61 changes: 56 additions & 5 deletions lib/libesp32/berry_matter/src/embedded/Matter_Plugin.be
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@
#@ solidify:Matter_Plugin,weak

class Matter_Plugin
# Global type system for plugins
static var TYPE = "" # name of the plug-in in json
static var NAME = "" # display name of the plug-in
static var ARG = "" # additional argument name (or empty if none)
static var ARG_TYPE = / x -> str(x) # function to convert argument to the right type
# Behavior of the plugin, frequency at which `update_shadow()` is called
static var UPDATE_TIME = 5000 # default is every 5 seconds
var update_next # next timestamp for update
# Configuration of the plugin: clusters and type
static var CLUSTERS = {
0x001D: [0,1,2,3,0xFFFC,0xFFFD], # Descriptor Cluster 9.5 p.453
}
Expand All @@ -53,6 +58,12 @@ class Matter_Plugin
self.clusters = self.consolidate_clusters()
end

#############################################################
# return the map of all types
def get_types()
return self.TYPES
end

#############################################################
# Stub for updating shadow values (local copies of what we published to the Matter gateway)
def update_shadow()
Expand Down Expand Up @@ -139,10 +150,11 @@ class Matter_Plugin

if attribute == 0x0000 # ---------- DeviceTypeList / list[DeviceTypeStruct] ----------
var dtl = TLV.Matter_TLV_array()
for dt: self.TYPES.keys()
var types = self.get_types()
for dt: types.keys()
var d1 = dtl.add_struct()
d1.add_TLV(0, TLV.U2, dt) # DeviceType
d1.add_TLV(1, TLV.U2, self.TYPES[dt]) # Revision
d1.add_TLV(1, TLV.U2, types[dt]) # Revision
end
return dtl
elif attribute == 0x0001 # ---------- ServerList / list[cluster-id] ----------
Expand Down Expand Up @@ -227,9 +239,48 @@ class Matter_Plugin
end

#############################################################
# every_second
def every_second()
self.update_shadow() # force reading value and sending subscriptions
# every_250ms
#
# check if the timer expired and update_shadow() needs to be called
def every_250ms()
if self.update_next == nil
# initialization to a random value within range
import crypto
var rand31 = crypto.random(4).get(0,4) & 0x7FFFFFFF # random int over 31 bits
self.update_next = tasmota.millis(rand31 % self.UPDATE_TIME)
else
if tasmota.time_reached(self.update_next)
self.update_shadow_lazy() # call update_shadow if not already called
self.update_next = tasmota.millis(self.UPDATE_TIME) # rearm timer
end
end
end

#############################################################
# UI Methods
#############################################################
# ui_conf_to_string
#
# Convert the current plugin parameters to a single string
static def ui_conf_to_string(cl, conf)
var arg_name = cl.ARG
var arg = arg_name ? str(conf.find(arg_name, '')) : ''
# print("MTR: ui_conf_to_string", conf, cl, arg_name, arg)
return arg
end

#############################################################
# ui_string_to_conf
#
# Convert the string in UI to actual parameters added to the map
static def ui_string_to_conf(cl, conf, arg)
var arg_name = cl.ARG
var arg_type = cl.ARG_TYPE
if arg && arg_name
conf[arg_name] = arg_type(arg)
end
# print("ui_string_to_conf", conf, arg)
return conf
end

end
Expand Down
228 changes: 228 additions & 0 deletions lib/libesp32/berry_matter/src/embedded/Matter_Plugin_Bridge_HTTP.be
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
#
# Matter_Plugin_Bridge_HTTP.be - implements base class for a Bridge via HTTP
#
# 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 core behavior

# dummy declaration for solidification
class Matter_Plugin_Device end

#@ solidify:Matter_Plugin_Bridge_HTTP,weak

class Matter_Plugin_Bridge_HTTP : Matter_Plugin_Device
static var TYPE = "" # name of the plug-in in json
static var NAME = "" # display name of the plug-in
static var ARG = "" # additional argument name (or empty if none)
static var ARG_HTTP = "url" # domain name
static var UPDATE_TIME = 3000 # update every 3s
static var HTTP_TIMEOUT = 300 # wait for 300ms max, since we're on LAN
static var CLUSTERS = {
# 0x001D: inherited # Descriptor Cluster 9.5 p.453
# 0x0003: inherited # Identify 1.2 p.16
# 0x0004: inherited # Groups 1.3 p.21
# 0x0005: inherited # Scenes 1.4 p.30 - no writable
# 0x0006: [0,0xFFFC,0xFFFD], # On/Off 1.5 p.48

# 0x0028: [0,1,2,3,4,5,6,7,8,9,0x0A,0x0F,0x12,0x13],# Basic Information Cluster cluster 11.1 p.565
0x0039: [0x11] # Bridged Device Basic Information 9.13 p.485

}
# static var TYPES = { 0x010A: 2 } # On/Off Light

var tasmota_http # domain name for HTTP, ex: 'http://192.168.1.10/'
var tasmota_status_8 # remote `Status 8` sensor values (last known)
var tasmota_status_11 # remote `Status 11` light values (last known)
# each contain a `_tick` attribute to known when they were last loaded
var reachable # is the device reachable
var reachable_tick # last tick when the reachability was seen (avoids sending superfluous ping commands)

#############################################################
# Constructor
def init(device, endpoint, arguments)
import string
super(self).init(device, endpoint, arguments)

var http = arguments.find(self.ARG_HTTP)
if http
if string.find(http, '://') < 0
http = "http://" + http + "/"
end
self.tasmota_http = http
else
tasmota.log(string.format("MTR: ERROR: 'url' is not configured for endpoint %i", endpoint), 2)
end
self.tasmota_status_8 = nil
self.tasmota_status_11 = nil
self.reachable = false # force a valid bool value
end

#############################################################
# return the map of all types, add the bridged type
def get_types()
var types = {}
for k: self.TYPES.keys()
types[k] = self.TYPES[k]
end
# Add Bridged Node
types[0x0013] = 1 # Bridged Node, v1
return types
end

#############################################################
# call_remote
#
# Call a remote Tasmota device, returns Berry native map or nil
def call_remote(cmd, arg)
if !self.tasmota_http return nil end
import json
import string
if !tasmota.wifi()['up'] && !tasmota.eth()['up'] return nil end # no network
var retry = 2 # try 2 times if first failed
while retry > 0
var cl = webclient()
cl.set_timeouts(1000, 1000)
cl.set_follow_redirects(false)
var url = string.format("%scm?cmnd=%s%%20%s", self.tasmota_http, cmd, arg ? arg : '')
tasmota.log("MTR: HTTP GET "+url, 3)
cl.begin(url)
var r = cl.GET()
tasmota.log("MTR: HTTP GET code=" + str(r), 3)
if r == 200
var s = cl.get_string()
cl.close()
tasmota.log("MTR: HTTP GET payload=" + s, 3)
var j = json.load(s)
# device is known to be reachable
self.reachable = true
self.reachable_tick = self.device.tick
return j
end
cl.close()

retry -= 1
tasmota.log("MTR: HTTP GET retrying", 3)
end
self.reachable = false
return nil
end

#############################################################
# is_reachable()
#
# Pings the device and checks if it's reachable
def is_reachable()
if self.device.tick != self.reachable_tick
var ret = self.call_remote("", "") # empty command works as a ping
self.reachable = (ret != nil)
# self.reachable_tick = cur_tick # done by caller
end
return self.reachable
end

#############################################################
# get_status_8()
#
# Get remote `Status 8` values of sensors, and cache for the current tick
def get_status_8()
var cur_tick = self.device.tick
if self.tasmota_status_8 == nil || self.tasmota_status_8.find("_tick") != cur_tick
var ret = self.call_remote("Status", "8") # call `Status 8`
if ret
ret["_tick"] = cur_tick
self.tasmota_status_8 = ret
return ret
else
return nil
end
else
return self.tasmota_status_8 # return cached value
end
end

#############################################################
# get_status_11()
#
# Get remote `Status 11` values of sensors, and cache for the current tick
def get_status_11()
var cur_tick = self.device.tick
if self.tasmota_status_11 == nil || self.tasmota_status_11.find("_tick") != cur_tick
var ret = self.call_remote("Status", "11") # call `Status 8`
if ret
ret["_tick"] = cur_tick
self.tasmota_status_11 = ret
return ret
else
return nil
end
else
return self.tasmota_status_11 # return cached value
end
end

#############################################################
# read attribute
#
def read_attribute(session, ctx)
var TLV = matter.TLV
var cluster = ctx.cluster
var attribute = ctx.attribute

# ====================================================================================================
if cluster == 0x0039 # ========== Bridged Device Basic Information 9.13 p.485 ==========

if attribute == 0x0000 # ---------- DataModelRevision / CommissioningWindowStatus ----------
# return TLV.create_TLV(TLV.U2, 1)
elif attribute == 0x0011 # ---------- Reachable / bool ----------
return TLV.create_TLV(TLV.BOOL, true) # TODO find a way to do a ping
end

else
return super(self).read_attribute(session, ctx)
end
end

#############################################################
# UI Methods
#############################################################
# ui_conf_to_string
#
# Convert the current plugin parameters to a single string
static def ui_conf_to_string(cl, conf)
var s = super(_class).ui_conf_to_string(cl, conf)

var url = str(conf.find(_class.ARG_HTTP, ''))
var arg = s + "," + url
print("MTR: ui_conf_to_string", conf, cl, arg)
return arg
end

#############################################################
# ui_string_to_conf
#
# Convert the string in UI to actual parameters added to the map
static def ui_string_to_conf(cl, conf, arg)
import string
var elts = string.split(arg + ',', ',', 3) # add ',' at the end to be sure to have at least 2 arguments
conf[_class.ARG_HTTP] = elts[1]
super(_class).ui_string_to_conf(cl, conf, elts[0])
print("ui_string_to_conf", conf, arg)
return conf
end

end
matter.Plugin_Bridge_HTTP = Matter_Plugin_Bridge_HTTP
Loading

0 comments on commit c26ec44

Please sign in to comment.