Skip to content

Commit 0e499ad

Browse files
yanggx24KKlimczukS
authored andcommitted
[WWSTCERT-13446, WWSTCERT-13450, WWSTCERT-13454, WWSTCERT-13458] SONOFF/SWV1C (#3157)
* feat:sonoff swv valve * Zigbee Valve:modify comment language and file name * Zigbee Valve:Add Sonoff valve subdriver lazy loading * test: fix zigbee valve lazy loading coverage * Fix SONOFF valve command handling * Configure Sonoff valve battery reporting * chore: simplify zigbee-valve license header * chore: remove test_lazy_load_subdriver.lua * Use default SONOFF valve reporting config * fix(zigbee-valve): restore default battery reporting configuration * Fix SONOFF valve command test expectations * Remove unnecessary line breaks in test_sonoff_valve.lua * Update drivers/SmartThings/zigbee-valve/src/test/test_sonoff_valve.lua Co-authored-by: Konrad K <33450498+KKlimczukS@users.noreply.github.com> * Update drivers/SmartThings/zigbee-valve/src/test/test_sonoff_valve.lua Co-authored-by: Konrad K <33450498+KKlimczukS@users.noreply.github.com> * Remove redundant comments from the SONOFF test file --------- Co-authored-by: Konrad K <33450498+KKlimczukS@users.noreply.github.com>
1 parent 2b1511a commit 0e499ad

7 files changed

Lines changed: 359 additions & 3 deletions

File tree

‎drivers/SmartThings/zigbee-valve/fingerprints.yml‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,23 @@ zigbeeManufacturer :
3939
manufacturer: Compacta
4040
model: ZBVC1(1023A)
4141
deviceProfileName: valve-battery-powerSource
42-
43-
42+
- id: SONOFF/SWV-ZFE
43+
deviceLabel: SONOFF Water Valve
44+
manufacturer: SONOFF
45+
model: SWV-ZFE
46+
deviceProfileName: valve-battery
47+
- id: SONOFF/SWV-ZFU
48+
deviceLabel: SONOFF Water Valve
49+
manufacturer: SONOFF
50+
model: SWV-ZFU
51+
deviceProfileName: valve-battery
52+
- id: SONOFF/SWV-ZNE
53+
deviceLabel: SONOFF Water Valve
54+
manufacturer: SONOFF
55+
model: SWV-ZNE
56+
deviceProfileName: valve-battery
57+
- id: SONOFF/SWV-ZNU
58+
deviceLabel: SONOFF Water Valve
59+
manufacturer: SONOFF
60+
model: SWV-ZNU
61+
deviceProfileName: valve-battery
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: valve-battery
2+
components:
3+
- id: main
4+
capabilities:
5+
- id: valve
6+
version: 1
7+
- id: battery
8+
version: 1
9+
- id: firmwareUpdate
10+
version: 1
11+
- id: refresh
12+
version: 1
13+
categories :
14+
- name: WaterValve

‎drivers/SmartThings/zigbee-valve/src/init.lua‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
-- Copyright 2022 SmartThings, Inc.
22
-- Licensed under the Apache License, Version 2.0
33

4-
54
local ZigbeeDriver = require "st.zigbee"
65
local defaults = require "st.zigbee.defaults"
76

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- Copyright 2026 SmartThings, Inc.
2+
-- Licensed under the Apache License, Version 2.0
3+
4+
local FINGERPRINTS = {
5+
{ mfr = "SONOFF", model = "SWV-ZFU" },
6+
{ mfr = "SONOFF", model = "SWV-ZFE" },
7+
{ mfr = "SONOFF", model = "SWV-ZNU" },
8+
{ mfr = "SONOFF", model = "SWV-ZNE" },
9+
}
10+
11+
local function sonoff_can_handle(opts, driver, device, ...)
12+
for _, fingerprint in ipairs(FINGERPRINTS) do
13+
if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then
14+
return true, require "sonoff"
15+
end
16+
end
17+
return false
18+
end
19+
20+
return sonoff_can_handle
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
-- Copyright 2026 SmartThings, Inc.
2+
-- Licensed under the Apache License, Version 2.0
3+
4+
local capabilities = require "st.capabilities"
5+
local zcl_clusters = require "st.zigbee.zcl.clusters"
6+
local OnOff = zcl_clusters.OnOff
7+
local PowerConfiguration = zcl_clusters.PowerConfiguration
8+
local utils = require "st.utils"
9+
10+
--- OnOff Property Reporting Handler → Valve Capability Point Event
11+
--- SONOFF reports valve state through the OnOff cluster, so keep an explicit mapping
12+
--- from OnOff reports to valve events while using the parent driver's default commands.
13+
--- @param driver table Driver instance
14+
--- @param device table Device instance
15+
--- @param value table Zigbee attribute value
16+
local function onoff_attr_handler(driver, device, value)
17+
local is_on = value.value ~= false and value.value ~= 0
18+
if is_on then
19+
device:emit_event(capabilities.valve.valve.open())
20+
else
21+
device:emit_event(capabilities.valve.valve.closed())
22+
end
23+
end
24+
25+
--- Battery Percentage Attribute Handler
26+
--- Zigbee BatteryPercentageRemaining range 0-200 (0%-100%), needs to be divided by 2
27+
--- @param driver table Driver instance
28+
--- @param device table Device instance
29+
--- @param value table Zigbee attribute value
30+
local function battery_percentage_handler(driver, device, value)
31+
local raw = value.value
32+
local percent = utils.round(raw / 2)
33+
device:emit_event(capabilities.battery.battery(percent))
34+
end
35+
36+
local sonoff_valve_handler = {
37+
NAME = "SONOFF Water Valve Handler",
38+
zigbee_handlers = {
39+
attr = {
40+
-- OnOff property reporting → valve event (to compensate for the defect of the parent driver's default mapping being skipped)
41+
[OnOff.ID] = {
42+
[OnOff.attributes.OnOff.ID] = onoff_attr_handler
43+
},
44+
-- Battery percentage attribute reporting → battery event
45+
[PowerConfiguration.ID] = {
46+
[PowerConfiguration.attributes.BatteryPercentageRemaining.ID] = battery_percentage_handler
47+
}
48+
}
49+
},
50+
can_handle = require "sonoff.can_handle",
51+
}
52+
53+
return sonoff_valve_handler

‎drivers/SmartThings/zigbee-valve/src/sub_drivers.lua‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ local lazy_load_if_possible = require "lazy_load_subdriver"
55
local sub_drivers = {
66
lazy_load_if_possible("sinope"),
77
lazy_load_if_possible("ezex"),
8+
lazy_load_if_possible("sonoff"),
89
}
910
return sub_drivers
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
-- Copyright 2026 SmartThings, Inc.
2+
-- Licensed under the Apache License, Version 2.0
3+
4+
local test = require "integration_test"
5+
local clusters = require "st.zigbee.zcl.clusters"
6+
local OnOff = clusters.OnOff
7+
local PowerConfiguration = clusters.PowerConfiguration
8+
local capabilities = require "st.capabilities"
9+
local data_types = require "st.zigbee.data_types"
10+
local zigbee_test_utils = require "integration_test.zigbee_test_utils"
11+
local t_utils = require "integration_test.utils"
12+
13+
local mock_device = test.mock_device.build_test_zigbee_device(
14+
{ profile = t_utils.get_profile_definition("valve-battery.yml"),
15+
zigbee_endpoints = {
16+
[1] = {
17+
id = 1,
18+
manufacturer = "SONOFF",
19+
model = "SWV-ZFE",
20+
server_clusters = {0x0000, 0x0001, 0x0006, 0x0404, 0xFC11}
21+
}
22+
}
23+
}
24+
)
25+
26+
zigbee_test_utils.prepare_zigbee_env_info()
27+
local function test_init()
28+
test.mock_device.add_test_device(mock_device)
29+
zigbee_test_utils.init_noop_health_check_timer()
30+
end
31+
32+
test.set_test_init_function(test_init)
33+
34+
test.register_message_test(
35+
"OnOff(on) should set valve to open",
36+
{
37+
{
38+
channel = "zigbee",
39+
direction = "receive",
40+
message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, true) }
41+
},
42+
{
43+
channel = "capability",
44+
direction = "send",
45+
message = mock_device:generate_test_message("main", capabilities.valve.valve.open())
46+
}
47+
}
48+
)
49+
50+
test.register_message_test(
51+
"OnOff(off) should set valve to closed",
52+
{
53+
{
54+
channel = "zigbee",
55+
direction = "receive",
56+
message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, false) }
57+
},
58+
{
59+
channel = "capability",
60+
direction = "send",
61+
message = mock_device:generate_test_message("main", capabilities.valve.valve.closed())
62+
}
63+
}
64+
)
65+
66+
local uint8_onoff_off_report = OnOff.attributes.OnOff:build_test_attr_report(mock_device, false)
67+
uint8_onoff_off_report.body.zcl_body.attr_records[1].data_type.value = data_types.Uint8.ID
68+
69+
test.register_message_test(
70+
"OnOff(off) report with Uint8 data type should set valve to closed",
71+
{
72+
{
73+
channel = "zigbee",
74+
direction = "receive",
75+
message = { mock_device.id, uint8_onoff_off_report }
76+
},
77+
{
78+
channel = "capability",
79+
direction = "send",
80+
message = mock_device:generate_test_message("main", capabilities.valve.valve.closed())
81+
}
82+
}
83+
)
84+
85+
test.register_message_test(
86+
"Battery percentage report should be handled",
87+
{
88+
{
89+
channel = "zigbee",
90+
direction = "receive",
91+
message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 55) }
92+
},
93+
{
94+
channel = "capability",
95+
direction = "send",
96+
message = mock_device:generate_test_message("main", capabilities.battery.battery(28))
97+
}
98+
}
99+
)
100+
101+
test.register_coroutine_test(
102+
"Init lifecycle should use default battery percentage reporting configuration",
103+
function ()
104+
test.socket.zigbee:__set_channel_ordering("relaxed")
105+
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" })
106+
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })
107+
test.socket.zigbee:__expect_send({
108+
mock_device.id,
109+
PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device)
110+
})
111+
test.socket.zigbee:__expect_send({
112+
mock_device.id,
113+
OnOff.attributes.OnOff:read(mock_device)
114+
})
115+
test.socket.zigbee:__expect_send({
116+
mock_device.id,
117+
zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID)
118+
})
119+
test.socket.zigbee:__expect_send({
120+
mock_device.id,
121+
PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(mock_device, 30, 21600, 1)
122+
})
123+
test.socket.zigbee:__expect_send({
124+
mock_device.id,
125+
zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID)
126+
})
127+
test.socket.zigbee:__expect_send({
128+
mock_device.id,
129+
OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 600, 0)
130+
})
131+
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
132+
end
133+
)
134+
135+
test.register_message_test(
136+
"Capability(valve) command(open) should send OnOff.On",
137+
{
138+
{
139+
channel = "capability",
140+
direction = "receive",
141+
message = { mock_device.id, { capability = "valve", component = "main", command = "open", args = { } } }
142+
},
143+
{
144+
channel = "zigbee",
145+
direction = "send",
146+
message = { mock_device.id, OnOff.server.commands.On(mock_device) }
147+
}
148+
}
149+
)
150+
151+
test.register_message_test(
152+
"Capability(valve) command(close) should send OnOff.Off",
153+
{
154+
{
155+
channel = "capability",
156+
direction = "receive",
157+
message = { mock_device.id, { capability = "valve", component = "main", command = "close", args = { } } }
158+
},
159+
{
160+
channel = "zigbee",
161+
direction = "send",
162+
message = { mock_device.id, OnOff.server.commands.Off(mock_device) }
163+
}
164+
}
165+
)
166+
167+
test.register_coroutine_test(
168+
"doConfigure lifecycle should configure device",
169+
function ()
170+
test.socket.zigbee:__set_channel_ordering("relaxed")
171+
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })
172+
test.socket.zigbee:__expect_send({
173+
mock_device.id,
174+
PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device)
175+
})
176+
test.socket.zigbee:__expect_send({
177+
mock_device.id,
178+
OnOff.attributes.OnOff:read(mock_device)
179+
})
180+
test.socket.zigbee:__expect_send({
181+
mock_device.id,
182+
zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID)
183+
})
184+
test.socket.zigbee:__expect_send({
185+
mock_device.id,
186+
PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(mock_device, 30, 21600, 1)
187+
})
188+
test.socket.zigbee:__expect_send({
189+
mock_device.id,
190+
zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID)
191+
})
192+
test.socket.zigbee:__expect_send({
193+
mock_device.id,
194+
OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 600, 0)
195+
})
196+
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
197+
end
198+
)
199+
200+
test.register_message_test(
201+
"Refresh should read all necessary attributes",
202+
{
203+
{
204+
channel = "capability",
205+
direction = "receive",
206+
message = {
207+
mock_device.id,
208+
{ capability = "refresh", component = "main", command = "refresh", args = {} }
209+
}
210+
},
211+
{
212+
channel = "zigbee",
213+
direction = "send",
214+
message = { mock_device.id, OnOff.attributes.OnOff:read(mock_device) }
215+
},
216+
{
217+
channel = "zigbee",
218+
direction = "send",
219+
message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }
220+
}
221+
},
222+
{
223+
inner_block_ordering = "relaxed"
224+
}
225+
)
226+
227+
test.register_message_test(
228+
"Device added event should refresh device states",
229+
{
230+
{
231+
channel = "device_lifecycle",
232+
direction = "receive",
233+
message = { mock_device.id, "added" },
234+
},
235+
{
236+
channel = "zigbee",
237+
direction = "send",
238+
message = { mock_device.id, OnOff.attributes.OnOff:read(mock_device) }
239+
},
240+
{
241+
channel = "zigbee",
242+
direction = "send",
243+
message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }
244+
}
245+
},
246+
{
247+
inner_block_ordering = "relaxed"
248+
}
249+
)
250+
251+
test.run_registered_tests()

0 commit comments

Comments
 (0)