Matter Soil Sensor device type with Soil Measurement cluster (Matter 1.6.0) (#25088)

This commit is contained in:
s-hadinger authored and GitHub committed 2026-09-29 15:50:13 +02:00
1 parent 1f543a3374
commit a242811ec8
9 files changed
+275 -7

No files matched your search

@@ -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
@@ -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 |
@@ -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"
@@ -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 <http://www.gnu.org/licenses/>.
#
#################################################################################
# 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("&#x1F331; %i%%", self.shadow_value != nil ? int(self.shadow_value) : nil))
end
#############################################################
#############################################################
end
matter.Plugin_Sensor_Soil = Matter_Plugin_Sensor_Soil
@@ -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 <http://www.gnu.org/licenses/>.
#
#################################################################################
# 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
@@ -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 <http://www.gnu.org/licenses/>.
#
#################################################################################
# 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
@@ -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
@@ -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