diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 32a4df6694..e758c1d2a1 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -278,6 +278,36 @@ matterManufacturer: vendorId: 0x130A productId: 0x0043 deviceProfileName: switch-binary + - id: "Eve MotionBlinds" + deviceLabel: Eve MotionBlinds + vendorId: 0x130A + productId: 0x55 + deviceProfileName: window-covering-modular + - id: "4874/109" + deviceLabel: Eve MotionBlinds for Roller Blinds + vendorId: 0x130A + productId: 0x006D + deviceProfileName: window-covering-modular + - id: "4874/98" + deviceLabel: Eve MotionBlinds for Honeycomb Blinds + vendorId: 0x130A + productId: 0x0062 + deviceProfileName: window-covering-modular + - id: "4874/99" + deviceLabel: Eve MotionBlinds for Venetian Blinds + vendorId: 0x130A + productId: 0x0063 + deviceProfileName: window-covering-modular + - id: "4874/100" + deviceLabel: Eve MotionBlinds for Curtains + vendorId: 0x130A + productId: 0x0064 + deviceProfileName: window-covering-modular + - id: "4874/96" + deviceLabel: Eve Shutter Switch + vendorId: 0x130A + productId: 0x0060 + deviceProfileName: window-covering-modular #Ezviz - id: "5172/4096" @@ -754,13 +784,18 @@ matterManufacturer: vendorId: 0x100B productId: 0x0800 deviceProfileName: light-level-colorTemperature +# Griesser + - id: "5435/14337" + deviceLabel: MSM-1 + vendorId: 0x153B + productId: 0x3801 + deviceProfileName: window-covering-modular # Intecular - id: "5226/32769" deviceLabel: InvisOutlet vendorId: 0x146A productId: 0x8001 deviceProfileName: plug-binary - #Ledvance - id: "4489/843" deviceLabel: Matter Filament RGBW @@ -1266,6 +1301,12 @@ matterManufacturer: vendorId: 0x102E productId: 0x2250 deviceProfileName: button-battery +# Mamaba + - id: "4965/4097" + deviceLabel: Wi-Fi Curtain + vendorId: 0x1365 + productId: 0x1001 + deviceProfileName: window-covering-modular #Meross - id: "4933/40987" deviceLabel: Smart Wi-Fi Switch @@ -1282,6 +1323,11 @@ matterManufacturer: vendorId: 0x1345 productId: 0xB001 deviceProfileName: switch-binary + - id: "4933/61453" + deviceLabel: Smart Wi-Fi Roller Shutter Timer + vendorId: 0x1345 + productId: 0xF00D + deviceProfileName: window-covering-modular #Nanoleaf - id: "Nanoleaf NL53" deviceLabel: Essentials BR30 @@ -1880,6 +1926,20 @@ matterManufacturer: productId: 0x0001 deviceProfileName: switch-binary +# SmartWave + - id: "5376/10001" + deviceLabel: SmartWave Motorized Roller Shades 100 Blackout Flex + vendorId: 0x1500 + productId: 0x2711 + deviceProfileName: window-covering-modular + +# SmartWings + - id: "5231/4097" + deviceLabel: SmartWings Window Covering + vendorId: 0x146F + productId: 0x1001 + deviceProfileName: window-covering-modular + #SONOFF - id: "SONOFF MINIR4M" deviceLabel: Smart Plug-in Unit @@ -1935,6 +1995,112 @@ matterManufacturer: vendorId: 0x147F productId: 0x0002 deviceProfileName: light-color-level-2700K-6500K +#WISTAR + - id: "5207/3" + deviceLabel: WISTAR WSERD16-B Smart Tubular Motor + vendorId: 0x1457 + productId: 0x0003 + deviceProfileName: window-covering-modular + - id: "5207/4" + deviceLabel: WISTAR WSERD24 Smart Tubular Motor + vendorId: 0x1457 + productId: 0x0004 + deviceProfileName: window-covering-modular + - id: "5207/5" + deviceLabel: WISTAR WSERD40-B Smart Tubular Motor + vendorId: 0x1457 + productId: 0x0005 + deviceProfileName: window-covering-modular + - id: "5207/6" + deviceLabel: WISTAR WSERD40-L Smart Tubular Motor + vendorId: 0x1457 + productId: 0x0006 + deviceProfileName: window-covering-modular + - id: "5207/7" + deviceLabel: WISTAR WSERD40-T Smart Tubular Motor + vendorId: 0x1457 + productId: 0x0007 + deviceProfileName: window-covering-modular + - id: "5207/8" + deviceLabel: WISTAR WSERD50-B Smart Tubular Motor + vendorId: 0x1457 + productId: 0x0008 + deviceProfileName: window-covering-modular + - id: "5207/9" + deviceLabel: WISTAR WSERD50-L Smart Tubular Motor + vendorId: 0x1457 + productId: 0x0009 + deviceProfileName: window-covering-modular + - id: "5207/16" + deviceLabel: WISTAR WSERD50-T Smart Tubular Motor + vendorId: 0x1457 + productId: 0x0010 + deviceProfileName: window-covering-modular + - id: "5207/19" + deviceLabel: WISTAR WSER60 Smart Tubular Motor + vendorId: 0x1457 + productId: 0x0013 + deviceProfileName: window-covering-modular + - id: "5207/17" + deviceLabel: WISTAR WSER40 Smart Tubular Motor + vendorId: 0x1457 + productId: 0x0011 + deviceProfileName: window-covering-modular + - id: "5207/18" + deviceLabel: WISTAR WSER50 Smart Tubular Motor + vendorId: 0x1457 + productId: 0x0012 + deviceProfileName: window-covering-modular + - id: "5207/2" + deviceLabel: WISTAR WSERD30-B Smart Tubular Motor + vendorId: 0x1457 + productId: 0x0002 + deviceProfileName: window-covering-modular + - id: "5207/22" + deviceLabel: WISTAR WSCMXH Smart Vertical Blind Motor + vendorId: 0x1457 + productId: 0x0016 + deviceProfileName: window-covering-modular + - id: "5207/23" + deviceLabel: WISTAR WSCMXF Smart Vertical Blind Motor + vendorId: 0x1457 + productId: 0x0017 + deviceProfileName: window-covering-modular + - id: "5207/24" + deviceLabel: WISTAR WSCMXF-LED Smart Vertical Blind Motor + vendorId: 0x1457 + productId: 0x0018 + deviceProfileName: window-covering-modular + - id: "5207/20" + deviceLabel: WISTAR WSCMQ Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0014 + deviceProfileName: window-covering-modular + - id: "5207/21" + deviceLabel: WISTAR WSCMXI Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0015 + deviceProfileName: window-covering-modular + - id: "5207/32" + deviceLabel: WISTAR WSCMT Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0020 + deviceProfileName: window-covering-modular + - id: "5207/34" + deviceLabel: WISTAR WSCMXB Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0022 + deviceProfileName: window-covering-modular + - id: "5207/35" + deviceLabel: WISTAR WSCMXC Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0023 + deviceProfileName: window-covering-modular + - id: "5207/38" + deviceLabel: WISTAR WSCMXJ Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0026 + deviceProfileName: window-covering-modular #WiZ - id: "WiZ A19" deviceLabel: WiZ A19 @@ -3751,6 +3917,42 @@ matterManufacturer: vendorId: 0x100B productId: 0x22F4 deviceProfileName: light-color-level +#Yooksmart + - id: "5411/1052" + deviceLabel: Smart WindowCovering Series + vendorId: 0x1523 + productId: 0x041C + deviceProfileName: window-covering-modular + - id: "5411/2660" + deviceLabel: Smart WindowCovering Series + vendorId: 0x1523 + productId: 0x0A64 + deviceProfileName: window-covering-modular + - id: "5411/2661" + deviceLabel: Smart WindowCovering Series + vendorId: 0x1523 + productId: 0x0A65 + deviceProfileName: window-covering-modular + - id: "5411/2662" + deviceLabel: Smart WindowCovering Series + vendorId: 0x1523 + productId: 0x0A66 + deviceProfileName: window-covering-modular + - id: "5411/2663" + deviceLabel: Smart WindowCovering Series + vendorId: 0x1523 + productId: 0x0A67 + deviceProfileName: window-covering-modular + - id: "5411/2664" + deviceLabel: Smart WindowCovering Series + vendorId: 0x1523 + productId: 0x0A68 + deviceProfileName: window-covering-modular + - id: "5411/2665" + deviceLabel: Smart WindowCovering Series + vendorId: 0x1523 + productId: 0x0A69 + deviceProfileName: window-covering-modular #Zemismart - id: "5020/61154" deviceLabel: Zemismart Inline Module @@ -3822,6 +4024,46 @@ matterManufacturer: vendorId: 0x139C productId: 0x0387 deviceProfileName: matter-bridge + - id: "Zemismart MT01 Slide Curtain" + deviceLabel: Zemismart MT01 Slide Curtain + vendorId: 0x139C + productId: 0xFFFE + deviceProfileName: window-covering-modular + - id: "5020/65376" + deviceLabel: Zemismart MT25B Roller Motor + vendorId: 0x139C + productId: 0xFF60 + deviceProfileName: window-covering-modular + - id: "5020/65296" + deviceLabel: Zemismart MT82 Smart Curtain + vendorId: 0x139C + productId: 0xFF10 + deviceProfileName: window-covering-modular + - id: "5020/65301" + deviceLabel: Zemismart MT25A Thread Roller Motor + vendorId: 0x139C + productId: 0xFF15 + deviceProfileName: window-covering-modular + - id: "5020/64050" + deviceLabel: Zemismart ZM02 Smart Curtain + vendorId: 0x139C + productId: 0xFA32 + deviceProfileName: window-covering-modular + - id: "5020/64017" + deviceLabel: Zemismart ZM25C Smart Curtain + vendorId: 0x139C + productId: 0xFA11 + deviceProfileName: window-covering-modular + - id: "5020/64049" + deviceLabel: Zemismart ZM01 Smart Curtain + vendorId: 0x139C + productId: 0xFA31 + deviceProfileName: window-covering-modular + - id: "5020/64023" + deviceLabel: Zemismart ZM24A Smart Curtain + vendorId: 0x139C + productId: 0xFA17 + deviceProfileName: window-covering-modular #Zimi - id: "5410/3" deviceLabel: Zimi Matter Connect @@ -4052,6 +4294,11 @@ matterGeneric: - id: 0x002B # Fan - id: 0x0110 # Mounted Dimmable Load Control deviceProfileName: fan-modular + - id: "matter/window/covering" + deviceLabel: Matter Window Covering + deviceTypes: + - id: 0x0202 # Window Covering + deviceProfileName: window-covering-modular matterThing: - id: SmartThings/MatterThing diff --git a/drivers/SmartThings/matter-switch/profiles/window-covering-modular.yml b/drivers/SmartThings/matter-switch/profiles/window-covering-modular.yml new file mode 100644 index 0000000000..79f46b9e2b --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/window-covering-modular.yml @@ -0,0 +1,29 @@ +name: window-covering-modular +components: +- id: main + capabilities: + - id: windowShade + version: 1 + - id: windowShadePreset + version: 1 + - id: windowShadeLevel + version: 1 + optional: true + - id: windowShadeTiltLevel + version: 1 + optional: true + - id: battery + version: 1 + optional: true + - id: batteryLevel + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Blind +preferences: + - preferenceId: reverse + explicit: true diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 6655f81e56..164620955a 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -8,9 +8,11 @@ local clusters = require "st.matter.clusters" local log = require "log" local version = require "version" local cfg = require "switch_utils.device_configuration" +local button_cfg = cfg.ButtonCfg +local child_cfg = cfg.ChildCfg local device_cfg = cfg.DeviceCfg local switch_cfg = cfg.SwitchCfg -local button_cfg = cfg.ButtonCfg +local window_covering_cfg = cfg.WindowCoveringCfg local fields = require "switch_utils.fields" local switch_utils = require "switch_utils.utils" local attribute_handlers = require "switch_handlers.attribute_handlers" @@ -41,6 +43,10 @@ function SwitchLifecycleHandlers.device_added(driver, device) switch_utils.handle_electrical_sensor_info(device) end + if #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) == 0 then + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist = true}) + end + -- call device init in case init is not called after added due to device caching SwitchLifecycleHandlers.device_init(driver, device) end @@ -50,6 +56,11 @@ function SwitchLifecycleHandlers.do_configure(driver, device) switch_cfg.set_device_control_options(device) device_cfg.match_profile(driver, device) elseif device.network_type == device_lib.NETWORK_TYPE_CHILD then + local window_covering_ep_ids = switch_utils.get_endpoints_by_device_type(device:get_parent_device(), fields.DEVICE_TYPE_ID.WINDOW_COVERING) + if #window_covering_ep_ids > 0 then + local default_endpoint_id = switch_utils.find_default_endpoint(device:get_parent_device()) + child_cfg.create_or_update_child_devices(driver, device:get_parent_device(), window_covering_ep_ids, default_endpoint_id, window_covering_cfg.assign_profile_for_window_covering_ep) + end -- because get_parent_device() may cause race conditions if used in init, an initial child subscribe is handled in doConfigure. -- all future calls to subscribe will be handled by the parent device in init device:subscribe() @@ -81,6 +92,16 @@ function SwitchLifecycleHandlers.info_changed(driver, device, event, args) device_cfg.match_profile(driver, device) end end + + if args.old_st_store.preferences.reverse ~= nil and device.preferences.reverse ~= nil and + args.old_st_store.preferences.reverse ~= device.preferences.reverse then + local closure_fields = require "sub_drivers.closures.closure_utils.fields" + if device.preferences.reverse then + device:set_field(closure_fields.REVERSE_POLARITY, true, { persist = true }) + else + device:set_field(closure_fields.REVERSE_POLARITY, false, { persist = true }) + end + end end function SwitchLifecycleHandlers.device_init(driver, device) @@ -94,11 +115,11 @@ function SwitchLifecycleHandlers.device_init(driver, device) device:extend_device("subscribe", switch_utils.subscribe) device:subscribe() - -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. + -- device energy reporting must be handled cumulatively, periodically, or by both simultaneously. -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID, {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY}) > 0 then - device:set_field(fields.CUMULATIVE_REPORTS_SUPPORTED, true, {persist = false}) + device:set_field(fields.CUMULATIVE_REPORTS_SUPPORTED, true, {persist = false}) end end end @@ -253,6 +274,16 @@ local matter_driver_template = { [capabilities.valve.ID] = { clusters.ValveConfigurationAndControl.attributes.CurrentState }, + [capabilities.windowShade.ID] = { + clusters.WindowCovering.attributes.OperationalStatus + }, + [capabilities.windowShadeLevel.ID] = { + clusters.LevelControl.attributes.CurrentLevel, + clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths, + }, + [capabilities.windowShadeTiltLevel.ID] = { + clusters.WindowCovering.attributes.CurrentPositionTiltPercent100ths, + }, }, subscribed_events = { [capabilities.button.ID] = { @@ -332,6 +363,7 @@ local matter_driver_template = { sub_drivers = { switch_utils.lazy_load_if_possible("sub_drivers.aqara_cube"), switch_utils.lazy_load("sub_drivers.camera"), + switch_utils.lazy_load_if_possible("sub_drivers.closures"), switch_utils.lazy_load_if_possible("sub_drivers.eve_energy"), switch_utils.lazy_load_if_possible("sub_drivers.ikea_scroll"), switch_utils.lazy_load_if_possible("sub_drivers.third_reality_mk1") diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/closures/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/closures/can_handle.lua new file mode 100644 index 0000000000..f5d069ac6e --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/closures/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local device_lib = require "st.device" + local fields = require "switch_utils.fields" + local switch_utils = require "switch_utils.utils" + if device.network_type == device_lib.NETWORK_TYPE_CHILD then + device = device:get_parent_device() + end + if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.WINDOW_COVERING) > 0 then + return true, require("sub_drivers.closures") + end + return false +end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/closures/closure_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/closures/closure_handlers/attribute_handlers.lua new file mode 100644 index 0000000000..2356c0fc8e --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/closures/closure_handlers/attribute_handlers.lua @@ -0,0 +1,68 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local closure_fields = require "sub_drivers.closures.closure_utils.fields" + +local ClosureAttributeHandlers = {} + +ClosureAttributeHandlers.current_pos_handler = function(attribute) + return function(driver, device, ib, response) + if ib.data.value == nil then return end + local windowShade = capabilities.windowShade.windowShade + local position = 100 - math.floor(ib.data.value / 100) + local reverse = device:get_field(closure_fields.REVERSE_POLARITY) + device:emit_event_for_endpoint(ib.endpoint_id, attribute(position)) + + if attribute == capabilities.windowShadeLevel.shadeLevel then + device:set_field(closure_fields.CURRENT_LIFT, position) + else + device:set_field(closure_fields.CURRENT_TILT, position) + end + + local lift_position = device:get_field(closure_fields.CURRENT_LIFT) + local tilt_position = device:get_field(closure_fields.CURRENT_TILT) + + if lift_position == nil then + if tilt_position == 0 then + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.open() or windowShade.closed()) + elseif tilt_position == 100 then + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.closed() or windowShade.open()) + else + device:emit_event_for_endpoint(ib.endpoint_id, windowShade.partially_open()) + end + elseif lift_position == 100 then + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.closed() or windowShade.open()) + elseif lift_position > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, windowShade.partially_open()) + elseif lift_position == 0 then + if tilt_position == nil or tilt_position == 0 then + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.open() or windowShade.closed()) + elseif tilt_position > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, windowShade.partially_open()) + end + end + end +end + +function ClosureAttributeHandlers.current_status_handler(driver, device, ib, response) + local windowShade = capabilities.windowShade.windowShade + local reverse = device:get_field(closure_fields.REVERSE_POLARITY) + local state = ib.data.value & clusters.WindowCovering.types.OperationalStatus.GLOBAL + if state == 1 then -- opening + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.closing() or windowShade.opening()) + elseif state == 2 then -- closing + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.opening() or windowShade.closing()) + elseif state ~= 0 then -- unknown + device:emit_event_for_endpoint(ib.endpoint_id, windowShade.unknown()) + end +end + +function ClosureAttributeHandlers.level_attr_handler(driver, device, ib, response) + if ib.data.value == nil then return end + local level = math.floor((ib.data.value / 254.0 * 100) + 0.5) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windowShadeLevel.shadeLevel(level)) +end + +return ClosureAttributeHandlers diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/closures/closure_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/closures/closure_handlers/capability_handlers.lua new file mode 100644 index 0000000000..6c8d47a3a7 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/closures/closure_handlers/capability_handlers.lua @@ -0,0 +1,70 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local closure_fields = require "sub_drivers.closures.closure_utils.fields" + +local ClosureCapabilityHandlers = {} + +function ClosureCapabilityHandlers.handle_preset(driver, device, cmd) + local lift_value = device:get_latest_state( + "main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME + ) or closure_fields.DEFAULT_PRESET_LEVEL + local hundredths_lift_percent = (100 - lift_value) * 100 + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.WindowCovering.server.commands.GoToLiftPercentage( + device, endpoint_id, hundredths_lift_percent + )) +end + +function ClosureCapabilityHandlers.handle_set_preset(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:set_field(closure_fields.PRESET_LEVEL_KEY, cmd.args.position) + device:emit_event_for_endpoint(endpoint_id, capabilities.windowShadePreset.position(cmd.args.position)) +end + +function ClosureCapabilityHandlers.handle_close(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = clusters.WindowCovering.server.commands.DownOrClose(device, endpoint_id) + if device:get_field(closure_fields.REVERSE_POLARITY) then + req = clusters.WindowCovering.server.commands.UpOrOpen(device, endpoint_id) + end + device:send(req) +end + +function ClosureCapabilityHandlers.handle_open(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = clusters.WindowCovering.server.commands.UpOrOpen(device, endpoint_id) + if device:get_field(closure_fields.REVERSE_POLARITY) then + req = clusters.WindowCovering.server.commands.DownOrClose(device, endpoint_id) + end + device:send(req) +end + +function ClosureCapabilityHandlers.handle_pause(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.WindowCovering.server.commands.StopMotion(device, endpoint_id)) +end + +function ClosureCapabilityHandlers.handle_shade_level(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local lift_percentage_value = 100 - cmd.args.shadeLevel + local hundredths_lift_percentage = lift_percentage_value * 100 + local req = clusters.WindowCovering.server.commands.GoToLiftPercentage( + device, endpoint_id, hundredths_lift_percentage + ) + device:send(req) +end + +function ClosureCapabilityHandlers.handle_shade_tilt_level(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local tilt_percentage_value = 100 - cmd.args.level + local hundredths_tilt_percentage = tilt_percentage_value * 100 + local req = clusters.WindowCovering.server.commands.GoToTiltPercentage( + device, endpoint_id, hundredths_tilt_percentage + ) + device:send(req) +end + +return ClosureCapabilityHandlers diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/closures/closure_utils/fields.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/closures/closure_utils/fields.lua new file mode 100644 index 0000000000..93e1b6f128 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/closures/closure_utils/fields.lua @@ -0,0 +1,12 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ClosureFields = {} + +ClosureFields.CURRENT_LIFT = "__current_lift" +ClosureFields.CURRENT_TILT = "__current_tilt" +ClosureFields.REVERSE_POLARITY = "__reverse_polarity" +ClosureFields.PRESET_LEVEL_KEY = "__preset_level_key" +ClosureFields.DEFAULT_PRESET_LEVEL = 50 + +return ClosureFields diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/closures/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/closures/init.lua new file mode 100644 index 0000000000..b2689a3aed --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/closures/init.lua @@ -0,0 +1,77 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +------------------------------------------------------------------------------------- +-- Matter Closures Sub Driver +------------------------------------------------------------------------------------- + +local attribute_handlers = require "sub_drivers.closures.closure_handlers.attribute_handlers" +local capabilities = require "st.capabilities" +local capability_handlers = require "sub_drivers.closures.closure_handlers.capability_handlers" +local closure_fields = require "sub_drivers.closures.closure_utils.fields" +local clusters = require "st.matter.clusters" +local switch_utils = require "switch_utils.utils" + +local ClosureLifecycleHandlers = {} + +function ClosureLifecycleHandlers.device_init(driver, device) + device:set_component_to_endpoint_fn(switch_utils.component_to_endpoint) + device:set_endpoint_to_component_fn(switch_utils.endpoint_to_component) + if device:supports_capability_by_id(capabilities.windowShadePreset.ID) and + device:get_latest_state("main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME) == nil then + device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed = false}})) + local preset_position = device:get_field(closure_fields.PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + closure_fields.DEFAULT_PRESET_LEVEL + device:emit_event(capabilities.windowShadePreset.position(preset_position, {visibility = {displayed = false}})) + device:set_field(closure_fields.PRESET_LEVEL_KEY, preset_position, {persist = true}) + end + device:extend_device("subscribe", switch_utils.subscribe) + device:subscribe() +end + +function ClosureLifecycleHandlers.device_added(driver, device) + device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, {visibility = {displayed = false}})) + device:set_field(closure_fields.REVERSE_POLARITY, false, { persist = true }) + switch_utils.handle_electrical_sensor_info(device) +end + +local closures_handler = { + NAME = "closures", + lifecycle_handlers = { + init = ClosureLifecycleHandlers.device_init, + added = ClosureLifecycleHandlers.device_added + }, + matter_handlers = { + attr = { + [clusters.LevelControl.ID] = { + [clusters.LevelControl.attributes.CurrentLevel.ID] = attribute_handlers.level_attr_handler, + }, + [clusters.WindowCovering.ID] = { + [clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID] = attribute_handlers.current_pos_handler(capabilities.windowShadeLevel.shadeLevel), + [clusters.WindowCovering.attributes.CurrentPositionTiltPercent100ths.ID] = attribute_handlers.current_pos_handler(capabilities.windowShadeTiltLevel.shadeTiltLevel), + [clusters.WindowCovering.attributes.OperationalStatus.ID] = attribute_handlers.current_status_handler + }, + }, + }, + capability_handlers = { + [capabilities.windowShadePreset.ID] = { + [capabilities.windowShadePreset.commands.presetPosition.NAME] = capability_handlers.handle_preset, + [capabilities.windowShadePreset.commands.setPresetPosition.NAME] = capability_handlers.handle_set_preset + }, + [capabilities.windowShade.ID] = { + [capabilities.windowShade.commands.close.NAME] = capability_handlers.handle_close, + [capabilities.windowShade.commands.open.NAME] = capability_handlers.handle_open, + [capabilities.windowShade.commands.pause.NAME] = capability_handlers.handle_pause + }, + [capabilities.windowShadeLevel.ID] = { + [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = capability_handlers.handle_shade_level + }, + [capabilities.windowShadeTiltLevel.ID] = { + [capabilities.windowShadeTiltLevel.commands.setShadeTiltLevel.NAME] = capability_handlers.handle_shade_tilt_level + }, + }, + can_handle = require("sub_drivers.closures.can_handle") +} + +return closures_handler 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 e8dfe3f120..ae76709be8 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -319,10 +319,10 @@ function AttributeHandlers.available_endpoints_handler(driver, device, ib, respo local set_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS) for i, set_ep_info in pairs(set_topology_eps or {}) do if ib.endpoint_id == set_ep_info.endpoint_id then - -- since EP reponse is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table + -- since EP response is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table switch_utils.remove_field_index(device, fields.ELECTRICAL_SENSOR_EPS, i) local available_endpoints_ids = {} - for _, element in pairs(ib.data.elements) do + for _, element in pairs(ib.data.elements or {}) do table.insert(available_endpoints_ids, element.value) end -- set the required profile elements ("-power", etc.) to one of these EP IDs for later profiling. @@ -344,10 +344,10 @@ function AttributeHandlers.parts_list_handler(driver, device, ib, response) local tree_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS) for i, tree_ep_info in pairs(tree_topology_eps or {}) do if ib.endpoint_id == tree_ep_info.endpoint_id then - -- since EP reponse is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table + -- since EP response is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table switch_utils.remove_field_index(device, fields.ELECTRICAL_SENSOR_EPS, i) local associated_endpoints_ids = {} - for _, element in pairs(ib.data.elements) do + for _, element in pairs(ib.data.elements or {}) do table.insert(associated_endpoints_ids, element.value) end -- set the required profile elements ("-power", etc.) to one of these EP IDs for later profiling. @@ -382,29 +382,19 @@ function AttributeHandlers.bat_charge_level_handler(driver, device, ib, response end function AttributeHandlers.power_source_attribute_list_handler(driver, device, ib, response) - local profile_name = "" - - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - for _, attr in ipairs(ib.data.elements) do - -- Re-profile the device if BatPercentRemaining (Attribute ID 0x0C) or - -- BatChargeLevel (Attribute ID 0x0E) is present. - if attr.value == 0x0C then - profile_name = "button-battery" - break - elseif attr.value == 0x0E then - profile_name = "button-batteryLevel" + local previous_battery_support = device:get_field(fields.profiling_data.BATTERY_SUPPORT) + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist=true}) + for _, attr in ipairs(ib.data.elements or {}) do + if attr.value == clusters.PowerSource.attributes.BatPercentRemaining.ID then + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_PERCENTAGE, {persist=true}) break + elseif attr.value == clusters.PowerSource.attributes.BatChargeLevel.ID and + device:get_field(fields.profiling_data.BATTERY_SUPPORT) ~= fields.battery_support.BATTERY_PERCENTAGE then -- don't overwrite if percentage support is already detected + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_LEVEL, {persist=true}) end end - if profile_name ~= "" then - if #button_eps > 1 then - profile_name = string.format("%d-", #button_eps) .. profile_name - end - - if switch_utils.get_product_override_field(device, "is_climate_sensor_w100") then - profile_name = profile_name .. "-temperature-humidity" - end - device:try_update_metadata({ profile = profile_name }) + if not previous_battery_support or previous_battery_support ~= device:get_field(fields.profiling_data.BATTERY_SUPPORT) then + device_cfg.match_profile(driver, device) end end 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 7ee4b6bf64..61a138692e 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -20,6 +20,7 @@ local ChildConfiguration = {} local SwitchDeviceConfiguration = {} local ButtonDeviceConfiguration = {} local FanDeviceConfiguration = {} +local WindowCoveringDeviceConfiguration = {} 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 @@ -30,7 +31,7 @@ function ChildConfiguration.create_or_update_child_devices(driver, device, serve for device_num, ep_id in ipairs(server_cluster_ep_ids) do if ep_id ~= default_endpoint_id then -- don't create a child device that maps to the main endpoint local label_and_name = string.format("%s %d", device.label, device_num) - local child_profile, _ = assign_profile_fn(device, ep_id, true) + local child_profile, optional_component_capabilities = assign_profile_fn(device, ep_id, true) local existing_child_device = device:get_field(fields.IS_PARENT_CHILD_DEVICE) and switch_utils.find_child(device, ep_id) if not existing_child_device then driver:try_create_device({ @@ -43,7 +44,8 @@ function ChildConfiguration.create_or_update_child_devices(driver, device, serve }) else existing_child_device:try_update_metadata({ - profile = child_profile + profile = child_profile, + optional_component_capabilities = optional_component_capabilities }) end end @@ -74,7 +76,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) @@ -123,12 +124,16 @@ function ButtonDeviceConfiguration.update_button_profile(device, default_endpoin if #motion_eps > 0 and (num_button_eps == 3 or num_button_eps == 6) then -- only these two devices are handled profile_name = profile_name .. "-motion" end - local battery_supported = #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) > 0 - if battery_supported then -- battery profiles are configured later, in power_source_attribute_list_handler - device:send(clusters.PowerSource.attributes.AttributeList:read(device)) - else - device:try_update_metadata({profile = profile_name}) + local battery_support = device:get_field(fields.profiling_data.BATTERY_SUPPORT) + if battery_support == fields.battery_support.BATTERY_PERCENTAGE then + profile_name = profile_name .. "-battery" + elseif battery_support == fields.battery_support.BATTERY_LEVEL then + profile_name = profile_name .. "-batteryLevel" + end + if switch_utils.get_product_override_field(device, "is_climate_sensor_w100") then + profile_name = "3-button-battery-temperature-humidity" end + return profile_name end function ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, button_eps) @@ -183,6 +188,33 @@ function ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep end end +function WindowCoveringDeviceConfiguration.assign_profile_for_window_covering_ep(device, server_window_covering_ep_id) + local ep_info = switch_utils.get_endpoint_info(device, server_window_covering_ep_id) + local window_covering_cluster_info = switch_utils.find_cluster_on_ep(ep_info, clusters.WindowCovering.ID) + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + + if clusters.WindowCovering.are_features_supported(clusters.WindowCovering.types.Feature.LIFT, window_covering_cluster_info.feature_map) then + table.insert(main_component_capabilities, capabilities.windowShadeLevel.ID) + end + if clusters.WindowCovering.are_features_supported(clusters.WindowCovering.types.Feature.TILT, window_covering_cluster_info.feature_map) then + table.insert(main_component_capabilities, capabilities.windowShadeTiltLevel.ID) + end + + local power_source_cluster_info = switch_utils.find_cluster_on_ep(ep_info, clusters.PowerSource.ID) + if power_source_cluster_info then + local battery_support = device:get_field(fields.profiling_data.BATTERY_SUPPORT) or fields.battery_support.NO_BATTERY + if battery_support == fields.battery_support.BATTERY_PERCENTAGE then + table.insert(main_component_capabilities, capabilities.battery.ID) + elseif battery_support == fields.battery_support.BATTERY_LEVEL then + table.insert(main_component_capabilities, capabilities.batteryLevel.ID) + end + end + + table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + return "window-covering-modular", optional_supported_component_capabilities +end + -- [[ PROFILE MATCHING AND CONFIGURATIONS ]] -- @@ -214,7 +246,6 @@ function DeviceConfiguration.match_profile(driver, device) if #server_onoff_ep_ids > 0 then ChildConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id, SwitchDeviceConfiguration.assign_profile_for_onoff_ep) end - if switch_utils.tbl_contains(server_onoff_ep_ids, default_endpoint_id) then updated_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, default_endpoint_id) local generic_profile = function(s) return string.find(updated_profile or "", s, 1, true) end @@ -238,20 +269,30 @@ function DeviceConfiguration.match_profile(driver, device) end -- initialize the main device card with buttons if applicable - local momemtary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #momemtary_switch_ep_ids) then - ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #momemtary_switch_ep_ids) + local momentary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #momentary_switch_ep_ids) then + updated_profile = ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #momentary_switch_ep_ids) -- All button endpoints found will be added as additional components in the profile containing the default_endpoint_id. - ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, momemtary_switch_ep_ids) - ButtonDeviceConfiguration.configure_buttons(device, momemtary_switch_ep_ids) - return + ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, momentary_switch_ep_ids) + ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep_ids) + end + + -- initialize the main device card with window covering if applicable + local window_covering_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.WINDOW_COVERING) + if #window_covering_ep_ids > 0 then + ChildConfiguration.create_or_update_child_devices(driver, device, window_covering_ep_ids, default_endpoint_id, WindowCoveringDeviceConfiguration.assign_profile_for_window_covering_ep) + end + if switch_utils.tbl_contains(window_covering_ep_ids, default_endpoint_id) then + updated_profile, optional_component_capabilities = WindowCoveringDeviceConfiguration.assign_profile_for_window_covering_ep(device, default_endpoint_id) end device:try_update_metadata({ profile = updated_profile, optional_component_capabilities = optional_component_capabilities }) end return { + ButtonCfg = ButtonDeviceConfiguration, + ChildCfg = ChildConfiguration, DeviceCfg = DeviceConfiguration, SwitchCfg = SwitchDeviceConfiguration, - ButtonCfg = ButtonDeviceConfiguration -} \ No newline at end of file + WindowCoveringCfg = WindowCoveringDeviceConfiguration, +} diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index f670b154c8..37cd52a0d1 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -44,6 +44,7 @@ SwitchFields.DEVICE_TYPE_ID = { DIMMER = 0x0104, COLOR_DIMMER = 0x0105, }, + WINDOW_COVERING = 0x0202, } SwitchFields.device_type_profile_map = { @@ -83,6 +84,8 @@ SwitchFields.LEVEL_MIN = "__level_min" SwitchFields.LEVEL_MAX = "__level_max" SwitchFields.COLOR_MODE = "__color_mode" +SwitchFields.SUBSCRIBED_ATTRIBUTES_KEY = "__subscribed_attributes" + SwitchFields.updated_fields = { { current_field_name = "__component_to_endpoint_map_button", updated_field_name = SwitchFields.COMPONENT_TO_ENDPOINT_MAP }, { current_field_name = "__switch_intialized", updated_field_name = nil }, @@ -135,13 +138,20 @@ SwitchFields.switch_category_vendor_overrides = { SwitchFields.ELECTRICAL_SENSOR_EPS = "__electrical_sensor_eps" --- used in tandem with an EP ID. Stores the required electrical tags "-power", "-energy-powerConsumption", etc. ---- for an Electrical Sensor EP with a "primary" endpoint, used during device profling. +--- for an Electrical Sensor EP with a "primary" endpoint, used during device profiling. SwitchFields.ELECTRICAL_TAGS = "__electrical_tags" SwitchFields.MODULAR_PROFILE_UPDATED = "__modular_profile_updated" SwitchFields.profiling_data = { POWER_TOPOLOGY = "__power_topology", + BATTERY_SUPPORT = "__battery_support", +} + +SwitchFields.battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_LEVEL = "BATTERY_LEVEL", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE", } SwitchFields.ENERGY_METER_OFFSET = "__energy_meter_offset" diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index c6299fba95..66edbb8ba4 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -153,7 +153,8 @@ function utils.find_default_endpoint(device) local onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) local momentary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - local fan_endpoint_ids = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN) + local fan_ep_ids = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN) + local window_covering_ep_ids = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.WINDOW_COVERING) local get_first_non_zero_endpoint = function(endpoints) table.sort(endpoints) @@ -166,8 +167,8 @@ function utils.find_default_endpoint(device) end -- Return the first fan endpoint as the default endpoint if any is found - if #fan_endpoint_ids > 0 then - return get_first_non_zero_endpoint(fan_endpoint_ids) + if #fan_ep_ids > 0 then + return get_first_non_zero_endpoint(fan_ep_ids) end -- Return the first onoff endpoint as the default endpoint if no momentary switch endpoints are present @@ -193,6 +194,11 @@ function utils.find_default_endpoint(device) end end + -- Return the first window covering endpoint as the default endpoint if any is found + if #window_covering_ep_ids > 0 then + return get_first_non_zero_endpoint(window_covering_ep_ids) + 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 @@ -443,7 +449,8 @@ end --- @param subscribed_attributes table key-value pairs mapping capability ids to subscribed attributes --- @param subscribed_events table key-value pairs mapping capability ids to subscribed events function utils.populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, subscribed_attributes, subscribed_events) - for _, component in pairs(checked_device.st_store.profile.components) do + local subscribed_attrs = {} + for _, component in pairs(checked_device.st_store.profile.components) do for _, capability in pairs(component.capabilities) do if not capabilities_seen[capability.id] then for _, attr in ipairs(subscribed_attributes[capability.id] or {}) do @@ -451,6 +458,8 @@ function utils.populate_subscribe_request_for_device(checked_device, subscribe_r local attr_id = attr.ID or attr.attribute if not attributes_seen[cluster_id..attr_id] then local ib = im.InteractionInfoBlock(nil, cluster_id, attr_id) + subscribed_attrs[cluster_id] = subscribed_attrs[cluster_id] or {} + subscribed_attrs[cluster_id][attr_id] = ib subscribe_request:with_info_block(ib) attributes_seen[cluster_id..attr_id] = true end @@ -468,6 +477,7 @@ function utils.populate_subscribe_request_for_device(checked_device, subscribe_r end end end + checked_device:set_field(fields.SUBSCRIBED_ATTRIBUTES_KEY, subscribed_attrs) end --- create and send a subscription request by checking all devices, accounting for both parent and child devices @@ -487,6 +497,13 @@ function utils.subscribe(device) end end + -- For devices supporting BATTERY, add the PowerSource AttributeList to the list of subscribed + -- attributes in order to determine whether to use the battery or batteryLevel capability + if #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) > 0 then + local ib = im.InteractionInfoBlock(nil, clusters.PowerSource.ID, clusters.PowerSource.attributes.AttributeList.ID) + subscribe_request:with_info_block(ib) + end + if #subscribe_request.info_blocks > 0 then device:send(subscribe_request) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua index 6371c6be44..d47c666166 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua @@ -1,14 +1,11 @@ -- Copyright © 2024 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local test = require "integration_test" -local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" -local utils = require "st.utils" -local dkjson = require "dkjson" -local uint32 = require "st.matter.data_types.Uint32" local clusters = require "st.matter.generated.zap_clusters" -local button_attr = capabilities.button.button +local t_utils = require "integration_test.utils" +local test = require "integration_test" +local uint32 = require "st.matter.data_types.Uint32" -- Mock a 3-button device with temperature and humidity sensor local aqara_mock_device = test.mock_device.build_test_matter_device({ @@ -100,19 +97,20 @@ local aqara_mock_device = test.mock_device.build_test_matter_device({ local function configure_buttons() test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 3)}) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", button_attr.pushed({state_change = false}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", capabilities.button.button.pushed({state_change = false}))) test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 4)}) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 5)}) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", capabilities.button.button.pushed({state_change = false}))) end local function test_init() test.disable_startup_messages() test.mock_device.add_test_device(aqara_mock_device) local cluster_subscribe_list = { + clusters.PowerSource.server.attributes.AttributeList, clusters.PowerSource.server.attributes.BatPercentRemaining, clusters.TemperatureMeasurement.attributes.MeasuredValue, clusters.TemperatureMeasurement.attributes.MinMeasuredValue, @@ -138,23 +136,16 @@ local function test_init() test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) - local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() - test.socket.matter:__expect_send({aqara_mock_device.id, read_attribute_list}) - configure_buttons() aqara_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - - local device_info_copy = utils.deep_copy(aqara_mock_device.raw_st_data) - device_info_copy.profile.id = "3-button-battery-temperature-humidity" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "infoChanged", device_info_json }) - test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) - configure_buttons() end test.set_test_init_function(test_init) local function update_profile() - test.socket.matter:__queue_receive({aqara_mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data(aqara_mock_device, 6, {uint32(0x0C)})}) + test.socket.matter:__queue_receive({aqara_mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( + aqara_mock_device, 6, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)} + )}) + configure_buttons() aqara_mock_device:expect_metadata_update({ profile = "3-button-battery-temperature-humidity" }) end @@ -315,7 +306,7 @@ test.register_coroutine_test( clusters.Switch.events.MultiPressComplete:build_test_event_report(aqara_mock_device, 4, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1}) } ) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", button_attr.double({state_change = true}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", capabilities.button.button.double({state_change = true}))) end ) @@ -335,7 +326,7 @@ test.register_coroutine_test( clusters.Switch.events.LongPress:build_test_event_report(aqara_mock_device, 3, {new_position = 1}) } ) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", button_attr.held({state_change = true}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", capabilities.button.button.held({state_change = true}))) test.socket.matter:__queue_receive( { aqara_mock_device.id, @@ -361,7 +352,7 @@ test.register_coroutine_test( clusters.Switch.events.LongPress:build_test_event_report(aqara_mock_device, 5, {new_position = 1}) } ) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", button_attr.held({state_change = true}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", capabilities.button.button.held({state_change = true}))) test.socket.matter:__queue_receive( { aqara_mock_device.id, @@ -382,7 +373,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send( - aqara_mock_device:generate_test_message("button1", button_attr.double({state_change = true})) + aqara_mock_device:generate_test_message("button1", capabilities.button.button.double({state_change = true})) ) end ) @@ -466,4 +457,3 @@ test.register_coroutine_test( ) test.run_registered_tests() - diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua index 58ba831074..ff5fa6b38d 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -4,14 +4,12 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" local button_attr = capabilities.button.button local utils = require "st.utils" local dkjson = require "dkjson" local uint32 = require "st.matter.data_types.Uint32" ---mock the actual device local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("button-battery.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, @@ -47,8 +45,8 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) --- add device for each mock device -local CLUSTER_SUBSCRIBE_LIST ={ +local CLUSTER_SUBSCRIBE_LIST = { + clusters.PowerSource.server.attributes.AttributeList, clusters.PowerSource.server.attributes.BatPercentRemaining, clusters.Switch.server.events.InitialPress, clusters.Switch.server.events.LongPress, @@ -56,18 +54,27 @@ local CLUSTER_SUBSCRIBE_LIST ={ clusters.Switch.server.events.MultiPressComplete, } -local function configure_buttons() +local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) +for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end +end + +local function expect_configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) end +local function update_profile() + test.socket.matter:__queue_receive({mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( + mock_device, 1, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)} + )}) + expect_configure_buttons() + mock_device:expect_metadata_update({ profile = "button-battery" }) +end + local function test_init() test.disable_startup_messages() test.mock_device.add_test_device(mock_device) - local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) - for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do - if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end - end test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) @@ -76,339 +83,332 @@ local function test_init() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() - test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) - configure_buttons() - - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "buttons-battery" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - configure_buttons() - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) end test.set_test_init_function(test_init) -test.register_message_test( - "Handle single press sequence, no hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1} --move to position 1? - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press - } -} +test.register_coroutine_test( + "Simulate the profile change update taking affect and the device info changing", + function() + test.socket.matter:__set_channel_ordering("relaxed") + update_profile() + test.wait_for_events() + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "buttons-battery" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) + expect_configure_buttons() + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + end ) -test.register_message_test( - "Handle single press sequence, with hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) - } -} +test.register_coroutine_test( + "Handle single press sequence, no hold", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} --move to position 1 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + ) + end ) -test.register_message_test( - "Handle release after short press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device, 1, {previous_position = 1} - ) - } - }, - { -- this is a double event because the test device in this test shouldn't support the above event +test.register_coroutine_test( + "Handle single press sequence, with hold", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.held({state_change = true})) + ) + end +) + +test.register_coroutine_test( + "Handle release after short press", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 1, {previous_position = 1} + ) + } + ) + -- this is a double event because the test device in this test shouldn't support the above event -- but we handle it anyway - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + ) + end ) -test.register_message_test( - "Handle release after long press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongRelease:build_test_event_report( - mock_device, 1, {previous_position = 1} - ) - } - }, - } +test.register_coroutine_test( + "Handle release after long press", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.held({state_change = true})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 1, {previous_position = 1} + ) + } + ) + end ) -test.register_message_test( - "Receiving a max press attribute of 2 should emit correct event", { - { - channel = "matter", - direction = "receive", - message = { +test.register_coroutine_test( + "Receiving a max press attribute of 2 should emit correct event", + function() + update_profile() + test.socket.matter:__queue_receive( + { mock_device.id, clusters.Switch.attributes.MultiPressMax:build_test_report_data( mock_device, 1, 2 ) - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) - }, - } + ) + end ) -test.register_message_test( - "Receiving a max press attribute of 3 should emit correct event", { - { - channel = "matter", - direction = "receive", - message = { +test.register_coroutine_test( + "Receiving a max press attribute of 3 should emit correct event", + function() + update_profile() + test.socket.matter:__queue_receive( + { mock_device.id, clusters.Switch.attributes.MultiPressMax:build_test_report_data( mock_device, 1, 3 ) - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "double", "pushed_3x"}, {visibility = {displayed = false}})) - }, - } + ) + end ) -test.register_message_test( - "Receiving a max press attribute of greater than 6 should only emit up to pushed_6x", { - { - channel = "matter", - direction = "receive", - message = { +test.register_coroutine_test( + "Receiving a max press attribute of greater than 6 should only emit up to pushed_6x", + function() + update_profile() + test.socket.matter:__queue_receive( + { mock_device.id, clusters.Switch.attributes.MultiPressMax:build_test_report_data( mock_device, 1, 7 ) - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "double", "pushed_3x", "pushed_4x", "pushed_5x", "pushed_6x"}, {visibility = {displayed = false}})) - }, - } + ) + end ) -test.register_message_test( - "Handle double press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ) - } - }, - { -- again, on a device that reports that it supports double press, this event +test.register_coroutine_test( + "Handle double press", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ) + } + ) + -- again, on a device that reports that it supports double press, this event -- will not be generated. See a multi-button test file for that case - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 1, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.double({state_change = true})) - }, - -} + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 1, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.double({state_change = true})) + ) + end ) -test.register_message_test( - "Handle multi press for 4 times", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1, total_number_of_presses_counted = 1, previous_position = 0} - ) - } - }, - { -- again, on a device that reports that it supports double press, this event +test.register_coroutine_test( + "Handle multi press for 4 times", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1, total_number_of_presses_counted = 1, previous_position = 0} + ) + } + ) + -- again, on a device that reports that it supports double press, this event -- will not be generated. See the multi-button test file for that case - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 1, {new_position = 1, total_number_of_presses_counted = 4, previous_position = 0} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed_4x({state_change = true})) - }, - -} + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 1, {new_position = 1, total_number_of_presses_counted = 4, previous_position = 0} + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed_4x({state_change = true})) + ) + end ) -test.register_message_test( - "Handle received BatPercentRemaining from device.", { - { - channel = "matter", - direction = "receive", - message = { +test.register_coroutine_test( + "Handle received BatPercentRemaining from device.", + function() + update_profile() + test.socket.matter:__queue_receive( + { mock_device.id, clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( mock_device, 1, 150 - ), - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message( + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( "main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5)) - ), - }, - } + ) + ) + end ) +local function reset_battery_profiling_info() + local fields = require "switch_utils.fields" + mock_device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist=true}) +end + test.register_coroutine_test( - "Test profile change to button-battery when battery percent remaining attribute (attribute ID 12) is available", + "Test profile does not change to button-battery when battery percent remaining attribute (attribute ID 12) is not available", function() + reset_battery_profiling_info() + test.wait_for_events() test.socket.matter:__queue_receive( { mock_device.id, - clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32(12)}) + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32(10)}) } ) - mock_device:expect_metadata_update({ profile = "button-battery" }) end ) test.register_coroutine_test( - "Test profile does not change to button-battery when battery percent remaining attribute (attribute ID 12) is not available", + "Test profile change to button-batteryLevel when battery percent remaining attribute (attribute ID 14) is available", function() + reset_battery_profiling_info() + test.wait_for_events() test.socket.matter:__queue_receive( { mock_device.id, - clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32(10)}) + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32( + clusters.PowerSource.attributes.BatChargeLevel.ID + )}) } ) + expect_configure_buttons() + mock_device:expect_metadata_update({ profile = "button-batteryLevel" }) + end +) + +test.register_coroutine_test( + "Test profile change to button-battery when battery percent remaining attribute (attribute ID 12) is available", + function() + reset_battery_profiling_info() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32( + clusters.PowerSource.attributes.BatPercentRemaining.ID + )}) + } + ) + expect_configure_buttons() + mock_device:expect_metadata_update({ profile = "button-battery" }) end ) --- run the tests test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua index 301fb36cf0..63d9fcc962 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua @@ -6,16 +6,16 @@ local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local utils = require "st.utils" local dkjson = require "dkjson" - local clusters = require "st.matter.generated.zap_clusters" local button_attr = capabilities.button.button +local uint32 = require "st.matter.data_types.Uint32" -- Mock a 5-button device using endpoints non-consecutive endpoints local mock_device = test.mock_device.build_test_matter_device( { profile = t_utils.get_profile_definition("5-button-battery.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, - matter_version = {hardware = 1, sofrware = 1}, + matter_version = {hardware = 1, software = 1}, endpoints = { { endpoint_id = 0, @@ -96,7 +96,8 @@ local mock_device = test.mock_device.build_test_matter_device( }) -- add device for each mock device -local CLUSTER_SUBSCRIBE_LIST ={ +local CLUSTER_SUBSCRIBE_LIST = { + clusters.PowerSource.server.attributes.AttributeList, clusters.PowerSource.server.attributes.BatPercentRemaining, clusters.Switch.server.events.InitialPress, clusters.Switch.server.events.LongPress, @@ -104,6 +105,11 @@ local CLUSTER_SUBSCRIBE_LIST ={ clusters.Switch.server.events.MultiPressComplete, } +local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) +for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end +end + local function expect_configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) @@ -121,6 +127,14 @@ local function expect_configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("button5", button_attr.pushed({state_change = false}))) end +local function update_profile() + test.socket.matter:__queue_receive({mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( + mock_device, 10, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)} + )}) + expect_configure_buttons() + mock_device:expect_metadata_update({ profile = "5-button-battery" }) +end + -- All messages queued and expectations set are done before the driver is actually run local function test_init() -- we dont want the integration test framework to generate init/doConfigure, we are doing that here @@ -129,10 +143,6 @@ local function test_init() test.mock_device.add_test_device(mock_device) -- make sure the cache is populated -- added sets a bunch of fields on the device, and calls init - local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) - for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do - if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end - end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) @@ -141,74 +151,74 @@ local function test_init() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) --doConfigure sets the provisioning state to provisioned - test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - expect_configure_buttons() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - - -- simulate the profile change update taking affect and the device info changing - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "5-buttons-battery" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - expect_configure_buttons() + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) -test.register_message_test( - "Handle single press sequence, no hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} --move to position 1? - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press - } -} +test.register_coroutine_test( + "Simulate the profile change update taking affect and the device info changing", + function() + test.socket.matter:__set_channel_ordering("relaxed") + update_profile() + test.wait_for_events() + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "5-buttons-battery" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + expect_configure_buttons() + end ) -test.register_message_test( - "Handle single press sequence for short release-supported button", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 20, {new_position = 1} --move to position 1? - ), - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device, 20, {previous_position = 0} --move to position 1? - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("button2", button_attr.pushed({state_change = true})) --should send initial press - } -} +test.register_coroutine_test( + "Handle single press sequence, no hold", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} --move to position 1 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + ) + end +) + +test.register_coroutine_test( + "Handle single press sequence for short release-supported button", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 20, {new_position = 1} --move to position 1? + ) + } + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 20, {previous_position = 0} --move to position 1 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button2", button_attr.pushed({state_change = true})) + ) + end ) test.register_coroutine_test( "Handle single press sequence for emulated hold on short-release-only button", function () + update_profile() test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.matter:__queue_receive({ mock_device.id, @@ -231,6 +241,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Handle single press sequence for a long hold on long-release-capable button", -- only a long press event should generate a held event function () + update_profile() test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.matter:__queue_receive({ mock_device.id, @@ -253,6 +264,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Handle single press sequence for a long hold on multi button", -- pushes should only be generated from multiPressComplete events function () + update_profile() test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.matter:__queue_receive({ mock_device.id, @@ -274,6 +286,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Handle single press sequence for a multi press on multi button", function () + update_profile() test.socket.matter:__queue_receive({ mock_device.id, clusters.Switch.events.InitialPress:build_test_event_report( @@ -311,6 +324,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Handle long press sequence for a long hold on long-release-capable button", -- only a long press event should generate a held event function () + update_profile() test.socket.matter:__queue_receive({ mock_device.id, clusters.Switch.events.InitialPress:build_test_event_report( @@ -336,6 +350,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Handle long press sequence for a long hold on multi button", function () + update_profile() test.socket.matter:__queue_receive({ mock_device.id, clusters.Switch.events.InitialPress:build_test_event_report( @@ -358,397 +373,340 @@ test.register_coroutine_test( end ) -test.register_message_test( - "Handle single press sequence, with hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) - } -} +test.register_coroutine_test( + "Handle single press sequence, with hold", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.held({state_change = true})) + ) + end ) -test.register_message_test( - "Handle release after short press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device, 10, {previous_position = 1} - ) - } - }, - { -- this is a double event because the test device in this test shouldn't support the above event +test.register_coroutine_test( + "Handle release after short press", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 10, {previous_position = 1} + ) + } + ) + -- this is a double event because the test device in this test shouldn't support the above event -- but we handle it anyway - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + ) + end ) -test.register_message_test( - "Handle release after long press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongRelease:build_test_event_report( - mock_device, 10, {previous_position = 1} - ) - } - }, - } +test.register_coroutine_test( + "Handle release after long press", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.held({state_change = true})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 10, {previous_position = 1} + ) + } + ) + end ) -test.register_message_test( - "Receiving a max press attribute of 2 should emit correct event", { - { - channel = "matter", - direction = "receive", - message = { +test.register_coroutine_test( + "Receiving a max press attribute of 2 should emit correct event", + function() + update_profile() + test.socket.matter:__queue_receive( + { mock_device.id, clusters.Switch.attributes.MultiPressMax:build_test_report_data( mock_device, 10, 2 ) - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) - }, - } + ) + end ) -test.register_message_test( - "Receiving a max press attribute of 3 should emit correct event", { - { - channel = "matter", - direction = "receive", - message = { +test.register_coroutine_test( + "Receiving a max press attribute of 3 should emit correct event", + function() + update_profile() + test.socket.matter:__queue_receive( + { mock_device.id, clusters.Switch.attributes.MultiPressMax:build_test_report_data( mock_device, 60, 3 ) - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("button5", + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button5", capabilities.button.supportedButtonValues({"pushed", "double", "held", "pushed_3x"}, {visibility = {displayed = false}})) - }, - } + ) + end ) -test.register_message_test( - "Receiving a max press attribute of greater than 6 should only emit up to pushed_6x", { - { - channel = "matter", - direction = "receive", - message = { +test.register_coroutine_test( + "Receiving a max press attribute of greater than 6 should only emit up to pushed_6x", + function() + update_profile() + test.socket.matter:__queue_receive( + { mock_device.id, clusters.Switch.attributes.MultiPressMax:build_test_report_data( mock_device, 10, 7 ) - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "double", "pushed_3x", "pushed_4x", "pushed_5x", "pushed_6x"}, {visibility = {displayed = false}})) - }, - } + ) + end ) -test.register_message_test( - "Handle double press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ) - } - }, - { -- again, on a device that reports that it supports double press, this event +test.register_coroutine_test( + "Handle double press", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + ) + -- again, on a device that reports that it supports double press, this event -- will not be generated. See a multi-button test file for that case - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 10, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.double({state_change = true})) - }, - -} + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 10, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.double({state_change = true})) + ) + end ) -test.register_message_test( - "Handle multi press for 4 times", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ) - } - }, - { -- again, on a device that reports that it supports double press, this event +test.register_coroutine_test( + "Handle multi press for 4 times", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + ) + -- again, on a device that reports that it supports double press, this event -- will not be generated. See a multi-button test file for that case - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 10, {new_position = 1, total_number_of_presses_counted = 4, previous_position=0} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed_4x({state_change = true})) - }, - -} + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 10, {new_position = 1, total_number_of_presses_counted = 4, previous_position=0} + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", button_attr.pushed_4x({state_change = true})) + ) + end ) -test.register_message_test( - "Receiving a max press attribute of 2 should emit correct event", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.attributes.MultiPressMax:build_test_report_data( - mock_device, 50, 2 - ) - }, - }, +test.register_coroutine_test( + "Receiving a max press attribute of 2 should emit correct event", + function() + update_profile() + test.socket.matter:__queue_receive( { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("button4", - capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) - }, - } + mock_device.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 50, 2 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button4", + capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) + ) + end ) -test.register_message_test( - "Handle received BatPercentRemaining from device.", { - { - channel = "matter", - direction = "receive", - message = { +test.register_coroutine_test( + "Handle received BatPercentRemaining from device.", + function() + update_profile() + test.socket.matter:__queue_receive( + { mock_device.id, clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( mock_device, 10, 150 - ), - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message( + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( "main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5)) - ), - }, - } + ) + ) + end ) -test.register_message_test( - "Handle a long press including MultiPressComplete", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 60, {new_position = 1} - ) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 60, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("button5", button_attr.held({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} - ) - } - } - -- no double event -} +test.register_coroutine_test( + "Handle a long press including MultiPressComplete", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button5", button_attr.held({state_change = true})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} + ) + } + ) + -- no double event + end ) -test.register_message_test( - "Handle long press followed by single press", { - { - channel = "matter", - direction = "receive", - message = { +test.register_coroutine_test( + "Handle long press followed by single press", + function() + update_profile() + test.socket.matter:__queue_receive( + { mock_device.id, clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 60, {new_position = 1} + mock_device, 60, {new_position = 1} ) } - }, - { - channel = "matter", - direction = "receive", - message = { + ) + test.socket.matter:__queue_receive( + { mock_device.id, clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 60, {new_position = 1} + mock_device, 60, {new_position = 1} ) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("button5", button_attr.held({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button5", button_attr.held({state_change = true})) + ) + test.socket.matter:__queue_receive( + { mock_device.id, clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 60, {new_position = 1} + mock_device, 60, {new_position = 1} ) } - }, - { - channel = "matter", - direction = "receive", - message = { + ) + test.socket.matter:__queue_receive( + { mock_device.id, clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} + mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} ) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("button5", button_attr.pushed({state_change = true})) - } - } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button5", button_attr.pushed({state_change = true})) + ) + end ) --- run the tests + test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_window_covering.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_window_covering.lua new file mode 100644 index 0000000000..d135068be2 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_window_covering.lua @@ -0,0 +1,1141 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" + +local WindowCovering = clusters.WindowCovering + +local mock_device = test.mock_device.build_test_matter_device( + { + label = "Matter Window Covering", + profile = t_utils.get_profile_definition("window-covering-modular.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = 10, + clusters = { + { + cluster_id = clusters.WindowCovering.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 3, + }, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} + }, + device_types = { + {device_type_id = 0x0202, device_type_revision = 1} -- WindowCovering + } + }, + { + endpoint_id = 20, + clusters = { + { + cluster_id = clusters.WindowCovering.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 3, + } + }, + device_types = { + {device_type_id = 0x0202, device_type_revision = 1} -- WindowCovering + } + }, + }, + } +) + +local mock_child = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("window-covering-modular.yml"), + device_network_id = string.format("%s:%d", mock_device.id, 20), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 20) +}) + +local function set_preset(device) + test.socket.capability:__expect_send( + device:generate_test_message( + "main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed = false}}) + ) + ) + test.socket.capability:__expect_send( + device:generate_test_message( + "main", capabilities.windowShadePreset.position(50, {visibility = {displayed = false}}) + ) + ) +end + +local subscribe_request + +local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, + {visibility = {displayed = false}}) + ) + ) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + set_preset(mock_device) + + subscribe_request = WindowCovering.server.attributes.OperationalStatus:subscribe(mock_device) + subscribe_request:merge(clusters.PowerSource.server.attributes.AttributeList:subscribe(mock_device)) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + test.mock_device.add_test_device(mock_child) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Window Covering 2", + profile = "window-covering-modular", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 20) + }) + + test.socket.device_lifecycle:__queue_receive({ mock_child.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + mock_child:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +test.set_test_init_function(test_init) + +local CLUSTER_SUBSCRIBE_LIST = { + WindowCovering.server.attributes.CurrentPositionLiftPercent100ths, + WindowCovering.server.attributes.CurrentPositionTiltPercent100ths, + WindowCovering.server.attributes.OperationalStatus, + clusters.LevelControl.server.attributes.CurrentLevel, +} + +local function update_profile() + test.socket.matter:__queue_receive({mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( + mock_device, 10, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)} + )}) + mock_child:expect_metadata_update({ profile = "window-covering-modular", optional_component_capabilities = {{"main", {"windowShadeLevel", "windowShadeTiltLevel"}}} }) + mock_device:expect_metadata_update({ profile = "window-covering-modular", optional_component_capabilities = {{"main", {"windowShadeLevel", "windowShadeTiltLevel", "battery"}}} }) + local updated_device_profile = t_utils.get_profile_definition("window-covering-modular.yml", + {enabled_optional_capabilities = {{"main", {"windowShadeLevel", "windowShadeTiltLevel"}}}} + ) + test.socket.device_lifecycle:__queue_receive(mock_child:generate_info_changed({ profile = updated_device_profile })) + subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + subscribe_request:merge(clusters.PowerSource.server.attributes.AttributeList:subscribe(mock_device)) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.wait_for_events() + updated_device_profile = t_utils.get_profile_definition("window-covering-modular.yml", + {enabled_optional_capabilities = {{"main", {"windowShadeLevel", "windowShadeTiltLevel", "battery"}}}} + ) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = updated_device_profile })) + subscribe_request:merge(clusters.PowerSource.server.attributes.BatPercentRemaining:subscribe(mock_device)) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) +end + +test.register_coroutine_test( + "WindowCovering OperationalStatus state closed following lift position update", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 10000 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(0) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.closed() + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) + end +) + +test.register_coroutine_test( + "WindowCovering OperationalStatus state closed following tilt position update", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_device, 10, 10000 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(0) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.closed() + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) + end +) + +test.register_coroutine_test( + "WindowCovering OperationalStatus state closed before lift position 0", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 10000 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(0) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.closed() + ) + ) + end +) + +test.register_coroutine_test( + "WindowCovering OperationalStatus state closed before tilt position 0", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_device, 10, 10000 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(0) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.closed() + ) + ) + end +) + +test.register_coroutine_test( + "WindowCovering OperationalStatus state open following lift position update", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 0 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(100) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.open() + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) + end +) + +test.register_coroutine_test( + "WindowCovering OperationalStatus state open following tilt position update", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_device, 10, 0 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(100) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.open() + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) + end +) + +test.register_coroutine_test( + "WindowCovering OperationalStatus state open before lift position event", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(100) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.open() + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 0 + ), + } + ) + end +) + +test.register_coroutine_test( + "WindowCovering OperationalStatus state open before tilt position event", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(100) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.open() + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_device, 10, 0 + ), + } + ) + end +) + +test.register_coroutine_test( + "WindowCovering OperationalStatus partially open following lift position update", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 25) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(25) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) + end +) + +test.register_coroutine_test( + "WindowCovering OperationalStatus partially open following tilt position update", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 15) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(15) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) + end +) + +test.register_coroutine_test( + "WindowCovering OperationalStatus partially open before lift position event", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 25) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(25) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + end +) + +test.register_coroutine_test( + "WindowCovering OperationalStatus partially open before tilt position event", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 65) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(65) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + end +) + +test.register_coroutine_test("WindowCovering OperationalStatus opening", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 25) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(25) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 1), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.opening() + ) + ) +end) + +test.register_coroutine_test("WindowCovering OperationalStatus closing", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 25) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(25) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 2), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.closing() + ) + ) +end) + +test.register_coroutine_test("WindowCovering OperationalStatus unknown", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 25) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(25) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 3), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.unknown() + ) + ) +end) + +test.register_coroutine_test( + "WindowShade open cmd handler", function() + update_profile() + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "windowShade", component = "main", command = "open", args = {}}, + } + ) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.UpOrOpen(mock_device, 10)} + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "WindowShade close cmd handler", function() + update_profile() + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "windowShade", component = "main", command = "close", args = {}}, + } + ) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.DownOrClose(mock_device, 10)} + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "WindowShade pause cmd handler", function() + update_profile() + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "windowShade", component = "main", command = "pause", args = {}}, + } + ) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.StopMotion(mock_device, 10)} + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Refresh necessary attributes", function() + update_profile() + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) + test.socket.capability:__expect_send( + { + mock_device.id, + { + capability_id = "windowShade", + component_id = "main", + attribute_id = "supportedWindowShadeCommands", + state = {value = {"open", "close", "pause"}}, + visibility = {displayed = false} + }, + } + ) + test.wait_for_events() + + test.socket.capability:__queue_receive( + {mock_device.id, {capability = "refresh", component = "main", command = "refresh", args = {}}} + ) + local read_request = CLUSTER_SUBSCRIBE_LIST[1]:read(mock_device) + for i, attr in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then read_request:merge(attr:read(mock_device)) end + end + read_request:merge(clusters.PowerSource.server.attributes.BatPercentRemaining:read(mock_device)) + test.socket.matter:__expect_send({mock_device.id, read_request}) + test.wait_for_events() + end +) + +test.register_coroutine_test("WindowShade setShadeLevel cmd handler", function() + update_profile() + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 20 }}, + } + ) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 8000)} + ) +end) + +test.register_coroutine_test("WindowShade setShadeTiltLevel cmd handler", function() + update_profile() + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "windowShadeTiltLevel", component = "main", command = "setShadeTiltLevel", args = { 60 }}, + } + ) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToTiltPercentage(mock_device, 10, 4000)} + ) +end) + +test.register_coroutine_test("LevelControl CurrentLevel handler", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 100), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(math.floor((100 / 254.0 * 100) + .5)) + ) + ) +end) + +--test battery +test.register_coroutine_test( + "Battery percent reports should generate correct messages", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( + mock_device, 10, 150 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.battery.battery(math.floor(150/2.0+0.5)) + ) + ) + end +) + +test.register_coroutine_test("OperationalStatus report contains current position report", function() + update_profile() + test.wait_for_events() + local report = WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 25) *100) + ) + table.insert(report.info_blocks, WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0).info_blocks[1]) + test.socket.matter:__queue_receive({ mock_device.id, report}) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(25) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) +end) + +test.register_coroutine_test( + "Handle preset commands", + function() + update_profile() + test.wait_for_events() + local PRESET_LEVEL = 30 + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = { PRESET_LEVEL }}, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadePreset.position(PRESET_LEVEL) + ) + ) + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShadePreset", component = "main", command = "presetPosition", args = {}}, + }) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, (100 - PRESET_LEVEL) * 100)} + ) + end +) + +test.register_coroutine_test( + "WindowCovering shade level adjusted by greater than 2%; status reflects Closing followed by Partially Open", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 25) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(25) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 19 }}, + } + ) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 8100)} + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 10), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.closing() + ) + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 23) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(23) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 21) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(21) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 19) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(19) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + end +) + +test.register_coroutine_test( + "WindowCovering shade level adjusted by less than or equal to 2%; status reflects Closing followed by Partially Open", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 25) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(25) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 23 }}, + } + ) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 7700)} + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 10), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.closing() + ) + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 23) *100) + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(23) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + end +) + +test.register_coroutine_test( + "Check that preference updates to reverse polarity after being set to true and that the shade lift operates as expected when opening and closing", function() + update_profile() + test.wait_for_events() + mock_device:set_field("__reverse_polarity", true) + test.socket.matter:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 100 * 100 + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(0) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.open() + ) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 0 + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(100) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.closed() + ) + ) + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 85 }}, + }) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 1500)} + ) + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 100 }}, + }) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 0)} + ) + end +) + +test.register_coroutine_test( + "Check that preference updates to reverse polarity after being set to true and that the shade tilt operates as expected when opening and closing", function() + update_profile() + test.wait_for_events() + mock_device:set_field("__reverse_polarity", true) + test.socket.matter:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_device, 10, 100 * 100 + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(0) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.open() + ) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_device, 10, 0 + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(100) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.closed() + ) + ) + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShadeTiltLevel", component = "main", command = "setShadeTiltLevel", args = { 15 }}, + }) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToTiltPercentage(mock_device, 10, 8500)} + ) + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShadeTiltLevel", component = "main", command = "setShadeTiltLevel", args = { 0 }}, + }) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToTiltPercentage(mock_device, 10, 10000)} + ) + end +) + +test.register_coroutine_test( + "WindowCovering OperationalStatus state closed following lift position update for child device", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_child, 20, 10000 + ), + } + ) + test.socket.capability:__expect_send( + mock_child:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(0) + ) + ) + test.socket.capability:__expect_send( + mock_child:generate_test_message( + "main", capabilities.windowShade.windowShade.closed() + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_child, 20, 0), + } + ) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml deleted file mode 100644 index 532ba1f304..0000000000 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ /dev/null @@ -1,251 +0,0 @@ -matterManufacturer: -#Eve - - id: "Eve MotionBlinds" - deviceLabel: Eve MotionBlinds - vendorId: 0x130A - productId: 0x55 - deviceProfileName: window-covering-battery - - id: "4874/109" - deviceLabel: Eve MotionBlinds for Roller Blinds - vendorId: 0x130A - productId: 0x006D - deviceProfileName: window-covering-battery - - id: "4874/98" - deviceLabel: Eve MotionBlinds for Honeycomb Blinds - vendorId: 0x130A - productId: 0x0062 - deviceProfileName: window-covering-battery - - id: "4874/99" - deviceLabel: Eve MotionBlinds for Venetian Blinds - vendorId: 0x130A - productId: 0x0063 - deviceProfileName: window-covering-battery - - id: "4874/100" - deviceLabel: Eve MotionBlinds for Curtains - vendorId: 0x130A - productId: 0x0064 - deviceProfileName: window-covering-battery - - id: "4874/96" - deviceLabel: Eve Shutter Switch - vendorId: 0x130A - productId: 0x0060 - deviceProfileName: window-covering -# Griesser - - id: "5435/14337" - deviceLabel: MSM-1 - vendorId: 0x153B - productId: 0x3801 - deviceProfileName: window-covering-tilt -# Meross - - id: "4933/61453" - deviceLabel: Smart Wi-Fi Roller Shutter Timer - vendorId: 0x1345 - productId: 0xF00D - deviceProfileName: window-covering -# Mamaba - - id: "4965/4097" - deviceLabel: Wi-Fi Curtain - vendorId: 0x1365 - productId: 0x1001 - deviceProfileName: window-covering -# SmartWave - - id: "5376/10001" - deviceLabel: SmartWave Motorized Roller Shades 100 Blackout Flex - vendorId: 0x1500 - productId: 0x2711 - deviceProfileName: window-covering-battery -# SmartWings - - id: "5231/4097" - deviceLabel: SmartWings Window Covering - vendorId: 0x146F - productId: 0x1001 - deviceProfileName: window-covering-battery -#Zemismart - - id: "Zemismart MT01 Slide Curtain" - deviceLabel: Zemismart MT01 Slide Curtain - vendorId: 0x139C - productId: 0xFFFE - deviceProfileName: window-covering - - id: "5020/65376" - deviceLabel: Zemismart MT25B Roller Motor - vendorId: 0x139C - productId: 0xFF60 - deviceProfileName: window-covering - - id: "5020/65296" - deviceLabel: Zemismart MT82 Smart Curtain - vendorId: 0x139C - productId: 0xFF10 - deviceProfileName: window-covering - - id: "5020/65301" - deviceLabel: Zemismart MT25A Thread Roller Motor - vendorId: 0x139C - productId: 0xFF15 - deviceProfileName: window-covering - - id: "5020/64050" - deviceLabel: Zemismart ZM02 Smart Curtain - vendorId: 0x139C - productId: 0xFA32 - deviceProfileName: window-covering - - id: "5020/64017" - deviceLabel: Zemismart ZM25C Smart Curtain - vendorId: 0x139C - productId: 0xFA11 - deviceProfileName: window-covering - - id: "5020/64049" - deviceLabel: Zemismart ZM01 Smart Curtain - vendorId: 0x139C - productId: 0xFA31 - deviceProfileName: window-covering - - id: "5020/64023" - deviceLabel: Zemismart ZM24A Smart Curtain - vendorId: 0x139C - productId: 0xFA17 - deviceProfileName: window-covering -#WISTAR - - id: "5207/3" - deviceLabel: WISTAR WSERD16-B Smart Tubular Motor - vendorId: 0x1457 - productId: 0x0003 - deviceProfileName: window-covering-battery - - id: "5207/4" - deviceLabel: WISTAR WSERD24 Smart Tubular Motor - vendorId: 0x1457 - productId: 0x0004 - deviceProfileName: window-covering - - id: "5207/5" - deviceLabel: WISTAR WSERD40-B Smart Tubular Motor - vendorId: 0x1457 - productId: 0x0005 - deviceProfileName: window-covering-battery - - id: "5207/6" - deviceLabel: WISTAR WSERD40-L Smart Tubular Motor - vendorId: 0x1457 - productId: 0x0006 - deviceProfileName: window-covering-battery - - id: "5207/7" - deviceLabel: WISTAR WSERD40-T Smart Tubular Motor - vendorId: 0x1457 - productId: 0x0007 - deviceProfileName: window-covering - - id: "5207/8" - deviceLabel: WISTAR WSERD50-B Smart Tubular Motor - vendorId: 0x1457 - productId: 0x0008 - deviceProfileName: window-covering-battery - - id: "5207/9" - deviceLabel: WISTAR WSERD50-L Smart Tubular Motor - vendorId: 0x1457 - productId: 0x0009 - deviceProfileName: window-covering-battery - - id: "5207/16" - deviceLabel: WISTAR WSERD50-T Smart Tubular Motor - vendorId: 0x1457 - productId: 0x0010 - deviceProfileName: window-covering - - id: "5207/19" - deviceLabel: WISTAR WSER60 Smart Tubular Motor - vendorId: 0x1457 - productId: 0x0013 - deviceProfileName: window-covering - - id: "5207/17" - deviceLabel: WISTAR WSER40 Smart Tubular Motor - vendorId: 0x1457 - productId: 0x0011 - deviceProfileName: window-covering - - id: "5207/18" - deviceLabel: WISTAR WSER50 Smart Tubular Motor - vendorId: 0x1457 - productId: 0x0012 - deviceProfileName: window-covering - - id: "5207/2" - deviceLabel: WISTAR WSERD30-B Smart Tubular Motor - vendorId: 0x1457 - productId: 0x0002 - deviceProfileName: window-covering-battery - - id: "5207/22" - deviceLabel: WISTAR WSCMXH Smart Vertical Blind Motor - vendorId: 0x1457 - productId: 0x0016 - deviceProfileName: window-covering-tilt - - id: "5207/23" - deviceLabel: WISTAR WSCMXF Smart Vertical Blind Motor - vendorId: 0x1457 - productId: 0x0017 - deviceProfileName: window-covering-tilt - - id: "5207/24" - deviceLabel: WISTAR WSCMXF-LED Smart Vertical Blind Motor - vendorId: 0x1457 - productId: 0x0018 - deviceProfileName: window-covering-tilt - - id: "5207/20" - deviceLabel: WISTAR WSCMQ Smart Curtain Motor - vendorId: 0x1457 - productId: 0x0014 - deviceProfileName: window-covering - - id: "5207/21" - deviceLabel: WISTAR WSCMXI Smart Curtain Motor - vendorId: 0x1457 - productId: 0x0015 - deviceProfileName: window-covering - - id: "5207/32" - deviceLabel: WISTAR WSCMT Smart Curtain Motor - vendorId: 0x1457 - productId: 0x0020 - deviceProfileName: window-covering - - id: "5207/34" - deviceLabel: WISTAR WSCMXB Smart Curtain Motor - vendorId: 0x1457 - productId: 0x0022 - deviceProfileName: window-covering - - id: "5207/35" - deviceLabel: WISTAR WSCMXC Smart Curtain Motor - vendorId: 0x1457 - productId: 0x0023 - deviceProfileName: window-covering - - id: "5207/38" - deviceLabel: WISTAR WSCMXJ Smart Curtain Motor - vendorId: 0x1457 - productId: 0x0026 - deviceProfileName: window-covering -#Yooksmart - - id: "5411/1052" - deviceLabel: Smart WindowCovering Series - vendorId: 0x1523 - productId: 0x041C - deviceProfileName: window-covering-battery - - id: "5411/2660" - deviceLabel: Smart WindowCovering Series - vendorId: 0x1523 - productId: 0x0A64 - deviceProfileName: window-covering-battery - - id: "5411/2661" - deviceLabel: Smart WindowCovering Series - vendorId: 0x1523 - productId: 0x0A65 - deviceProfileName: window-covering-battery - - id: "5411/2662" - deviceLabel: Smart WindowCovering Series - vendorId: 0x1523 - productId: 0x0A66 - deviceProfileName: window-covering-battery - - id: "5411/2663" - deviceLabel: Smart WindowCovering Series - vendorId: 0x1523 - productId: 0x0A67 - deviceProfileName: window-covering-battery - - id: "5411/2664" - deviceLabel: Smart WindowCovering Series - vendorId: 0x1523 - productId: 0x0A68 - deviceProfileName: window-covering-battery - - id: "5411/2665" - deviceLabel: Smart WindowCovering Series - vendorId: 0x1523 - productId: 0x0A69 - deviceProfileName: window-covering-battery -matterGeneric: - - id: "windowcovering" - deviceLabel: Matter Window Covering - deviceTypes: - - id: 0x0202 # Window Covering - deviceProfileName: window-covering