Skip to content

Commit 01d94a4

Browse files
Support Soil Sensor device type
1 parent 0298090 commit 01d94a4

File tree

8 files changed

+318
-11
lines changed

8 files changed

+318
-11
lines changed

drivers/SmartThings/matter-sensor/fingerprints.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,3 +350,8 @@ matterGeneric:
350350
deviceTypes:
351351
- id: 0x0306 # Flow Sensor
352352
deviceProfileName: flow-battery
353+
- id: "matter/soil/sensor"
354+
deviceLabel: Matter Soil Sensor
355+
deviceTypes:
356+
- id: 0x0045 # Soil Sensor
357+
deviceProfileName: humidity
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
local cluster_base = require "st.matter.cluster_base"
2+
local SoilMeasurementServerAttributes = require "embedded_clusters.SoilMeasurement.server.attributes"
3+
4+
local SoilMeasurement = {}
5+
6+
SoilMeasurement.ID = 0x0430
7+
SoilMeasurement.NAME = "SoilMeasurement"
8+
SoilMeasurement.server = {}
9+
SoilMeasurement.client = {}
10+
SoilMeasurement.server.attributes = SoilMeasurementServerAttributes:set_parent_cluster(SoilMeasurement)
11+
12+
function SoilMeasurement:get_attribute_by_id(attr_id)
13+
local attr_id_map = {
14+
[0x0000] = "SoilMoistureMeasurementLimits",
15+
[0x0001] = "SoilMoistureMeasuredValue",
16+
[0xFFF9] = "AcceptedCommandList",
17+
[0xFFFB] = "AttributeList",
18+
}
19+
local attr_name = attr_id_map[attr_id]
20+
if attr_name ~= nil then
21+
return self.attributes[attr_name]
22+
end
23+
return nil
24+
end
25+
26+
SoilMeasurement.attribute_direction_map = {
27+
["SoilMoistureMeasurementLimits"] = "server",
28+
["SoilMoistureMeasuredValue"] = "server",
29+
["AcceptedCommandList"] = "server",
30+
["AttributeList"] = "server",
31+
}
32+
33+
local attribute_helper_mt = {}
34+
attribute_helper_mt.__index = function(self, key)
35+
local direction = SoilMeasurement.attribute_direction_map[key]
36+
if direction == nil then
37+
error(string.format("Referenced unknown attribute %s on cluster %s", key, SoilMeasurement.NAME))
38+
end
39+
return SoilMeasurement[direction].attributes[key]
40+
end
41+
SoilMeasurement.attributes = {}
42+
setmetatable(SoilMeasurement.attributes, attribute_helper_mt)
43+
44+
setmetatable(SoilMeasurement, {__index = cluster_base})
45+
46+
return SoilMeasurement
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
local cluster_base = require "st.matter.cluster_base"
2+
local data_types = require "st.matter.data_types"
3+
local TLVParser = require "st.matter.TLV.TLVParser"
4+
5+
local SoilMoistureMeasuredValue = {
6+
ID = 0x0001,
7+
NAME = "SoilMoistureMeasuredValue",
8+
base_type = require "st.matter.data_types.Uint8",
9+
}
10+
11+
function SoilMoistureMeasuredValue:new_value(...)
12+
local o = self.base_type(table.unpack({...}))
13+
14+
return o
15+
end
16+
17+
function SoilMoistureMeasuredValue:read(device, endpoint_id)
18+
return cluster_base.read(
19+
device,
20+
endpoint_id,
21+
self._cluster.ID,
22+
self.ID,
23+
nil
24+
)
25+
end
26+
27+
function SoilMoistureMeasuredValue:subscribe(device, endpoint_id)
28+
return cluster_base.subscribe(
29+
device,
30+
endpoint_id,
31+
self._cluster.ID,
32+
self.ID,
33+
nil
34+
)
35+
end
36+
37+
function SoilMoistureMeasuredValue:set_parent_cluster(cluster)
38+
self._cluster = cluster
39+
return self
40+
end
41+
42+
function SoilMoistureMeasuredValue:build_test_report_data(
43+
device,
44+
endpoint_id,
45+
value,
46+
status
47+
)
48+
local data = data_types.validate_or_build_type(value, self.base_type)
49+
50+
return cluster_base.build_test_report_data(
51+
device,
52+
endpoint_id,
53+
self._cluster.ID,
54+
self.ID,
55+
data,
56+
status
57+
)
58+
end
59+
60+
function SoilMoistureMeasuredValue:deserialize(tlv_buf)
61+
local data = TLVParser.decode_tlv(tlv_buf)
62+
63+
return data
64+
end
65+
66+
setmetatable(SoilMoistureMeasuredValue, {__call = SoilMoistureMeasuredValue.new_value, __index = SoilMoistureMeasuredValue.base_type})
67+
return SoilMoistureMeasuredValue
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
local attr_mt = {}
2+
attr_mt.__index = function(self, key)
3+
local req_loc = string.format("embedded_clusters.SoilMeasurement.server.attributes.%s", key)
4+
local raw_def = require(req_loc)
5+
local cluster = rawget(self, "_cluster")
6+
raw_def:set_parent_cluster(cluster)
7+
return raw_def
8+
end
9+
10+
local SoilMeasurementServerAttributes = {}
11+
12+
function SoilMeasurementServerAttributes:set_parent_cluster(cluster)
13+
self._cluster = cluster
14+
return self
15+
end
16+
17+
setmetatable(SoilMeasurementServerAttributes, attr_mt)
18+
19+
return SoilMeasurementServerAttributes

drivers/SmartThings/matter-sensor/src/init.lua

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ if not pcall(function(cluster) return clusters[cluster] end,
1616
clusters.PressureMeasurement = require "embedded_clusters.PressureMeasurement"
1717
end
1818

19+
-- This can be removed once LuaLibs supports the SoilMeasurement cluster
20+
if not pcall(function(cluster) return clusters[cluster] end,
21+
"SoilMeasurement") then
22+
clusters.SoilMeasurement = require "embedded_clusters.SoilMeasurement"
23+
end
24+
1925
-- Include driver-side definitions when lua libs api version is < 10
2026
if version.api < 10 then
2127
clusters.AirQuality = require "embedded_clusters.AirQuality"
@@ -117,6 +123,9 @@ local matter_driver_template = {
117123
[clusters.RelativeHumidityMeasurement.ID] = {
118124
[clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.humidity_measured_value_handler
119125
},
126+
[clusters.SoilMeasurement.ID] = {
127+
[clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue.ID] = attribute_handlers.soil_moisture_measured_value_handler
128+
},
120129
[clusters.TemperatureMeasurement.ID] = {
121130
[clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.temperature_measured_value_handler,
122131
[clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.TEMP_MIN),
@@ -164,7 +173,8 @@ local matter_driver_template = {
164173
clusters.BooleanState.attributes.StateValue,
165174
},
166175
[capabilities.relativeHumidityMeasurement.ID] = {
167-
clusters.RelativeHumidityMeasurement.attributes.MeasuredValue
176+
clusters.RelativeHumidityMeasurement.attributes.MeasuredValue,
177+
clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue
168178
},
169179
[capabilities.temperatureAlarm.ID] = {
170180
clusters.BooleanState.attributes.StateValue,
@@ -243,9 +253,6 @@ local matter_driver_template = {
243253
clusters.RadonConcentrationMeasurement.attributes.MeasuredValue,
244254
clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit,
245255
},
246-
[capabilities.relativeHumidityMeasurement.ID] = {
247-
clusters.RelativeHumidityMeasurement.attributes.MeasuredValue
248-
},
249256
[capabilities.tvocHealthConcern.ID] = {
250257
clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue
251258
},

drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ local fields = require "sensor_utils.fields"
99
local device_cfg = require "sensor_utils.device_configuration"
1010
local version = require "version"
1111

12+
clusters.SoilMeasurement = require "embedded_clusters.SoilMeasurement"
13+
1214
local AttributeHandlers = {}
1315

1416

@@ -69,16 +71,24 @@ function AttributeHandlers.humidity_measured_value_handler(driver, device, ib, r
6971
end
7072

7173

74+
-- [[ SOIL MEASUREMENT CLUSTER ATTRIBUTES ]] --
75+
76+
function AttributeHandlers.soil_moisture_measured_value_handler(driver, device, ib, response)
77+
if ib.data.value == nil then return end
78+
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(ib.data.value))
79+
end
80+
81+
7282
-- [[ BOOLEAN STATE CLUSTER ATTRIBUTES ]] --
7383

7484
function AttributeHandlers.boolean_state_value_handler(driver, device, ib, response)
7585
local name
7686
for dt_name, _ in pairs(fields.BOOLEAN_DEVICE_TYPE_INFO) do
77-
local dt_ep_id = device:get_field(dt_name)
78-
if ib.endpoint_id == dt_ep_id then
79-
name = dt_name
80-
break
81-
end
87+
local dt_ep_id = device:get_field(dt_name)
88+
if ib.endpoint_id == dt_ep_id then
89+
name = dt_name
90+
break
91+
end
8292
end
8393
if name then
8494
device:emit_event_for_endpoint(ib.endpoint_id, fields.BOOLEAN_CAP_EVENT_MAP[ib.data.value][name])

drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ function DeviceConfiguration.match_profile(driver, device, battery_supported)
5959

6060
if device:supports_capability(capabilities.relativeHumidityMeasurement) then
6161
profile_name = profile_name .. "-humidity"
62+
-- Soil Sensor fingerprints to the humidity profile, so we should also check for
63+
-- TemperatureMeasurement, which is an optional cluster for this device type.
64+
if #device:get_endpoints(clusters.SoilMeasurement.ID) > 0 and
65+
#device:get_endpoints(clusters.TemperatureMeasurement.ID) > 0 then
66+
profile_name = "-temperature" .. profile_name
67+
end
6268
end
6369

6470
if device:supports_capability(capabilities.atmosphericPressureMeasurement) then
@@ -117,8 +123,7 @@ function DeviceConfiguration.match_profile(driver, device, battery_supported)
117123
-- remove leading "-"
118124
profile_name = string.sub(profile_name, 2)
119125

120-
device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name))
121126
device:try_update_metadata({profile = profile_name})
122127
end
123128

124-
return DeviceConfiguration
129+
return DeviceConfiguration
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
-- Copyright © 2026 SmartThings, Inc.
2+
-- Licensed under the Apache License, Version 2.0
3+
4+
local capabilities = require "st.capabilities"
5+
local clusters = require "st.matter.clusters"
6+
local t_utils = require "integration_test.utils"
7+
local test = require "integration_test"
8+
9+
local mock_device = test.mock_device.build_test_matter_device({
10+
profile = t_utils.get_profile_definition("humidity.yml"),
11+
manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000 },
12+
endpoints = {
13+
{
14+
endpoint_id = 0,
15+
clusters = {
16+
{ cluster_id = clusters.Basic.ID, cluster_type = "SERVER" },
17+
},
18+
device_types = {
19+
{ device_type_id = 0x0016, device_type_revision = 1 } -- RootNode
20+
}
21+
},
22+
{
23+
endpoint_id = 1,
24+
clusters = {
25+
{ cluster_id = clusters.SoilMeasurement.ID, cluster_type = "SERVER" },
26+
{ cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER" },
27+
},
28+
device_types = {
29+
{ device_type_id = 0x0045, device_type_revision = 1 } -- Soil Sensor
30+
}
31+
},
32+
}
33+
})
34+
35+
local subscribe_request
36+
37+
local cluster_subscribe_list = {
38+
clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue,
39+
}
40+
41+
local additional_subscribed_attributes = {
42+
clusters.TemperatureMeasurement.attributes.MeasuredValue,
43+
clusters.TemperatureMeasurement.attributes.MinMeasuredValue,
44+
clusters.TemperatureMeasurement.attributes.MaxMeasuredValue
45+
}
46+
47+
local function test_init()
48+
subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device)
49+
for i, cluster in ipairs(cluster_subscribe_list) do
50+
if i > 1 then
51+
subscribe_request:merge(cluster:subscribe(mock_device))
52+
end
53+
end
54+
55+
test.disable_startup_messages()
56+
test.mock_device.add_test_device(mock_device)
57+
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })
58+
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" })
59+
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
60+
end
61+
test.set_test_init_function(test_init)
62+
63+
local function update_device_profile()
64+
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })
65+
mock_device:expect_metadata_update({ profile = "temperature-humidity" })
66+
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
67+
68+
test.wait_for_events()
69+
70+
local updated_device_profile = t_utils.get_profile_definition("temperature-humidity.yml")
71+
test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = updated_device_profile }))
72+
for _, attr in ipairs(additional_subscribed_attributes) do
73+
subscribe_request:merge(attr:subscribe(mock_device))
74+
end
75+
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
76+
end
77+
78+
test.register_coroutine_test(
79+
"Relative humidity reports should generate correct messages",
80+
function()
81+
update_device_profile()
82+
test.wait_for_events()
83+
test.socket.matter:__queue_receive(
84+
{
85+
mock_device.id,
86+
clusters.RelativeHumidityMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 4049)
87+
}
88+
)
89+
test.socket.capability:__expect_send(
90+
mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 40 }))
91+
)
92+
93+
test.socket.matter:__queue_receive(
94+
{
95+
mock_device.id,
96+
clusters.RelativeHumidityMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 4050)
97+
}
98+
)
99+
test.socket.capability:__expect_send(
100+
mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 41 }))
101+
)
102+
end
103+
)
104+
105+
test.register_coroutine_test(
106+
"Temperature reports should generate correct messages",
107+
function()
108+
update_device_profile()
109+
test.wait_for_events()
110+
test.socket.matter:__queue_receive(
111+
{
112+
mock_device.id,
113+
clusters.TemperatureMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 40*100)
114+
}
115+
)
116+
test.socket.capability:__expect_send(
117+
mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 40.0, unit = "C" }))
118+
)
119+
end
120+
)
121+
122+
test.register_coroutine_test(
123+
"Min and max temperature attributes set capability constraint",
124+
function()
125+
update_device_profile()
126+
test.wait_for_events()
127+
test.socket.matter:__queue_receive(
128+
{
129+
mock_device.id,
130+
clusters.TemperatureMeasurement.attributes.MinMeasuredValue:build_test_report_data(mock_device, 1, 500)
131+
}
132+
)
133+
test.socket.matter:__queue_receive(
134+
{
135+
mock_device.id,
136+
clusters.TemperatureMeasurement.attributes.MaxMeasuredValue:build_test_report_data(mock_device, 1, 4000)
137+
}
138+
)
139+
test.socket.capability:__expect_send(
140+
mock_device:generate_test_message(
141+
"main",
142+
capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = 5.00, maximum = 40.00 }, unit = "C" })
143+
)
144+
)
145+
end
146+
)
147+
148+
test.run_registered_tests()

0 commit comments

Comments
 (0)