Skip to content

Commit 00cad14

Browse files
Add support for optional clusters and modular profiling for irrigation
system This commit adds support for optional clusters: * OperationalState * FlowMeasurement Additionally, the level, operationalState, and flowSensor capabilities are supported via modular profiling.
1 parent 2d5cab2 commit 00cad14

File tree

8 files changed

+179
-41
lines changed

8 files changed

+179
-41
lines changed

drivers/SmartThings/matter-switch/profiles/irrigation-system-level.yml

Lines changed: 0 additions & 19 deletions
This file was deleted.

drivers/SmartThings/matter-switch/profiles/irrigation-system.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ components:
44
capabilities:
55
- id: valve
66
version: 1
7+
- id: level
8+
version: 1
9+
config:
10+
values:
11+
- key: "level.value"
12+
range: [0, 100]
13+
optional: true
14+
- id: flowSensor
15+
version: 1
16+
optional: true
17+
- id: operationalState
18+
version: 1
19+
optional: true
720
- id: firmwareUpdate
821
version: 1
922
- id: refresh

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

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ local matter_driver_template = {
147147
[clusters.FanControl.attributes.FanModeSequence.ID] = attribute_handlers.fan_mode_sequence_handler,
148148
[clusters.FanControl.attributes.PercentCurrent.ID] = attribute_handlers.percent_current_handler
149149
},
150+
[clusters.FlowMeasurement.ID] = {
151+
[clusters.FlowMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.flow_attr_handler,
152+
[clusters.FlowMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.flow_attr_handler_factory(FLOW_MIN),
153+
[clusters.FlowMeasurement.attributes.MaxMeasuredValue.ID] = attribute_handlers.flow_attr_handler_factory(FLOW_MAX)
154+
},
150155
[clusters.IlluminanceMeasurement.ID] = {
151156
[clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.illuminance_measured_value_handler
152157
},
@@ -158,6 +163,11 @@ local matter_driver_template = {
158163
[clusters.OccupancySensing.ID] = {
159164
[clusters.OccupancySensing.attributes.Occupancy.ID] = attribute_handlers.occupancy_handler,
160165
},
166+
[clusters.OperationalState.ID] = {
167+
[clusters.OperationalState.attributes.AcceptedCommandList.ID] = attribute_handlers.operational_state_accepted_command_list_attr_handler,
168+
[clusters.OperationalState.attributes.OperationalState.ID] = attribute_handlers.operational_state_attr_handler,
169+
[clusters.OperationalState.attributes.OperationalError.ID] = attribute_handlers.operational_error_attr_handler
170+
},
161171
[clusters.OnOff.ID] = {
162172
[clusters.OnOff.attributes.OnOff.ID] = attribute_handlers.on_off_attr_handler,
163173
},
@@ -225,24 +235,34 @@ local matter_driver_template = {
225235
[capabilities.fanSpeedPercent.ID] = {
226236
clusters.FanControl.attributes.PercentCurrent
227237
},
238+
[capabilities.flowMeasurement.ID] = {
239+
clusters.FlowMeasurement.attributes.MeasuredValue,
240+
clusters.FlowMeasurement.attributes.MinMeasuredValue,
241+
clusters.FlowMeasurement.attributes.MaxMeasuredValue
242+
},
228243
[capabilities.illuminanceMeasurement.ID] = {
229244
clusters.IlluminanceMeasurement.attributes.MeasuredValue
230245
},
231-
[capabilities.motionSensor.ID] = {
232-
clusters.OccupancySensing.attributes.Occupancy
233-
},
234246
[capabilities.level.ID] = {
235247
clusters.ValveConfigurationAndControl.attributes.CurrentLevel
236248
},
237-
[capabilities.switch.ID] = {
238-
clusters.OnOff.attributes.OnOff
249+
[capabilities.motionSensor.ID] = {
250+
clusters.OccupancySensing.attributes.Occupancy
251+
},
252+
[capabilities.operationalState.ID] = {
253+
clusters.OperationalState.attributes.AcceptedCommandList,
254+
clusters.OperationalState.attributes.OperationalState,
255+
clusters.OperationalState.attributes.OperationalError
239256
},
240257
[capabilities.powerMeter.ID] = {
241258
clusters.ElectricalPowerMeasurement.attributes.ActivePower
242259
},
243260
[capabilities.relativeHumidityMeasurement.ID] = {
244261
clusters.RelativeHumidityMeasurement.attributes.MeasuredValue
245262
},
263+
[capabilities.switch.ID] = {
264+
clusters.OnOff.attributes.OnOff
265+
},
246266
[capabilities.switchLevel.ID] = {
247267
clusters.LevelControl.attributes.CurrentLevel,
248268
clusters.LevelControl.attributes.MaxLevel,
@@ -286,6 +306,10 @@ local matter_driver_template = {
286306
[capabilities.level.ID] = {
287307
[capabilities.level.commands.setLevel.NAME] = capability_handlers.handle_set_level
288308
},
309+
[capabilities.operationalState.ID] = {
310+
[capabilities.operationalState.commands.pause.NAME] = capability_handlers.handle_operational_state_pause,
311+
[capabilities.operationalState.commands.resume.NAME] = capability_handlers.handle_operational_state_resume
312+
},
289313
[capabilities.switch.ID] = {
290314
[capabilities.switch.commands.off.NAME] = capability_handlers.handle_switch_off,
291315
[capabilities.switch.commands.on.NAME] = capability_handlers.handle_switch_on,

drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,4 +538,74 @@ function AttributeHandlers.percent_current_handler(driver, device, ib, response)
538538
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(ib.data.value))
539539
end
540540

541+
542+
-- [[ OPERATIONAL STATE CLUSTER ATTRIBUTES ]] --
543+
544+
function AttributeHandlers.operational_state_accepted_command_list_attr_handler(driver, device, ib, response)
545+
local accepted_command_list = {}
546+
for _, accepted_command in ipairs(ib.data.elements) do
547+
local accepted_command_id = accepted_command.value
548+
if fields.operational_state_command_map[accepted_command_id] ~= nil then
549+
table.insert(accepted_command_list, fields.operational_state_command_map[accepted_command_id])
550+
end
551+
end
552+
local event = capabilities.operationalState.supportedCommands(accepted_command_list, {visibility = {displayed = false}})
553+
device:emit_event_for_endpoint(ib.endpoint_id, event)
554+
end
555+
556+
function AttributeHandlers.operational_state_attr_handler(driver, device, ib, response)
557+
if ib.data.value == clusters.OperationalState.types.OperationalStateEnum.STOPPED then
558+
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.stopped())
559+
elseif ib.data.value == clusters.OperationalState.types.OperationalStateEnum.RUNNING then
560+
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.running())
561+
elseif ib.data.value == clusters.OperationalState.types.OperationalStateEnum.PAUSED then
562+
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.paused())
563+
end
564+
end
565+
566+
function AttributeHandlers.operational_error_attr_handler(driver, device, ib, response)
567+
if version.api < 10 then
568+
clusters.OperationalState.types.ErrorStateStruct:augment_type(ib.data)
569+
end
570+
local operationalError = ib.data.elements.error_state_id.value
571+
if operationalError == clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_START_OR_RESUME then
572+
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.unableToStartOrResume())
573+
elseif operationalError == clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_COMPLETE_OPERATION then
574+
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.unableToCompleteOperation())
575+
elseif operationalError == clusters.OperationalState.types.ErrorStateEnum.COMMAND_INVALID_IN_STATE then
576+
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.commandInvalidInCurrentState())
577+
end
578+
end
579+
580+
function AttributeHandlers.flow_attr_handler(driver, device, ib, response)
581+
local measured_value = ib.data.value
582+
if measured_value ~= nil then
583+
local flow = measured_value / 10.0
584+
local unit = "m^3/h"
585+
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flow({value = flow, unit = unit}))
586+
end
587+
end
588+
589+
function AttributeHandlers.flow_attr_handler_factory(minOrMax)
590+
return function(driver, device, ib, response)
591+
if ib.data.value == nil then
592+
return
593+
end
594+
local flow_bound = ib.data.value / 10.0
595+
local unit = "m^3/h"
596+
set_field_for_endpoint(device, FLOW_BOUND_RECEIVED..minOrMax, ib.endpoint_id, flow_bound)
597+
local min = get_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MIN, ib.endpoint_id)
598+
local max = get_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MAX, ib.endpoint_id)
599+
if min ~= nil and max ~= nil then
600+
if min < max then
601+
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flowRange({ value = { minimum = min, maximum = max }, unit = unit }))
602+
set_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MIN, ib.endpoint_id, nil)
603+
set_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MAX, ib.endpoint_id, nil)
604+
else
605+
device.log.warn_with({hub_logs = true}, string.format("Device reported a min flow measurement %d that is not lower than the reported max flow measurement %d", min, max))
606+
end
607+
end
608+
end
609+
end
610+
541611
return AttributeHandlers

drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,21 @@ function CapabilityHandlers.handle_reset_energy_meter(driver, device, cmd)
189189
end
190190
end
191191

192+
193+
-- [[ OPERATIONAL STATE CAPABILITY COMMANDS ]] --
194+
195+
function CapabilityHandlers.handle_operational_state_resume(driver, device, cmd)
196+
local endpoint_id = device:component_to_endpoint(cmd.component)
197+
device:send(clusters.OperationalState.server.commands.Resume(device, endpoint_id))
198+
device:send(clusters.OperationalState.attributes.OperationalState:read(device, endpoint_id))
199+
device:send(clusters.OperationalState.attributes.OperationalError:read(device, endpoint_id))
200+
end
201+
202+
function CapabilityHandlers.handle_operational_state_pause(driver, device, cmd)
203+
local endpoint_id = device:component_to_endpoint(cmd.component)
204+
device:send(clusters.OperationalState.server.commands.Pause(device, endpoint_id))
205+
device:send(clusters.OperationalState.attributes.OperationalState:read(device, endpoint_id))
206+
device:send(clusters.OperationalState.attributes.OperationalError:read(device, endpoint_id))
207+
end
208+
192209
return CapabilityHandlers

drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ function ButtonDeviceConfiguration.configure_buttons(device)
158158
end
159159

160160
function ValveDeviceConfiguration.assign_profile_for_valve_ep(device, valve_ep_id)
161-
local profile = "irrigation-system"
161+
local profile = "water-valve"
162162

163163
for _, ep in ipairs(device.endpoints) do
164164
if ep.endpoint_id == valve_ep_id then
@@ -231,21 +231,42 @@ function DeviceConfiguration.match_profile(driver, device)
231231

232232
if is_irrigation_system then
233233
updated_profile = "irrigation-system"
234-
table.sort(valve_ep_ids)
235-
for _, ep in ipairs(device.endpoints) do
236-
if ep.endpoint_id == valve_ep_ids[1] then
237-
if switch_utils.find_cluster_on_ep(ep, clusters.ValveConfigurationAndControl.ID) then
238-
for _, cluster in ipairs(ep.clusters) do
239-
if cluster.cluster_id == clusters.ValveConfigurationAndControl.ID and
240-
cluster.feature_map and (cluster.feature_map & clusters.ValveConfigurationAndControl.types.Feature.LEVEL) ~= 0 then
241-
updated_profile = updated_profile .. "-level"
242-
break
243-
end
244-
end
234+
if version.api >= 14 and version.rpc >= 8 then
235+
local main_component_capabilities = {}
236+
local optional_supported_component_capabilities = {}
237+
local MAIN_COMPONENT_IDX, CAPABILITIES_LIST_IDX = 1, 2
238+
239+
table.sort(valve_ep_ids)
240+
local main_valve_ep = switch_utils.get_endpoint_info(device, valve_ep_ids[1])
241+
for _, cluster in ipairs(main_valve_ep.clusters) do
242+
if cluster.cluster_id == clusters.ValveConfigurationAndControl.ID and
243+
cluster.feature_map and (cluster.feature_map & clusters.ValveConfigurationAndControl.types.Feature.LEVEL) ~= 0 then
244+
table.insert(main_component_capabilities, capabilities.level.ID)
245+
elseif cluster.cluster_id == clusters.OperationalState.ID then
246+
table.insert(main_component_capabilities, capabilities.operationalState.ID)
247+
elseif cluster.cluster_id == clusters.FlowMeasurement.ID then
248+
table.insert(main_component_capabilities, capabilities.flowSensor.ID)
245249
end
246-
break
247250
end
251+
252+
table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities})
253+
254+
device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities})
255+
256+
-- earlier modular profile gating (min api v14, rpc 8) ensures we are running >= 0.57 FW.
257+
-- This gating specifies a workaround required only for 0.57 FW, which is not needed for 0.58 and higher.
258+
if version.api < 15 or version.rpc < 9 then
259+
-- add mandatory capabilities for subscription
260+
local total_supported_capabilities = optional_supported_component_capabilities
261+
table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.refresh.ID)
262+
table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.firmwareUpdate.ID)
263+
264+
device:set_field(fields.SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true })
265+
end
266+
else
267+
device:try_update_metadata({profile = profile_name})
248268
end
269+
249270
if #valve_ep_ids > 1 then
250271
table.remove(valve_ep_ids, 1) -- the first valve ep is accounted for in the main irrigation system profile
251272
ValveDeviceConfiguration.create_child_devices(driver, device, valve_ep_ids, default_endpoint_id)

drivers/SmartThings/matter-switch/src/switch_utils/fields.lua

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ SwitchFields.LEVEL_BOUND_RECEIVED = "__level_bound_received"
9797
SwitchFields.LEVEL_MIN = "__level_min"
9898
SwitchFields.LEVEL_MAX = "__level_max"
9999
SwitchFields.COLOR_MODE = "__color_mode"
100+
SwitchFields.FLOW_BOUND_RECEIVED = "__flow_bound_received"
101+
SwitchFields.FLOW_MIN = "__flow_min"
102+
SwitchFields.FLOW_MAX = "__flow_max"
100103

101104
SwitchFields.updated_fields = {
102105
{ current_field_name = "__component_to_endpoint_map_button", updated_field_name = SwitchFields.COMPONENT_TO_ENDPOINT_MAP },
@@ -191,6 +194,8 @@ SwitchFields.TRANSITION_TIME = 0 --1/10ths of a second
191194
SwitchFields.OPTIONS_MASK = 0x01
192195
SwitchFields.OPTIONS_OVERRIDE = 0x01
193196

197+
SwitchFields.SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities"
198+
194199

195200
SwitchFields.supported_capabilities = {
196201
capabilities.audioMute,
@@ -206,6 +211,7 @@ SwitchFields.supported_capabilities = {
206211
capabilities.energyMeter,
207212
capabilities.fanMode,
208213
capabilities.fanSpeedPercent,
214+
capabilities.flowSensor,
209215
capabilities.hdr,
210216
capabilities.illuminanceMeasurement,
211217
capabilities.imageControl,
@@ -214,6 +220,7 @@ SwitchFields.supported_capabilities = {
214220
capabilities.mechanicalPanTiltZoom,
215221
capabilities.motionSensor,
216222
capabilities.nightVision,
223+
capabilities.operationalState,
217224
capabilities.powerMeter,
218225
capabilities.powerConsumptionReport,
219226
capabilities.relativeHumidityMeasurement,
@@ -308,4 +315,9 @@ SwitchFields.device_type_attribute_map = {
308315
}
309316
}
310317

318+
SwitchFields.operational_state_command_map = {
319+
[clusters.OperationalState.commands.Pause.ID] = "pause",
320+
[clusters.OperationalState.commands.Resume.ID] = "resume"
321+
}
322+
311323
return SwitchFields

drivers/SmartThings/matter-switch/src/test/test_matter_irrigation_system.lua

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ local endpoints = {
2323
-- Mock device representing an irrigation system with 3 valve endpoints
2424
local mock_irrigation_system = test.mock_device.build_test_matter_device({
2525
label = "Matter Irrigation System",
26-
profile = t_utils.get_profile_definition("irrigation-system-level.yml"),
26+
profile = t_utils.get_profile_definition("irrigation-system.yml"),
2727
manufacturer_info = {
2828
vendor_id = 0x0000,
2929
product_id = 0x0000,
@@ -96,7 +96,7 @@ local mock_children = {}
9696
for i, endpoint in ipairs(mock_irrigation_system.endpoints) do
9797
if endpoint.endpoint_id == 3 or endpoint.endpoint_id == 4 then
9898
local child_data = {
99-
profile = t_utils.get_profile_definition("irrigation-system-level.yml"),
99+
profile = t_utils.get_profile_definition("irrigation-system.yml"),
100100
device_network_id = string.format("%s:%d", mock_irrigation_system.id, endpoint.endpoint_id),
101101
parent_device_id = mock_irrigation_system.id,
102102
parent_assigned_child_key = string.format("%d", endpoint.endpoint_id)
@@ -128,14 +128,14 @@ local function test_init()
128128
mock_irrigation_system:expect_device_create({
129129
type = "EDGE_CHILD",
130130
label = string.format("Matter Irrigation System Valve %d", i - 2),
131-
profile = "irrigation-system-level",
131+
profile = "water-valve-level",
132132
parent_device_id = mock_irrigation_system.id,
133133
parent_assigned_child_key = string.format("%d", i)
134134
})
135135
end
136136
test.socket.matter:__expect_send({mock_irrigation_system.id, subscribe_request})
137137
test.socket.device_lifecycle:__queue_receive({ mock_irrigation_system.id, "doConfigure" })
138-
mock_irrigation_system:expect_metadata_update({ profile = "irrigation-system-level" })
138+
mock_irrigation_system:expect_metadata_update({ profile = "irrigation-system" })
139139
mock_irrigation_system:expect_metadata_update({ provisioning_state = "PROVISIONED" })
140140
end
141141
test.set_test_init_function(test_init)

0 commit comments

Comments
 (0)