Skip to content

Commit d1b2016

Browse files
Merge branch 'main' into Bosch_motion_sensor_support
2 parents 86d63ce + 5de3b1f commit d1b2016

15 files changed

Lines changed: 331 additions & 50 deletions

File tree

drivers/SmartThings/matter-lock/src/init.lua

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,13 @@ local function lock_state_handler(driver, device, ib, response)
8383
}
8484

8585
if ib.data.value ~= nil then
86-
device:emit_event(LOCK_STATE[ib.data.value])
86+
local event = LOCK_STATE[ib.data.value]
87+
if event ~= nil then
88+
device:emit_event(event)
89+
else
90+
device.log.warn(string.format("Received unknown lock state value (%s), emitting unknown", ib.data.value))
91+
device:emit_event(attr.unknown())
92+
end
8793
else
8894
device:emit_event(LOCK_STATE[LockState.NOT_FULLY_LOCKED])
8995
end

drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ local NEW_MATTER_LOCK_PRODUCTS = {
77
{0x115f, 0x2807}, -- AQARA, U200 Lite
88
{0x115f, 0x2804}, -- AQARA, U400
99
{0x115f, 0x286A}, -- AQARA, U200 US
10+
{0x115f, 0x2805}, -- Aqara Smart Lock J200 Set
11+
{0x115f, 0x280e}, -- AQARA Smart Gate Lock U500
12+
{0x115f, 0x280f}, -- AQARA Smart Rim Lock U500
13+
{0x115f, 0x2810}, -- AQARA Smart Glass Door Lock U500
1014
{0x147F, 0x0001}, -- U-tec
1115
{0x147F, 0x0007}, -- ULTRALOQ Bolt Pro Smart Matter Door Lock
1216
{0x147F, 0x0008}, -- Ultraloq, Bolt Smart Matter Door Lock

drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,29 @@ test.register_message_test(
191191
}
192192
)
193193

194+
test.register_message_test(
195+
"Handle unknown LockState value from Matter device.", {
196+
{
197+
channel = "matter",
198+
direction = "receive",
199+
message = {
200+
mock_device.id,
201+
clusters.DoorLock.attributes.LockState:build_test_report_data(
202+
mock_device, 10, 0xFF
203+
),
204+
},
205+
},
206+
{
207+
channel = "capability",
208+
direction = "send",
209+
message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown()),
210+
},
211+
},
212+
{
213+
min_api_version = 17
214+
}
215+
)
216+
194217
test.register_message_test(
195218
"Handle received BatPercentRemaining from device.", {
196219
{

drivers/SmartThings/matter-switch/fingerprints.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ matterManufacturer:
185185
deviceLabel: Linkind Smart Ceiling Light
186186
vendorId: 0x1396
187187
productId: 0x10B1
188-
deviceProfileName: light-level-colorTemperature
188+
deviceProfileName: light-color-level
189189
- id: "5014/4164"
190190
deviceLabel: Linkind Smart Light Bulb
191191
vendorId: 0x1396
@@ -2075,6 +2075,11 @@ matterManufacturer:
20752075
vendorId: 0x1189
20762076
productId: 0x0892
20772077
deviceProfileName: switch-binary
2078+
- id: "4489/4097"
2079+
deviceLabel: OSRAM PLANON SLIM
2080+
vendorId: 0x1189
2081+
productId: 0x1001
2082+
deviceProfileName: light-color-level
20782083
#Shelly
20792084
- id: "5264/1"
20802085
deviceLabel: Shelly Plug S MTR Gen3

drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/event_handlers.lua

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,46 @@
33

44
local camera_fields = require "sub_drivers.camera.camera_utils.fields"
55
local capabilities = require "st.capabilities"
6-
local switch_utils = require "switch_utils.utils"
76

87
local CameraEventHandlers = {}
98

9+
local function has_triggered_zone(triggered_zones, zone_id)
10+
for _, zone in ipairs(triggered_zones or {}) do
11+
if zone.zoneId == zone_id then
12+
return true
13+
end
14+
end
15+
return false
16+
end
17+
1018
function CameraEventHandlers.zone_triggered_handler(driver, device, ib, response)
1119
local triggered_zones = device:get_field(camera_fields.TRIGGERED_ZONES) or {}
12-
if not switch_utils.tbl_contains(triggered_zones, ib.data.elements.zone.value) then
13-
table.insert(triggered_zones, {zoneId = ib.data.elements.zone.value})
20+
local zone_id = ib.data.elements.zone.value
21+
if not has_triggered_zone(triggered_zones, zone_id) then
22+
table.insert(triggered_zones, { zoneId = zone_id })
1423
device:set_field(camera_fields.TRIGGERED_ZONES, triggered_zones)
1524
device:emit_event_for_endpoint(ib, capabilities.zoneManagement.triggeredZones(triggered_zones))
1625
end
1726
end
1827

1928
function CameraEventHandlers.zone_stopped_handler(driver, device, ib, response)
2029
local triggered_zones = device:get_field(camera_fields.TRIGGERED_ZONES) or {}
21-
for i, v in pairs(triggered_zones) do
22-
if v.zoneId == ib.data.elements.zone.value then
23-
table.remove(triggered_zones, i)
24-
device:set_field(camera_fields.TRIGGERED_ZONES, triggered_zones)
25-
device:emit_event_for_endpoint(ib, capabilities.zoneManagement.triggeredZones(triggered_zones))
30+
local zone_id = ib.data.elements.zone.value
31+
local updated_triggered_zones = {}
32+
local zone_removed = false
33+
34+
for _, zone in ipairs(triggered_zones) do
35+
if zone.zoneId ~= zone_id then
36+
table.insert(updated_triggered_zones, zone)
37+
else
38+
zone_removed = true
2639
end
2740
end
41+
42+
if zone_removed then
43+
device:set_field(camera_fields.TRIGGERED_ZONES, updated_triggered_zones)
44+
device:emit_event_for_endpoint(ib, capabilities.zoneManagement.triggeredZones(updated_triggered_zones))
45+
end
2846
end
2947

3048
return CameraEventHandlers

drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,47 @@ test.register_coroutine_test(
14711471
}
14721472
)
14731473

1474+
test.register_coroutine_test(
1475+
"Duplicate ZoneTriggered events should not duplicate triggeredZones state",
1476+
function()
1477+
update_device_profile()
1478+
test.wait_for_events()
1479+
1480+
test.socket.matter:__queue_receive({
1481+
mock_device.id,
1482+
clusters.ZoneManagement.events.ZoneTriggered:build_test_event_report(mock_device, CAMERA_EP, {
1483+
zone = 2,
1484+
reason = clusters.ZoneManagement.types.ZoneEventTriggeredReasonEnum.MOTION
1485+
})
1486+
})
1487+
test.socket.capability:__expect_send(
1488+
mock_device:generate_test_message("main", capabilities.zoneManagement.triggeredZones({{zoneId = 2}}))
1489+
)
1490+
1491+
test.socket.matter:__queue_receive({
1492+
mock_device.id,
1493+
clusters.ZoneManagement.events.ZoneTriggered:build_test_event_report(mock_device, CAMERA_EP, {
1494+
zone = 2,
1495+
reason = clusters.ZoneManagement.types.ZoneEventTriggeredReasonEnum.MOTION
1496+
})
1497+
})
1498+
1499+
test.socket.matter:__queue_receive({
1500+
mock_device.id,
1501+
clusters.ZoneManagement.events.ZoneStopped:build_test_event_report(mock_device, CAMERA_EP, {
1502+
zone = 2,
1503+
reason = clusters.ZoneManagement.types.ZoneEventStoppedReasonEnum.ACTION_STOPPED
1504+
})
1505+
})
1506+
test.socket.capability:__expect_send(
1507+
mock_device:generate_test_message("main", capabilities.zoneManagement.triggeredZones({}))
1508+
)
1509+
end,
1510+
{
1511+
min_api_version = 17
1512+
}
1513+
)
1514+
14741515
test.register_coroutine_test(
14751516
"Button events should generate appropriate events",
14761517
function()

drivers/SmartThings/samsung-audio/src/command.lua

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ local function is_empty(t)
3434
return not t or (type(t) == "table" and #t == 0)
3535
end
3636

37+
local function get_uic_response(ret, command_name)
38+
local root = ret and ret.handler_res and ret.handler_res.root
39+
local uic = root and root.UIC
40+
local response = uic and uic.response
41+
42+
if not uic then
43+
log.warn(string.format("Missing UIC data in %s response", tostring(command_name)))
44+
return nil, nil
45+
end
46+
47+
return uic, response
48+
end
49+
3750
local function tr(s,mappings)
3851
return string.gsub(s,
3952
"(.)",
@@ -94,8 +107,9 @@ function Command.volume(ip)
94107
if ip then
95108
local url = format_url(ip, "/UIC?cmd=<name>GetVolume</name>")
96109
local ret = handle_http_request(ip, url)
97-
if ret then
98-
response_map = { volume = ret.handler_res.root.UIC.response.volume, }
110+
local _, response = get_uic_response(ret, "GetVolume")
111+
if response and response.volume ~= nil then
112+
response_map = { volume = response.volume, }
99113
end
100114
end
101115
return response_map
@@ -114,8 +128,9 @@ function Command.set_volume(ip, level)
114128
local encoded_str_vol = "/UIC?cmd=%3Cpwron%3Eon%3C/pwron%3E%3Cname%3ESetVolume%3C/name%3E%3Cp%20type=%22dec%22%20name=%22volume%22%20val=%22" .. level .. "%22%3E%3C/p%3E"
115129
local url = format_url(ip, encoded_str_vol)
116130
local ret = handle_http_request(ip, url)
117-
if ret then
118-
response_map = { volume = ret.handler_res.root.UIC.response.volume, }
131+
local _, response = get_uic_response(ret, "SetVolume")
132+
if response and response.volume ~= nil then
133+
response_map = { volume = response.volume, }
119134
end
120135
end
121136
return response_map
@@ -326,8 +341,9 @@ function Command.getMute(ip)
326341
if ip then
327342
local url = format_url(ip, "/UIC?cmd=<name>GetMute</name>")
328343
local ret = handle_http_request(ip, url)
329-
if ret then
330-
response_map = { muted = ret.handler_res.root.UIC.response.mute,}
344+
local _, response = get_uic_response(ret, "GetMute")
345+
if response and response.mute ~= nil then
346+
response_map = { muted = response.mute,}
331347
end
332348
end
333349
return response_map
@@ -342,8 +358,9 @@ function Command.getPlayStatus(ip)
342358
if ip then
343359
local url = format_url(ip, "/UIC?cmd=<name>GetPlayStatus</name>")
344360
local ret = handle_http_request(ip, url)
345-
if ret then
346-
response_map = { playstatus = ret.handler_res.root.UIC.response.playstatus,}
361+
local _, response = get_uic_response(ret, "GetPlayStatus")
362+
if response and response.playstatus ~= nil then
363+
response_map = { playstatus = response.playstatus,}
347364
end
348365
end
349366
return response_map

drivers/SmartThings/samsung-audio/src/handlers.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ end
137137
function CapabilityHandlers.handle_audio_notification(driver, device, cmd)
138138
local ip = device:get_field("ip")
139139
local mute_status = command.getMute(ip)
140-
if mute_status.muted ~= "off" then
140+
if mute_status and mute_status.muted ~= nil and mute_status.muted ~= "off" then
141141
--unmute before playig notification
142142
command.unmute(ip)
143143
end

drivers/SmartThings/samsung-audio/src/init.lua

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,23 @@ local function emit_refresh_data_to_server(driver, device, cmd)
9494

9595
-- get volume
9696
local vol = command.volume(device:get_field("ip"))
97-
device:emit_event(capabilities.audioVolume.volume(tonumber(vol.volume)))
97+
local current_volume = vol and tonumber(vol.volume)
98+
if current_volume ~= nil then
99+
device:emit_event(capabilities.audioVolume.volume(current_volume))
100+
else
101+
log.warn("Unable to read speaker volume from refresh response")
102+
end
98103

99104
-- get mute status
100105
local muteStatus = command.getMute(device:get_field("ip"))
101-
if muteStatus.muted ~= "off" then
102-
device:emit_event(capabilities.audioMute.mute.muted())
106+
if muteStatus and muteStatus.muted ~= nil then
107+
if muteStatus.muted ~= "off" then
108+
device:emit_event(capabilities.audioMute.mute.muted())
109+
else
110+
device:emit_event(capabilities.audioMute.mute.unmuted())
111+
end
103112
else
104-
device:emit_event(capabilities.audioMute.mute.unmuted())
113+
log.warn("Unable to read speaker mute state from refresh response")
105114
end
106115
end
107116

drivers/SmartThings/zigbee-switch/src/stateless_handlers/init.lua

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
local capabilities = require "st.capabilities"
55
local st_utils = require "st.utils"
6+
local constants = require "st.zigbee.constants"
67
local clusters = require "st.zigbee.zcl.clusters"
78
local switch_utils = require "switch_utils"
89

@@ -11,19 +12,47 @@ local DEFAULT_MIRED_MAX_BOUND = 370 -- 2700 Kelvin (Mireds are the inverse of Ke
1112
local DEFAULT_MIRED_MIN_BOUND = 154 -- 6500 Kelvin (Mireds are the inverse of Kelvin)
1213

1314
-- Transition Time: The time that shall be taken to perform the step change, in units of 1/10ths of a second.
15+
-- Specific fields can store custom transition times for stateless capabilities
16+
local SWITCH_LEVEL_STEP_TRANSITION_TIME = "__switch_level_step_transition_time"
17+
local COLOR_TEMP_STEP_TRANSITION_TIME = "__color_temp_step_transition_time"
1418
local DEFAULT_STEP_TRANSITION_TIME = 3 -- 0.3 seconds
1519

1620
-- Options Mask & Override: Indicates which options are being overridden by the Level/ColorControl cluster commands
1721
local OPTIONS_MASK = 0x01 -- default: The `ExecuteIfOff` option is overriden
1822
local IGNORE_COMMAND_IF_OFF = 0x00 -- default: the command will not be executed if the device is off
1923

24+
-- Indicates whether a delayed refresh for ZLL devices is in progress, to prevent multiple refreshes in a quick series of step commands
25+
local IS_REFRESH_CALLBACK_QUEUED = "__is_refresh_callback_queued"
26+
-- Stores a timer object, which is required to cancel a timer early
27+
local REFRESH_CALLBACK_TIMER = "__refresh_callback_timer"
28+
29+
-- Note: These commands' native handlers do not match the driver's ZLL behavior 1-1.
30+
-- Instead, they will queue a 2s timer and read refresh for each command, in all cases.
31+
local function trigger_delayed_refresh_if_zll(device)
32+
if device:get_profile_id() ~= constants.ZLL_PROFILE_ID then
33+
return
34+
end
35+
36+
-- If a refresh callback is already queued, cancel it and create a new one with the updated time
37+
if device:get_field(IS_REFRESH_CALLBACK_QUEUED) then
38+
device.thread:cancel_timer(device:get_field(REFRESH_CALLBACK_TIMER))
39+
end
40+
local delay_s = 2
41+
local new_timer = device.thread:call_with_delay(delay_s, function()
42+
device:refresh()
43+
device:set_field(IS_REFRESH_CALLBACK_QUEUED, nil)
44+
end)
45+
device:set_field(REFRESH_CALLBACK_TIMER, new_timer)
46+
device:set_field(IS_REFRESH_CALLBACK_QUEUED, true)
47+
end
48+
2049
local function step_color_temperature_by_percent_handler(driver, device, cmd)
2150
if type(device.register_native_capability_cmd_handler) == "function" then
2251
device:register_native_capability_cmd_handler(cmd.capability, cmd.command)
2352
end
2453
local step_percent_change = cmd.args and cmd.args.stepSize or 0
2554
if step_percent_change == 0 then return end
26-
local transition_time = device:get_field(switch_utils.COLOR_TEMP_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME
55+
local transition_time = device:get_field(COLOR_TEMP_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME
2756
-- Reminder, stepSize > 0 == Kelvin UP == Mireds DOWN. stepSize < 0 == Kelvin DOWN == Mireds UP
2857
local step_mode = (step_percent_change > 0) and clusters.ColorControl.types.CcStepMode.DOWN or clusters.ColorControl.types.CcStepMode.UP
2958
-- note: the field containing the color temp bounds will be associated with a parent device
@@ -37,6 +66,7 @@ local function step_color_temperature_by_percent_handler(driver, device, cmd)
3766
end
3867
local step_size_in_mireds = st_utils.round((max_mireds - min_mireds) * (math.abs(step_percent_change)/100.0))
3968
device:send(clusters.ColorControl.server.commands.StepColorTemperature(device, step_mode, step_size_in_mireds, transition_time, min_mireds, max_mireds, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF))
69+
trigger_delayed_refresh_if_zll(device)
4070
end
4171

4272
local function step_level_handler(driver, device, cmd)
@@ -45,9 +75,10 @@ local function step_level_handler(driver, device, cmd)
4575
end
4676
local step_size = st_utils.round((cmd.args and cmd.args.stepSize or 0)/100.0 * 254)
4777
if step_size == 0 then return end
48-
local transition_time = device:get_field(switch_utils.SWITCH_LEVEL_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME
78+
local transition_time = device:get_field(SWITCH_LEVEL_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME
4979
local step_mode = (step_size > 0) and clusters.Level.types.MoveStepMode.UP or clusters.Level.types.MoveStepMode.DOWN
5080
device:send(clusters.Level.server.commands.Step(device, step_mode, math.abs(step_size), transition_time, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF))
81+
trigger_delayed_refresh_if_zll(device)
5182
end
5283

5384
local stateless_handlers = {

0 commit comments

Comments
 (0)