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-sensor/fingerprints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@ matterManufacturer:
vendorId: 0x134E
productId: 0x0006
deviceProfileName: temperature-humidity
#ThingsOne
- id: "4673/32784"
deviceLabel: ThingsOne Presence Sensor
vendorId: 0x1241
productId: 0x8010
deviceProfileName: thingsone-presence-sensor
#Tuo
- id: "5150/3"
deviceLabel: "TUO Temperature Sensor"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: thingsone-presence-sensor
components:
- id: main
capabilities:
- id: presenceSensor
version: 1
- id: temperatureMeasurement
version: 1
- id: relativeHumidityMeasurement
version: 1
- id: illuminanceMeasurement
version: 1
- id: battery
version: 1
- id: firmwareUpdate
version: 1
- id: refresh
version: 1
categories:
- name: PresenceSensor
preferences:
- preferenceId: tempOffset
explicit: true
- preferenceId: humidityOffset
explicit: true
- preferenceId: stse.sensitivity
explicit: true
- preferenceId: stse.presenceHoldTime
explicit: true
3 changes: 3 additions & 0 deletions drivers/SmartThings/matter-sensor/src/sensor_utils/fields.lua
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ SensorFields.BOOLEAN_CAP_EVENT_MAP = {
SensorFields.vendor_overrides = {
[0x115F] = { -- AQARA_MANUFACTURER_ID
[0x2009] = { is_aqara_fp400 = true }
},
[0x1241] = { -- SAMJIN_MANUFACTURER_ID
[0x8010] = { is_thingsone_presence_sensor = true }
}
}

Expand Down
1 change: 1 addition & 0 deletions drivers/SmartThings/matter-sensor/src/sub_drivers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ local sub_drivers = {
lazy_load_if_possible("sub_drivers.smoke_co_alarm"),
lazy_load_if_possible("sub_drivers.bosch_button_contact"),
lazy_load_if_possible("sub_drivers.aqara_fp400"),
lazy_load_if_possible("sub_drivers.thingsone_presence_sensor"),
}
return sub_drivers
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local function is_thingsone_presence_sensor(opts, driver, device)
local sensor_utils = require "sensor_utils.utils"
if sensor_utils.get_product_override_field(device, "is_thingsone_presence_sensor") then
return true, require("sub_drivers.thingsone_presence_sensor")
end
return false
end

return is_thingsone_presence_sensor
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

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

local PROFILE = "thingsone-presence-sensor"
local RETRY_LIMIT = 3
-- nil on hub libraries that predate this attribute
local HOLD_TIME_ATTR = clusters.OccupancySensing.attributes.HoldTime

local settings = {
{
-- The firmware reads this attribute as its operation mode: 1 low power, 2 normal, 3 high power
attr = clusters.OccupancySensing.attributes.PIRUnoccupiedToOccupiedThreshold,
pref = "stse.sensitivity",
default = 2, min = 1, max = 3,
pending = "__thingsone_mode_pending",
},
}
if HOLD_TIME_ATTR ~= nil then
table.insert(settings, {
-- HoldTime is the device's shipped 10 s plus the presence hold time; max keeps it within HoldTimeLimits 1..300
attr = HOLD_TIME_ATTR,
pref = "stse.presenceHoldTime",
default = 0, min = 0, max = 290,
to_device = function(seconds) return 10 + seconds end,
pending = "__thingsone_hold_time_pending",
})
end

-- An unset preference stands for the preference default, which setting.default must equal
local function preference_value(setting, preferences)
local value = tonumber((preferences or {})[setting.pref]) or setting.default
return st_utils.clamp_value(math.floor(value), setting.min, setting.max)
end

local function write_setting(device, setting, value, tries)
local ep = device:get_endpoints(clusters.OccupancySensing.ID)[1]
if ep == nil then
return
end
device:set_field(setting.pending, {value = value, tries = tries})
device:send(setting.attr:write(device, ep, value))
device:send(setting.attr:read(device))
end

-- Only a write made here is checked and retried; values written by other controllers are left alone
local function setting_report_handler(setting)
return function(driver, device, ib, response)
local pending = device:get_field(setting.pending)
if pending == nil or ib.data.value == nil then
return
end
if ib.data.value == pending.value then
device:set_field(setting.pending, nil)
elseif pending.tries < RETRY_LIMIT then
device.log.info(string.format("%s: device reports %d, rewriting %d (try %d)", setting.pref, ib.data.value, pending.value, pending.tries + 1))
write_setting(device, setting, pending.value, pending.tries + 1)
else
device.log.warn(string.format("%s: device still reports %d, giving up on %d", setting.pref, ib.data.value, pending.value))
device:set_field(setting.pending, nil)
end
end
end

local occupancy_attr_handlers = {}
for _, setting in ipairs(settings) do
occupancy_attr_handlers[setting.attr.ID] = setting_report_handler(setting)
end

local ThingsOneLifecycleHandlers = {}

-- Keeps the base driver from re-matching the device to a profile without these settings
function ThingsOneLifecycleHandlers.do_configure(driver, device)
device:try_update_metadata({profile = PROFILE})
end

function ThingsOneLifecycleHandlers.driver_switched(driver, device)
device:try_update_metadata({profile = PROFILE})
device:try_update_metadata({provisioning_state = "PROVISIONED"})
end

function ThingsOneLifecycleHandlers.device_init(driver, device)
if HOLD_TIME_ATTR == nil then
device.log.warn("OccupancySensing.HoldTime is missing from the hub libraries; the presence hold time setting has no effect")
end
for _, setting in ipairs(settings) do
device:add_subscribed_attribute(setting.attr)
end
device:subscribe()
end

function ThingsOneLifecycleHandlers.info_changed(driver, device, event, args)
if device.profile.id ~= args.old_st_store.profile.id then
device:subscribe()
end
local old = args.old_st_store.preferences or {}
local new = device.preferences or {}
-- The platform adds the offset to emitted values, so re-read to show it before the next report
if new.tempOffset ~= nil and new.tempOffset ~= old.tempOffset then
device:send(clusters.TemperatureMeasurement.attributes.MeasuredValue:read(device))
end
if new.humidityOffset ~= nil and new.humidityOffset ~= old.humidityOffset then
device:send(clusters.RelativeHumidityMeasurement.attributes.MeasuredValue:read(device))
end
for _, setting in ipairs(settings) do
local value = preference_value(setting, new)
if value ~= preference_value(setting, old) then
write_setting(device, setting, setting.to_device and setting.to_device(value) or value, 0)
end
end
end

local thingsone_presence_sensor_handler = {
NAME = "thingsone-presence-sensor",
lifecycle_handlers = {
doConfigure = ThingsOneLifecycleHandlers.do_configure,
driverSwitched = ThingsOneLifecycleHandlers.driver_switched,
infoChanged = ThingsOneLifecycleHandlers.info_changed,
init = ThingsOneLifecycleHandlers.device_init,
},
matter_handlers = {
attr = {
[clusters.OccupancySensing.ID] = occupancy_attr_handlers,
},
},
can_handle = require("sub_drivers.thingsone_presence_sensor.can_handle"),
}

return thingsone_presence_sensor_handler
Loading
Loading