From a242811ec8697f00726f28cb1b8128c6e8f372bf Mon Sep 17 00:00:00 2001
From: s-hadinger <49731213+s-hadinger@users.noreply.github.com>
Date: Tue, 29 Sep 2026 15:50:13 +0200
Subject: [PATCH] Matter Soil Sensor device type with Soil Measurement cluster
(Matter 1.6.0) (#25088)
---
CHANGELOG.md | 3 +-
.../MATTER_1.4.1_TO_1.6.0_CHANGELOG.md | 9 +-
.../MATTER_1.6.0_DETAILED_GAP_ANALYSIS.md | 2 +-
.../berry_matter/src/be_matter_module.c | 3 +
.../embedded/Matter_Plugin_3_Sensor_Soil.be | 160 ++++++++++++++++++
.../Matter_Plugin_8_Bridge_Sensor_Soil.be | 40 +++++
.../Matter_Plugin_9_Virt_Sensor_Soil.be | 46 +++++
.../berry_matter/src/embedded/Matter_UI.be | 8 +-
.../src/embedded/Matter_z_Autoconf.be | 11 ++
9 files changed, 275 insertions(+), 7 deletions(-)
create mode 100644 lib/libesp32/berry_matter/src/embedded/Matter_Plugin_3_Sensor_Soil.be
create mode 100644 lib/libesp32/berry_matter/src/embedded/Matter_Plugin_8_Bridge_Sensor_Soil.be
create mode 100644 lib/libesp32/berry_matter/src/embedded/Matter_Plugin_9_Virt_Sensor_Soil.be
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4e79daf20..0540d5028 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,8 @@ All notable changes to this project will be documented in this file.
## [15.6.0.2]
### Added
- DALI-2 push button bindings to control gear targets with command `DaliBind` (toggle, on, off, up, down) (#25073)
-- Matter add Electrical Power Measurement cluster to On/Off Plug-in Unit (#24922)
+- Matter Electrical Power Measurement cluster to On/Off Plug-in Unit (#24922)
+- Matter Soil Sensor device type with Soil Measurement cluster (Matter 1.6.0)
### Breaking Changed
diff --git a/lib/libesp32/berry_matter/MATTER_1.4.1_TO_1.6.0_CHANGELOG.md b/lib/libesp32/berry_matter/MATTER_1.4.1_TO_1.6.0_CHANGELOG.md
index eced43498..e291b401e 100644
--- a/lib/libesp32/berry_matter/MATTER_1.4.1_TO_1.6.0_CHANGELOG.md
+++ b/lib/libesp32/berry_matter/MATTER_1.4.1_TO_1.6.0_CHANGELOG.md
@@ -225,7 +225,12 @@ The following changes are part of Matter 1.6.1 (released September 2026, PR #735
- Commodity Tariff cluster (0x0700) read implementation
- Device Energy Management cluster (0x0098) for power measurement + demand response hints
- **Timeframe:** 3-4 weeks
-- **Integration:** Extend existing power sensor plugins; enable smart scheduling based on tariff
+
+### Phase 2b: Tariff & Price (deferred)
+- Commodity Price (0x0095, Rev 4) and Commodity Tariff (0x0700, Rev 1)
+- **Why deferred:** per the Device Library, both clusters are only hosted by the Electrical Energy Tariff device type (0x0513), which must be a child of a Meter Reference Point (0x0512) endpoint with Identify, and must carry Descriptor TagList semantic tags (Commodity Tariff Commodity/Chronology namespaces, plus Grid/Import/AC in the basic topology). The Root Node also needs the TimeSyncCond condition. Commodity Tariff alone carries 14+ attributes with deeply nested structs.
+- **Prerequisites:** Namespace Specification 1.6 (to confirm namespace/tag IDs), a parent/child virtual composition (PartsList override, as in `Matter_Plugin_9_Virt_HVAC.be`), Descriptor TagList support (FeatureMap bit 0 on 0x001D)
+- **Data source:** none native in Tasmota; values would be pushed via `MtrUpdate` (rules/scripts/MQTT)
### Phase 3: Closures Unified Architecture (v1.6.0, medium-value features)
- Closure device type (0x0230) with Closure Control (0x0104) and Closure Dimension (0x0105) clusters
@@ -240,6 +245,8 @@ The following changes are part of Matter 1.6.1 (released September 2026, PR #735
- **Timeframe:** 1 week
- **Integration:** Extends existing humidity/temperature sensor plugins
+- Doorbell device type (0x0148) — optional, can defer to maintenance release
+
### Phase 5: Validation & Compliance Testing (v1.6.0)
- Multi-controller interoperability (Apple Home, Google Home, Alexa, SmartThings, Home Assistant)
- DataModelRevision = 20 verification
diff --git a/lib/libesp32/berry_matter/MATTER_1.6.0_DETAILED_GAP_ANALYSIS.md b/lib/libesp32/berry_matter/MATTER_1.6.0_DETAILED_GAP_ANALYSIS.md
index d7c4df5a9..e313af6ac 100644
--- a/lib/libesp32/berry_matter/MATTER_1.6.0_DETAILED_GAP_ANALYSIS.md
+++ b/lib/libesp32/berry_matter/MATTER_1.6.0_DETAILED_GAP_ANALYSIS.md
@@ -342,7 +342,7 @@ The Tasmota Matter implementation is currently aligned with **Matter 1.4.1** (Da
| **DataModelRevision** | 18 | 20 | ⚠️ Not updated | <1 day |
| **Closures clusters** | Window Covering (legacy) | Closure Control/Dimension (new) | ❌ Missing | 2 weeks |
| **Energy Management** | 0x0090/0x0091 only | Add Commodity Price/Tariff, Device EM, EVSE | ❌ Missing | 3-4 weeks |
-| **Soil Measurement** | None | Soil Sensor (0x0045) + cluster | ❌ Missing | 1 week |
+| **Soil Measurement** | None | Soil Sensor (0x0045) + cluster | ✅ Done | — |
| **Doorbell** | None | 0x0148/0x0141/0x0143 | ❌ Missing | 1 week |
| **Access Control** | Partial | Complete read + rev updates | ⚠️ Partial | 3-5 days |
| **Group Key Management** | Rev 2 | Rev 4 | ⚠️ Outdated | 1 week |
diff --git a/lib/libesp32/berry_matter/src/be_matter_module.c b/lib/libesp32/berry_matter/src/be_matter_module.c
index 29200308a..d9333fb22 100644
--- a/lib/libesp32/berry_matter/src/be_matter_module.c
+++ b/lib/libesp32/berry_matter/src/be_matter_module.c
@@ -240,6 +240,8 @@ extern const bclass be_class_Matter_TLV; // need to declare it upfront because
#include "solidify/solidified_Matter_Plugin_9_Virt_Sensor_Illuminance.h"
#include "solidify/solidified_Matter_Plugin_3_Sensor_Humidity.h"
#include "solidify/solidified_Matter_Plugin_9_Virt_Sensor_Humidity.h"
+#include "solidify/solidified_Matter_Plugin_3_Sensor_Soil.h"
+#include "solidify/solidified_Matter_Plugin_9_Virt_Sensor_Soil.h"
#include "solidify/solidified_Matter_Plugin_2_Sensor_Boolean.h"
#include "solidify/solidified_Matter_Plugin_3_Sensor_Occupancy.h"
#include "solidify/solidified_Matter_Plugin_3_Sensor_OnOff.h"
@@ -266,6 +268,7 @@ extern const bclass be_class_Matter_TLV; // need to declare it upfront because
#include "solidify/solidified_Matter_Plugin_8_Bridge_Sensor_Temp.h"
#include "solidify/solidified_Matter_Plugin_8_Bridge_Sensor_Illuminance.h"
#include "solidify/solidified_Matter_Plugin_8_Bridge_Sensor_Humidity.h"
+#include "solidify/solidified_Matter_Plugin_8_Bridge_Sensor_Soil.h"
#include "solidify/solidified_Matter_Plugin_8_Bridge_Sensor_Occupancy.h"
#include "solidify/solidified_Matter_Plugin_8_Bridge_Sensor_Contact.h"
#include "solidify/solidified_Matter_Plugin_8_Bridge_Sensor_Flow.h"
diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_3_Sensor_Soil.be b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_3_Sensor_Soil.be
new file mode 100644
index 000000000..d5a9013f9
--- /dev/null
+++ b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_3_Sensor_Soil.be
@@ -0,0 +1,160 @@
+#
+# Matter_Plugin_Sensor_Soil.be - implements the behavior for a Soil Sensor
+#
+# Copyright (C) 2026 Ludovic BOUÉ
+#
+# 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.6.0 Device Specification
+#################################################################################
+# Device Type: Soil Sensor (0x0045)
+# Device Type Revision: 1 (Matter 1.5+)
+# Class: Simple | Scope: Endpoint
+#
+# CLUSTERS (Server):
+# - 0x0430: Soil Measurement (M)
+# - 0x0003: Identify (M)
+# - 0x0402: Temperature Measurement (O) - not exposed by this plugin
+#
+# NOTES:
+# - Reads the `Moisture` value (percent) reported by Tasmota soil sensors,
+# e.g. Adafruit Seesaw capacitive soil sensor (xsns_81), or BLE plant sensors
+# - Soil temperature (optional on this device type) can be exposed with a
+# separate `temperature` endpoint using the same sensor filter prefix
+#################################################################################
+
+#################################################################################
+# Matter 1.6.0 Soil Measurement Cluster (0x0430)
+#################################################################################
+# Cluster Revision: 1 (Matter 1.5+)
+# Role: Application | Scope: Endpoint | PICS: SOIL
+#
+# ATTRIBUTES:
+# ID | Name | Type | Quality | Conf
+# -------|--------------------------------|-------------------------|---------|-----
+# 0x0000 | SoilMoistureMeasurementLimits | MeasurementAccuracyStruct | F | M
+# 0x0001 | SoilMoistureMeasuredValue | percent (uint8) | X | M
+#
+# SoilMoistureMeasurementLimits constraints (spec 2.15.4.1):
+# - MeasurementType = SoilMoisture (17)
+# - MinMeasuredValue 0..99, MaxMeasuredValue (Min+1)..100
+# - a single AccuracyRanges entry covering [Min, Max], with only PercentMax
+# set; PercentMax (percent100ths) must be <= 10 (checked by TC_SOIL_2_1)
+#
+# SoilMoistureMeasuredValue: water content of the soil in percent,
+# null until the first reading.
+#################################################################################
+
+import matter
+
+# Matter plug-in for core behavior
+
+#@ solidify:Matter_Plugin_Sensor_Soil,weak
+
+class Matter_Plugin_Sensor_Soil : Matter_Plugin_Sensor
+ static var TYPE = "soil" # name of the plug-in in json
+ static var DISPLAY_NAME = "Soil Moisture" # display name of the plug-in
+ static var JSON_NAME = "Moisture" # Name of the sensor attribute in JSON payloads
+ static var UPDATE_COMMANDS = matter.UC_LIST(_class, "Moisture")
+ static var CLUSTERS = matter.consolidate_clusters(_class, {
+ 0x0430: [0,1], # Soil Measurement 2.15 - no writable
+ })
+ static var TYPES = { 0x0045: 1 } # Soil Sensor, rev 1
+
+ # static var MT_SOIL_MOISTURE = 17 # MeasurementTypeEnum SoilMoisture
+ # static var PERCENT_MAX = 10 # percent100ths, upper bound checked by TC_SOIL_2_1 (and used by the CHIP reference server)
+
+ #############################################################
+ # Pre-process value
+ #
+ # Tasmota reports moisture in percent, Matter uses a uint8 percent
+ # Clamp in the float domain before converting, since `int()` of NaN
+ # or out-of-range floats is undefined, then round to nearest
+ def pre_value(val)
+ if val == nil || val != val return nil end # nil or NaN
+ if val < 0 val = 0 end
+ if val > 100 val = 100 end
+ return int(val + 0.5)
+ end
+
+ #############################################################
+ # Called when the value changed compared to shadow value
+ def value_changed()
+ self.attribute_updated(0x0430, 0x0001)
+ end
+
+ #############################################################
+ # update_virtual
+ #
+ # Clamp values received from `MtrUpdate {"Name":"...", "Moisture":42}`
+ def update_virtual(payload)
+ var val = payload.find(self.JSON_NAME)
+ if val != nil
+ payload[self.JSON_NAME] = self.pre_value(real(val))
+ end
+ super(self).update_virtual(payload)
+ end
+
+ #############################################################
+ # build SoilMoistureMeasurementLimits
+ def _build_limits()
+ var TLV = matter.TLV
+ var lim = TLV.Matter_TLV_struct()
+ lim.add_TLV(0, 0x06 #-TLV.U4-#, 17 #-MT_SOIL_MOISTURE-#) # MeasurementType
+ lim.add_TLV(1, 0x08 #-TLV.BOOL-#, true) # Measured
+ lim.add_TLV(2, 0x03 #-TLV.I8-#, 0) # MinMeasuredValue
+ lim.add_TLV(3, 0x03 #-TLV.I8-#, 100) # MaxMeasuredValue
+ var ranges = lim.add_array(4) # AccuracyRanges
+ var r = ranges.add_struct()
+ r.add_TLV(0, 0x03 #-TLV.I8-#, 0) # RangeMin
+ r.add_TLV(1, 0x03 #-TLV.I8-#, 100) # RangeMax
+ r.add_TLV(2, 0x06 #-TLV.U4-#, 10 #-PERCENT_MAX-#) # PercentMax
+ return lim
+ end
+
+ #############################################################
+ # read an attribute
+ #
+ def read_attribute(session, ctx, tlv_solo)
+ var cluster = ctx.cluster
+ var attribute = ctx.attribute
+
+ # ====================================================================================================
+ if cluster == 0x0430 # ========== Soil Measurement 2.15 ==========
+ if attribute == 0x0000 # ---------- SoilMoistureMeasurementLimits / MeasurementAccuracyStruct ----------
+ return self._build_limits()
+ elif attribute == 0x0001 # ---------- SoilMoistureMeasuredValue / percent ----------
+ return tlv_solo.set_or_nil(0x06 #-TLV.U4-#, self.shadow_value != nil ? int(self.shadow_value) : nil)
+ end
+
+ end
+ return super(self).read_attribute(session, ctx, tlv_solo)
+ end
+
+ #############################################################
+ # web_values
+ #
+ # Show values of the remote device as HTML
+ def web_values()
+ import webserver
+ self.web_values_prefix() # display '| ' and name if present
+ webserver.content_send(format("🌱 %i%%", self.shadow_value != nil ? int(self.shadow_value) : nil))
+ end
+ #############################################################
+ #############################################################
+
+end
+matter.Plugin_Sensor_Soil = Matter_Plugin_Sensor_Soil
diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_8_Bridge_Sensor_Soil.be b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_8_Bridge_Sensor_Soil.be
new file mode 100644
index 000000000..d1e52becc
--- /dev/null
+++ b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_8_Bridge_Sensor_Soil.be
@@ -0,0 +1,40 @@
+#
+# Matter_Plugin_Bridge_Sensor_Soil.be - implements base class for a Soil Sensor via HTTP to Tasmota
+#
+# Copyright (C) 2026 Ludovic BOUÉ
+#
+# 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.6.0 Bridge Variant - Soil Sensor via HTTP
+#################################################################################
+# INHERITS FROM: Matter_Plugin_Sensor_Soil (Matter_Plugin_3_Sensor_Soil.be)
+# VARIANT TYPE: Bridge (Remote HTTP Device)
+# DEVICE TYPE: Soil Sensor (0x0045) - See base class
+# CLUSTERS: Soil Measurement (0x0430) - See Matter_Plugin_3_Sensor_Soil.be
+# TYPE: "http_soil" | BRIDGE: true
+#################################################################################
+
+import matter
+
+# Matter plug-in for core behavior
+
+#@ solidify:Matter_Plugin_Bridge_Sensor_Soil,weak
+
+class Matter_Plugin_Bridge_Sensor_Soil : Matter_Plugin_Sensor_Soil
+ static var BRIDGE = true # flag as bridged device
+ static var TYPE = "http_soil" # name of the plug-in in json
+end
+matter.Plugin_Bridge_Sensor_Soil = Matter_Plugin_Bridge_Sensor_Soil
diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_9_Virt_Sensor_Soil.be b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_9_Virt_Sensor_Soil.be
new file mode 100644
index 000000000..cb61d32b0
--- /dev/null
+++ b/lib/libesp32/berry_matter/src/embedded/Matter_Plugin_9_Virt_Sensor_Soil.be
@@ -0,0 +1,46 @@
+#
+# Matter_Plugin_9_Virt_Sensor_Soil.be - implements Virtual Soil Sensor
+#
+# Copyright (C) 2026 Ludovic BOUÉ
+#
+# 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.6.0 Virtual Variant - Soil Sensor
+#################################################################################
+# INHERITS FROM: Matter_Plugin_Sensor_Soil (Matter_Plugin_3_Sensor_Soil.be)
+# VARIANT TYPE: Virtual
+# DEVICE TYPE: Soil Sensor (0x0045) - See base class for specification
+# CLUSTERS: Soil Measurement (0x0430) - See Matter_Plugin_3_Sensor_Soil.be
+# TYPE: "v_soil" | VIRTUAL: true
+#
+# TESTING:
+# MtrUpdate {"Name":"Garden", "Moisture":42}
+#################################################################################
+
+import matter
+
+# Matter plug-in for core behavior
+
+#@ solidify:Matter_Plugin_Virt_Sensor_Soil,weak
+
+class Matter_Plugin_Virt_Sensor_Soil : Matter_Plugin_Sensor_Soil
+ static var TYPE = "v_soil" # name of the plug-in in json
+ static var DISPLAY_NAME = "v.Soil Moisture" # display name of the plug-in
+
+ static var SCHEMA = nil # no parameter
+ static var VIRTUAL = true # virtual device
+end
+matter.Plugin_Virt_Sensor_Soil = Matter_Plugin_Virt_Sensor_Soil
diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_UI.be b/lib/libesp32/berry_matter/src/embedded/Matter_UI.be
index 49f0db1e6..24514cbb4 100644
--- a/lib/libesp32/berry_matter/src/embedded/Matter_UI.be
+++ b/lib/libesp32/berry_matter/src/embedded/Matter_UI.be
@@ -190,20 +190,20 @@ class Matter_UI
"|relay|relay_power|light0|light1|light2|light3|shutter|shutter+tilt"
"|gensw_btn"
"|temperature|pressure|illuminance|humidity|occupancy|onoff|contact|flow|rain|waterleak"
- "|airquality"
+ "|airquality|soil"
static var _CLASSES_TYPES_VIRTUAL =
"-virtual|v_relay|v_relay_power|v_light0|v_light1|v_light2|v_light3"
"|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"
+ "|v_airquality|v_soil"
static var _CLASSES_TYPES2= "|http_relay|http_relay_power|http_light0|http_light1|http_light2|http_light3"
"|http_temperature|http_pressure|http_illuminance|http_humidity"
"|http_occupancy|http_contact|http_flow|http_rain|http_waterleak"
- "|http_airquality"
+ "|http_airquality|http_soil"
static var _CLASSES_TYPES3= "|mqtt_relay|mqtt_light0|mqtt_light1|mqtt_light2|mqtt_light3"
"|mqtt_temperature|mqtt_pressure|mqtt_illuminance|mqtt_humidity"
"|mqtt_occupancy|mqtt_contact|mqtt_flow|mqtt_rain|mqtt_waterleak"
- "|mqtt_airquality"
+ "|mqtt_airquality|mqtt_soil"
var device
var matter_enabled
diff --git a/lib/libesp32/berry_matter/src/embedded/Matter_z_Autoconf.be b/lib/libesp32/berry_matter/src/embedded/Matter_z_Autoconf.be
index cc1d06394..d673154e3 100644
--- a/lib/libesp32/berry_matter/src/embedded/Matter_z_Autoconf.be
+++ b/lib/libesp32/berry_matter/src/embedded/Matter_z_Autoconf.be
@@ -233,6 +233,17 @@ class Matter_Autoconf
end
end
+ # soil moisture sensors
+ # Skip CHIRP sensors (xsns_48): their `Moisture` is raw capacitance, not percent
+ import string
+ for k1: k2l(sensors)
+ var sensor_2 = sensors[k1]
+ if isinstance(sensor_2, map) && sensor_2.contains("Moisture") && string.find(k1, "CHIRP") != 0
+ var soil_rule = k1 + "#Moisture"
+ ret.push({'type':'soil','filter':soil_rule})
+ end
+ end
+
return ret
end