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
56 changes: 56 additions & 0 deletions drivers/SmartThings/matter-sensor/profiles/aqara-fp400.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,59 @@ components:
version: 1
categories:
- name: PresenceSensor
- id: sensor1
capabilities:
- id: presenceSensor
version: 1
optional: true
categories:
- name: PresenceSensor
- id: sensor2
capabilities:
- id: presenceSensor
version: 1
optional: true
categories:
- name: PresenceSensor
- id: sensor3
capabilities:
- id: presenceSensor
version: 1
optional: true
categories:
- name: PresenceSensor
- id: sensor4
capabilities:
- id: presenceSensor
version: 1
optional: true
categories:
- name: PresenceSensor
- id: sensor5
capabilities:
- id: presenceSensor
version: 1
optional: true
categories:
- name: PresenceSensor
- id: sensor6
capabilities:
- id: presenceSensor
version: 1
optional: true
categories:
- name: PresenceSensor
- id: sensor7
capabilities:
- id: presenceSensor
version: 1
optional: true
categories:
- name: PresenceSensor
- id: sensor8
capabilities:
- id: presenceSensor
version: 1
optional: true
categories:
- name: PresenceSensor
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,9 @@ end

function AttributeHandlers.occupancy_measured_value_handler(driver, device, ib, response)
if device:supports_capability(capabilities.motionSensor) then
device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive())
device:emit_event_for_endpoint(ib.endpoint_id, ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive())
else
device:emit_event(ib.data.value == 0x01 and capabilities.presenceSensor.presence("present") or capabilities.presenceSensor.presence("not present"))
device:emit_event_for_endpoint(ib.endpoint_id, ib.data.value == 0x01 and capabilities.presenceSensor.presence("present") or capabilities.presenceSensor.presence("not present"))
end
end

Expand Down
44 changes: 44 additions & 0 deletions drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,48 @@ function utils.tbl_contains(array, value)
return false
end

--- Deeply compare two values.
--- Handles metatables. Can optionally ignore cycle checking and/or function differences.
---
--- @param a any
--- @param b any
--- @param opts table|nil { ignore_functions = boolean, ignore_cycles = boolean }
--- @param seen table|nil
--- @return boolean
function utils.deep_equals(a, b, opts, seen)
if a == b then return true end -- same object
if type(a) ~= type(b) then return false end -- different type
if type(a) == "function" and opts and opts.ignore_functions then return true end
if type(a) ~= "table" then return false end -- same type but not table, thus was already compared

-- check for cycles in table references and preserve reference topology.
if not (opts and opts.ignore_cycles) then
seen = seen or {}
seen[a] = seen[a] or {}
if seen[a][b] then
return seen[a][b]
end
seen[a][b] = true
end

-- Compare keys/values from a
for k, v in pairs(a) do
if not utils.deep_equals(v, b[k], opts, seen) then
return false
end
end

-- Ensure b doesn't have extra keys
for k in pairs(b) do
if a[k] == nil then
return false
end
end

-- Compare metatables
local mt_a = getmetatable(a)
local mt_b = getmetatable(b)
return utils.deep_equals(mt_a, mt_b, opts, seen)
end

return utils
Original file line number Diff line number Diff line change
Expand Up @@ -77,48 +77,4 @@ function AirQualitySensorUtils.set_supported_health_concern_values(device)
end
end

--- Deeply compare two values.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this is now being used in 2 subdrivers, I just moved this to the main utils file

--- Handles metatables. Can optionally ignore cycle checking and/or function differences.
---
--- @param a any
--- @param b any
--- @param opts table|nil { ignore_functions = boolean, ignore_cycles = boolean }
--- @param seen table|nil
--- @return boolean
function AirQualitySensorUtils.deep_equals(a, b, opts, seen)
if a == b then return true end -- same object
if type(a) ~= type(b) then return false end -- different type
if type(a) == "function" and opts and opts.ignore_functions then return true end
if type(a) ~= "table" then return false end -- same type but not table, thus was already compared

-- check for cycles in table references and preserve reference topology.
if not (opts and opts.ignore_cycles) then
seen = seen or {}
seen[a] = seen[a] or {}
if seen[a][b] then
return seen[a][b]
end
seen[a][b] = true
end

-- Compare keys/values from a
for k, v in pairs(a) do
if not AirQualitySensorUtils.deep_equals(v, b[k], opts, seen) then
return false
end
end

-- Ensure b doesn't have extra keys
for k in pairs(b) do
if a[k] == nil then
return false
end
end

-- Compare metatables
local mt_a = getmetatable(a)
local mt_b = getmetatable(b)
return AirQualitySensorUtils.deep_equals(mt_a, mt_b, opts, seen)
end

return AirQualitySensorUtils
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
local version = require "version"
local capabilities = require "st.capabilities"
local clusters = require "st.matter.clusters"
local sensor_utils = require "sensor_utils.utils"
local aqs_utils = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.utils"
local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields"
local attribute_handlers = require "sub_drivers.air_quality_sensor.air_quality_sensor_handlers.attribute_handlers"
Expand Down Expand Up @@ -67,7 +68,7 @@ function AirQualitySensorLifecycleHandlers.device_init(driver, device)
end

function AirQualitySensorLifecycleHandlers.info_changed(driver, device, event, args)
if not aqs_utils.deep_equals(device.profile, args.old_st_store.profile, { ignore_functions = true }) then
if not sensor_utils.deep_equals(device.profile, args.old_st_store.profile, { ignore_functions = true }) then
if device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then
--re-up subscription with new capabilities using the modular supports_capability override
device:extend_device("supports_capability_by_id", aqs_utils.supports_capability_by_id_modular)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,65 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local capabilities = require "st.capabilities"
local sensor_utils = require "sensor_utils.utils"

local COMPONENT_TO_ENDPOINT_MAP = "__COMPONENT_TO_ENDPOINT_MAP"

local function endpoint_to_component(device, endpoint_id)
local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {}
for component_id, ep_id in pairs(map) do
if ep_id == endpoint_id then return component_id end
end
return "main"
end

local function match_profile(device)
local enabled_optional_component_capabilities = {}
local component_to_endpoint_map = {}
for _, ep in ipairs(device.endpoints) do
-- since EP0 is the root node, EP1 is a permanent Presence Sensor, and EP2 is a permanent Light Sensor
if ep.endpoint_id > 2 then
component_to_endpoint_map[string.format("sensor%d", ep.endpoint_id - 2)] = ep.endpoint_id
-- ex. sensor1, sensor2, etc. for EP3, EP4, etc.
table.insert(enabled_optional_component_capabilities,{ string.format("sensor%d", ep.endpoint_id - 2), { capabilities.presenceSensor.ID } })
end
end
device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, { persist = true })
device:try_update_metadata({profile = "aqara-fp400", optional_component_capabilities = enabled_optional_component_capabilities })
end


local Fp400LifecycleHandlers = {}

-- overwrite to avoid unnecessary metadata update calls
function Fp400LifecycleHandlers.do_configure() end
function Fp400LifecycleHandlers.do_configure(driver, device)
match_profile(device)
end

-- overwrite to avoid unnecessary metadata update calls
function Fp400LifecycleHandlers.driver_switched(driver, device)
match_profile(device)
device:try_update_metadata({provisioning_state = "PROVISIONED"})
end

function Fp400LifecycleHandlers.info_changed(driver, device, event, args)
if not sensor_utils.deep_equals(args.old_st_store.endpoints, device.endpoints) then
match_profile(device)
device:subscribe()
end
end

function Fp400LifecycleHandlers.device_init(driver, device)
device:set_endpoint_to_component_fn(endpoint_to_component)
device:subscribe()
end

local aqara_fp400_handler = {
NAME = "aqara-fp400",
lifecycle_handlers = {
doConfigure = Fp400LifecycleHandlers.do_configure,
driverSwitched = Fp400LifecycleHandlers.driver_switched,
infoChanged = Fp400LifecycleHandlers.info_changed,
init = Fp400LifecycleHandlers.device_init,
},
can_handle = require("sub_drivers.aqara_fp400.can_handle"),
}
Expand Down
Loading
Loading