diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index e418d34d3b..74f1799509 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -3992,6 +3992,11 @@ matterGeneric: deviceTypes: - id: 0x0110 # Mounted Dimmable Load Control deviceProfileName: switch-level + - id: "matter/irrigation-system" + deviceLabel: Matter Irrigation System + deviceTypes: + - id: 0x0040 # Irrigation System + deviceProfileName: irrigation-system - id: "matter/water-valve" deviceLabel: Matter Water Valve deviceTypes: diff --git a/drivers/SmartThings/matter-switch/profiles/irrigation-system.yml b/drivers/SmartThings/matter-switch/profiles/irrigation-system.yml new file mode 100644 index 0000000000..03321887d7 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/irrigation-system.yml @@ -0,0 +1,26 @@ +name: irrigation-system +components: +- id: main + capabilities: + - id: valve + version: 1 + - id: level + version: 1 + config: + values: + - key: "level.value" + range: [0, 100] + optional: true + - id: flowMeasurement + version: 1 + optional: true + - id: operationalState + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: IrrigationSystem + diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index e8d1b8330a..61f6e64bca 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -159,6 +159,11 @@ local matter_driver_template = { [clusters.FanControl.attributes.FanModeSequence.ID] = attribute_handlers.fan_mode_sequence_handler, [clusters.FanControl.attributes.PercentCurrent.ID] = attribute_handlers.percent_current_handler }, + [clusters.FlowMeasurement.ID] = { + [clusters.FlowMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.flow_attr_handler, + [clusters.FlowMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.flow_attr_handler_factory(fields.FLOW_MIN), + [clusters.FlowMeasurement.attributes.MaxMeasuredValue.ID] = attribute_handlers.flow_attr_handler_factory(fields.FLOW_MAX) + }, [clusters.IlluminanceMeasurement.ID] = { [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.illuminance_measured_value_handler }, @@ -170,6 +175,11 @@ local matter_driver_template = { [clusters.OccupancySensing.ID] = { [clusters.OccupancySensing.attributes.Occupancy.ID] = attribute_handlers.occupancy_handler, }, + [clusters.OperationalState.ID] = { + [clusters.OperationalState.attributes.AcceptedCommandList.ID] = attribute_handlers.operational_state_accepted_command_list_attr_handler, + [clusters.OperationalState.attributes.OperationalState.ID] = attribute_handlers.operational_state_attr_handler, + [clusters.OperationalState.attributes.OperationalError.ID] = attribute_handlers.operational_error_attr_handler + }, [clusters.OnOff.ID] = { [clusters.OnOff.attributes.OnOff.ID] = attribute_handlers.on_off_attr_handler, }, @@ -237,17 +247,24 @@ local matter_driver_template = { [capabilities.fanSpeedPercent.ID] = { clusters.FanControl.attributes.PercentCurrent }, + [capabilities.flowMeasurement.ID] = { + clusters.FlowMeasurement.attributes.MeasuredValue, + clusters.FlowMeasurement.attributes.MinMeasuredValue, + clusters.FlowMeasurement.attributes.MaxMeasuredValue + }, [capabilities.illuminanceMeasurement.ID] = { clusters.IlluminanceMeasurement.attributes.MeasuredValue }, - [capabilities.motionSensor.ID] = { - clusters.OccupancySensing.attributes.Occupancy - }, [capabilities.level.ID] = { clusters.ValveConfigurationAndControl.attributes.CurrentLevel }, - [capabilities.switch.ID] = { - clusters.OnOff.attributes.OnOff + [capabilities.motionSensor.ID] = { + clusters.OccupancySensing.attributes.Occupancy + }, + [capabilities.operationalState.ID] = { + clusters.OperationalState.attributes.AcceptedCommandList, + clusters.OperationalState.attributes.OperationalState, + clusters.OperationalState.attributes.OperationalError }, [capabilities.powerMeter.ID] = { clusters.ElectricalPowerMeasurement.attributes.ActivePower @@ -255,6 +272,9 @@ local matter_driver_template = { [capabilities.relativeHumidityMeasurement.ID] = { clusters.RelativeHumidityMeasurement.attributes.MeasuredValue }, + [capabilities.switch.ID] = { + clusters.OnOff.attributes.OnOff + }, [capabilities.switchLevel.ID] = { clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, @@ -298,6 +318,10 @@ local matter_driver_template = { [capabilities.level.ID] = { [capabilities.level.commands.setLevel.NAME] = capability_handlers.handle_set_level }, + [capabilities.operationalState.ID] = { + [capabilities.operationalState.commands.pause.NAME] = capability_handlers.handle_operational_state_pause, + [capabilities.operationalState.commands.resume.NAME] = capability_handlers.handle_operational_state_resume + }, [capabilities.switch.ID] = { [capabilities.switch.commands.off.NAME] = capability_handlers.handle_switch_off, [capabilities.switch.commands.on.NAME] = capability_handlers.handle_switch_on, @@ -324,6 +348,7 @@ local matter_driver_template = { capabilities.energyMeter, capabilities.fanMode, capabilities.fanSpeedPercent, + capabilities.flowMeasurement, capabilities.hdr, capabilities.illuminanceMeasurement, capabilities.imageControl, @@ -332,6 +357,7 @@ local matter_driver_template = { capabilities.mechanicalPanTiltZoom, capabilities.motionSensor, capabilities.nightVision, + capabilities.operationalState, capabilities.powerMeter, capabilities.powerConsumptionReport, capabilities.relativeHumidityMeasurement, diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua index ae76709be8..48d7a3c383 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -534,4 +534,74 @@ function AttributeHandlers.percent_current_handler(driver, device, ib, response) device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(ib.data.value)) end + +-- [[ OPERATIONAL STATE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.operational_state_accepted_command_list_attr_handler(driver, device, ib, response) + local accepted_command_list = {} + for _, accepted_command in ipairs(ib.data.elements) do + local accepted_command_id = accepted_command.value + if fields.operational_state_command_map[accepted_command_id] ~= nil then + table.insert(accepted_command_list, fields.operational_state_command_map[accepted_command_id]) + end + end + local event = capabilities.operationalState.supportedCommands(accepted_command_list, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.operational_state_attr_handler(driver, device, ib, response) + if ib.data.value == clusters.OperationalState.types.OperationalStateEnum.STOPPED then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.stopped()) + elseif ib.data.value == clusters.OperationalState.types.OperationalStateEnum.RUNNING then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.running()) + elseif ib.data.value == clusters.OperationalState.types.OperationalStateEnum.PAUSED then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.paused()) + end +end + +function AttributeHandlers.operational_error_attr_handler(driver, device, ib, response) + if version.api < 10 then + clusters.OperationalState.types.ErrorStateStruct:augment_type(ib.data) + end + local operationalError = ib.data.elements.error_state_id.value + if operationalError == clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_START_OR_RESUME then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.unableToStartOrResume()) + elseif operationalError == clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_COMPLETE_OPERATION then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.unableToCompleteOperation()) + elseif operationalError == clusters.OperationalState.types.ErrorStateEnum.COMMAND_INVALID_IN_STATE then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.commandInvalidInCurrentState()) + end +end + +function AttributeHandlers.flow_attr_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local flow = measured_value / 10.0 + local unit = "m^3/h" + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flow({value = flow, unit = unit})) + end +end + +function AttributeHandlers.flow_attr_handler_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local flow_bound = ib.data.value / 10.0 + local unit = "m^3/h" + switch_utils.set_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..minOrMax, ib.endpoint_id, flow_bound) + local min = switch_utils.get_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MIN, ib.endpoint_id) + local max = switch_utils.get_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MAX, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flowRange({ value = { minimum = min, maximum = max }, unit = unit })) + switch_utils.set_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MIN, ib.endpoint_id, nil) + switch_utils.set_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MAX, ib.endpoint_id, nil) + else + 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)) + end + end + end +end + return AttributeHandlers diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua index f96686b73a..ec127845c4 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua @@ -189,4 +189,21 @@ function CapabilityHandlers.handle_reset_energy_meter(driver, device, cmd) end end + +-- [[ OPERATIONAL STATE CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_operational_state_resume(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.OperationalState.server.commands.Resume(device, endpoint_id)) + device:send(clusters.OperationalState.attributes.OperationalState:read(device, endpoint_id)) + device:send(clusters.OperationalState.attributes.OperationalError:read(device, endpoint_id)) +end + +function CapabilityHandlers.handle_operational_state_pause(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.OperationalState.server.commands.Pause(device, endpoint_id)) + device:send(clusters.OperationalState.attributes.OperationalState:read(device, endpoint_id)) + device:send(clusters.OperationalState.attributes.OperationalError:read(device, endpoint_id)) +end + return CapabilityHandlers diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua index 8542972320..aa26e09fec 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -20,10 +20,11 @@ local ChildConfiguration = {} local SwitchDeviceConfiguration = {} local ButtonDeviceConfiguration = {} local FanDeviceConfiguration = {} +local ValveDeviceConfiguration = {} function ChildConfiguration.create_or_update_child_devices(driver, device, server_cluster_ep_ids, default_endpoint_id, assign_profile_fn) if #server_cluster_ep_ids == 1 and server_cluster_ep_ids[1] == default_endpoint_id then -- no children will be created - return + return end table.sort(server_cluster_ep_ids) @@ -74,7 +75,6 @@ function FanDeviceConfiguration.assign_profile_for_fan_ep(device, server_fan_ep_ return "fan-modular", optional_supported_component_capabilities end - function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_onoff_ep_id, is_child_device) local ep_info = switch_utils.get_endpoint_info(device, server_onoff_ep_id) @@ -187,6 +187,47 @@ function ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep end end +function ValveDeviceConfiguration.assign_profile_for_valve_ep(device, valve_ep_id, is_child_device) + local profile_name = "irrigation-system" + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + + if is_child_device then + profile_name = "water-valve" + if switch_utils.find_cluster_on_ep( + switch_utils.get_endpoint_info(device, valve_ep_id), + clusters.ValveConfigurationAndControl.ID, + {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL} + ) then + profile_name = profile_name .. "-level" + end + return profile_name + end + + local valve_ep_info = switch_utils.get_endpoint_info(device, valve_ep_id) + for _, cluster in ipairs(valve_ep_info.clusters) do + if cluster.cluster_id == clusters.ValveConfigurationAndControl.ID and + cluster.feature_map and (cluster.feature_map & clusters.ValveConfigurationAndControl.types.Feature.LEVEL) ~= 0 then + table.insert(main_component_capabilities, capabilities.level.ID) + break + end + end + + local irrigation_system_device_type_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.IRRIGATION_SYSTEM) + if #irrigation_system_device_type_ep_ids > 0 then + local irrigation_system_ep_info = switch_utils.get_endpoint_info(device, irrigation_system_device_type_ep_ids[1]) + for _, cluster in ipairs(irrigation_system_ep_info.clusters) do + if cluster.cluster_id == clusters.FlowMeasurement.ID then + table.insert(main_component_capabilities, capabilities.flowMeasurement.ID) + elseif cluster.cluster_id == clusters.OperationalState.ID then + table.insert(main_component_capabilities, capabilities.operationalState.ID) + end + end + end + table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + return profile_name, optional_supported_component_capabilities +end + -- [[ PROFILE MATCHING AND CONFIGURATIONS ]] -- @@ -206,7 +247,15 @@ function DeviceConfiguration.match_profile(driver, device) local optional_component_capabilities local updated_profile - if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) > 0 then + local irrigation_system_device_type_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.IRRIGATION_SYSTEM) + local valve_ep_ids = device:get_endpoints(clusters.ValveConfigurationAndControl.ID) + if #irrigation_system_device_type_ep_ids > 0 then + updated_profile, optional_component_capabilities = ValveDeviceConfiguration.assign_profile_for_valve_ep(device, default_endpoint_id, false) + device:set_field(fields.MODULAR_PROFILE_UPDATED, true) + if #valve_ep_ids > 1 then + ChildConfiguration.create_or_update_child_devices(driver, device, valve_ep_ids, default_endpoint_id, ValveDeviceConfiguration.assign_profile_for_valve_ep) + end + elseif #valve_ep_ids > 0 then updated_profile = "water-valve" if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then @@ -256,5 +305,5 @@ end return { DeviceCfg = DeviceConfiguration, SwitchCfg = SwitchDeviceConfiguration, - ButtonCfg = ButtonDeviceConfiguration + ButtonCfg = ButtonDeviceConfiguration, } diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index db66c2965c..d785b02453 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -1,6 +1,8 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local clusters = require "st.matter.clusters" + local SwitchFields = {} SwitchFields.MOST_RECENT_TEMP = "mostRecentTemp" @@ -30,9 +32,11 @@ SwitchFields.DEVICE_TYPE_ID = { ELECTRICAL_SENSOR = 0x0510, FAN = 0x002B, GENERIC_SWITCH = 0x000F, + IRRIGATION_SYSTEM = 0x0040, MOUNTED_ON_OFF_CONTROL = 0x010F, MOUNTED_DIMMABLE_LOAD_CONTROL = 0x0110, ON_OFF_PLUG_IN_UNIT = 0x010A, + WATER_VALVE = 0x0042, LIGHT = { ON_OFF = 0x0100, DIMMABLE = 0x0101, @@ -82,6 +86,11 @@ SwitchFields.LEVEL_BOUND_RECEIVED = "__level_bound_received" SwitchFields.LEVEL_MIN = "__level_min" SwitchFields.LEVEL_MAX = "__level_max" SwitchFields.COLOR_MODE = "__color_mode" +SwitchFields.FLOW_BOUND_RECEIVED = "__flow_bound_received" +SwitchFields.FLOW_MIN = "__flow_min" +SwitchFields.FLOW_MAX = "__flow_max" + +SwitchFields.SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities" SwitchFields.SUBSCRIBED_ATTRIBUTES_KEY = "__subscribed_attributes" @@ -134,6 +143,11 @@ SwitchFields.switch_category_vendor_overrides = { {0xEEE2, 0xAB08, 0xAB31, 0xAB04, 0xAB01, 0xAB43, 0xAB02, 0xAB03, 0xAB05} } +SwitchFields.operational_state_command_map = { + [clusters.OperationalState.commands.Pause.ID] = "pause", + [clusters.OperationalState.commands.Resume.ID] = "resume" +} + --- stores a table of endpoints that support the Electrical Sensor device type, used during profiling --- in AvailableEndpoints and PartsList handlers for SET and TREE PowerTopology features, respectively SwitchFields.ELECTRICAL_SENSOR_EPS = "__electrical_sensor_eps" diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index da5376f031..e2f2b55f79 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -193,6 +193,11 @@ function utils.find_default_endpoint(device) end end + local water_valve_eps = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.WATER_VALVE) + if #water_valve_eps > 0 then + return get_first_non_zero_endpoint(water_valve_eps) + end + device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) return device.MATTER_DEFAULT_ENDPOINT end diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_irrigation_system.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_irrigation_system.lua new file mode 100644 index 0000000000..4b68728f20 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_irrigation_system.lua @@ -0,0 +1,435 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local test = require "integration_test" +local version = require "version" + +if version.api < 11 then + clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" +end + +local endpoints = { + ROOT_EP = 0, + IRRIGATION_SYSTEM_EP = 1, + VALVE_1_EP = 2, + VALVE_2_EP = 3, + VALVE_3_EP = 4 +} + +-- Mock device representing an irrigation system with 3 valve endpoints +local mock_irrigation_system = test.mock_device.build_test_matter_device({ + label = "Matter Irrigation System", + profile = t_utils.get_profile_definition("irrigation-system.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = endpoints.ROOT_EP, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = endpoints.IRRIGATION_SYSTEM_EP, + clusters = { + {cluster_id = clusters.Descriptor.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.FlowMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.OperationalState.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0040, device_type_revision = 1} -- Irrigation System + } + }, + { + endpoint_id = endpoints.VALVE_1_EP, + clusters = { + { + cluster_id = clusters.ValveConfigurationAndControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 2 -- LEVEL feature + }, + }, + device_types = { + {device_type_id = 0x0042, device_type_revision = 1} -- Water Valve + } + }, + { + endpoint_id = endpoints.VALVE_2_EP, + clusters = { + { + cluster_id = clusters.ValveConfigurationAndControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 2 -- LEVEL feature + }, + }, + device_types = { + {device_type_id = 0x0042, device_type_revision = 1} -- Water Valve + } + }, + { + endpoint_id = endpoints.VALVE_3_EP, + clusters = { + { + cluster_id = clusters.ValveConfigurationAndControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 2 -- LEVEL feature + }, + }, + device_types = { + {device_type_id = 0x0042, device_type_revision = 1} -- Water Valve + } + } + } +}) + +local mock_children = {} +for _, endpoint in ipairs(mock_irrigation_system.endpoints) do + if endpoint.endpoint_id == 3 or endpoint.endpoint_id == 4 then + local child_data = { + profile = t_utils.get_profile_definition("water-valve-level.yml"), + device_network_id = string.format("%s:%d", mock_irrigation_system.id, endpoint.endpoint_id), + parent_device_id = mock_irrigation_system.id, + parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) + } + mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) + end +end + +local subscribe_request + +local function test_init() + test.mock_device.add_test_device(mock_irrigation_system) + local cluster_subscribe_list = { + clusters.ValveConfigurationAndControl.attributes.CurrentState, + clusters.ValveConfigurationAndControl.attributes.CurrentLevel, + } + subscribe_request = cluster_subscribe_list[1]:subscribe(mock_irrigation_system) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_irrigation_system)) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_irrigation_system.id, "added" }) + test.socket.matter:__expect_send({mock_irrigation_system.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_irrigation_system.id, "init" }) + test.socket.matter:__expect_send({mock_irrigation_system.id, subscribe_request}) + for _, child in pairs(mock_children) do + test.mock_device.add_test_device(child) + end + for i = 3,4 do + mock_irrigation_system:expect_device_create({ + type = "EDGE_CHILD", + label = string.format("Matter Irrigation System %d", i - 1), + profile = "water-valve-level", + parent_device_id = mock_irrigation_system.id, + parent_assigned_child_key = string.format("%d", i) + }) + end + test.socket.matter:__expect_send({mock_irrigation_system.id, subscribe_request}) +end +test.set_test_init_function(test_init) + + +local additional_subscribed_attributes = { + clusters.FlowMeasurement.attributes.MeasuredValue, + clusters.FlowMeasurement.attributes.MaxMeasuredValue, + clusters.FlowMeasurement.attributes.MinMeasuredValue, + clusters.OperationalState.attributes.AcceptedCommandList, + clusters.OperationalState.attributes.OperationalError, + clusters.OperationalState.attributes.OperationalState, +} + +local expected_metadata = { + optional_component_capabilities = { + { + "main", + { + "level", + "flowMeasurement", + "operationalState", + } + }, + }, + profile = "irrigation-system" +} + +local function update_device_profile() + test.socket.device_lifecycle:__queue_receive({ mock_irrigation_system.id, "doConfigure" }) + mock_irrigation_system:expect_metadata_update(expected_metadata) + mock_irrigation_system:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + local updated_device_profile = t_utils.get_profile_definition( + "irrigation-system.yml", { enabled_optional_capabilities = expected_metadata.optional_component_capabilities } + ) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_irrigation_system:generate_info_changed({ profile = updated_device_profile })) + for _, attr in ipairs(additional_subscribed_attributes) do + subscribe_request:merge(attr:subscribe(mock_irrigation_system)) + end + test.socket.matter:__expect_send({mock_irrigation_system.id, subscribe_request}) +end + +test.register_coroutine_test( + "Parent device: Open command should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_irrigation_system.id, + { capability = "valve", component = "main", command = "open", args = { } } + }) + + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Open(mock_irrigation_system, endpoints.VALVE_1_EP) + }) + end +) + +test.register_coroutine_test( + "Parent device: Close command should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_irrigation_system.id, + { capability = "valve", component = "main", command = "close", args = { } } + }) + + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Close(mock_irrigation_system, endpoints.VALVE_1_EP) + }) + end +) + +test.register_coroutine_test( + "Parent device: Set level command should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_irrigation_system.id, + { capability = "level", component = "main", command = "setLevel", args = { 75 } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Open(mock_irrigation_system, endpoints.VALVE_1_EP, nil, 75) + }) + end +) + +test.register_coroutine_test( + "Parent device: Current state closed should generate closed event", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentState:build_test_report_data( + mock_irrigation_system, + endpoints.VALVE_1_EP, + 0 + ) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message("main", capabilities.valve.valve.closed()) + ) + end +) + +test.register_coroutine_test( + "Parent device: Current level reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentLevel:build_test_report_data( + mock_irrigation_system, + endpoints.VALVE_1_EP, + 60 + ) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message("main", capabilities.level.level(60)) + ) + end +) + +test.register_coroutine_test( + "Flow reports should generate correct messages", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.FlowMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_irrigation_system, 1, 20 * 10) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message( + "main", + capabilities.flowMeasurement.flow({ value = 20.0, unit = "m^3/h" }) + ) + ) + end +) + +test.register_coroutine_test( + "Min and max flow attributes set capability constraint", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.FlowMeasurement.attributes.MinMeasuredValue:build_test_report_data(mock_irrigation_system, 1, 20) + }) + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.FlowMeasurement.attributes.MaxMeasuredValue:build_test_report_data(mock_irrigation_system, 1, 5000) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message( + "main", + capabilities.flowMeasurement.flowRange({ + value = { minimum = 2.0, maximum = 500.0 }, + unit = "m^3/h" + }) + ) + ) + end +) + +test.register_coroutine_test( + "Child device valve 2: Open command should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_children[endpoints.VALVE_2_EP].id, + { capability = "valve", component = "main", command = "open", args = { } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Open(mock_irrigation_system, endpoints.VALVE_2_EP) + }) + end +) + +test.register_coroutine_test( + "Child device valve 2: Set level command should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_children[endpoints.VALVE_2_EP].id, + { capability = "level", component = "main", command = "setLevel", args = { 40 } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Open(mock_irrigation_system, endpoints.VALVE_2_EP, nil, 40) + }) + end +) + +test.register_coroutine_test( + "Child device valve 2: Current state closed should generate closed event", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentState:build_test_report_data( + mock_irrigation_system, + endpoints.VALVE_2_EP, + 0 + ) + }) + test.socket.capability:__expect_send( + mock_children[endpoints.VALVE_2_EP]:generate_test_message("main", capabilities.valve.valve.closed()) + ) + end +) + +test.register_coroutine_test( + "Child device valve 3: Close command should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_children[endpoints.VALVE_3_EP].id, + { capability = "valve", component = "main", command = "close", args = { } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Close(mock_irrigation_system, endpoints.VALVE_3_EP) + }) + end +) + +test.register_coroutine_test( + "Child device valve 3: Current level reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentLevel:build_test_report_data( + mock_irrigation_system, + endpoints.VALVE_3_EP, + 100 + ) + }) + test.socket.capability:__expect_send( + mock_children[endpoints.VALVE_3_EP]:generate_test_message("main", capabilities.level.level(100)) + ) + end +) + +test.register_coroutine_test( + "OperationalState attribute running should generate running event", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.OperationalState.attributes.OperationalState:build_test_report_data( + mock_irrigation_system, + endpoints.IRRIGATION_SYSTEM_EP, + clusters.OperationalState.types.OperationalStateEnum.RUNNING + ) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message("main", capabilities.operationalState.operationalState.running()) + ) + end +) + +test.register_coroutine_test( + "OperationalState OperationalError UNABLE_TO_COMPLETE_OPERATION should generate unableToCompleteOperation event", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data( + mock_irrigation_system, + endpoints.IRRIGATION_SYSTEM_EP, { error_state_id = clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_COMPLETE_OPERATION } + ) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message("main", capabilities.operationalState.operationalState.unableToCompleteOperation()) + ) + end +) + +test.run_registered_tests() +