Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d3ca759
feat:sonoff swv valve
yanggx24 Jul 31, 2026
244e0ee
Zigbee Valve:modify comment language and file name
yanggx24 Aug 1, 2026
539f602
Zigbee Valve:Add Sonoff valve subdriver lazy loading
yanggx24 Aug 5, 2026
caee9a2
Merge branch 'SmartThingsCommunity:main' into main
yanggx24 Aug 10, 2026
b938a79
Merge branch 'main' into main
yanggx24 Aug 19, 2026
3270a93
test: fix zigbee valve lazy loading coverage
yanggx24 Aug 19, 2026
30cf526
Merge branch 'main' into main
yanggx24 Aug 21, 2026
e8492f8
Fix SONOFF valve command handling
yanggx24 Sep 16, 2026
7b39c10
Merge branch 'main' of https://github.com/yanggx24/SmartThingsEdgeDri…
yanggx24 Sep 16, 2026
2106620
Configure Sonoff valve battery reporting
yanggx24 Sep 18, 2026
b375355
chore: simplify zigbee-valve license header
yanggx24 Sep 22, 2026
d9350e5
chore: remove test_lazy_load_subdriver.lua
yanggx24 Sep 23, 2026
dbd6e4c
Use default SONOFF valve reporting config
yanggx24 Sep 24, 2026
293f87c
Merge branch 'main' into main
yanggx24 Sep 24, 2026
ee923ab
fix(zigbee-valve): restore default battery reporting configuration
yanggx24 Oct 8, 2026
7664d94
Merge branch 'main' of https://github.com/yanggx24/SmartThingsEdgeDri…
yanggx24 Oct 8, 2026
6626f37
Fix SONOFF valve command test expectations
yanggx24 Oct 8, 2026
b8cf885
Remove unnecessary line breaks in test_sonoff_valve.lua
yanggx24 Oct 8, 2026
82ebeb6
Update drivers/SmartThings/zigbee-valve/src/test/test_sonoff_valve.lua
yanggx24 Oct 9, 2026
2e35b1e
Update drivers/SmartThings/zigbee-valve/src/test/test_sonoff_valve.lua
yanggx24 Oct 9, 2026
3daa3d6
Merge branch 'main' into main
yanggx24 Oct 9, 2026
ae0db68
Remove redundant comments from the SONOFF test file
yanggx24 Oct 10, 2026
94fc997
Merge branch 'main' of https://github.com/yanggx24/SmartThingsEdgeDri…
yanggx24 Oct 10, 2026
1e566cb
Merge branch 'main' into main
KKlimczukS Oct 12, 2026
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
22 changes: 20 additions & 2 deletions drivers/SmartThings/zigbee-valve/fingerprints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,23 @@ zigbeeManufacturer :
manufacturer: Compacta
model: ZBVC1(1023A)
deviceProfileName: valve-battery-powerSource


- id: SONOFF/SWV-ZFE
deviceLabel: SONOFF Water Valve
manufacturer: SONOFF
model: SWV-ZFE
deviceProfileName: valve-battery
- id: SONOFF/SWV-ZFU
deviceLabel: SONOFF Water Valve
manufacturer: SONOFF
model: SWV-ZFU
deviceProfileName: valve-battery
- id: SONOFF/SWV-ZNE
deviceLabel: SONOFF Water Valve
manufacturer: SONOFF
model: SWV-ZNE
deviceProfileName: valve-battery
- id: SONOFF/SWV-ZNU
deviceLabel: SONOFF Water Valve
manufacturer: SONOFF
model: SWV-ZNU
deviceProfileName: valve-battery
14 changes: 14 additions & 0 deletions drivers/SmartThings/zigbee-valve/profiles/valve-battery.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: valve-battery
components:
- id: main
capabilities:
- id: valve
version: 1
- id: battery
version: 1
- id: firmwareUpdate
version: 1
- id: refresh
version: 1
categories :
- name: WaterValve
1 change: 0 additions & 1 deletion drivers/SmartThings/zigbee-valve/src/init.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
-- Copyright 2022 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0


local ZigbeeDriver = require "st.zigbee"
local defaults = require "st.zigbee.defaults"

Expand Down
20 changes: 20 additions & 0 deletions drivers/SmartThings/zigbee-valve/src/sonoff/can_handle.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local FINGERPRINTS = {
{ mfr = "SONOFF", model = "SWV-ZFU" },
{ mfr = "SONOFF", model = "SWV-ZFE" },
{ mfr = "SONOFF", model = "SWV-ZNU" },
{ mfr = "SONOFF", model = "SWV-ZNE" },
}

local function sonoff_can_handle(opts, driver, device, ...)
for _, fingerprint in ipairs(FINGERPRINTS) do
if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then
return true, require "sonoff"
end
end
return false
end

return sonoff_can_handle
53 changes: 53 additions & 0 deletions drivers/SmartThings/zigbee-valve/src/sonoff/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local capabilities = require "st.capabilities"
local zcl_clusters = require "st.zigbee.zcl.clusters"
local OnOff = zcl_clusters.OnOff
local PowerConfiguration = zcl_clusters.PowerConfiguration
local utils = require "st.utils"

--- OnOff Property Reporting Handler → Valve Capability Point Event
--- SONOFF reports valve state through the OnOff cluster, so keep an explicit mapping
--- from OnOff reports to valve events while using the parent driver's default commands.
--- @param driver table Driver instance
--- @param device table Device instance
--- @param value table Zigbee attribute value
local function onoff_attr_handler(driver, device, value)
local is_on = value.value ~= false and value.value ~= 0
if is_on then
device:emit_event(capabilities.valve.valve.open())
else
device:emit_event(capabilities.valve.valve.closed())
end
end

--- Battery Percentage Attribute Handler
--- Zigbee BatteryPercentageRemaining range 0-200 (0%-100%), needs to be divided by 2
--- @param driver table Driver instance
--- @param device table Device instance
--- @param value table Zigbee attribute value
local function battery_percentage_handler(driver, device, value)
local raw = value.value
local percent = utils.round(raw / 2)
device:emit_event(capabilities.battery.battery(percent))
end

local sonoff_valve_handler = {
NAME = "SONOFF Water Valve Handler",
zigbee_handlers = {
attr = {
-- OnOff property reporting → valve event (to compensate for the defect of the parent driver's default mapping being skipped)
[OnOff.ID] = {
[OnOff.attributes.OnOff.ID] = onoff_attr_handler
},
Comment thread
KKlimczukS marked this conversation as resolved.
-- Battery percentage attribute reporting → battery event
[PowerConfiguration.ID] = {
[PowerConfiguration.attributes.BatteryPercentageRemaining.ID] = battery_percentage_handler
}
}
},
can_handle = require "sonoff.can_handle",
}

return sonoff_valve_handler
1 change: 1 addition & 0 deletions drivers/SmartThings/zigbee-valve/src/sub_drivers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ local lazy_load_if_possible = require "lazy_load_subdriver"
local sub_drivers = {
lazy_load_if_possible("sinope"),
lazy_load_if_possible("ezex"),
lazy_load_if_possible("sonoff"),
}
return sub_drivers
251 changes: 251 additions & 0 deletions drivers/SmartThings/zigbee-valve/src/test/test_sonoff_valve.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local test = require "integration_test"
local clusters = require "st.zigbee.zcl.clusters"
local OnOff = clusters.OnOff
local PowerConfiguration = clusters.PowerConfiguration
local capabilities = require "st.capabilities"
local data_types = require "st.zigbee.data_types"
local zigbee_test_utils = require "integration_test.zigbee_test_utils"
local t_utils = require "integration_test.utils"

local mock_device = test.mock_device.build_test_zigbee_device(
{ profile = t_utils.get_profile_definition("valve-battery.yml"),
zigbee_endpoints = {
[1] = {
id = 1,
manufacturer = "SONOFF",
model = "SWV-ZFE",
server_clusters = {0x0000, 0x0001, 0x0006, 0x0404, 0xFC11}
}
}
}
)

zigbee_test_utils.prepare_zigbee_env_info()
local function test_init()
test.mock_device.add_test_device(mock_device)
zigbee_test_utils.init_noop_health_check_timer()
end

test.set_test_init_function(test_init)

test.register_message_test(
"OnOff(on) should set valve to open",
{
{
channel = "zigbee",
direction = "receive",
message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, true) }
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.valve.valve.open())
}
}
)

test.register_message_test(
"OnOff(off) should set valve to closed",
{
{
channel = "zigbee",
direction = "receive",
message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, false) }
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.valve.valve.closed())
}
}
)

local uint8_onoff_off_report = OnOff.attributes.OnOff:build_test_attr_report(mock_device, false)
uint8_onoff_off_report.body.zcl_body.attr_records[1].data_type.value = data_types.Uint8.ID

test.register_message_test(
"OnOff(off) report with Uint8 data type should set valve to closed",
{
{
channel = "zigbee",
direction = "receive",
message = { mock_device.id, uint8_onoff_off_report }
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.valve.valve.closed())
}
}
)

test.register_message_test(
"Battery percentage report should be handled",
{
{
channel = "zigbee",
direction = "receive",
message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 55) }
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.battery.battery(28))
}
}
)

test.register_coroutine_test(
"Init lifecycle should use default battery percentage reporting configuration",
function ()
test.socket.zigbee:__set_channel_ordering("relaxed")
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" })
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })
test.socket.zigbee:__expect_send({
mock_device.id,
PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device)
})
test.socket.zigbee:__expect_send({
mock_device.id,
OnOff.attributes.OnOff:read(mock_device)
})
test.socket.zigbee:__expect_send({
mock_device.id,
zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID)
})
test.socket.zigbee:__expect_send({
mock_device.id,
PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(mock_device, 30, 21600, 1)
})
test.socket.zigbee:__expect_send({
mock_device.id,
zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID)
})
test.socket.zigbee:__expect_send({
mock_device.id,
OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 600, 0)
})
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end
)

test.register_message_test(
"Capability(valve) command(open) should send OnOff.On",
{
{
channel = "capability",
direction = "receive",
message = { mock_device.id, { capability = "valve", component = "main", command = "open", args = { } } }
},
{
channel = "zigbee",
direction = "send",
message = { mock_device.id, OnOff.server.commands.On(mock_device) }
}
}
)

test.register_message_test(
"Capability(valve) command(close) should send OnOff.Off",
{
{
channel = "capability",
direction = "receive",
message = { mock_device.id, { capability = "valve", component = "main", command = "close", args = { } } }
},
{
channel = "zigbee",
direction = "send",
message = { mock_device.id, OnOff.server.commands.Off(mock_device) }
}
}
)
Comment thread
KKlimczukS marked this conversation as resolved.

test.register_coroutine_test(
"doConfigure lifecycle should configure device",
function ()
test.socket.zigbee:__set_channel_ordering("relaxed")
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })
test.socket.zigbee:__expect_send({
mock_device.id,
PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device)
})
test.socket.zigbee:__expect_send({
mock_device.id,
OnOff.attributes.OnOff:read(mock_device)
})
test.socket.zigbee:__expect_send({
mock_device.id,
zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID)
})
test.socket.zigbee:__expect_send({
mock_device.id,
PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(mock_device, 30, 21600, 1)
})
test.socket.zigbee:__expect_send({
mock_device.id,
zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID)
})
test.socket.zigbee:__expect_send({
mock_device.id,
OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 600, 0)
})
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end
)

test.register_message_test(
"Refresh should read all necessary attributes",
{
{
channel = "capability",
direction = "receive",
message = {
mock_device.id,
{ capability = "refresh", component = "main", command = "refresh", args = {} }
}
},
{
channel = "zigbee",
direction = "send",
message = { mock_device.id, OnOff.attributes.OnOff:read(mock_device) }
},
{
channel = "zigbee",
direction = "send",
message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }
}
},
{
inner_block_ordering = "relaxed"
}
)

test.register_message_test(
"Device added event should refresh device states",
{
{
channel = "device_lifecycle",
direction = "receive",
message = { mock_device.id, "added" },
},
{
channel = "zigbee",
direction = "send",
message = { mock_device.id, OnOff.attributes.OnOff:read(mock_device) }
},
{
channel = "zigbee",
direction = "send",
message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }
}
},
{
inner_block_ordering = "relaxed"
}
)

test.run_registered_tests()
Loading