Skip to content
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 20 s (a 10 s rest after motion, then 10 s of radar check that each radar detection restarts) plus the presence hold time; max keeps it within HoldTimeLimits 1..300
attr = HOLD_TIME_ATTR,
pref = "stse.presenceHoldTime",
default = 0, min = 0, max = 280,
to_device = function(seconds) return 20 + 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