From a60c57f3377b92733be2edbec6e709f963a21692 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Wed, 5 Nov 2025 19:11:44 -0700 Subject: [PATCH 1/8] adding support for vzm30 and adding supported button events to all devices --- .../zigbee-switch/fingerprints.yml | 5 + .../profiles/inovelli-vzm30-sn.yml | 271 ++++++++++ .../SmartThings/zigbee-switch/src/init.lua | 2 + .../zigbee-switch/src/inovelli/common.lua | 41 +- .../zigbee-switch/src/inovelli/init.lua | 26 +- .../src/inovelli/vzm30-sn/init.lua | 66 +++ .../src/test/test_inovelli_vzm30_sn.lua | 474 ++++++++++++++++++ .../src/test/test_inovelli_vzm30_sn_child.lua | 356 +++++++++++++ .../test_inovelli_vzm30_sn_preferences.lua | 210 ++++++++ .../src/test/test_inovelli_vzm31_sn.lua | 127 ++--- .../src/test/test_inovelli_vzm32_sn.lua | 130 ++--- 11 files changed, 1573 insertions(+), 135 deletions(-) create mode 100644 drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml create mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_child.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 236758b30d..ff1b8142df 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2359,6 +2359,11 @@ zigbeeManufacturer: model: NEXENTRO Dimming Actuator deviceProfileName: on-off-level # Inovelli + - id: "Inovelli/VZM30-SN" + deviceLabel: "Inovelli On/Off Blue Series" + manufacturer: Inovelli + model: VZM30-SN + deviceProfileName: inovelli-vzm30-sn - id: "Inovelli/VZM31-SN" deviceLabel: "Inovelli 2-in-1 Blue Series" manufacturer: Inovelli diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml new file mode 100644 index 0000000000..2b44625e71 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml @@ -0,0 +1,271 @@ +name: inovelli-vzm30-sn +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + - id: temperatureMeasurement + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + - id: configuration + version: 1 + - id: firmwareUpdate + version: 1 + categories: + - name: Switch + - id: button1 + label: Down Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + label: Up Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + label: Config Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +preferences: + - name: "notificationChild" + title: "Add Child Device - Notification" + description: "Create Separate Child Device for Notification Control" + required: false + preferenceType: boolean + definition: + default: false + - name: "notificationType" + title: "Notification Effect" + description: "This is the notification effect used by the notification child device" + required: false + preferenceType: enumeration + definition: + options: + "255": "Clear" + "1": "Solid" + "2": "Fast Blink" + "3": "Slow Blink" + "4": "Pulse" + "5": "Chase" + "6": "Open/Close" + "7": "Small-to-Big" + "8": "Aurora" + "9": "Slow Falling" + "10": "Medium Falling" + "11": "Fast Falling" + "12": "Slow Rising" + "13": "Medium Rising" + "14": "Fast Rising" + "15": "Medium Blink" + "16": "Slow Chase" + "17": "Fast Chase" + "18": "Fast Siren" + "19": "Slow Siren" + default: 1 + - name: "parameter258" + title: "258. Switch Mode" + description: "Use as a Dimmer or an On/Off switch" + required: false + preferenceType: enumeration + definition: + options: + "0": "Dimmer" + "1": "On/Off (default)" + default: 1 + - name: "parameter22" + title: "22. Aux Switch Type" + description: "Set the Aux switch type. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." + required: false + preferenceType: enumeration + definition: + options: + "0": "None (default)" + "1": "3-Way Aux Switch" + default: 0 + - name: "parameter52" + title: "52. Smart Bulb Mode" + description: "For use with Smart Bulbs that need constant power and are controlled via commands rather than power. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." + required: false + preferenceType: enumeration + definition: + options: + "0": "Disabled (default)" + "1": "Smart Bulb Mode" + default: 0 + - name: "parameter1" + title: "1. Dimming Speed (Remote)" + description: "This changes the speed that the light dims up when controlled from the hub. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + Default=25 (2500ms or 2.5s)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 126 + default: 25 + - name: "parameter2" + title: "2. Dimming Speed (Local)" + description: "This changes the speed that the light dims up when controlled at the switch. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=127 (Sync with parameter 1)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 127 + default: 127 + - name: "parameter3" + title: "3. Ramp Rate (Remote)" + description: "This changes the speed that the light turns on when controlled from the hub. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=127 (Sync with parameter 1)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 127 + default: 127 + - name: "parameter4" + title: "4. Ramp Rate (Local)" + description: "This changes the speed that the light turns on when controlled at the switch. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=127 (Sync with parameter 3)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 127 + default: 127 + - name: "parameter9" + title: "9. Minimum Level" + description: "The minimum level that the light can be dimmed. Useful when the user has a light that does not turn on or flickers at a lower level." + required: false + preferenceType: number + definition: + minimum: 1 + maximum: 99 + default: 1 + - name: "parameter10" + title: "10. Maximum Level" + description: "The maximum level that the light can be dimmed. Useful when the user wants to limit the maximum brighness." + required: false + preferenceType: number + definition: + minimum: 2 + maximum: 100 + default: 100 + - name: "parameter11" + title: "11. Invert Switch" + description: "Inverts the orientation of the switch. Useful when the switch is installed upside down. Essentially up becomes down and down becomes up." + required: false + preferenceType: enumeration + definition: + options: + "0": "No (default)" + "1": "Yes" + default: 0 + - name: "parameter15" + title: "15. Level After Power Restored" + description: "The level the switch will return to when power is restored after power failure. + 0=Off + 1-100=Set Level + 101=Use previous level." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 101 + default: 101 + - name: "parameter17" + title: "17. Load Level Indicator Timeout" + description: "Shows the level that the load is at for x number of seconds after the load is adjusted and then returns to the Default LED state." + required: false + preferenceType: enumeration + definition: + options: + "0": "Do not display Load Level" + "1": "1 Second" + "2": "2 Seconds" + "3": "3 Seconds" + "4": "4 Seconds" + "5": "5 Seconds" + "6": "6 Seconds" + "7": "7 Seconds" + "8": "8 Seconds" + "9": "9 Seconds" + "10": "10 Seconds" + "11": "Display Load Level with no timeout" + default: 11 + - name: "parameter95" + title: "95. LED Indicator Color (w/On)" + description: "Set the color of the Full LED Indicator when the load is on." + required: false + preferenceType: enumeration + definition: + options: + "0": "Red" + "7": "Orange" + "28": "Lemon" + "64": "Lime" + "85": "Green" + "106": "Teal" + "127": "Cyan" + "148": "Aqua" + "170": "Blue (default)" + "190": "Violet" + "212": "Magenta" + "234": "Pink" + "255": "White" + default: 170 + - name: "parameter96" + title: "96. LED Indicator Color (w/Off)" + description: "Set the color of the Full LED Indicator when the load is off." + required: false + preferenceType: enumeration + definition: + options: + "0": "Red" + "7": "Orange" + "28": "Lemon" + "64": "Lime" + "85": "Green" + "106": "Teal" + "127": "Cyan" + "148": "Aqua" + "170": "Blue (default)" + "190": "Violet" + "212": "Magenta" + "234": "Pink" + "255": "White" + default: 170 + - name: "parameter97" + title: "97. LED Indicator Intensity (w/On)" + description: "Set the intensity of the Full LED Indicator when the load is on." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 100 + default: 50 + - name: "parameter98" + title: "98. LED Indicator Intensity (w/Off)" + description: "Set the intensity of the Full LED Indicator when the load is off." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 100 + default: 5 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index dfaba9e573..88657c823c 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -86,6 +86,8 @@ local zigbee_switch_driver_template = { capabilities.energyMeter, capabilities.motionSensor, capabilities.illuminanceMeasurement, + capabilities.relativeHumidityMeasurement, + capabilities.temperatureMeasurement, }, sub_drivers = { lazy_load_if_possible("non_zigbee_devices"), diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua index c80d0deca9..ef84756772 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua @@ -15,9 +15,27 @@ local clusters = require "st.zigbee.zcl.clusters" local device_management = require "st.zigbee.device_management" local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade +local zigbee_constants = require "st.zigbee.constants" +local capabilities = require "st.capabilities" local M = {} +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} +} + +-- Utility function to check if device is VZM32-SN +function M.is_vzm32(device) + return device:get_model() == "VZM32-SN" +end + +-- Utility function to check if device is VZM32-SN +function M.is_vzm30(device) + return device:get_model() == "VZM30-SN" +end + -- Sends a generic configure for Inovelli devices (all models): -- - device:configure -- - send OTA ImageNotify @@ -36,10 +54,31 @@ function M.base_device_configure(driver, device, private_cluster_id, mfg_code) device:send(device_management.build_bind_request(device, private_cluster_id, driver.environment_info.hub_zigbee_eui, 2)) -- Read divisors/multipliers for power/energy reporting - device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) + -- Set default divisor to 1000 for VZM32-SN and VZM30-SN. In initial firmware the divisor is incorrectly set to 100. + if M.is_vzm32(device) or M.is_vzm30(device) then + device:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 1000, {persist = true}) + else + device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) + end device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) device:send(clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(device)) device:send(clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(device)) + + for _, component in pairs(device.profile.components) do + if component.id ~= "main" then + device:emit_component_event( + component, + capabilities.button.supportedButtonValues( + supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + device:emit_component_event( + component, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + end + end end return M \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua index 253dcca86c..8af6c2a73c 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -26,6 +26,7 @@ local OccupancySensing = clusters.OccupancySensing local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" local INOVELLI_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM30-SN" }, { mfr = "Inovelli", model = "VZM31-SN" }, { mfr = "Inovelli", model = "VZM32-SN" } } @@ -57,6 +58,11 @@ local base_preference_map = { -- Model-specific overrides/additions local model_preference_overrides = { + ["VZM30-SN"] = { + parameter11 = {parameter_number = 11, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, + parameter17 = {parameter_number = 17, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter22 = {parameter_number = 22, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + }, ["VZM31-SN"] = { parameter11 = {parameter_number = 11, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, parameter17 = {parameter_number = 17, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, @@ -226,18 +232,6 @@ local function device_configure(driver, device) end end -local function energy_meter_handler(driver, device, value, zb_rx) - local raw_value = value.value - raw_value = raw_value / 100 - device:emit_event(capabilities.energyMeter.energy({value = raw_value, unit = "kWh" })) -end - -local function power_meter_handler(driver, device, value, zb_rx) - local raw_value = value.value - raw_value = raw_value / 10 - device:emit_event(capabilities.powerMeter.power({value = raw_value, unit = "W" })) -end - local function huePercentToValue(value) if value <= 2 then return 0 elseif value >= 98 then return 255 @@ -362,13 +356,6 @@ local inovelli = { }, zigbee_handlers = { attr = { - [clusters.SimpleMetering.ID] = { - [clusters.SimpleMetering.attributes.InstantaneousDemand.ID] = power_meter_handler, - [clusters.SimpleMetering.attributes.CurrentSummationDelivered.ID] = energy_meter_handler - }, - [clusters.ElectricalMeasurement.ID] = { - [clusters.ElectricalMeasurement.attributes.ActivePower.ID] = power_meter_handler - }, [OccupancySensing.ID] = { [OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler }, @@ -380,6 +367,7 @@ local inovelli = { } }, sub_drivers = { + require("inovelli/vzm30-sn"), require("inovelli/vzm32-sn"), }, capability_handlers = { diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua new file mode 100644 index 0000000000..b5b04ab362 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua @@ -0,0 +1,66 @@ +-- 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. + +local clusters = require "st.zigbee.zcl.clusters" +local st_device = require "st.device" +local inovelli_common = require "inovelli.common" + +local TemperatureMeasurement = clusters.TemperatureMeasurement +local RelativeHumidity = clusters.RelativeHumidity + +local INOVELLI_VZM30_SN_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM30-SN" }, +} + +local PRIVATE_CLUSTER_ID = 0xFC31 +local MFG_CODE = 0x122F + +local function can_handle_inovelli_vzm30_sn(opts, driver, device) + for _, fp in ipairs(INOVELLI_VZM30_SN_FINGERPRINTS) do + if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then + return true + end + end + return false +end + +local function configure_temperature_reporting(device) + local min_temp_change = 50 -- 0.5°C in 0.01°C units + device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 3600, min_temp_change)) +end + +local function configure_humidity_reporting(device) + local min_humidity_change = 50 -- 0.5% in 0.01% units + device:send(RelativeHumidity.attributes.MeasuredValue:configure_reporting(device, 30, 3600, min_humidity_change)) +end + +local function device_configure(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + inovelli_common.base_device_configure(driver, device, PRIVATE_CLUSTER_ID, MFG_CODE) + configure_temperature_reporting(device) + configure_humidity_reporting(device) + else + device:configure() + end +end + +local vzm30_sn = { + NAME = "inovelli vzm30-sn device-specific", + can_handle = can_handle_inovelli_vzm30_sn, + lifecycle_handlers = { + doConfigure = device_configure, + }, +} + +return vzm30_sn \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua new file mode 100644 index 0000000000..d39f40e67e --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua @@ -0,0 +1,474 @@ +-- 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. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" +local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade +local device_management = require "st.zigbee.device_management" +local zigbee_constants = require "st.zigbee.constants" + +local OnOff = clusters.OnOff +local Level = clusters.Level +local TemperatureMeasurement = clusters.TemperatureMeasurement +local RelativeHumidity = clusters.RelativeHumidity + +-- Inovelli VZM30-SN device identifiers +local INOVELLI_MANUFACTURER_ID = "Inovelli" +local INOVELLI_VZM30_SN_MODEL = "VZM30-SN" + +-- Device endpoints with supported clusters +local inovelli_vzm30_sn_endpoints = { + [1] = { + id = 1, + manufacturer = INOVELLI_MANUFACTURER_ID, + model = INOVELLI_VZM30_SN_MODEL, + server_clusters = {0x0006, 0x0008, 0x0300, 0x0402, 0x0405} -- OnOff, Level, ColorControl, TemperatureMeasurement, RelativeHumidity + } +} + +local mock_inovelli_vzm30_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm30-sn.yml"), + zigbee_endpoints = inovelli_vzm30_sn_endpoints, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm30_sn) +end +test.set_test_init_function(test_init) + +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} + } + +-- Test device initialization +test.register_message_test( + "Device should initialize properly on added lifecycle event", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_inovelli_vzm30_sn.id, "added" }, + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + Level.attributes.CurrentLevel:read(mock_inovelli_vzm30_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + OnOff.attributes.OnOff:read(mock_inovelli_vzm30_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_inovelli_vzm30_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + RelativeHumidity.attributes.MeasuredValue:read(mock_inovelli_vzm30_sn) + } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test refresh capability +test.register_message_test( + "Refresh capability should send read commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + { capability = "refresh", command = "refresh", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, OnOff.attributes.OnOff:read(mock_inovelli_vzm30_sn) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, Level.attributes.CurrentLevel:read(mock_inovelli_vzm30_sn) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_inovelli_vzm30_sn) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, RelativeHumidity.attributes.MeasuredValue:read(mock_inovelli_vzm30_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch on command +test.register_message_test( + "Switch on command should send OnOff On command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + { capability = "switch", command = "on", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, clusters.OnOff.server.commands.On(mock_inovelli_vzm30_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch off command +test.register_message_test( + "Switch off command should send OnOff Off command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + { capability = "switch", command = "off", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, clusters.OnOff.server.commands.Off(mock_inovelli_vzm30_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch level command +test.register_message_test( + "Switch level command should send Level MoveToLevelWithOnOff command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + { capability = "switchLevel", command = "setLevel", args = { 50 } } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + clusters.Level.server.commands.MoveToLevelWithOnOff(mock_inovelli_vzm30_sn, math.floor(50/100.0 * 254), 0xFFFF) + } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Build test message for Inovelli private cluster button press +local function build_inovelli_button_message(device, button_number, key_attribute) + local messages = require "st.zigbee.messages" + local zcl_messages = require "st.zigbee.zcl" + local zb_const = require "st.zigbee.constants" + local data_types = require "st.zigbee.data_types" + local frameCtrl = require "st.zigbee.zcl.frame_ctrl" + + -- Combine button_number and key_attribute into a single value + -- button_number in lower byte, key_attribute in upper byte + local combined_value = (key_attribute * 256) + button_number + + -- Create the command body using serialize_int + local command_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_messages.ZclHeader({ + frame_ctrl = frameCtrl(0x15), -- Manufacturer specific, client to server + mfg_code = data_types.Uint16(0x122F), -- Inovelli manufacturer code + seqno = data_types.Uint8(0x6D), + cmd = data_types.ZCLCommandId(0x00) -- Scene command + }), + zcl_body = data_types.Uint16(combined_value) + }) + + local addrh = messages.AddressHeader( + device:get_short_address(), + 0x02, -- src_endpoint from real device log + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + 0xFC31 -- PRIVATE_CLUSTER_ID + ) + + return messages.ZigbeeMessageRx({ + address_header = addrh, + body = command_body + }) +end + +-- Test button1 pushed +test.register_message_test( + "Button1 pushed should emit button event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_inovelli_vzm30_sn.id, build_inovelli_button_message(mock_inovelli_vzm30_sn, 0x01, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + } + } +) + +-- Test button2 pressed 4 times +test.register_message_test( + "Button2 pressed 4 times should emit button event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_inovelli_vzm30_sn.id, build_inovelli_button_message(mock_inovelli_vzm30_sn, 0x02, 0x05) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) + } + } +) + +-- Test temperature measurement +test.register_message_test( + "Temperature measurement should emit temperature events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_inovelli_vzm30_sn, 2500) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message("main", capabilities.temperatureMeasurement.temperature({value = 25.0, unit = "C"})) + } + } +) + +-- Test humidity measurement +test.register_message_test( + "Humidity measurement should emit humidity events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + RelativeHumidity.attributes.MeasuredValue:build_test_attr_report(mock_inovelli_vzm30_sn, 6500) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity(65)) + } + } +) + +-- Test power meter from ElectricalMeasurement +test.register_coroutine_test( + "Power meter from ElectricalMeasurement should emit power events", + function() + -- Set the divisor field (default handlers use 10 if not set, but we can set it for consistency) + -- The default handler will use 10 if ELECTRICAL_MEASUREMENT_DIVISOR_KEY is not set + -- Since the test expects 2000 -> 200.0 W, that means divisor of 10 is being used + mock_inovelli_vzm30_sn:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm30_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm30_sn, 2000) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) + ) + end +) + +-- Test energy meter +test.register_coroutine_test( + "Energy meter should emit energy events", + function() + -- Set the divisor field as the device does during configuration + -- For VZM30-SN, the divisor is set to 1000 (like VZM32-SN) + mock_inovelli_vzm30_sn:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 1000, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm30_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm30_sn, 212) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 0.212, unit = "kWh"})) + ) + end +) + +-- Test energy meter reset command +test.register_message_test( + "Energy meter reset command should send reset commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + { capability = "energyMeter", command = "resetEnergyMeter", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + cluster_base.build_manufacturer_specific_command( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x02, -- PRIVATE_CMD_ENERGY_RESET_ID + 0x122F, -- MFG_CODE + utils.serialize_int(0, 1, false, false) + ) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(mock_inovelli_vzm30_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:read(mock_inovelli_vzm30_sn) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + + +test.register_coroutine_test( + "doConfigure runs base + VZM30 extras", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm30_sn.id, "doConfigure" }) + + -- Button capability messages from base_device_configure + for _, component in pairs(mock_inovelli_vzm30_sn.profile.components) do + if component.id ~= "main" then + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message( + component.id, + capabilities.button.supportedButtonValues( + supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message( + component.id, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + ) + end + end + + -- device:configure() sends bind requests and configure reporting (default handler) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm30_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm30_sn, 1, 3600, 1) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, device_management.build_bind_request(mock_inovelli_vzm30_sn, RelativeHumidity.ID, require("integration_test.zigbee_test_utils").mock_hub_eui) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, RelativeHumidity.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm30_sn, 30, 3600, 100) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm30_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm30_sn, 0, 300) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, device_management.build_bind_request(mock_inovelli_vzm30_sn, TemperatureMeasurement.ID, require("integration_test.zigbee_test_utils").mock_hub_eui) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm30_sn, 30, 600, 100) }) + + -- base_device_configure sends OTA ImageNotify and private cluster bind + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm30_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, device_management.build_bind_request(mock_inovelli_vzm30_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) + + -- Read divisors/multipliers + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm30_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm30_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm30_sn) }) + + -- VZM30-specific: temperature and humidity reporting configuration + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm30_sn, 30, 3600, 50) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, RelativeHumidity.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm30_sn, 30, 3600, 50) }) + + mock_inovelli_vzm30_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_child.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_child.lua new file mode 100644 index 0000000000..e40ead721a --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_child.lua @@ -0,0 +1,356 @@ +-- 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. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" + +-- Device endpoints with supported clusters +local inovelli_vzm30_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM30-SN", + server_clusters = {0x0006, 0x0008, 0x0300} -- OnOff, Level, ColorControl + } +} + +local mock_parent_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm30-sn.yml"), + zigbee_endpoints = inovelli_vzm30_sn_endpoints, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local mock_child_device = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("rgbw-bulb.yml"), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = "notification" +}) + +local function test_init() + test.mock_device.add_test_device(mock_parent_device) + test.mock_device.add_test_device(mock_child_device) +end +test.set_test_init_function(test_init) + +-- Test child device initialization +test.register_message_test( + "Child device should initialize with default color values", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_child_device.id, "added" }, + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.hue(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(6500)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switchLevel.level(100)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test child device switch on command +test.register_coroutine_test( + "Child device switch on should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "on", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device switch off command +test.register_coroutine_test( + "Child device switch off should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "off", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(0, 4, false, false) + ) + }) + end +) + +-- Test child device level command +test.register_coroutine_test( + "Child device level command should emit events and send configuration to parent", + function() + local level = math.random(1, 99) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local effect = 1 -- Default notificationType + local color = 100 -- Default color for child devices (since device starts with no hue state) + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) -- Use the actual level from command + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switchLevel", command = "setLevel", args = { level } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switchLevel.level(level)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device color command +test.register_coroutine_test( + "Child device color command should emit events and send configuration to parent", + function() + local color = math.random(0, 100) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorControl", command = "setColor", args = {{ hue = color, saturation = 100 }} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(color)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device color temperature command +test.register_coroutine_test( + "Child device color temperature command should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorTemperature", command = "setColorTemperature", args = { 3000 } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(3000)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua new file mode 100644 index 0000000000..8735454724 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua @@ -0,0 +1,210 @@ +-- 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. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" + +-- Device endpoints with supported clusters +local inovelli_vzm30_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM30-SN", + server_clusters = {0x0006, 0x0008} -- OnOff, Level + } +} + +local mock_inovelli_vzm30_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm30-sn.yml"), + zigbee_endpoints = inovelli_vzm30_sn_endpoints, + fingerprinted_endpoint_id = 0x01, + label = "Inovelli VZM30-SN" +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm30_sn) +end +test.set_test_init_function(test_init) + +-- Test parameter1 preference change +test.register_coroutine_test( + "parameter1 preference should send configuration command", + function() + local new_param_value = 50 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter1 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 1, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test parameter9 preference change +test.register_coroutine_test( + "parameter9 preference should send configuration command", + function() + local new_param_value = 10 + local expected_value = utils.round(new_param_value / 100 * 254) + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter9 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 9, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + expected_value + ) + }) + end +) + +-- Test parameter52 preference change +test.register_coroutine_test( + "parameter52 preference should send configuration command", + function() + local new_param_value = true + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter52 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 52, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter258 preference change +test.register_coroutine_test( + "parameter258 preference should send configuration command", + function() + local new_param_value = false + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter258 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 258, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter11 preference change (VZM30-only, same as VZM31) +test.register_coroutine_test( + "parameter11 preference should send configuration command", + function() + local new_param_value = true + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter11 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 11, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter17 preference change (VZM30-only, same as VZM31) +test.register_coroutine_test( + "parameter17 preference should send configuration command", + function() + local new_param_value = 5 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter17 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 17, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test parameter22 preference change (VZM30-only, same as VZM31) +test.register_coroutine_test( + "parameter22 preference should send configuration command", + function() + local new_param_value = 2 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter22 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 22, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test notificationChild preference change +test.register_coroutine_test( + "notificationChild preference should create child device when enabled", + function() + mock_inovelli_vzm30_sn:expect_device_create({ + type = "EDGE_CHILD", + label = "Inovelli VZM30-SN Notification", + profile = "rgbw-bulb-2700K-6500K", + parent_device_id = mock_inovelli_vzm30_sn.id, + parent_assigned_child_key = "notification" + }) + + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {notificationChild = true}})) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua index 65a3084022..9807380ba1 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua @@ -21,6 +21,7 @@ local cluster_base = require "st.zigbee.cluster_base" local utils = require "st.utils" local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade local device_management = require "st.zigbee.device_management" +local zigbee_constants = require "st.zigbee.constants" -- Inovelli VZM31-SN device identifiers local INOVELLI_MANUFACTURER_ID = "Inovelli" @@ -49,6 +50,12 @@ local function test_init() end test.set_test_init_function(test_init) +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} +} + -- Test device initialization test.register_message_test( "Device should initialize properly on added lifecycle event", @@ -224,64 +231,42 @@ test.register_message_test( } ) --- Test power meter from SimpleMetering -test.register_message_test( - "Power meter from SimpleMetering should emit power events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm31_sn.id, - clusters.SimpleMetering.attributes.InstantaneousDemand:build_test_attr_report(mock_inovelli_vzm31_sn, 1500) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.powerMeter.power({value = 150.0, unit = "W"})) - } - } -) - -- Test power meter from ElectricalMeasurement -test.register_message_test( +test.register_coroutine_test( "Power meter from ElectricalMeasurement should emit power events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm31_sn.id, - clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm31_sn, 2000) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) - } - } + function() + -- Set the divisor field (default handlers use 10 if not set, but we can set it for consistency) + -- The default handler will use 10 if ELECTRICAL_MEASUREMENT_DIVISOR_KEY is not set + -- Since the test expects 2000 -> 200.0 W, that means divisor of 10 is being used + mock_inovelli_vzm31_sn:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm31_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm31_sn, 2000) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) + ) + end ) -- Test energy meter -test.register_message_test( +test.register_coroutine_test( "Energy meter should emit energy events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm31_sn.id, - clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm31_sn, 50000) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 500.0, unit = "kWh"})) - } - } + function() + -- Set the divisor field as the device reads during configuration + -- For VZM31, the divisor is read from the device, but for testing we need to set it + -- The test expects 50000 -> 500.0 kWh, which means divisor of 100 + mock_inovelli_vzm31_sn:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 100, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm31_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm31_sn, 50000) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 500.0, unit = "kWh"})) + ) + end ) -- Test energy meter reset command @@ -336,21 +321,49 @@ test.register_message_test( test.register_coroutine_test( "doConfigure runs base config (VZM31)", function() + test.socket.capability:__set_channel_ordering("relaxed") test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm31_sn.id, "doConfigure" }) + + -- Button capability messages from base_device_configure + for _, component in pairs(mock_inovelli_vzm31_sn.profile.components) do + if component.id ~= "main" then + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message( + component.id, + capabilities.button.supportedButtonValues( + supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message( + component.id, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + ) + end + end + + -- device:configure() sends bind requests and configure reporting (default handler) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm31_sn, 0, 300) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm31_sn, 1, 3600, 1) }) + + -- base_device_configure sends OTA ImageNotify and private cluster bind test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm31_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, device_management.build_bind_request(mock_inovelli_vzm31_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) + + -- Read divisors/multipliers test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.SimpleMetering.attributes.Divisor:read(mock_inovelli_vzm31_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm31_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm31_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm31_sn) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm31_sn, 0, 300) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm31_sn, 1, 3600, 1) }) + mock_inovelli_vzm31_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) test.run_registered_tests() - diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua index 452b98454d..a60f6fd531 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -21,6 +21,7 @@ local cluster_base = require "st.zigbee.cluster_base" local utils = require "st.utils" local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade local device_management = require "st.zigbee.device_management" +local zigbee_constants = require "st.zigbee.constants" local OnOff = clusters.OnOff local Level = clusters.Level @@ -52,6 +53,12 @@ local function test_init() end test.set_test_init_function(test_init) +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} +} + -- Test device initialization test.register_message_test( "Device should initialize properly on added lifecycle event", @@ -328,64 +335,41 @@ test.register_message_test( } ) --- Test power meter from SimpleMetering -test.register_message_test( - "Power meter from SimpleMetering should emit power events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm32_sn.id, - clusters.SimpleMetering.attributes.InstantaneousDemand:build_test_attr_report(mock_inovelli_vzm32_sn, 1500) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.powerMeter.power({value = 150.0, unit = "W"})) - } - } -) - -- Test power meter from ElectricalMeasurement -test.register_message_test( +test.register_coroutine_test( "Power meter from ElectricalMeasurement should emit power events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm32_sn.id, - clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm32_sn, 2000) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) - } - } + function() + -- Set the divisor field (default handlers use 10 if not set, but we can set it for consistency) + -- The default handler will use 10 if ELECTRICAL_MEASUREMENT_DIVISOR_KEY is not set + -- Since the test expects 2000 -> 200.0 W, that means divisor of 10 is being used + -- For VZM32, the actual device reads ACPowerDivisor, but default is 10 + mock_inovelli_vzm32_sn:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm32_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm32_sn, 2000) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) + ) + end ) -- Test energy meter -test.register_message_test( +test.register_coroutine_test( "Energy meter should emit energy events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm32_sn.id, - clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm32_sn, 50000) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 500.0, unit = "kWh"})) - } - } + function() + -- Set the divisor field as the device does during configuration + mock_inovelli_vzm32_sn:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 1000, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm32_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm32_sn, 212) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 0.212, unit = "kWh"})) + ) + end ) -- Test energy meter reset command @@ -440,22 +424,52 @@ test.register_message_test( test.register_coroutine_test( "doConfigure runs base + VZM32 extras", function() + test.socket.capability:__set_channel_ordering("relaxed") test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm32_sn.id, "doConfigure" }) + + -- Button capability messages from base_device_configure + for _, component in pairs(mock_inovelli_vzm32_sn.profile.components) do + if component.id ~= "main" then + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message( + component.id, + capabilities.button.supportedButtonValues( + supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message( + component.id, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + ) + end + end + + -- device:configure() sends bind requests and configure reporting (default handler) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm32_sn, 0, 300) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm32_sn, 1, 3600, 1) }) + + -- base_device_configure sends OTA ImageNotify and private cluster bind test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm32_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, device_management.build_bind_request(mock_inovelli_vzm32_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.SimpleMetering.attributes.Divisor:read(mock_inovelli_vzm32_sn) }) + + -- Read divisors/multipliers test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm32_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm32_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm32_sn) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm32_sn, 0, 300) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm32_sn, 1, 3600, 1) }) + + -- VZM32-specific: occupancy and illuminance reporting configuration test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, device_management.build_bind_request(mock_inovelli_vzm32_sn, clusters.OccupancySensing.ID, require("integration_test.zigbee_test_utils").mock_hub_eui) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm32_sn, 10, 600, 11761) }) + mock_inovelli_vzm32_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file From a9219cdd6e2eb6b4e0b8acebb5c6d48af3653bb4 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Thu, 6 Nov 2025 12:27:23 -0700 Subject: [PATCH 2/8] removing some preferences. update test unit --- .../profiles/inovelli-vzm30-sn.yml | 50 ++----------------- .../zigbee-switch/src/inovelli/init.lua | 7 +-- .../test_inovelli_vzm30_sn_preferences.lua | 14 +++--- 3 files changed, 16 insertions(+), 55 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml index 2b44625e71..ff670c0097 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml @@ -16,8 +16,6 @@ components: version: 1 - id: refresh version: 1 - - id: configuration - version: 1 - id: firmwareUpdate version: 1 categories: @@ -96,9 +94,9 @@ preferences: preferenceType: enumeration definition: options: - "0": "None (default)" - "1": "3-Way Aux Switch" - default: 0 + "0": "None" + "1": "3-Way Aux Switch (default)" + default: 1 - name: "parameter52" title: "52. Smart Bulb Mode" description: "For use with Smart Bulbs that need constant power and are controlled via commands rather than power. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." @@ -132,13 +130,13 @@ preferences: - name: "parameter3" title: "3. Ramp Rate (Remote)" description: "This changes the speed that the light turns on when controlled from the hub. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. - (i.e 25 = 2500ms or 2.5s) Default=127 (Sync with parameter 1)" + (i.e 25 = 2500ms or 2.5s) Default=0" required: false preferenceType: number definition: minimum: 0 maximum: 127 - default: 127 + default: 0 - name: "parameter4" title: "4. Ramp Rate (Local)" description: "This changes the speed that the light turns on when controlled at the switch. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. @@ -149,24 +147,6 @@ preferences: minimum: 0 maximum: 127 default: 127 - - name: "parameter9" - title: "9. Minimum Level" - description: "The minimum level that the light can be dimmed. Useful when the user has a light that does not turn on or flickers at a lower level." - required: false - preferenceType: number - definition: - minimum: 1 - maximum: 99 - default: 1 - - name: "parameter10" - title: "10. Maximum Level" - description: "The maximum level that the light can be dimmed. Useful when the user wants to limit the maximum brighness." - required: false - preferenceType: number - definition: - minimum: 2 - maximum: 100 - default: 100 - name: "parameter11" title: "11. Invert Switch" description: "Inverts the orientation of the switch. Useful when the switch is installed upside down. Essentially up becomes down and down becomes up." @@ -189,26 +169,6 @@ preferences: minimum: 0 maximum: 101 default: 101 - - name: "parameter17" - title: "17. Load Level Indicator Timeout" - description: "Shows the level that the load is at for x number of seconds after the load is adjusted and then returns to the Default LED state." - required: false - preferenceType: enumeration - definition: - options: - "0": "Do not display Load Level" - "1": "1 Second" - "2": "2 Seconds" - "3": "3 Seconds" - "4": "4 Seconds" - "5": "5 Seconds" - "6": "6 Seconds" - "7": "7 Seconds" - "8": "8 Seconds" - "9": "9 Seconds" - "10": "10 Seconds" - "11": "Display Load Level with no timeout" - default: 11 - name: "parameter95" title: "95. LED Indicator Color (w/On)" description: "Set the color of the Full LED Indicator when the load is on." diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua index 8af6c2a73c..f92f2a7183 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -47,8 +47,6 @@ local base_preference_map = { parameter2 = {parameter_number = 2, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter3 = {parameter_number = 3, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter4 = {parameter_number = 4, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, - parameter9 = {parameter_number = 9, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, - parameter10 = {parameter_number = 10, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter15 = {parameter_number = 15, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter95 = {parameter_number = 95, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter96 = {parameter_number = 96, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, @@ -60,15 +58,18 @@ local base_preference_map = { local model_preference_overrides = { ["VZM30-SN"] = { parameter11 = {parameter_number = 11, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, - parameter17 = {parameter_number = 17, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter22 = {parameter_number = 22, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, }, ["VZM31-SN"] = { + parameter9 = {parameter_number = 9, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter10 = {parameter_number = 10, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter11 = {parameter_number = 11, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, parameter17 = {parameter_number = 17, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter22 = {parameter_number = 22, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, }, ["VZM32-SN"] = { + parameter9 = {parameter_number = 9, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter10 = {parameter_number = 10, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter34 = {parameter_number = 34, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter101 = {parameter_number = 101, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, parameter102 = {parameter_number = 102, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua index 8735454724..0726a2d575 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua @@ -66,18 +66,18 @@ test.register_coroutine_test( -- Test parameter9 preference change test.register_coroutine_test( - "parameter9 preference should send configuration command", + "parameter15 preference should send configuration command", function() local new_param_value = 10 local expected_value = utils.round(new_param_value / 100 * 254) - test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter9 = new_param_value}})) + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter15 = new_param_value}})) test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, cluster_base.write_manufacturer_specific_attribute( mock_inovelli_vzm30_sn, 0xFC31, -- PRIVATE_CLUSTER_ID - 9, -- parameter_number + 15, -- parameter_number 0x122F, -- MFG_CODE data_types.Uint8, expected_value @@ -151,17 +151,17 @@ test.register_coroutine_test( -- Test parameter17 preference change (VZM30-only, same as VZM31) test.register_coroutine_test( - "parameter17 preference should send configuration command", + "parameter95 preference should send configuration command", function() - local new_param_value = 5 - test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter17 = new_param_value}})) + local new_param_value = 64 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter95 = new_param_value}})) test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, cluster_base.write_manufacturer_specific_attribute( mock_inovelli_vzm30_sn, 0xFC31, -- PRIVATE_CLUSTER_ID - 17, -- parameter_number + 95, -- parameter_number 0x122F, -- MFG_CODE data_types.Uint8, new_param_value From baa4450796ebf93ed7b78e1040642a3d6553b0f4 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Thu, 6 Nov 2025 13:24:39 -0700 Subject: [PATCH 3/8] fix linter errors for test files --- .../src/test/test_inovelli_vzm30_sn.lua | 14 +++++++------- .../src/test/test_inovelli_vzm31_sn.lua | 12 ++++++------ .../src/test/test_inovelli_vzm32_sn.lua | 14 +++++++------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua index d39f40e67e..d1453b2517 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua @@ -356,7 +356,7 @@ test.register_coroutine_test( -- Set the divisor field as the device does during configuration -- For VZM30-SN, the divisor is set to 1000 (like VZM32-SN) mock_inovelli_vzm30_sn:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 1000, {persist = true}) - + test.socket.zigbee:__queue_receive({ mock_inovelli_vzm30_sn.id, clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm30_sn, 212) @@ -422,7 +422,7 @@ test.register_coroutine_test( test.socket.capability:__set_channel_ordering("relaxed") test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm30_sn.id, "doConfigure" }) - + -- Button capability messages from base_device_configure for _, component in pairs(mock_inovelli_vzm30_sn.profile.components) do if component.id ~= "main" then @@ -443,7 +443,7 @@ test.register_coroutine_test( ) end end - + -- device:configure() sends bind requests and configure reporting (default handler) test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm30_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm30_sn, 1, 3600, 1) }) @@ -453,20 +453,20 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm30_sn, 0, 300) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, device_management.build_bind_request(mock_inovelli_vzm30_sn, TemperatureMeasurement.ID, require("integration_test.zigbee_test_utils").mock_hub_eui) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm30_sn, 30, 600, 100) }) - + -- base_device_configure sends OTA ImageNotify and private cluster bind test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm30_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, device_management.build_bind_request(mock_inovelli_vzm30_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) - + -- Read divisors/multipliers test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm30_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm30_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm30_sn) }) - + -- VZM30-specific: temperature and humidity reporting configuration test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm30_sn, 30, 3600, 50) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, RelativeHumidity.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm30_sn, 30, 3600, 50) }) - + mock_inovelli_vzm30_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua index f03a62507b..1558c35253 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua @@ -247,7 +247,7 @@ test.register_coroutine_test( -- For VZM31, the divisor is read from the device, but for testing we need to set it -- The test expects 50000 -> 500.0 kWh, which means divisor of 100 mock_inovelli_vzm31_sn:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 100, {persist = true}) - + test.socket.zigbee:__queue_receive({ mock_inovelli_vzm31_sn.id, clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm31_sn, 50000) @@ -313,7 +313,7 @@ test.register_coroutine_test( test.socket.capability:__set_channel_ordering("relaxed") test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm31_sn.id, "doConfigure" }) - + -- Button capability messages from base_device_configure for _, component in pairs(mock_inovelli_vzm31_sn.profile.components) do if component.id ~= "main" then @@ -334,23 +334,23 @@ test.register_coroutine_test( ) end end - + -- device:configure() sends bind requests and configure reporting (default handler) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm31_sn, 0, 300) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm31_sn, 1, 3600, 1) }) - + -- base_device_configure sends OTA ImageNotify and private cluster bind test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm31_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, device_management.build_bind_request(mock_inovelli_vzm31_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) - + -- Read divisors/multipliers test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.SimpleMetering.attributes.Divisor:read(mock_inovelli_vzm31_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm31_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm31_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm31_sn) }) - + mock_inovelli_vzm31_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua index 0e6380675d..4bd06413d7 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -350,7 +350,7 @@ test.register_coroutine_test( function() -- Set the divisor field as the device does during configuration mock_inovelli_vzm32_sn:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 1000, {persist = true}) - + test.socket.zigbee:__queue_receive({ mock_inovelli_vzm32_sn.id, clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm32_sn, 212) @@ -416,7 +416,7 @@ test.register_coroutine_test( test.socket.capability:__set_channel_ordering("relaxed") test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm32_sn.id, "doConfigure" }) - + -- Button capability messages from base_device_configure for _, component in pairs(mock_inovelli_vzm32_sn.profile.components) do if component.id ~= "main" then @@ -437,26 +437,26 @@ test.register_coroutine_test( ) end end - + -- device:configure() sends bind requests and configure reporting (default handler) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm32_sn, 0, 300) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm32_sn, 1, 3600, 1) }) - + -- base_device_configure sends OTA ImageNotify and private cluster bind test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm32_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, device_management.build_bind_request(mock_inovelli_vzm32_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) - + -- Read divisors/multipliers test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm32_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm32_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm32_sn) }) - + -- VZM32-specific: occupancy and illuminance reporting configuration test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, device_management.build_bind_request(mock_inovelli_vzm32_sn, clusters.OccupancySensing.ID, require("integration_test.zigbee_test_utils").mock_hub_eui) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm32_sn, 10, 600, 11761) }) - + mock_inovelli_vzm32_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) From 586e24c3f7138b91d931ba85f2a27586d15f4539 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Thu, 6 Nov 2025 15:07:06 -0700 Subject: [PATCH 4/8] better naming for drivers --- drivers/SmartThings/zigbee-switch/src/inovelli/init.lua | 2 +- .../SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua | 2 +- .../SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua index 47c8b45e91..c118c8f6cb 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -322,7 +322,7 @@ local function handle_resetEnergyMeter(self, device) end local inovelli = { - NAME = "inovelli combined handler", + NAME = "Inovelli Zigbee Switch", lifecycle_handlers = { doConfigure = device_configure, infoChanged = info_changed, diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua index 9476dca4e1..6ad3ec758d 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua @@ -43,7 +43,7 @@ local function device_configure(driver, device) end local vzm30_sn = { - NAME = "inovelli vzm30-sn device-specific", + NAME = "Inovelli VZM30-SN Zigbee Switch", can_handle = require("inovelli.vzm30-sn.can_handle"), lifecycle_handlers = { doConfigure = device_configure, diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua index edadf85b90..4e0151aeb6 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua @@ -52,7 +52,7 @@ local function device_configure(driver, device) end local vzm32_sn = { - NAME = "inovelli vzm32-sn device-specific", + NAME = "Inovelli VZM32-SN mmWave Dimmer", can_handle = require("inovelli.vzm32-sn.can_handle"), lifecycle_handlers = { added = device_added, From a46ec6bb9f801b660dda25914d7ea9283567c3fa Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Thu, 20 Nov 2025 18:25:58 -0700 Subject: [PATCH 5/8] updating supportedValues for devices that may have been included with the old driver --- .../zigbee-switch/src/inovelli/common.lua | 4 +- .../zigbee-switch/src/inovelli/init.lua | 50 +++++++++++++- .../src/test/test_inovelli_vzm30_sn.lua | 67 ++++++++++++++++++- .../src/test/test_inovelli_vzm31_sn.lua | 67 ++++++++++++++++++- .../src/test/test_inovelli_vzm32_sn.lua | 67 ++++++++++++++++++- 5 files changed, 245 insertions(+), 10 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua index fadf47c031..c402731328 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua @@ -9,7 +9,7 @@ local capabilities = require "st.capabilities" local M = {} -local supported_button_values = { +M.supported_button_values = { ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} @@ -58,7 +58,7 @@ function M.base_device_configure(driver, device, private_cluster_id, mfg_code) device:emit_component_event( component, capabilities.button.supportedButtonValues( - supported_button_values[component.id], + M.supported_button_values[component.id], { visibility = { displayed = false } } ) ) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua index c118c8f6cb..89b1ab046a 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -132,9 +132,55 @@ local function scene_handler(driver, device, zb_rx) local capability_attribute = map_key_attribute_to_capability[bytes:byte(2)] local additional_fields = { state_change = true } local event = capability_attribute and capability_attribute(additional_fields) or nil - local comp = device.profile.components[button_to_component(button_number)] + local comp_name = button_to_component(button_number) + local comp = device.profile.components[comp_name] if comp ~= nil and event ~= nil then - device:emit_component_event(comp, event) + -- Check if the event is in the supportedButtonValues before emitting + -- This ensures backward compatibility with devices installed with previous driver versions + local expected_values = inovelli_common.supported_button_values[comp_name] + local supportedEvents = device:get_latest_state( + comp_name, + capabilities.button.ID, + capabilities.button.supportedButtonValues.NAME, + {capabilities.button.button.pushed.NAME} -- default fallback for older devices + ) + + -- Check if supportedButtonValues needs to be updated + -- This handles devices installed with previous driver versions that don't have + -- the updated supportedButtonValues attribute. If the current value only contains + -- "pushed" (the fallback), update it to the full list. + local needs_update = false + if expected_values then + -- Check if current supportedEvents is exactly the fallback (only "pushed") + -- This indicates the state was never set and we're using the fallback value + if #supportedEvents == 1 and supportedEvents[1] == capabilities.button.button.pushed.NAME then + needs_update = true + end + + if needs_update then + device:emit_component_event( + comp, + capabilities.button.supportedButtonValues( + expected_values, + { visibility = { displayed = false } } + ) + ) + supportedEvents = expected_values -- Update local reference for event check + end + end + + -- Check if the event is supported + local event_supported = false + for _, event_name in pairs(supportedEvents) do + if event.value.value == event_name then + event_supported = true + break + end + end + + if event_supported then + device:emit_component_event(comp, event) + end end end diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua index d1453b2517..83807d070b 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua @@ -258,38 +258,101 @@ end -- Test button1 pushed test.register_message_test( - "Button1 pushed should emit button event", + "Button1 pushed should emit button event and update supportedButtonValues", { { channel = "zigbee", direction = "receive", message = { mock_inovelli_vzm30_sn.id, build_inovelli_button_message(mock_inovelli_vzm30_sn, 0x01, 0x00) } }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + }, { channel = "capability", direction = "send", message = mock_inovelli_vzm30_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) } + }, + { + inner_block_ordering = "relaxed" } ) -- Test button2 pressed 4 times test.register_message_test( - "Button2 pressed 4 times should emit button event", + "Button2 pressed 4 times should emit button event and update supportedButtonValues", { { channel = "zigbee", direction = "receive", message = { mock_inovelli_vzm30_sn.id, build_inovelli_button_message(mock_inovelli_vzm30_sn, 0x02, 0x05) } }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message( + "button2", + capabilities.button.supportedButtonValues( + supported_button_values["button2"], + { visibility = { displayed = false } } + ) + ) + }, { channel = "capability", direction = "send", message = mock_inovelli_vzm30_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) } + }, + { + inner_block_ordering = "relaxed" } ) +-- Test consecutive button events - supportedButtonValues should only be sent on first event +test.register_coroutine_test( + "Consecutive button events should only send supportedButtonValues on first event", + function() + test.socket.capability:__set_channel_ordering("relaxed") + + -- First button event: button1 pushed - should send supportedButtonValues + button event + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm30_sn.id, + build_inovelli_button_message(mock_inovelli_vzm30_sn, 0x01, 0x00) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + ) + + -- Second button event: button1 pushed_2x - should only send button event, NOT supportedButtonValues + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm30_sn.id, + build_inovelli_button_message(mock_inovelli_vzm30_sn, 0x01, 0x03) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message("button1", capabilities.button.button.pushed_2x({ state_change = true })) + ) + end +) + -- Test temperature measurement test.register_message_test( "Temperature measurement should emit temperature events", diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua index 1558c35253..cb0b2e11ed 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua @@ -188,38 +188,101 @@ end -- Test button1 pushed test.register_message_test( - "Button1 pushed should emit button event", + "Button1 pushed should emit button event and update supportedButtonValues", { { channel = "zigbee", direction = "receive", message = { mock_inovelli_vzm31_sn.id, build_inovelli_button_message(mock_inovelli_vzm31_sn, 0x01, 0x00) } }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm31_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + }, { channel = "capability", direction = "send", message = mock_inovelli_vzm31_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) } + }, + { + inner_block_ordering = "relaxed" } ) -- Test button2 pressed 4 times test.register_message_test( - "Button2 pressed 4 times should emit button event", + "Button2 pressed 4 times should emit button event and update supportedButtonValues", { { channel = "zigbee", direction = "receive", message = { mock_inovelli_vzm31_sn.id, build_inovelli_button_message(mock_inovelli_vzm31_sn, 0x02, 0x05) } }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm31_sn:generate_test_message( + "button2", + capabilities.button.supportedButtonValues( + supported_button_values["button2"], + { visibility = { displayed = false } } + ) + ) + }, { channel = "capability", direction = "send", message = mock_inovelli_vzm31_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) } + }, + { + inner_block_ordering = "relaxed" } ) +-- Test consecutive button events - supportedButtonValues should only be sent on first event +test.register_coroutine_test( + "Consecutive button events should only send supportedButtonValues on first event", + function() + test.socket.capability:__set_channel_ordering("relaxed") + + -- First button event: button1 pushed - should send supportedButtonValues + button event + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm31_sn.id, + build_inovelli_button_message(mock_inovelli_vzm31_sn, 0x01, 0x00) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + ) + + -- Second button event: button1 pushed_2x - should only send button event, NOT supportedButtonValues + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm31_sn.id, + build_inovelli_button_message(mock_inovelli_vzm31_sn, 0x01, 0x03) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message("button1", capabilities.button.button.pushed_2x({ state_change = true })) + ) + end +) + -- Test power meter from ElectricalMeasurement test.register_coroutine_test( "Power meter from ElectricalMeasurement should emit power events", diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua index 4bd06413d7..2f994feade 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -232,38 +232,101 @@ end -- Test button1 pushed test.register_message_test( - "Button1 pushed should emit button event", + "Button1 pushed should emit button event and update supportedButtonValues", { { channel = "zigbee", direction = "receive", message = { mock_inovelli_vzm32_sn.id, build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x01, 0x00) } }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + }, { channel = "capability", direction = "send", message = mock_inovelli_vzm32_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) } + }, + { + inner_block_ordering = "relaxed" } ) -- Test button2 pressed 4 times test.register_message_test( - "Button2 pressed 4 times should emit button event", + "Button2 pressed 4 times should emit button event and update supportedButtonValues", { { channel = "zigbee", direction = "receive", message = { mock_inovelli_vzm32_sn.id, build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x02, 0x05) } }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message( + "button2", + capabilities.button.supportedButtonValues( + supported_button_values["button2"], + { visibility = { displayed = false } } + ) + ) + }, { channel = "capability", direction = "send", message = mock_inovelli_vzm32_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) } + }, + { + inner_block_ordering = "relaxed" } ) +-- Test consecutive button events - supportedButtonValues should only be sent on first event +test.register_coroutine_test( + "Consecutive button events should only send supportedButtonValues on first event", + function() + test.socket.capability:__set_channel_ordering("relaxed") + + -- First button event: button1 pushed - should send supportedButtonValues + button event + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm32_sn.id, + build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x01, 0x00) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + ) + + -- Second button event: button1 pushed_2x - should only send button event, NOT supportedButtonValues + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm32_sn.id, + build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x01, 0x03) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message("button1", capabilities.button.button.pushed_2x({ state_change = true })) + ) + end +) + -- Test illuminance measurement test.register_message_test( "Illuminance measurement should emit illuminance events", From 0a945e0f2b457fe01d38a813ddc790b8eb61f75a Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Thu, 20 Nov 2025 18:33:55 -0700 Subject: [PATCH 6/8] fixing linter errors --- .../zigbee-switch/src/test/test_inovelli_vzm30_sn.lua | 4 ++-- .../zigbee-switch/src/test/test_inovelli_vzm31_sn.lua | 4 ++-- .../zigbee-switch/src/test/test_inovelli_vzm32_sn.lua | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua index 83807d070b..79e7a66b54 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua @@ -323,7 +323,7 @@ test.register_coroutine_test( "Consecutive button events should only send supportedButtonValues on first event", function() test.socket.capability:__set_channel_ordering("relaxed") - + -- First button event: button1 pushed - should send supportedButtonValues + button event test.socket.zigbee:__queue_receive({ mock_inovelli_vzm30_sn.id, @@ -341,7 +341,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_inovelli_vzm30_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) ) - + -- Second button event: button1 pushed_2x - should only send button event, NOT supportedButtonValues test.socket.zigbee:__queue_receive({ mock_inovelli_vzm30_sn.id, diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua index cb0b2e11ed..a3d57415b1 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua @@ -253,7 +253,7 @@ test.register_coroutine_test( "Consecutive button events should only send supportedButtonValues on first event", function() test.socket.capability:__set_channel_ordering("relaxed") - + -- First button event: button1 pushed - should send supportedButtonValues + button event test.socket.zigbee:__queue_receive({ mock_inovelli_vzm31_sn.id, @@ -271,7 +271,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_inovelli_vzm31_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) ) - + -- Second button event: button1 pushed_2x - should only send button event, NOT supportedButtonValues test.socket.zigbee:__queue_receive({ mock_inovelli_vzm31_sn.id, diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua index 2f994feade..0a288f27d4 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -297,7 +297,7 @@ test.register_coroutine_test( "Consecutive button events should only send supportedButtonValues on first event", function() test.socket.capability:__set_channel_ordering("relaxed") - + -- First button event: button1 pushed - should send supportedButtonValues + button event test.socket.zigbee:__queue_receive({ mock_inovelli_vzm32_sn.id, @@ -315,7 +315,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_inovelli_vzm32_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) ) - + -- Second button event: button1 pushed_2x - should only send button event, NOT supportedButtonValues test.socket.zigbee:__queue_receive({ mock_inovelli_vzm32_sn.id, From ad68c29df04dbb91454eeeeb9719f8dd95957520 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Thu, 20 Nov 2025 18:37:15 -0700 Subject: [PATCH 7/8] fixing linter errors --- drivers/SmartThings/zigbee-switch/src/inovelli/init.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua index 89b1ab046a..fcabd44df8 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -144,7 +144,7 @@ local function scene_handler(driver, device, zb_rx) capabilities.button.supportedButtonValues.NAME, {capabilities.button.button.pushed.NAME} -- default fallback for older devices ) - + -- Check if supportedButtonValues needs to be updated -- This handles devices installed with previous driver versions that don't have -- the updated supportedButtonValues attribute. If the current value only contains @@ -156,7 +156,7 @@ local function scene_handler(driver, device, zb_rx) if #supportedEvents == 1 and supportedEvents[1] == capabilities.button.button.pushed.NAME then needs_update = true end - + if needs_update then device:emit_component_event( comp, @@ -168,7 +168,7 @@ local function scene_handler(driver, device, zb_rx) supportedEvents = expected_values -- Update local reference for event check end end - + -- Check if the event is supported local event_supported = false for _, event_name in pairs(supportedEvents) do @@ -177,7 +177,7 @@ local function scene_handler(driver, device, zb_rx) break end end - + if event_supported then device:emit_component_event(comp, event) end From 089afa7569931676962c471cb4ef1ee11b9b9c15 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Thu, 20 Nov 2025 19:19:04 -0700 Subject: [PATCH 8/8] fixing linter errors --- drivers/SmartThings/zigbee-switch/src/inovelli/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua index fcabd44df8..a83a343663 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -156,7 +156,7 @@ local function scene_handler(driver, device, zb_rx) if #supportedEvents == 1 and supportedEvents[1] == capabilities.button.button.pushed.NAME then needs_update = true end - + if needs_update then device:emit_component_event( comp,