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
5 changes: 5 additions & 0 deletions drivers/SmartThings/matter-sensor/fingerprints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ matterManufacturer:
vendorId: 0x115F
productId: 0x2005
deviceProfileName: presence-illuminance-temperature-humidity-battery
- id: "4447/8201"
deviceLabel: Spatial Multi-Sensor FP400
vendorId: 0x115F
productId: 0x2009
deviceProfileName: aqara-fp400
#Bosch
- id: 4617/12309
deviceLabel: "Door/window contact II [M]"
Expand Down
14 changes: 14 additions & 0 deletions drivers/SmartThings/matter-sensor/profiles/aqara-fp400.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: aqara-fp400
components:
- id: main
capabilities:
- id: presenceSensor
version: 1
- id: illuminanceMeasurement
version: 1
- id: firmwareUpdate
version: 1
- id: refresh
version: 1
categories:
- name: PresenceSensor
6 changes: 6 additions & 0 deletions drivers/SmartThings/matter-sensor/src/sensor_utils/fields.lua
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,10 @@ SensorFields.BOOLEAN_CAP_EVENT_MAP = {
}
}

SensorFields.vendor_overrides = {
[0x115F] = { -- AQARA_MANUFACTURER_ID
[0x2009] = { is_aqara_fp400 = true }
}
}

return SensorFields
11 changes: 11 additions & 0 deletions drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
-- Copyright © 2025 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local fields = require "sensor_utils.fields"

local utils = {}

-- Sanity check bounds for soil moisture measurement limits (percent)
Expand All @@ -15,6 +17,15 @@ function utils.set_field_for_endpoint(device, field, endpoint, value, additional
device:set_field(string.format("%s_%d", field, endpoint), value, additional_params)
end

function utils.get_product_override_field(device, override_key)
if device.manufacturer_info
and fields.vendor_overrides[device.manufacturer_info.vendor_id]
and fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id]
then
return fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id][override_key]
end
end

function utils.tbl_contains(array, value)
if value == nil then return false end
for _, element in pairs(array or {}) do
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 @@ -6,5 +6,6 @@ local sub_drivers = {
lazy_load_if_possible("sub_drivers.air_quality_sensor"),
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"),
}
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_aqara_fp400(opts, driver, device)
local sensor_utils = require "sensor_utils.utils"
if sensor_utils.get_product_override_field(device, "is_aqara_fp400") then
return true, require("sub_drivers.aqara_fp400")
end
return false
end

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

local Fp400LifecycleHandlers = {}

-- overwrite to avoid unnecessary metadata update calls
function Fp400LifecycleHandlers.do_configure() end

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

local aqara_fp400_handler = {
NAME = "aqara-fp400",
lifecycle_handlers = {
doConfigure = Fp400LifecycleHandlers.do_configure,
driverSwitched = Fp400LifecycleHandlers.driver_switched,
},
can_handle = require("sub_drivers.aqara_fp400.can_handle"),
}

return aqara_fp400_handler
141 changes: 141 additions & 0 deletions drivers/SmartThings/matter-sensor/src/test/test_matter_aqara_fp400.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local test = require "integration_test"
local capabilities = require "st.capabilities"
local clusters = require "st.matter.clusters"
local t_utils = require "integration_test.utils"

local matter_endpoints = {
{
endpoint_id = 0,
clusters = {
{cluster_id = clusters.Basic.ID, cluster_type = "SERVER"},
},
device_types = {
{device_type_id = 0x0016, device_type_revision = 1} -- RootNode
}
},
{
endpoint_id = 1,
clusters = {
{cluster_id = clusters.OccupancySensing.ID, cluster_type = "SERVER"},
{cluster_id = clusters.IlluminanceMeasurement.ID, cluster_type = "SERVER"},
},
device_types = {
{device_type_id = 0x0107, device_type_revision = 1} -- Occupancy Sensor
}
}
}

local mock_device = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("aqara-fp400.yml"),
manufacturer_info = {
vendor_id = 0x115F,
product_id = 0x2009,
},
endpoints = matter_endpoints
})

local function subscribe_on_init(dev)
local subscribe_request = clusters.OccupancySensing.attributes.Occupancy:subscribe(dev)
subscribe_request:merge(clusters.IlluminanceMeasurement.attributes.MeasuredValue:subscribe(dev))
return subscribe_request
end

local function test_init()
test.socket.matter:__set_channel_ordering("relaxed")
local subscribe_request = subscribe_on_init(mock_device)
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
test.mock_device.add_test_device(mock_device)
end
test.set_test_init_function(test_init)

test.register_coroutine_test(
"Test no profile change on doConfigure for FP400",
function()
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })
-- The FP400 sub-driver overrides doConfigure to be a no-op
-- When doConfigure completes successfully, the framework automatically provisions the device
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
test.wait_for_events()
end,
{
min_api_version = 17
}
)

test.register_coroutine_test(
"Test no profile change on driverSwitched for FP400",
function()
local current_profile = mock_device.profile.id
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "driverSwitched" })
-- The FP400 sub-driver overrides driverSwitched to only update provisioning state
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
-- Ensure profile has not changed
test.wait_for_events()
assert(mock_device.profile.id == current_profile, "Profile should not change on driverSwitched")
end,
{
min_api_version = 17
}
)

test.register_message_test(
"Occupancy reports should generate correct presence messages",
{
{
channel = "matter",
direction = "receive",
message = {
mock_device.id,
clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 1, 1)
}
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.presenceSensor.presence("present"))
},
{
channel = "matter",
direction = "receive",
message = {
mock_device.id,
clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 1, 0)
}
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.presenceSensor.presence("not present"))
}
},
{
min_api_version = 17
}
)

test.register_message_test(
"Illuminance reports should generate correct messages",
{
{
channel = "matter",
direction = "receive",
message = {
mock_device.id,
clusters.IlluminanceMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 21370)
}
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance({ value = 137 }))
}
},
{
min_api_version = 17
}
)

test.run_registered_tests()
Loading