Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ components:
capabilities:
- id: button
version: 1
- id: knob
version: 1
- id: battery
version: 1
- id: firmwareUpdate
Expand All @@ -18,12 +20,16 @@ components:
capabilities:
- id: button
version: 1
- id: knob
version: 1
categories:
- name: RemoteController
- id: group3
label: Group 3
capabilities:
- id: button
version: 1
- id: knob
version: 1
categories:
- name: RemoteController
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
-- Copyright © 2025 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local clusters = require "st.matter.clusters"
local switch_utils = require "switch_utils.utils"
local scroll_utils = require "sub_drivers.ikea_scroll.scroll_utils.utils"
local scroll_cfg = require "sub_drivers.ikea_scroll.scroll_utils.device_configuration"
local event_handlers = require "sub_drivers.ikea_scroll.scroll_handlers.event_handlers"

local IkeaScrollLifecycleHandlers = {}

Expand Down Expand Up @@ -44,7 +46,15 @@ local ikea_scroll_handler = {
infoChanged = IkeaScrollLifecycleHandlers.info_changed,
init = IkeaScrollLifecycleHandlers.device_init,
},
matter_handlers = {
event = {
[clusters.Switch.ID] = {
[clusters.Switch.events.InitialPress.ID] = event_handlers.initial_press_handler,
[clusters.Switch.events.MultiPressOngoing.ID] = event_handlers.multi_press_ongoing_handler,
}
}
},
can_handle = require("sub_drivers.ikea_scroll.can_handle")
}

return ikea_scroll_handler
return ikea_scroll_handler
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
-- Copyright © 2025 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local st_utils = require "st.utils"
local capabilities = require "st.capabilities"
local switch_utils = require "switch_utils.utils"
local generic_event_handlers = require "switch_handlers.event_handlers"
local scroll_fields = require "sub_drivers.ikea_scroll.scroll_utils.fields"

local IkeaScrollEventHandlers = {}

local function rotate_amount_event_helper(device, endpoint_id, num_presses_to_handle)
-- to cut down on checks, we can assume that if the endpoint is not in ENDPOINTS_UP_SCROLL, it is in ENDPOINTS_DOWN_SCROLL
local scroll_direction = switch_utils.tbl_contains(scroll_fields.ENDPOINTS_UP_SCROLL, endpoint_id) and 1 or -1
local scroll_amount = st_utils.clamp_value(scroll_direction * scroll_fields.PER_SCROLL_EVENT_ROTATION * num_presses_to_handle, -100, 100)
device:emit_event_for_endpoint(endpoint_id, capabilities.knob.rotateAmount(scroll_amount, {state_change = true}))
end

function IkeaScrollEventHandlers.initial_press_handler(driver, device, ib, response)
-- use the generic handler logic for the push endpoints. Else, use custom logic.
if switch_utils.tbl_contains(scroll_fields.ENDPOINTS_PUSH, ib.endpoint_id) then
generic_event_handlers.initial_press_handler(driver, device, ib, response)
else
device:set_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED, 1)
rotate_amount_event_helper(device, ib.endpoint_id, 1)
end
end

-- Used by ENDPOINTS_UP_SCROLL and ENDPOINTS_DOWN_SCROLL, not ENDPOINTS_PUSH
function IkeaScrollEventHandlers.multi_press_ongoing_handler(driver, device, ib, response)
local cur_num_presses_counted = ib.data and ib.data.elements and ib.data.elements.current_number_of_presses_counted.value or 0
local num_presses_to_handle = cur_num_presses_counted - (device:get_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED) or 0)
if num_presses_to_handle > 0 then
device:set_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED, cur_num_presses_counted)
rotate_amount_event_helper(device, ib.endpoint_id, num_presses_to_handle)
end
end

return IkeaScrollEventHandlers
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ local IkeaScrollConfiguration = {}

function IkeaScrollConfiguration.build_button_component_map(device)
local component_map = {
main = scroll_fields.ENDPOINTS_PRESS[1],
group2 = scroll_fields.ENDPOINTS_PRESS[2],
group3 = scroll_fields.ENDPOINTS_PRESS[3],
main = {scroll_fields.ENDPOINTS_PUSH[1], scroll_fields.ENDPOINTS_UP_SCROLL[1], scroll_fields.ENDPOINTS_DOWN_SCROLL[1]},
group2 = {scroll_fields.ENDPOINTS_PUSH[2], scroll_fields.ENDPOINTS_UP_SCROLL[2], scroll_fields.ENDPOINTS_DOWN_SCROLL[2]},
group3 = {scroll_fields.ENDPOINTS_PUSH[3], scroll_fields.ENDPOINTS_UP_SCROLL[3], scroll_fields.ENDPOINTS_DOWN_SCROLL[3]},
}
device:set_field(switch_fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true})
end

function IkeaScrollConfiguration.configure_buttons(device)
for _, ep in ipairs(scroll_fields.ENDPOINTS_PRESS) do
for _, ep in ipairs(scroll_fields.ENDPOINTS_PUSH) do
device:send(clusters.Switch.attributes.MultiPressMax:read(device, ep))
switch_utils.set_field_for_endpoint(device, switch_fields.SUPPORTS_MULTI_PRESS, ep, true, {persist = true})
device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false}))
-- though unrelated to the knob capability, the push endpoints all map to components including a knob
device:emit_event_for_endpoint(ep, capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}}))
end
end

Expand All @@ -32,4 +34,4 @@ function IkeaScrollConfiguration.match_profile(driver, device)
IkeaScrollConfiguration.configure_buttons(device)
end

return IkeaScrollConfiguration
return IkeaScrollConfiguration
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
-- Copyright © 2025 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local st_utils = require "st.utils"
local clusters = require "st.matter.clusters"

local IkeaScrollFields = {}

-- PowerSource supported on Root Node
IkeaScrollFields.ENDPOINT_POWER_SOURCE = 0

-- Switch Endpoints used for basic press functionality
IkeaScrollFields.ENDPOINTS_PRESS = {3, 6, 9}
-- Generic Switch Endpoints used for basic push functionality
IkeaScrollFields.ENDPOINTS_PUSH = {3, 6, 9}

-- Required Events for the ENDPOINTS_PRESS.
-- Generic Switch Endpoints used for Up Scroll functionality
IkeaScrollFields.ENDPOINTS_UP_SCROLL = {1, 4, 7}

-- Generic Switch Endpoints used for Down Scroll functionality
IkeaScrollFields.ENDPOINTS_DOWN_SCROLL = {2, 5, 8}

IkeaScrollFields.MAX_SCROLL_PRESSES = 18
IkeaScrollFields.PER_SCROLL_EVENT_ROTATION = st_utils.round(1 / IkeaScrollFields.MAX_SCROLL_PRESSES * 100)

IkeaScrollFields.LATEST_NUMBER_OF_PRESSES_COUNTED = "__latest_number_of_presses_counted"

-- Required Events for the ENDPOINTS_PUSH.
IkeaScrollFields.switch_press_subscribed_events = {
clusters.Switch.events.InitialPress.ID,
clusters.Switch.events.MultiPressComplete.ID,
clusters.Switch.events.LongPress.ID,
}

return IkeaScrollFields
-- Required Events for the ENDPOINTS_UP_SCROLL and ENDPOINTS_DOWN_SCROLL. Adds a
-- MultiPressOngoing subscription to handle step functionality in real-time, and
-- removes MultiPressComplete subscription due to the real-time handling
IkeaScrollFields.switch_scroll_subscribed_events = {
clusters.Switch.events.InitialPress.ID,
clusters.Switch.events.MultiPressOngoing.ID,
}


return IkeaScrollFields
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,24 @@ local scroll_fields = require "sub_drivers.ikea_scroll.scroll_utils.fields"
local IkeaScrollUtils = {}

-- override subscribe function to prevent subscribing to additional events from the main driver
-- and to subscribe to extra events for particular endpoints
function IkeaScrollUtils.subscribe(device)
local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {})
for _, ep_press in ipairs(scroll_fields.ENDPOINTS_PRESS) do
for _, ep_push in ipairs(scroll_fields.ENDPOINTS_PUSH) do
for _, switch_event in ipairs(scroll_fields.switch_press_subscribed_events) do
local ib = im.InteractionInfoBlock(ep_press, clusters.Switch.ID, nil, switch_event)
local ib = im.InteractionInfoBlock(ep_push, clusters.Switch.ID, nil, switch_event)
subscribe_request:with_info_block(ib)
end
end
for _, ep_up in ipairs(scroll_fields.ENDPOINTS_UP_SCROLL) do
for _, switch_event in ipairs(scroll_fields.switch_scroll_subscribed_events) do
local ib = im.InteractionInfoBlock(ep_up, clusters.Switch.ID, nil, switch_event)
subscribe_request:with_info_block(ib)
end
end
for _, ep_down in ipairs(scroll_fields.ENDPOINTS_DOWN_SCROLL) do
for _, switch_event in ipairs(scroll_fields.switch_scroll_subscribed_events) do
local ib = im.InteractionInfoBlock(ep_down, clusters.Switch.ID, nil, switch_event)
subscribe_request:with_info_block(ib)
end
end
Expand Down
20 changes: 15 additions & 5 deletions drivers/SmartThings/matter-switch/src/switch_utils/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ end

--- An extension of the library function endpoint_to_component, used to support a mapping scheme
--- that optionally includes cluster and attribute ids so that multiple components can be mapped
--- to a single endpoint.
--- to a single endpoint. This extension also handles the case that multiple endpoints map to the
--- same component
---
--- @param device any a Matter device object
--- @param ep_info number|table either an ep_id or a table { endpoint_id, optional(cluster_id), optional(attribute_id) }
Expand All @@ -214,10 +215,19 @@ function utils.endpoint_to_component(device, ep_info)
for component, map_info in pairs(device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {}) do
if type(map_info) == "number" and map_info == ep_info.endpoint_id then
return component
elseif type(map_info) == "table" and map_info.endpoint_id == ep_info.endpoint_id
and (not map_info.cluster_id or (map_info.cluster_id == ep_info.cluster_id
and (not map_info.attribute_ids or utils.tbl_contains(map_info.attribute_ids, ep_info.attribute_id)))) then
return component
elseif type(map_info) == "table" then
if type(map_info.endpoint_id) == "number" then
map_info = {map_info}
end
for _, ep_map_info in ipairs(map_info) do
if type(ep_map_info) == "number" and ep_map_info == ep_info.endpoint_id then
return component
elseif type(ep_map_info) == "table" and ep_map_info.endpoint_id == ep_info.endpoint_id
and (not ep_map_info.cluster_id or (ep_map_info.cluster_id == ep_info.cluster_id
and (not ep_map_info.attribute_ids or utils.tbl_contains(ep_map_info.attribute_ids, ep_info.attribute_id)))) then
return component
end
end
end
end
return "main"
Expand Down
Loading
Loading