Skip to content

Conversation

@marcintyminski
Copy link
Contributor

Check all that apply

Type of Change

  • WWST Certification Request
    • If this is your first time contributing code:
      • I have reviewed the README.md file
      • I have reviewed the CODE_OF_CONDUCT.md file
      • I have signed the CLA
    • I plan on entering a WWST Certification Request or have entered a request through the WWST Certification console at developer.smartthings.com
  • Bug fix
  • New feature
  • Refactor

Checklist

  • I have performed a self-review of my code
  • I have commented my code in hard-to-understand areas
  • I have verified my changes by testing with a device or have communicated a plan for testing
  • I am adding new behavior, such as adding a sub-driver, and have added and run new unit tests to cover the new behavior

Description of Change

Summary of Completed Tests

@github-actions
Copy link

github-actions bot commented Dec 2, 2025

Duplicate profile check: Passed - no duplicate profiles detected.

@github-actions
Copy link

github-actions bot commented Dec 2, 2025

@github-actions
Copy link

github-actions bot commented Dec 2, 2025

File Coverage
All files 95%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/aqara/version/init.lua 94%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua 87%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/frient/init.lua 94%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua 89%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/configurations/init.lua 98%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/init.lua 94%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/preferences.lua 97%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/lazy_load_subdriver.lua 57%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/frient-IO/unbind_request.lua 71%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/frient-IO/init.lua 85%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua 70%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua 95%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/aqara/init.lua 94%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua 97%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/tuya-multi/can_handle.lua 90%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/init.lua 93%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua 98%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/jasco/init.lua 92%
/home/runner/work/SmartThingsEdgeDrivers/SmartThingsEdgeDrivers/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua 95%

Minimum allowed coverage is 90%

Generated by 🐒 cobertura-action against 3f16e4d

@github-actions
Copy link

github-actions bot commented Dec 2, 2025

Test Results

   71 files    481 suites   0s ⏱️
2 493 tests 2 493 ✅ 0 💤 0 ❌
4 275 runs  4 275 ✅ 0 💤 0 ❌

Results for commit 3f16e4d.

♻️ This comment has been updated with latest results.

Comment on lines 130 to 133
if value == nil then return nil end
if type(value) == "number" then return math.tointeger(value) end
local num = tonumber(value)
return num and math.tointeger(num) or nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

math.tointeger accepts non-numeric values, passes through nil, and returns nil if the value cannot be parsed.

This function seems redundant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Comment on lines 136 to 144
local function sanitize_timing(value)
local int = to_integer(value) or 0
if int < 0 then
int = 0
elseif int > 0xFFFF then
int = 0xFFFF
end
return int
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we provide a utils.clamp_value function that you could use here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines 404 to 471
else
-- Input 1
if args.old_st_store.preferences.reversePolarity1 ~= device.preferences.reversePolarity1 then
write_basic_input_polarity_attr(device, ZIGBEE_ENDPOINTS.INPUT_1, device.preferences.reversePolarity1)
end

if args.old_st_store.preferences.controlOutput11 ~= device.preferences.controlOutput11 then
device:send(device.preferences.controlOutput11
and build_bind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_1, ZIGBEE_ENDPOINTS.OUTPUT_1)
or build_unbind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_1, ZIGBEE_ENDPOINTS.OUTPUT_1))
end

if args.old_st_store.preferences.controlOutput21 ~= device.preferences.controlOutput21 then
device:send(device.preferences.controlOutput21
and build_bind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_1, ZIGBEE_ENDPOINTS.OUTPUT_2)
or build_unbind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_1, ZIGBEE_ENDPOINTS.OUTPUT_2))
end

-- Input 2
if args.old_st_store.preferences.reversePolarity2 ~= device.preferences.reversePolarity2 then
write_basic_input_polarity_attr(device, ZIGBEE_ENDPOINTS.INPUT_2, device.preferences.reversePolarity2)
end

if args.old_st_store.preferences.controlOutput12 ~= device.preferences.controlOutput12 then
device:send(device.preferences.controlOutput12
and build_bind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_2, ZIGBEE_ENDPOINTS.OUTPUT_1)
or build_unbind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_2, ZIGBEE_ENDPOINTS.OUTPUT_1))
end

if args.old_st_store.preferences.controlOutput22 ~= device.preferences.controlOutput22 then
device:send(device.preferences.controlOutput22
and build_bind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_2, ZIGBEE_ENDPOINTS.OUTPUT_2)
or build_unbind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_2, ZIGBEE_ENDPOINTS.OUTPUT_2))
end

-- Input 3
if args.old_st_store.preferences.reversePolarity3 ~= device.preferences.reversePolarity3 then
write_basic_input_polarity_attr(device, ZIGBEE_ENDPOINTS.INPUT_3, device.preferences.reversePolarity3)
end

if args.old_st_store.preferences.controlOutput13 ~= device.preferences.controlOutput13 then
device:send(device.preferences.controlOutput13
and build_bind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_3, ZIGBEE_ENDPOINTS.OUTPUT_1)
or build_unbind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_3, ZIGBEE_ENDPOINTS.OUTPUT_1))
end

if args.old_st_store.preferences.controlOutput23 ~= device.preferences.controlOutput23 then
device:send(device.preferences.controlOutput23
and build_bind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_3, ZIGBEE_ENDPOINTS.OUTPUT_2)
or build_unbind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_3, ZIGBEE_ENDPOINTS.OUTPUT_2))
end

-- Input 4
if args.old_st_store.preferences.reversePolarity4 ~= device.preferences.reversePolarity4 then
write_basic_input_polarity_attr(device, ZIGBEE_ENDPOINTS.INPUT_4, device.preferences.reversePolarity4)
end

if args.old_st_store.preferences.controlOutput14 ~= device.preferences.controlOutput14 then
device:send(device.preferences.controlOutput14
and build_bind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_4, ZIGBEE_ENDPOINTS.OUTPUT_1)
or build_unbind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_4, ZIGBEE_ENDPOINTS.OUTPUT_1))
end

if args.old_st_store.preferences.controlOutput24 ~= device.preferences.controlOutput24 then
device:send(device.preferences.controlOutput24
and build_bind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_4, ZIGBEE_ENDPOINTS.OUTPUT_2)
or build_unbind_request(device, BasicInput.ID, ZIGBEE_ENDPOINTS.INPUT_4, ZIGBEE_ENDPOINTS.OUTPUT_2))
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you pull parts of this out into a function to avoid the repetition?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines 505 to 512
local parent = device:get_parent_device()
if parent then
local info = OUTPUT_BY_KEY[device.parent_assigned_child_key]
if info then
handle_output_command(parent, info.suffix, "on")
return
end
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code flow is a bit unclear to me here, but I believe you might try using the set_find_child command to handle some of the mapping of parent/child commands a bit more automatically

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that set_find_child only helps when every component call should go straight to a child device. We still need separate handling for real child outputs, parent output components that share timing logic, and input components that never leave the parent, so the helper wouldn’t simplify anything.

Copy link
Contributor

@greens greens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, our code uses 2-space tabs.

Comment on lines +1 to +13
-- Copyright 2025 SmartThings
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you use the new shorter copyright/license statement here?

Comment on lines +174 to +181
if child then
local on_time = math.floor((sanitize_timing(child.preferences.configOnTime)) * 10)
local off_wait = math.floor((sanitize_timing(child.preferences.configOffWaitTime)) * 10)
return on_time, off_wait
end
local on_time = math.floor((sanitize_timing(device.preferences["configOnTime" .. suffix]))*10)
local off_wait = math.floor((sanitize_timing(device.preferences["configOffWaitTime" .. suffix]))*10)
return on_time, off_wait
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if child then
local on_time = math.floor((sanitize_timing(child.preferences.configOnTime)) * 10)
local off_wait = math.floor((sanitize_timing(child.preferences.configOffWaitTime)) * 10)
return on_time, off_wait
end
local on_time = math.floor((sanitize_timing(device.preferences["configOnTime" .. suffix]))*10)
local off_wait = math.floor((sanitize_timing(device.preferences["configOffWaitTime" .. suffix]))*10)
return on_time, off_wait
local on_time = math.floor((sanitize_timing(device.preferences["configOnTime" .. suffix]))*10)
local off_wait = math.floor((sanitize_timing(device.preferences["configOffWaitTime" .. suffix]))*10)
if child then
local on_time = math.floor((sanitize_timing(child.preferences.configOnTime)) * 10)
local off_wait = math.floor((sanitize_timing(child.preferences.configOffWaitTime)) * 10)
end
return on_time, off_wait

also I appreciate the caution, but it's impossible for OUTPUT_INFO[suffix] to ever be nil anywhere you call it

if config_on_time == 0 then
device:send(OnOff.server.commands.Off(device):to_endpoint(endpoint))
else
device:send(OnOff.server.commands.OnWithTimedOff(device, data_types.Uint8(0),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be clear, this is meant to be an OnWithTimedOff?

It seems like the behavior between on and off is identical as long as the config_on_time > 0. Is that correct?

}
},
FRIENT_IO_MODULE = {
FINGERPRINTS = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the spacing here is inconsistent with the rest of the file

Comment on lines +440 to +496
local function switch_on_handler(driver, device, command)
local parent = device:get_parent_device()
if parent then
local info = OUTPUT_BY_KEY[device.parent_assigned_child_key]
if info then
handle_output_command(parent, info.suffix, "on")
return
end
end

local num = command.component and command.component:match("output(%d)")
if num then
handle_output_command(device, num, "on")
return
end
num = command.component:match("input(%d)")
if num then
local component = device.profile.components[command.component]
local value = device:get_latest_state(command.component, Switch.ID, Switch.switch.NAME)
if value == "on" then
device:emit_component_event(component,
Switch.switch.on({ state_change = true, visibility = { displayed = false } }))
elseif value == "off" then
device:emit_component_event(component,
Switch.switch.off({ state_change = true, visibility = { displayed = false } }))
end
end
end

local function switch_off_handler(driver, device, command)
local parent = device:get_parent_device()
if parent then
local info = OUTPUT_BY_KEY[device.parent_assigned_child_key]
if info then
handle_output_command(parent, info.suffix, "off")
return
end
end

local num = command.component and command.component:match("output(%d)")
if num then
handle_output_command(device, num, "off")
return
end
num = command.component:match("input(%d)")
if num then
local component = device.profile.components[command.component]
local value = device:get_latest_state(command.component, Switch.ID, Switch.switch.NAME)
if value == "on" then
device:emit_component_event(component,
Switch.switch.on({ state_change = true, visibility = { displayed = false } }))
elseif value == "off" then
device:emit_component_event(component,
Switch.switch.off({ state_change = true, visibility = { displayed = false } }))
end
end
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you consolidate these functions? it might be useful to use a factory function like we do for thermostats: https://github.com/SmartThingsCommunity/SmartThingsEdgeDrivers/blob/main/drivers/SmartThings/zwave-thermostat/src/init.lua#L44

Comment on lines +42 to +43
OUTPUT_1 = "output1",
OUTPUT_2 = "output2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these aren't actually components, they're child devices, right?

Comment on lines +207 to +217
local function emit_switch_event_for_endpoint(device, endpoint, event)
local info = OUTPUT_BY_ENDPOINT[endpoint]
if info ~= nil then
local child = device:get_child_by_parent_assigned_key(info.key)
if child then
child:emit_event(event)
return
end
end
device:emit_event_for_endpoint(endpoint, event)
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will the native handler actually work for this unique behavior you have here?

The mapping system you've constructed is very confusing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants