-
Notifications
You must be signed in to change notification settings - Fork 563
[WWSTCERT-13462, WWSTCERT-13465] Sonoff SWV2C valve support #3171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| name: valve-battery-2 | ||
| components: | ||
| - id: main | ||
| capabilities: | ||
| - id: valve | ||
| version: 1 | ||
| - id: battery | ||
| version: 1 | ||
| - id: powerSource | ||
| version: 1 | ||
| config: | ||
| values: | ||
| - key: "powerSource.value" | ||
| enabledValues: | ||
| - battery | ||
| - id: firmwareUpdate | ||
| version: 1 | ||
| - id: refresh | ||
| version: 1 | ||
| categories: | ||
| - name: WaterValve |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| name: valve | ||
| components: | ||
| - id: main | ||
| capabilities: | ||
| - id: valve | ||
| version: 1 | ||
| - id: refresh | ||
| version: 1 | ||
| categories: | ||
| - name: WaterValve |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| -- Copyright 2026 SmartThings, Inc. | ||
| -- Licensed under the Apache License, Version 2.0 | ||
|
|
||
| local FINGERPRINTS = { | ||
| { mfr = "SONOFF", model = "SWV-ZF2U" }, | ||
| { mfr = "SONOFF", model = "SWV-ZF2" } | ||
| } | ||
|
|
||
| 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 | ||
| if device.parent_device_id ~= nil then | ||
| return true, require "sonoff" | ||
| end | ||
| return false | ||
| end | ||
|
|
||
| return sonoff_can_handle | ||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,140 @@ | ||||
| -- Copyright 2026 SmartThings, Inc. | ||||
| -- Licensed under the Apache License, Version 2.0 | ||||
|
|
||||
| local capabilities = require "st.capabilities" | ||||
| local log = require "log" | ||||
| local zcl_clusters = require "st.zigbee.zcl.clusters" | ||||
| local Basic = zcl_clusters.Basic | ||||
| local OnOff = zcl_clusters.OnOff | ||||
| local PowerConfiguration = zcl_clusters.PowerConfiguration | ||||
| local st_device = require "st.device" | ||||
| local utils = require "st.utils" | ||||
|
|
||||
| local BATTERY_POLL_INTERVAL = 7200 | ||||
|
|
||||
| local function find_child(parent, ep_id) | ||||
| return parent:get_child_by_parent_assigned_key(string.format("%02X", ep_id)) | ||||
| end | ||||
|
|
||||
| --- OnOff Property Reporting → Valve Capability Point Event | ||||
| local function onoff_attr_handler(driver, device, value, zb_rx) | ||||
| local ep = zb_rx.address_header.src_endpoint.value | ||||
| local target = device | ||||
| if ep == 0x02 then | ||||
| local child = find_child(device, 2) | ||||
| if child then target = child end | ||||
| end | ||||
| log.info(string.format("[SWV] OnOff report ep=%s value=%s", tostring(ep), tostring(value.value))) | ||||
| if value.value == true or value.value == 1 then | ||||
| target:emit_event(capabilities.valve.valve.open()) | ||||
| else | ||||
| target:emit_event(capabilities.valve.valve.closed()) | ||||
| end | ||||
| end | ||||
|
|
||||
| --- Battery Percentage Attribute Handler | ||||
| local function battery_percentage_handler(driver, device, value) | ||||
| device:emit_event(capabilities.battery.battery(utils.round(value.value / 2))) | ||||
| end | ||||
|
|
||||
| --- Lifecycle initialization handler | ||||
| local function device_init(driver, device) | ||||
| if device.network_type == st_device.NETWORK_TYPE_ZIGBEE then | ||||
| device:set_find_child(find_child) | ||||
| device.thread:call_on_schedule( | ||||
| BATTERY_POLL_INTERVAL, | ||||
| function() | ||||
| device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) | ||||
| end | ||||
| ) | ||||
| end | ||||
| end | ||||
|
Comment on lines
+41
to
+51
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @yezi289 Don't You want to configure PowerConfiguration (instead of polling) and other clusters (for reporting) ? here is the example of how to do it in the subdriver SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-power-meter/src/frient/EMIZB-151/init.lua Line 98 in 71bbd3d
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @yezi289
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we should not be polling these devices. They should have cluster bindings and attribute reporting setup by using |
||||
|
|
||||
| --- doConfigure | ||||
| local function do_configure(driver, device) | ||||
| device:try_update_metadata({ provisioning_state = "PROVISIONED" }) | ||||
| if device.network_type == st_device.NETWORK_TYPE_ZIGBEE then | ||||
| device:emit_event(capabilities.valve.valve.closed()) | ||||
| device:emit_event(capabilities.battery.battery(100)) | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what's the source for the 100% battery.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should also not have the valve events emitted without the device telling us. If the device is actually open it would not be accurately reflected in the app for the user. |
||||
| else | ||||
| device:emit_event(capabilities.valve.valve.closed()) | ||||
| end | ||||
| end | ||||
|
|
||||
| --- added | ||||
| local function device_added(driver, device) | ||||
| if device.network_type == st_device.NETWORK_TYPE_ZIGBEE then | ||||
| if find_child(device, 2) == nil then | ||||
| driver:try_create_device({ | ||||
| type = "EDGE_CHILD", | ||||
| label = string.format("%s 2", device.label), | ||||
| profile = "valve", | ||||
| parent_device_id = device.id, | ||||
| parent_assigned_child_key = string.format("%02X", 2), | ||||
| vendor_provided_label = string.format("%s 2", device.label), | ||||
| }) | ||||
| end | ||||
| else | ||||
| device:emit_event(capabilities.valve.valve.closed()) | ||||
| end | ||||
| end | ||||
|
|
||||
| --- driverSwitched | ||||
| local function driver_switched(driver, device) | ||||
| device_added(driver, device) | ||||
| end | ||||
|
|
||||
| --- valve.open | ||||
| local function valve_open_handler(driver, device, command) | ||||
| device:send(OnOff.server.commands.On(device)) | ||||
| device:send(OnOff.attributes.OnOff:read(device)) | ||||
| end | ||||
|
|
||||
| --- valve.close | ||||
| local function valve_close_handler(driver, device, command) | ||||
| device:send(OnOff.server.commands.Off(device)) | ||||
| device:send(OnOff.attributes.OnOff:read(device)) | ||||
| end | ||||
|
Comment on lines
+88
to
+97
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be handled by the framework. |
||||
|
|
||||
| --- Identity cluster handler:button pressed on the device, sync valve states | ||||
| local function identify_handler(driver, device, zb_rx) | ||||
| log.info("[SWV] Identify received, syncing valve states") | ||||
| device.thread:call_with_delay(2, function() | ||||
| device:send(OnOff.attributes.OnOff:read(device)) | ||||
| device:send(OnOff.attributes.OnOff:read(device):to_endpoint(0x02)) | ||||
| end) | ||||
| end | ||||
|
|
||||
| local sonoff_valve_handler = { | ||||
| NAME = "SONOFF Water Valve Handler", | ||||
| lifecycle_handlers = { | ||||
| init = device_init, | ||||
| added = device_added, | ||||
| doConfigure = do_configure, | ||||
| driverSwitched = driver_switched, | ||||
| }, | ||||
| capability_handlers = { | ||||
| [capabilities.valve.ID] = { | ||||
| [capabilities.valve.commands.open.NAME] = valve_open_handler, | ||||
| [capabilities.valve.commands.close.NAME] = valve_close_handler, | ||||
| } | ||||
|
Comment on lines
+117
to
+120
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be handled by the framework. |
||||
| }, | ||||
| zigbee_handlers = { | ||||
| attr = { | ||||
| [OnOff.ID] = { | ||||
| [OnOff.attributes.OnOff.ID] = onoff_attr_handler | ||||
| }, | ||||
| [PowerConfiguration.ID] = { | ||||
| [PowerConfiguration.attributes.BatteryPercentageRemaining.ID] = battery_percentage_handler | ||||
| } | ||||
| }, | ||||
| cluster = { | ||||
| [Basic.ID] = { | ||||
| [Basic.server.commands.ResetToFactoryDefaults.ID] = identify_handler | ||||
| } | ||||
| } | ||||
| }, | ||||
| can_handle = require "sonoff.can_handle", | ||||
| } | ||||
|
|
||||
| return sonoff_valve_handler | ||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will match any child device in this driver, when we only want children of sonoff devices. The functionality that we have built on top of Child devices means the
get_manufacturerandget_modelfunctions should just work on child devices (it gets the parent device and uses its mfr/model).