From 8264136736cfe58357127dfff788eb019b52c821 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Tue, 10 Dec 2024 15:47:37 -0600 Subject: [PATCH] Support the tilt feature --- .../profiles/window-covering-tilt-battery.yml | 23 ++ .../window-covering-tilt-only-battery.yml | 21 ++ .../profiles/window-covering-tilt-only.yml | 19 ++ .../profiles/window-covering-tilt.yml | 21 ++ .../matter-window-covering/src/init.lua | 69 ++++-- .../src/test/test_matter_window_covering.lua | 207 +++++++++++++++++- 6 files changed, 333 insertions(+), 27 deletions(-) create mode 100644 drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-battery.yml create mode 100644 drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-only-battery.yml create mode 100644 drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-only.yml create mode 100644 drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt.yml diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-battery.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-battery.yml new file mode 100644 index 0000000000..7a39456b52 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-battery.yml @@ -0,0 +1,23 @@ +name: window-covering-tilt-battery +components: +- id: main + capabilities: + - id: windowShade + version: 1 + - id: windowShadePreset + version: 1 + - id: windowShadeLevel + version: 1 + - id: windowShadeTiltLevel + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Blind +preferences: + - preferenceId: presetPosition + explicit: true diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-only-battery.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-only-battery.yml new file mode 100644 index 0000000000..e8dc16d688 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-only-battery.yml @@ -0,0 +1,21 @@ +name: window-covering-tilt-only-battery +components: +- id: main + capabilities: + - id: windowShade + version: 1 + - id: windowShadePreset + version: 1 + - id: windowShadeTiltLevel + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Blind +preferences: + - preferenceId: presetPosition + explicit: true diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-only.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-only.yml new file mode 100644 index 0000000000..861ffbe47e --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-only.yml @@ -0,0 +1,19 @@ +name: window-covering-tilt-only +components: +- id: main + capabilities: + - id: windowShade + version: 1 + - id: windowShadePreset + version: 1 + - id: windowShadeTiltLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Blind +preferences: + - preferenceId: presetPosition + explicit: true diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt.yml new file mode 100644 index 0000000000..6ed5bfac82 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt.yml @@ -0,0 +1,21 @@ +name: window-covering-tilt +components: +- id: main + capabilities: + - id: windowShade + version: 1 + - id: windowShadePreset + version: 1 + - id: windowShadeLevel + version: 1 + - id: windowShadeTiltLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Blind +preferences: + - preferenceId: presetPosition + explicit: true diff --git a/drivers/SmartThings/matter-window-covering/src/init.lua b/drivers/SmartThings/matter-window-covering/src/init.lua index 939448d55c..bbcdaa1f6d 100644 --- a/drivers/SmartThings/matter-window-covering/src/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/init.lua @@ -41,6 +41,15 @@ end local function match_profile(device) local profile_name = "window-covering" + local lift_eps = device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.LIFT}) + local tilt_eps = device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.TILT}) + if #tilt_eps > 0 then + if #lift_eps > 0 then + profile_name = profile_name .. "-tilt" + else + profile_name = profile_name .. "-tilt-only" + end + end local battery_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) @@ -111,8 +120,7 @@ local function handle_pause(driver, device, cmd) device:send(req) end --- move to shade level --- beteween 0-100 +-- move to shade level between 0-100 local function handle_shade_level(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) local lift_percentage_value = 100 - cmd.args.shadeLevel @@ -123,29 +131,42 @@ local function handle_shade_level(driver, device, cmd) device:send(req) end --- current lift percentage, changed to 100ths percent -local function current_pos_handler(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)) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windowShadeLevel.shadeLevel(position)) - if position == 0 then - device:emit_event_for_endpoint(ib.endpoint_id, windowShade.closed()) - elseif position == 100 then - device:emit_event_for_endpoint(ib.endpoint_id, windowShade.open()) - elseif position > 0 and position < 100 then - device:emit_event_for_endpoint(ib.endpoint_id, windowShade.partially_open()) - else - device:emit_event_for_endpoint(ib.endpoint_id, windowShade.unknown()) +-- move to shade tilt level between 0-100 +local function 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 + +-- current lift/tilt percentage, changed to 100ths percent +local 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)) + device:emit_event_for_endpoint(ib.endpoint_id, attribute(position)) + if position == 0 then + device:emit_event_for_endpoint(ib.endpoint_id, windowShade.closed()) + elseif position == 100 then + device:emit_event_for_endpoint(ib.endpoint_id, windowShade.open()) + elseif position > 0 and position < 100 then + device:emit_event_for_endpoint(ib.endpoint_id, windowShade.partially_open()) + else + device:emit_event_for_endpoint(ib.endpoint_id, windowShade.unknown()) + end end end -- checks the current position of the shade local function current_status_handler(driver, device, ib, response) local windowShade = capabilities.windowShade.windowShade - local state = ib.data.value & clusters.WindowCovering.types.OperationalStatus.GLOBAL --Could use LIFT instead + local state = ib.data.value & clusters.WindowCovering.types.OperationalStatus.GLOBAL if state == 1 then -- opening device:emit_event_for_endpoint(ib.endpoint_id, windowShade.opening()) elseif state == 2 then -- closing @@ -180,7 +201,8 @@ local matter_driver_template = { }, [clusters.WindowCovering.ID] = { --uses percent100ths more often - [clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID] = current_pos_handler, + [clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID] = current_pos_handler(capabilities.windowShadeLevel.shadeLevel), + [clusters.WindowCovering.attributes.CurrentPositionTiltPercent100ths.ID] = current_pos_handler(capabilities.windowShadeTiltLevel.shadeTiltLevel), [clusters.WindowCovering.attributes.OperationalStatus.ID] = current_status_handler, }, [clusters.PowerSource.ID] = { @@ -196,6 +218,9 @@ local matter_driver_template = { clusters.LevelControl.attributes.CurrentLevel, clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths, }, + [capabilities.windowShadeTiltLevel.ID] = { + clusters.WindowCovering.attributes.CurrentPositionTiltPercent100ths, + }, [capabilities.battery.ID] = { clusters.PowerSource.attributes.BatPercentRemaining } @@ -215,9 +240,13 @@ local matter_driver_template = { [capabilities.windowShadeLevel.ID] = { [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = handle_shade_level, }, + [capabilities.windowShadeTiltLevel.ID] = { + [capabilities.windowShadeTiltLevel.commands.setShadeTiltLevel.NAME] = handle_shade_tilt_level, + }, }, supported_capabilities = { capabilities.windowShadeLevel, + capabilities.windowShadeTiltLevel, capabilities.windowShade, capabilities.windowShadePreset, capabilities.battery, diff --git a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua index fb2087fa59..e09731ff41 100644 --- a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua +++ b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua @@ -21,7 +21,7 @@ local WindowCovering = clusters.WindowCovering local mock_device = test.mock_device.build_test_matter_device( { - profile = t_utils.get_profile_definition("window-covering-battery.yml"), + profile = t_utils.get_profile_definition("window-covering-tilt-battery.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, preferences = { presetPosition = 30 }, endpoints = { @@ -118,6 +118,7 @@ local mock_device_mains_powered = test.mock_device.build_test_matter_device( local CLUSTER_SUBSCRIBE_LIST = { clusters.LevelControl.server.attributes.CurrentLevel, WindowCovering.server.attributes.CurrentPositionLiftPercent100ths, + WindowCovering.server.attributes.CurrentPositionTiltPercent100ths, WindowCovering.server.attributes.OperationalStatus, clusters.PowerSource.server.attributes.BatPercentRemaining } @@ -161,7 +162,7 @@ end test.set_test_init_function(test_init) test.register_coroutine_test( - "WindowCovering OperationalStatus state closed", function() + "WindowCovering OperationalStatus state closed following lift position update", function() test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { @@ -191,7 +192,37 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "WindowCovering OperationalStatus state closed before position 0", function() + "WindowCovering OperationalStatus state closed following tilt position update", function() + test.socket.capability:__set_channel_ordering("relaxed") + 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() test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { @@ -221,7 +252,37 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "WindowCovering OperationalStatus state open", function() + "WindowCovering OperationalStatus state closed before tilt position 0", function() + test.socket.capability:__set_channel_ordering("relaxed") + 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() test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { @@ -251,7 +312,37 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "WindowCovering OperationalStatus state open before position event", function() + "WindowCovering OperationalStatus state open following tilt position update", function() + test.socket.capability:__set_channel_ordering("relaxed") + 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() test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { @@ -281,7 +372,37 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "WindowCovering OperationalStatus partially open", function() + "WindowCovering OperationalStatus state open before tilt position event", function() + test.socket.capability:__set_channel_ordering("relaxed") + 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() test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { @@ -311,7 +432,37 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "WindowCovering OperationalStatus partially open before position event", function() + "WindowCovering OperationalStatus partially open following tilt position update", function() + test.socket.capability:__set_channel_ordering("relaxed") + 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() test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { @@ -340,6 +491,36 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "WindowCovering OperationalStatus partially open before tilt position event", function() + test.socket.capability:__set_channel_ordering("relaxed") + 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() test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( @@ -525,6 +706,18 @@ test.register_coroutine_test("WindowShade setShadeLevel cmd handler", function() ) end) +test.register_coroutine_test("WindowShade setShadeTiltLevel cmd handler", function() + 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() test.socket.matter:__queue_receive( {