diff --git a/drivers/SmartThings/matter-window-covering/src/init.lua b/drivers/SmartThings/matter-window-covering/src/init.lua index a8e6e80114..159390fe33 100644 --- a/drivers/SmartThings/matter-window-covering/src/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/init.lua @@ -15,19 +15,28 @@ --Note: Currently only support for window shades with the PositionallyAware Feature --Note: No support for setting device into calibration mode, it must be done manually local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" local im = require "st.matter.interaction_model" local log = require "log" -local clusters = require "st.matter.clusters" local MatterDriver = require "st.matter.driver" local CURRENT_LIFT = "__current_lift" local CURRENT_TILT = "__current_tilt" +local REVERSE_POLARITY = "__reverse_polarity" +local STATE_MACHINE = "__state_machine" + +local StateMachineEnum = { + STATE_IDLE = 0x00, + STATE_MOVING = 0x01, + STATE_OPERATIONAL_STATE_FIRED = 0x02, + STATE_CURRENT_POSITION_FIRED = 0x03 +} + local battery_support = { NO_BATTERY = "NO_BATTERY", BATTERY_LEVEL = "BATTERY_LEVEL", BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" } -local REVERSE_POLARITY = "__reverse_polarity" local function find_default_endpoint(device, cluster) local res = device.MATTER_DEFAULT_ENDPOINT @@ -70,6 +79,7 @@ end local function device_init(driver, device) device:set_component_to_endpoint_fn(component_to_endpoint) device:subscribe() + device:set_field(STATE_MACHINE, StateMachineEnum.STATE_IDLE) end local function do_configure(driver, device) @@ -187,15 +197,49 @@ local function handle_shade_tilt_level(driver, device, cmd) device:send(req) end +--- Update the window shade status according to the lift and tilt positions. +--- LIFT TILT Window Shade +--- 100 any Open +--- 1-99 any Partially Open +--- 0 1-100 Partially Open +--- 0 0 Closed +--- 0 nil Closed +--- nil 100 Open +--- nil 1-99 Partially Open +--- nil 0 Closed +--- Note that lift or tilt may be nil if either the window shade does not +--- support them or if they haven't been received from a device report yet. +local function update_shade_status(device, endpoint_id, lift_position, tilt_position) + local windowShade = capabilities.windowShade.windowShade + local reverse = device:get_field(REVERSE_POLARITY) + if lift_position == nil then + if tilt_position == 0 then + device:emit_event_for_endpoint(endpoint_id, reverse and windowShade.open() or windowShade.closed()) + elseif tilt_position == 100 then + device:emit_event_for_endpoint(endpoint_id, reverse and windowShade.closed() or windowShade.open()) + else + device:emit_event_for_endpoint(endpoint_id, windowShade.partially_open()) + end + elseif lift_position == 100 then + device:emit_event_for_endpoint(endpoint_id, reverse and windowShade.closed() or windowShade.open()) + elseif lift_position > 0 then + device:emit_event_for_endpoint(endpoint_id, windowShade.partially_open()) + elseif lift_position == 0 then + if tilt_position == nil or tilt_position == 0 then + device:emit_event_for_endpoint(endpoint_id, reverse and windowShade.open() or windowShade.closed()) + elseif tilt_position > 0 then + device:emit_event_for_endpoint(endpoint_id, windowShade.partially_open()) + end + else + device:emit_event_for_endpoint(endpoint_id, windowShade.unknown()) + end +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 + if ib.data.value == nil then return end local position = 100 - math.floor(ib.data.value / 100) - local reverse = device:get_field(REVERSE_POLARITY) device:emit_event_for_endpoint(ib.endpoint_id, attribute(position)) if attribute == capabilities.windowShadeLevel.shadeLevel then @@ -204,58 +248,48 @@ local current_pos_handler = function(attribute) device:set_field(CURRENT_TILT, position) end - local lift_position = device:get_field(CURRENT_LIFT) - local tilt_position = device:get_field(CURRENT_TILT) - - -- Update the window shade status according to the lift and tilt positions. - -- LIFT TILT Window Shade - -- 100 any Open - -- 1-99 any Partially Open - -- 0 1-100 Partially Open - -- 0 0 Closed - -- 0 nil Closed - -- nil 100 Open - -- nil 1-99 Partially Open - -- nil 0 Closed - -- Note that lift or tilt may be nil if either the window shade does not - -- support them or if they haven't been received from a device report yet. - - 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 + local state_machine = device:get_field(STATE_MACHINE) + -- When state_machine is STATE_CURRENT_POSITION_FIRED, nothing to do + if state_machine == StateMachineEnum.STATE_MOVING then + device:set_field(STATE_MACHINE, StateMachineEnum.STATE_CURRENT_POSITION_FIRED) + elseif state_machine == StateMachineEnum.STATE_OPERATIONAL_STATE_FIRED or + state_machine == StateMachineEnum.STATE_IDLE or state_machine == nil then + update_shade_status(device, ib.endpoint_id, device:get_field(CURRENT_LIFT), device:get_field(CURRENT_TILT)) + device:set_field(STATE_MACHINE, StateMachineEnum.STATE_IDLE) end end end -- checks the current position of the shade local function current_status_handler(driver, device, ib, response) + if ib.data.value == nil then return end local windowShade = capabilities.windowShade.windowShade local reverse = device:get_field(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()) + local state_machine = device:get_field(STATE_MACHINE) + -- When state_machine is STATE_OPERATIONAL_STATE_FIRED, nothing to do + if state_machine == StateMachineEnum.STATE_IDLE then + if state == 1 then -- opening + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.closing() or windowShade.opening()) + device:set_field(STATE_MACHINE, StateMachineEnum.STATE_MOVING) + elseif state == 2 then -- closing + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.opening() or windowShade.closing()) + device:set_field(STATE_MACHINE, StateMachineEnum.STATE_MOVING) + end + elseif state_machine == StateMachineEnum.STATE_MOVING then + if state == 0 then -- not moving + device:set_field(STATE_MACHINE, StateMachineEnum.STATE_OPERATIONAL_STATE_FIRED) + elseif 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()) + else + device:emit_event_for_endpoint(ib.endpoint_id, windowShade.unknown()) + device:set_field(STATE_MACHINE, StateMachineEnum.STATE_IDLE) + end + elseif state_machine == StateMachineEnum.STATE_CURRENT_POSITION_FIRED then + update_shade_status(device, ib.endpoint_id, device:get_field(CURRENT_LIFT), device:get_field(CURRENT_TILT)) + device:set_field(STATE_MACHINE, StateMachineEnum.STATE_IDLE) end end @@ -368,10 +402,6 @@ local matter_driver_template = { capabilities.windowShadePreset, capabilities.battery, capabilities.batteryLevel, - }, - sub_drivers = { - -- for devices sending a position update while device is in motion - require("matter-window-covering-position-updates-while-moving") } } diff --git a/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/init.lua b/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/init.lua deleted file mode 100644 index 11ad2d8ef9..0000000000 --- a/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/init.lua +++ /dev/null @@ -1,151 +0,0 @@ --- Copyright 2023 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -local capabilities = require "st.capabilities" -local clusters = require "st.matter.clusters" -local device_lib = require "st.device" - -local DEFAULT_LEVEL = 0 -local STATE_MACHINE = "__state_machine" -local REVERSE_POLARITY = "__reverse_polarity" - -local StateMachineEnum = { - STATE_IDLE = 0x00, - STATE_MOVING = 0x01, - STATE_OPERATIONAL_STATE_FIRED = 0x02, - STATE_CURRENT_POSITION_FIRED = 0x03 -} - -local SUB_WINDOW_COVERING_VID_PID = { - {0x10e1, 0x1005} -- VDA -} - -local function is_matter_window_covering_position_updates_while_moving(opts, driver, device) - if device.network_type ~= device_lib.NETWORK_TYPE_MATTER then - return false - end - for i, v in ipairs(SUB_WINDOW_COVERING_VID_PID) do - if device.manufacturer_info.vendor_id == v[1] and - device.manufacturer_info.product_id == v[2] then - return true - end - end - return false -end - -local function device_init(driver, device) - device:subscribe() -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 position = 100 - math.floor(ib.data.value / 100) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windowShadeLevel.shadeLevel(position)) - local windowShade = capabilities.windowShade.windowShade - local reverse = device:get_field(REVERSE_POLARITY) - local state_machine = device:get_field(STATE_MACHINE) - -- When state_machine is STATE_IDLE or STATE_CURRENT_POSITION_FIRED, nothing to do - if state_machine == StateMachineEnum.STATE_MOVING then - device:set_field(STATE_MACHINE, StateMachineEnum.STATE_CURRENT_POSITION_FIRED) - elseif state_machine == StateMachineEnum.STATE_OPERATIONAL_STATE_FIRED or state_machine == nil then - if position == 0 then - device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.open() or windowShade.closed()) - elseif position == 100 then - device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.closed() or 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 - device:set_field(STATE_MACHINE, StateMachineEnum.STATE_IDLE) - end -end - --- checks the current position of the shade -local function current_status_handler(driver, device, ib, response) - local attr = capabilities.windowShade.windowShade - local position = device:get_latest_state( - "main", capabilities.windowShadeLevel.ID, - capabilities.windowShadeLevel.shadeLevel.NAME - ) or DEFAULT_LEVEL - for _, rb in ipairs(response.info_blocks) do - if rb.info_block.attribute_id == clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID and - rb.info_block.cluster_id == clusters.WindowCovering.ID and - rb.info_block.data ~= nil and - rb.info_block.data.value ~= nil then - position = math.floor(rb.info_block.data.value / 100) - end - end - position = 100 - position - local reverse = device:get_field(REVERSE_POLARITY) - local state = ib.data.value & clusters.WindowCovering.types.OperationalStatus.GLOBAL - local state_machine = device:get_field(STATE_MACHINE) - -- When state_machine is STATE_OPERATIONAL_STATE_FIRED, nothing to do - if state_machine == StateMachineEnum.STATE_IDLE then - if state == 1 then -- opening - device:emit_event_for_endpoint(ib.endpoint_id, reverse and attr.closing() or attr.opening()) - device:set_field(STATE_MACHINE, StateMachineEnum.STATE_MOVING) - elseif state == 2 then -- closing - device:emit_event_for_endpoint(ib.endpoint_id, reverse and attr.opening() or attr.closing()) - device:set_field(STATE_MACHINE, StateMachineEnum.STATE_MOVING) - end - elseif state_machine == StateMachineEnum.STATE_MOVING then - if state == 0 then -- not moving - device:set_field(STATE_MACHINE, StateMachineEnum.STATE_OPERATIONAL_STATE_FIRED) - elseif state == 1 then -- opening - device:emit_event_for_endpoint(ib.endpoint_id, reverse and attr.closing() or attr.opening()) - elseif state == 2 then -- closing - device:emit_event_for_endpoint(ib.endpoint_id, reverse and attr.opening() or attr.closing()) - else - device:emit_event_for_endpoint(ib.endpoint_id, attr.unknown()) - device:set_field(STATE_MACHINE, StateMachineEnum.STATE_IDLE) - end - elseif state_machine == StateMachineEnum.STATE_CURRENT_POSITION_FIRED then - if state == 0 then -- not moving - if position == 100 then - device:emit_event_for_endpoint(ib.endpoint_id, reverse and attr.closed() or attr.open()) - elseif position == 0 then - device:emit_event_for_endpoint(ib.endpoint_id, reverse and attr.open() or attr.closed()) - else - device:emit_event_for_endpoint(ib.endpoint_id, attr.partially_open()) - end - else - device:emit_event_for_endpoint(ib.endpoint_id, attr.unknown()) - end - device:set_field(STATE_MACHINE, StateMachineEnum.STATE_IDLE) - end -end - -local matter_window_covering_position_updates_while_moving_handler = { - NAME = "matter-window-covering-position-updates-while-moving", - lifecycle_handlers = { - init = device_init, - }, - matter_handlers = { - attr = { - [clusters.WindowCovering.ID] = { - [clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID] = current_pos_handler, - [clusters.WindowCovering.attributes.OperationalStatus.ID] = current_status_handler, - } - } - }, - capability_handlers = { - }, - can_handle = is_matter_window_covering_position_updates_while_moving, -} - -return matter_window_covering_position_updates_while_moving_handler 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 4c1eab7443..5a1d949af1 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 @@ -593,6 +593,17 @@ test.register_coroutine_test("WindowCovering OperationalStatus unknown", functio "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() + ) + ) test.socket.matter:__queue_receive( { mock_device.id, @@ -878,11 +889,6 @@ test.register_coroutine_test( "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( { @@ -897,11 +903,6 @@ test.register_coroutine_test( "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( { @@ -916,6 +917,12 @@ test.register_coroutine_test( "main", capabilities.windowShadeLevel.shadeLevel(19) ) ) + 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.windowShade.windowShade.partially_open() @@ -987,6 +994,12 @@ test.register_coroutine_test( "main", capabilities.windowShadeLevel.shadeLevel(23) ) ) + 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.windowShade.windowShade.partially_open() @@ -1028,6 +1041,10 @@ test.register_coroutine_test( "main", capabilities.windowShadeLevel.shadeLevel(100) ) ) + 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.windowShade.windowShade.closed() @@ -1067,6 +1084,10 @@ test.register_coroutine_test( "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(0) ) ) + 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.windowShade.windowShade.open() @@ -1083,6 +1104,10 @@ test.register_coroutine_test( "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(100) ) ) + 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.windowShade.windowShade.closed()