From 5acc4a368b4d3d8762d91c6165f311d6c7cccd27 Mon Sep 17 00:00:00 2001 From: Jean-Laurent Girod Date: Tue, 7 Jul 2026 20:00:24 +0200 Subject: [PATCH] Add Matter virtual IR HVAC thermostat support (#24821) * Add Matter virtual IR HVAC thermostat support Introduce a generic Matter thermostat plugin and a virtual HVAC plugin that maps thermostat state changes to Tasmota IRHVAC commands. Add virtual HVAC option switches for IRHVAC boolean options such as Econo, Quiet, Turbo, Light, Filter, Clean, Beep, and iFeel. These option endpoints are attached to the HVAC endpoint PartsList instead of being listed as top-level aggregator parts. Expose the new virtual device types in the Matter UI and refresh endpoint PartsList attributes when endpoint composition changes. * Warn on invalid Matter v.HVAC vendor * Fix regression * Matter virtual IR HVAC thermostat support * Revert --------- Co-authored-by: Jean-Laurent Girod Co-authored-by: s-hadinger <49731213+s-hadinger@users.noreply.github.com> --- .../berry_matter/src/be_matter_module.c | 3 + .../src/embedded/Matter_Plugin_0.be | 2 + .../embedded/Matter_Plugin_1_Aggregator.be | 8 +- .../embedded/Matter_Plugin_2_Thermostat.be | 242 ++++++++++++++++++ .../src/embedded/Matter_Plugin_9_Virt_HVAC.be | 187 ++++++++++++++ .../Matter_Plugin_9_Virt_HVAC_Option.be | 66 +++++ .../berry_matter/src/embedded/Matter_UI.be | 2 +- .../src/embedded/Matter_zz_Device.be | 5 +- 8 files changed, 509 insertions(+), 6 deletions(-) create mode 100644 lib/libesp32/berry_matter/src/embedded/Matter_Plugin_2_Thermostat.be create mode 100644 lib/libesp32/berry_matter/src/embedded/Matter_Plugin_9_Virt_HVAC.be create mode 100644 lib/libesp32/berry_matter/src/embedded/Matter_Plugin_9_Virt_HVAC_Option.be diff --git a/lib/libesp32/berry_matter/src/be_matter_module.c b/lib/libesp32/berry_matter/src/be_matter_module.c index 65917083e..255c2dc7b 100644 --- a/lib/libesp32/berry_matter/src/be_matter_module.c +++ b/lib/libesp32/berry_matter/src/be_matter_module.c @@ -244,8 +244,11 @@ extern const bclass be_class_Matter_TLV; // need to declare it upfront because #include "solidify/solidified_Matter_Plugin_3_Sensor_Rain.h" #include "solidify/solidified_Matter_Plugin_3_Sensor_Waterleak.h" #include "solidify/solidified_Matter_Plugin_2_Fan.h" +#include "solidify/solidified_Matter_Plugin_2_Thermostat.h" #include "solidify/solidified_Matter_Plugin_2_Sensor_GenericSwitch_Btn.h" #include "solidify/solidified_Matter_Plugin_9_Virt_Fan.h" +#include "solidify/solidified_Matter_Plugin_9_Virt_HVAC.h" +#include "solidify/solidified_Matter_Plugin_9_Virt_HVAC_Option.h" #include "solidify/solidified_Matter_Plugin_9_Virt_Sensor_Contact.h" #include "solidify/solidified_Matter_Plugin_9_Virt_Sensor_Occupancy.h" #include "solidify/solidified_Matter_Plugin_9_Virt_Sensor_Rain.h" diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_0.be b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_0.be index 3433a1497..4e241a0c5 100644 --- a/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_0.be +++ b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_0.be @@ -95,6 +95,7 @@ class Matter_Plugin static var VIRTUAL = false # set to true only for virtual devices static var BRIDGE = false # set to true only for bridged devices (ESP8266 or OpenBK) static var ZIGBEE = false # set to true only when mapped to a zigbee device + static var AGGREGATE = true # list this endpoint under the Aggregator PartsList var update_next # next timestamp for update # Configuration of the plugin: clusters and type static var CLUSTERS = matter.consolidate_clusters(_class, { @@ -113,6 +114,7 @@ class Matter_Plugin 0x0046: 0x00, # ICD Management: 0x00 = no optional features (base SIT mode, no CIP/UAT/LITS) 0x0062: 0x01, # Scenes Management: SceneNames (bit 0) 0x0102: 1 + 4, # Window Covering: Lift (bit 0) + PA_LF (bit 2) + 0x0201: 0x23, # Thermostat: HEAT + COOL + AUTO 0x0202: 2, # Fan Control: Auto (bit 1) } # `CLUSTER_REVISIONS` contains revision numbers for each cluster, or `1` if not present diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_1_Aggregator.be b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_1_Aggregator.be index ac93bd9b5..86fd439c6 100644 --- a/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_1_Aggregator.be +++ b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_1_Aggregator.be @@ -92,10 +92,10 @@ class Matter_Plugin_Aggregator : Matter_Plugin # overwrite PartsList elif attribute == 0x0003 # ---------- PartsList / list[endpoint-no]---------- var pl = TLV.Matter_TLV_array() - var eps = self.device.get_active_endpoints(true) - for ep: eps - if ep != 0x0001 #-matter.AGGREGATOR_ENDPOINT-# - pl.add_TLV(nil, 0x05 #-TLV.U2-#, ep) # add each endpoint + for plugin: self.device.plugins + var ep = plugin.get_endpoint() + if ep != 0x0001 #-matter.AGGREGATOR_ENDPOINT-# && ep != 0 && plugin.AGGREGATE + pl.add_TLV(nil, 0x05 #-TLV.U2-#, ep) # add each top-level endpoint end end return pl diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_2_Thermostat.be b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_2_Thermostat.be new file mode 100644 index 000000000..6920d3e9a --- /dev/null +++ b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_2_Thermostat.be @@ -0,0 +1,242 @@ +# +# Matter_Plugin_2_Thermostat.be - implements a generic Thermostat +# +# Copyright (C) 2026 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 . +# + +################################################################################# +# Matter 1.4.1 Device Specification - Thermostat (0x0301) +################################################################################# +# CLUSTERS (Server): +# - 0x0201: Thermostat +# +# MVP: +# - LocalTemperature, OccupiedCoolingSetpoint, OccupiedHeatingSetpoint +# - ControlSequenceOfOperation, SystemMode, ThermostatRunningState +# - Local shadow state only; subclasses can publish native commands +################################################################################# + +import matter + +#@ solidify:Matter_Plugin_Thermostat,weak + +class Matter_Plugin_Thermostat : Matter_Plugin_Device + static var TYPE = "thermostat" + static var DISPLAY_NAME = "Thermostat" + static var CLUSTERS = matter.consolidate_clusters(_class, { + 0x0201: [0,0x11,0x12,0x1B,0x1C,0x29], # Thermostat + }) + static var UPDATE_COMMANDS = matter.UC_LIST(_class, "SystemMode", "Temp", "CoolSetpoint", "HeatSetpoint") + static var TYPES = { 0x0301: 4 } # Thermostat - Matter 1.4.1 Device Library Rev 4 + + var shadow_local_temperature + var shadow_cooling_setpoint + var shadow_heating_setpoint + var shadow_system_mode + var shadow_running_state + var min_temp + var max_temp + + ############################################################# + # Thermostat SystemMode: + # 0: Off + # 1: Auto + # 3: Cool + # 4: Heat + # 7: Fan only + # 8: Dry + + def init(device, endpoint, config) + super(self).init(device, endpoint, config) + if self.min_temp == nil self.min_temp = 1600 end + if self.max_temp == nil self.max_temp = 3000 end + self.shadow_local_temperature = config.find("local_temp", nil) + if self.shadow_local_temperature != nil + self.shadow_local_temperature = int(self.shadow_local_temperature) * 100 + end + self.shadow_cooling_setpoint = self.clamp_temp(int(config.find("cool_temp", 2400))) + self.shadow_heating_setpoint = self.clamp_temp(int(config.find("heat_temp", 2000))) + self.shadow_system_mode = int(config.find("mode", 0)) + self.shadow_running_state = self.compute_running_state() + end + + def parse_configuration(config) + super(self).parse_configuration(config) + self.min_temp = int(config.find("min_temp", 16)) * 100 + self.max_temp = int(config.find("max_temp", 30)) * 100 + end + + def clamp_temp(temp) + temp = int(temp) + if temp < self.min_temp temp = self.min_temp end + if temp > self.max_temp temp = self.max_temp end + return temp + end + + def active_setpoint() + if self.shadow_system_mode == 4 + return self.shadow_heating_setpoint + else + return self.shadow_cooling_setpoint + end + end + + def compute_running_state() + if self.shadow_system_mode == 3 + return 0x0002 # Cool Stage 1 + elif self.shadow_system_mode == 4 + return 0x0001 # Heat Stage 1 + elif self.shadow_system_mode == 7 + return 0x0004 # Fan + elif self.shadow_system_mode == 8 + return 0x0004 # Dry: closest exposed state is fan + else + return 0 + end + end + + def thermostat_feature_map() + return 0x23 # HEAT + COOL + AUTO + end + + def set_system_mode(mode) + mode = int(mode) + if mode != 0 && mode != 1 && mode != 3 && mode != 4 && mode != 7 && mode != 8 + return false + end + if mode != self.shadow_system_mode + self.shadow_system_mode = mode + self.attribute_updated(0x0201, 0x001C) + var running_state = self.compute_running_state() + if running_state != self.shadow_running_state + self.shadow_running_state = running_state + self.attribute_updated(0x0201, 0x0029) + end + end + return true + end + + def set_cooling_setpoint(temp) + temp = self.clamp_temp(temp) + if temp != self.shadow_cooling_setpoint + self.shadow_cooling_setpoint = temp + self.attribute_updated(0x0201, 0x0011) + end + end + + def set_heating_setpoint(temp) + temp = self.clamp_temp(temp) + if temp != self.shadow_heating_setpoint + self.shadow_heating_setpoint = temp + self.attribute_updated(0x0201, 0x0012) + end + end + + def read_attribute(session, ctx, tlv_solo) + var cluster = ctx.cluster + var attribute = ctx.attribute + + if cluster == 0x0201 # Thermostat + self.update_shadow_lazy() + if attribute == 0xFFFC # FeatureMap / map32 + return tlv_solo.set(0x06 #-TLV.U4-#, self.thermostat_feature_map()) + elif attribute == 0x0000 # LocalTemperature / int16 + if self.shadow_local_temperature == nil + return tlv_solo.set(0x14 #-TLV.NULL-#, nil) + else + return tlv_solo.set(0x01 #-TLV.I2-#, self.shadow_local_temperature) + end + elif attribute == 0x0011 # OccupiedCoolingSetpoint / int16 + return tlv_solo.set(0x01 #-TLV.I2-#, self.shadow_cooling_setpoint) + elif attribute == 0x0012 # OccupiedHeatingSetpoint / int16 + return tlv_solo.set(0x01 #-TLV.I2-#, self.shadow_heating_setpoint) + elif attribute == 0x001B # ControlSequenceOfOperation / enum8 + return tlv_solo.set(0x04 #-TLV.U1-#, 4) # Cooling and heating + elif attribute == 0x001C # SystemMode / enum8 + return tlv_solo.set(0x04 #-TLV.U1-#, self.shadow_system_mode) + elif attribute == 0x0029 # ThermostatRunningState / bitmap16 + return tlv_solo.set(0x05 #-TLV.U2-#, self.shadow_running_state) + end + end + return super(self).read_attribute(session, ctx, tlv_solo) + end + + def write_attribute(session, ctx, write_data) + var cluster = ctx.cluster + var attribute = ctx.attribute + + if cluster == 0x0201 # Thermostat + self.update_shadow_lazy() + if attribute == 0x0011 # OccupiedCoolingSetpoint + if type(write_data) == 'int' + self.set_cooling_setpoint(write_data) + self.thermostat_state_changed() + self.publish_command('CoolSetpoint', self.shadow_cooling_setpoint, 'Temp', self.active_setpoint(), nil, nil) + return true + end + elif attribute == 0x0012 # OccupiedHeatingSetpoint + if type(write_data) == 'int' + self.set_heating_setpoint(write_data) + self.thermostat_state_changed() + self.publish_command('HeatSetpoint', self.shadow_heating_setpoint, 'Temp', self.active_setpoint(), nil, nil) + return true + end + elif attribute == 0x001C # SystemMode + if type(write_data) == 'int' && self.set_system_mode(write_data) + self.thermostat_state_changed() + self.publish_command('SystemMode', self.shadow_system_mode, 'Temp', self.active_setpoint(), nil, nil) + return true + end + end + ctx.status = 0x87 #-matter.CONSTRAINT_ERROR-# + return false + end + return nil + end + + def thermostat_state_changed() + end + + def update_virtual(payload) + var val_mode = payload.find("SystemMode") + if val_mode != nil + self.set_system_mode(int(val_mode)) + end + var val_cool = payload.find("CoolSetpoint") + if val_cool != nil + self.set_cooling_setpoint(int(val_cool)) + end + var val_heat = payload.find("HeatSetpoint") + if val_heat != nil + self.set_heating_setpoint(int(val_heat)) + end + var val_temp = payload.find("Temp") + if val_temp != nil + self.set_cooling_setpoint(int(val_temp)) + self.set_heating_setpoint(int(val_temp)) + end + var val_local = payload.find("LocalTemperature") + if val_local != nil + val_local = int(val_local) + if val_local != self.shadow_local_temperature + self.shadow_local_temperature = val_local + self.attribute_updated(0x0201, 0x0000) + end + end + end + +end +matter.Plugin_Thermostat = Matter_Plugin_Thermostat diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_9_Virt_HVAC.be b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_9_Virt_HVAC.be new file mode 100644 index 000000000..34bfb1299 --- /dev/null +++ b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_9_Virt_HVAC.be @@ -0,0 +1,187 @@ +# +# Matter_Plugin_9_Virt_HVAC.be - implements a virtual IR HVAC thermostat +# +# Copyright (C) 2026 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 . +# + +################################################################################# +# Matter 1.4.1 Virtual Variant - HVAC / IRHVAC Thermostat +################################################################################# +# TYPE: "v_hvac" | VIRTUAL: true +# Maps Matter Thermostat writes to a complete Tasmota IRHVAC JSON command. +################################################################################# + +import matter + +#@ solidify:Matter_Plugin_Virt_HVAC,weak + +class Matter_Plugin_Virt_HVAC : Matter_Plugin_Thermostat + static var TYPE = "v_hvac" + static var DISPLAY_NAME = "v.HVAC" + static var SCHEMA = "vendor|l:Vendor|t:t|h:IRHVAC vendor/protocol, for example MITSUBISHI_AC or DAIKIN|r:1" + static var SCHEMA2 = "model|l:Model|t:i|h:IRHVAC model, usually -1|d:-1" + static var SCHEMA3 = "min_temp|l:Min temp|t:i|h:Minimum setpoint in Celsius|d:16" + static var SCHEMA4 = "max_temp|l:Max temp|t:i|h:Maximum setpoint in Celsius|d:30" + static var SCHEMA5 = "fan|l:Fan|t:s|d:Auto|o:Auto:Auto,Min:Min,Low:Low,Medium:Medium,High:High,Max:Max" + static var SCHEMA6 = "temp_filter|l:Temp sensor|t:t|h:Tasmota sensor filter, for example SHT3X#Temperature or DS18B20#Temperature" + static var UPDATE_TIME = 5000 + static var VIRTUAL = true + + var ir_vendor + var ir_model + var ir_fan + var local_temp_filter + var local_temp_matcher + + def init(device, endpoint, config) + super(self).init(device, endpoint, config) + if self.local_temp_matcher + device.add_read_sensors_schedule(self.UPDATE_TIME) + end + end + + def parse_configuration(config) + super(self).parse_configuration(config) + var vendor = config.find("vendor", nil) + self.ir_vendor = vendor == nil ? "" : str(vendor) + self.ir_model = config.find("model", -1) + self.ir_fan = config.find("fan", "Auto") + self.local_temp_filter = config.find("temp_filter") + if self.local_temp_filter + self.local_temp_matcher = tasmota.Rule_Matcher.parse(self.local_temp_filter) + end + if self.ir_vendor == "" + log("MTR: v.HVAC endpoint " + str(self.endpoint) + " missing IRHVAC vendor; commands will be ignored", 2) + end + end + + def thermostat_state_changed() + self.send_irhvac() + end + + def mode_to_irhvac() + if self.shadow_system_mode == 4 + return "Heat" + elif self.shadow_system_mode == 1 + return "Auto" + elif self.shadow_system_mode == 7 + return "Fan" + elif self.shadow_system_mode == 8 + return "Dry" + else + return "Cool" + end + end + + def thermostat_feature_map() + var feature_map = super(self).thermostat_feature_map() + if self.local_temp_matcher == nil + feature_map = feature_map | 0x40 # LTNE: LocalTemperatureNotExposed + end + return feature_map + end + + def send_irhvac() + if self.ir_vendor == nil || self.ir_vendor == "" + return nil + end + + import json + var power = self.shadow_system_mode == 0 ? "Off" : "On" + var temp = int(self.active_setpoint() / 100) + var payload = { + "Vendor": self.ir_vendor, + "Model": self.ir_model, + "Power": power, + "Mode": self.mode_to_irhvac(), + "Celsius": "On", + "Temp": temp, + "FanSpeed": self.ir_fan, + "SwingV": "Off", + "SwingH": "Off", + "Quiet": self.option_state("Quiet"), + "Turbo": self.option_state("Turbo"), + "Econo": self.option_state("Econo"), + "Light": self.option_state("Light"), + "Filter": self.option_state("Filter"), + "Clean": self.option_state("Clean"), + "Beep": self.option_state("Beep"), + "iFeel": self.option_state("iFeel"), + "Sleep": -1 + } + var cmd = "IRHVAC " + json.dump(payload) + log("MTR: v.HVAC " + cmd, 2) + var resp = tasmota.cmd(cmd, true) + if isinstance(resp, map) + var irhvac_resp = resp.find("IRHVAC", nil) + if type(irhvac_resp) == 'string' + log("MTR: v.HVAC endpoint " + str(self.endpoint) + " IRHVAC rejected vendor '" + self.ir_vendor + "': " + irhvac_resp, 2) + end + end + return resp + end + + def option_state(option_name) + for pl: self.device.plugins + if pl.TYPE == "v_hvac_option" && pl.hvac_ep == self.endpoint && pl.option_name == option_name + return pl.shadow_onoff ? "On" : "Off" + end + end + return "Off" + end + + def read_attribute(session, ctx, tlv_solo) + if ctx.cluster == 0x001D && ctx.attribute == 0x0003 + var TLV = matter.TLV + var parts = TLV.Matter_TLV_array() + for pl: self.device.plugins + if pl.TYPE == "v_hvac_option" && pl.hvac_ep == self.endpoint + parts.add_TLV(nil, 0x05 #-TLV.U2-#, pl.endpoint) + end + end + return parts + end + return super(self).read_attribute(session, ctx, tlv_solo) + end + + def update_virtual(payload) + super(self).update_virtual(payload) + self.send_irhvac() + end + + def parse_sensors(payload) + if self.local_temp_matcher + var val = self.local_temp_matcher.match(payload) + if isinstance(val, map) + val = val.find("Temperature") + end + if val != nil + val = real(val) + if tasmota.get_option(8) == 1 + val = (val - 32) / 1.8 + end + val = int(val * 100) + if val != self.shadow_local_temperature + self.shadow_local_temperature = val + self.attribute_updated(0x0201, 0x0000) + end + end + end + super(self).parse_sensors(payload) + end + +end +matter.Plugin_Virt_HVAC = Matter_Plugin_Virt_HVAC diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_9_Virt_HVAC_Option.be b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_9_Virt_HVAC_Option.be new file mode 100644 index 000000000..c58aae6aa --- /dev/null +++ b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_9_Virt_HVAC_Option.be @@ -0,0 +1,66 @@ +# +# Matter_Plugin_9_Virt_HVAC_Option.be - virtual switch bound to a v.HVAC IRHVAC option +# +# Copyright (C) 2026 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 . +# + +################################################################################# +# Virtual On/Off switch that toggles an extra IRHVAC option on a v.HVAC endpoint. +################################################################################# + +import matter + +#@ solidify:Matter_Plugin_Virt_HVAC_Option,weak + +class Matter_Plugin_Virt_HVAC_Option : Matter_Plugin_OnOff + static var TYPE = "v_hvac_option" + static var DISPLAY_NAME = "v.HVAC option" + static var SCHEMA = "hvac_ep|l:HVAC endpoint|t:i|h:Endpoint number of the v.HVAC device, usually 2|r:1" + static var SCHEMA2 = "option|l:Option|t:s|d:Econo|o:Econo:Econo,Quiet:Quiet,Turbo:Turbo,Light:Light,Filter:Filter,Clean:Clean,Beep:Beep,iFeel:iFeel" + static var VIRTUAL = true + static var AGGREGATE = false + + var hvac_ep + var option_name + + def parse_configuration(config) + super(self).parse_configuration(config) + self.hvac_ep = int(config.find("hvac_ep", 0)) + self.option_name = config.find("option", "Econo") + end + + def set_onoff(pow) + var old_onoff = self.shadow_onoff + super(self).set_onoff(pow) + if old_onoff != self.shadow_onoff + self.trigger_hvac() + end + end + + def trigger_hvac() + if self.hvac_ep <= 0 + return nil + end + var hvac = self.device.find_plugin_by_endpoint(self.hvac_ep) + if hvac != nil && hvac.TYPE == "v_hvac" + return hvac.send_irhvac() + end + log("MTR: v.HVAC option could not find v.HVAC endpoint " + str(self.hvac_ep), 2) + return nil + end + +end +matter.Plugin_Virt_HVAC_Option = Matter_Plugin_Virt_HVAC_Option diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_UI.be b/lib/libesp32/berry_matter/src/embedded/Matter_UI.be index 1d2c0f51d..3fe3f5047 100644 --- a/lib/libesp32/berry_matter/src/embedded/Matter_UI.be +++ b/lib/libesp32/berry_matter/src/embedded/Matter_UI.be @@ -192,7 +192,7 @@ class Matter_UI "|airquality" static var _CLASSES_TYPES_VIRTUAL = "-virtual|v_relay|v_light0|v_light1|v_light2|v_light3" - "|v_fan" + "|v_fan|v_hvac|v_hvac_option" "|v_temp|v_pressure|v_illuminance|v_humidity|v_occupancy|v_contact|v_flow|v_rain|v_waterleak" "|v_airquality" static var _CLASSES_TYPES2= "|http_relay|http_light0|http_light1|http_light2|http_light3" diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_zz_Device.be b/lib/libesp32/berry_matter/src/embedded/Matter_zz_Device.be index ba9c9e97c..d0541c5f4 100644 --- a/lib/libesp32/berry_matter/src/embedded/Matter_zz_Device.be +++ b/lib/libesp32/berry_matter/src/embedded/Matter_zz_Device.be @@ -730,7 +730,10 @@ class Matter_Device def signal_endpoints_changed() # mark parts lists as changed self.attribute_updated(0x0000, 0x001D, 0x0003, false) - self.attribute_updated(0x0001 #-matter.AGGREGATOR_ENDPOINT-#, 0x001D, 0x0003, false) + var eps = self.get_active_endpoints(true) + for ep: eps + self.attribute_updated(ep, 0x001D, 0x0003, false) + end end #############################################################