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
#############################################################