diff --git a/.gitignore b/.gitignore index ea01709e90..72da442d81 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ lua_libs-api_* *.out tools/test_output/* tools/coverage_output/* +tools/coverage_output_html/ +tools/__pycache__/ .DS_Store +.venv/ diff --git a/Jenkinsfile b/Jenkinsfile index 92d915bac5..425da986db 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -28,13 +28,15 @@ pipeline { agent { docker { image 'python:3.10' - label 'production' + label "${params.NODE_LABEL ?: 'production'}" args '--entrypoint= -u 0:0' } } environment { BRANCH = getEnvName() CHANGED_DRIVERS = getChangedDrivers() + ENVIRONMENT = "${env.NODE_LABEL.toUpperCase()}" + FAILURE_FILE = "failures.log" } stages { stage('requirements') { @@ -51,17 +53,15 @@ pipeline { } } stage('update') { - matrix { - axes { - axis { - name 'ENVIRONMENT' - values 'DEV', 'STAGING', 'ACCEPTANCE', 'PRODUCTION' - } - } - stages { - stage('environment_update') { - steps { - sh 'python3 tools/deploy.py' + stages { + stage('environment_update') { + steps { + sh 'python3 tools/deploy.py' + script { + if (fileExists(env.FAILURE_FILE)) { + currentBuild.description += readFile(env.FAILURE_FILE) + currentBuild.result = 'UNSTABLE' + } } } } @@ -69,4 +69,3 @@ pipeline { } } } - diff --git a/README.md b/README.md index 3eb6fb07cc..c87d2c66c2 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ By submitting a pull request, you represent that you have the right to license your contribution to SmartThings and agree by submitting your patch that your contributions are licensed under the [Apache 2.0 license](LICENSE). Before submitting your pull request, please make sure you have tested your changes and that -they follow the project guidelines for [contributing code](https://developer.smartthings.com/docs/devices/hub-connected/certify-your-device#code-formatting-and-submission-criteria). +they follow the project guidelines for [contributing code](https://developer.smartthings.com/docs/devices/hub-connected/code-formatting-criteria). Before contributions can be merged, all contributors must agree to the [SmartThings Individual Contributor License diff --git a/drivers/ABB/insite-scu200/config.yml b/drivers/ABB/insite-scu200/config.yml new file mode 100644 index 0000000000..2cda5384d4 --- /dev/null +++ b/drivers/ABB/insite-scu200/config.yml @@ -0,0 +1,7 @@ +name: 'SCU200 InSite Energy Management System' +packageKey: 'ABB.SCU200' +description: "SmartThings driver for SCU200 InSite Energy Management System" +vendorSupportInformation: "https://support.smartthings.com" +permissions: + lan: {} + discovery: {} \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/profiles/auxiliary-contact.yml b/drivers/ABB/insite-scu200/profiles/auxiliary-contact.yml new file mode 100644 index 0000000000..92c4a2fcb6 --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/auxiliary-contact.yml @@ -0,0 +1,34 @@ +name: abb.scu200.auxiliary-contact.v1 +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: refresh + version: 1 + categories: + - name: Switch +deviceConfig: + dashboard: + states: + - component: main + capability: switch + version: 1 + actions: [] + detailView: + - component: main + capability: switch + version: 1 + visibleCondition: + capability: switch + version: 1 + component: main + value: switch.value + operator: ONE_OF + operand: '[""]' + automation: + conditions: + - component: main + capability: switch + version: 1 + actions: [] \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/profiles/bridge.yml b/drivers/ABB/insite-scu200/profiles/bridge.yml new file mode 100644 index 0000000000..d45ebc4943 --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/bridge.yml @@ -0,0 +1,8 @@ +name: abb.scu200.bridge.v1 +components: +- id: main + capabilities: + - id: refresh + version: 1 + categories: + - name: Bridges diff --git a/drivers/ABB/insite-scu200/profiles/current-sensor-consumption.yml b/drivers/ABB/insite-scu200/profiles/current-sensor-consumption.yml new file mode 100644 index 0000000000..5843dae90f --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/current-sensor-consumption.yml @@ -0,0 +1,16 @@ +name: abb.scu200.current-sensor-consumption.v1 +components: +- id: main + capabilities: + - id: currentMeasurement + version: 1 + - id: powerMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/profiles/current-sensor-production.yml b/drivers/ABB/insite-scu200/profiles/current-sensor-production.yml new file mode 100644 index 0000000000..ec4e12aeae --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/current-sensor-production.yml @@ -0,0 +1,16 @@ +name: abb.scu200.current-sensor-production.v1 +components: +- id: main + capabilities: + - id: currentMeasurement + version: 1 + - id: powerMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/profiles/energy-meter.yml b/drivers/ABB/insite-scu200/profiles/energy-meter.yml new file mode 100644 index 0000000000..37fab87dbe --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/energy-meter.yml @@ -0,0 +1,44 @@ +name: abb.scu200.energy-meter.v1 +components: +- id: main + capabilities: + - id: voltageMeasurement + version: 1 + - id: currentMeasurement + version: 1 + - id: powerMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter +- id: consumptionMeter + label: "From Grid" + capabilities: + - id: powerConsumptionReport + version: 1 + - id: energyMeter + version: 1 + categories: + - name: CurbPowerMeter +- id: productionMeter + label: "To Grid" + capabilities: + - id: powerConsumptionReport + version: 1 + - id: energyMeter + version: 1 + categories: + - name: CurbPowerMeter +- id: surplus + capabilities: + - id: powerConsumptionReport + version: 1 + - id: energyMeter + version: 1 + categories: + - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/profiles/gas-meter.yml b/drivers/ABB/insite-scu200/profiles/gas-meter.yml new file mode 100644 index 0000000000..cc3297eb05 --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/gas-meter.yml @@ -0,0 +1,10 @@ +name: abb.scu200.gas-meter.v1 +components: +- id: main + capabilities: + - id: gasMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: Others \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/profiles/output-module.yml b/drivers/ABB/insite-scu200/profiles/output-module.yml new file mode 100644 index 0000000000..d6e0395da8 --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/output-module.yml @@ -0,0 +1,10 @@ +name: abb.scu200.output-module.v1 +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: refresh + version: 1 + categories: + - name: Switch diff --git a/drivers/ABB/insite-scu200/profiles/usb-energy-meter.yml b/drivers/ABB/insite-scu200/profiles/usb-energy-meter.yml new file mode 100644 index 0000000000..8eb09ce365 --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/usb-energy-meter.yml @@ -0,0 +1,14 @@ +name: abb.scu200.usb-energy-meter.v1 +components: +- id: main + capabilities: + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/profiles/water-meter.yml b/drivers/ABB/insite-scu200/profiles/water-meter.yml new file mode 100644 index 0000000000..bf5f50068d --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/water-meter.yml @@ -0,0 +1,10 @@ +name: abb.scu200.water-meter.v1 +components: +- id: main + capabilities: + - id: waterMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: Others \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/search-parameters.yaml b/drivers/ABB/insite-scu200/search-parameters.yaml new file mode 100644 index 0000000000..5cfaf45c83 --- /dev/null +++ b/drivers/ABB/insite-scu200/search-parameters.yaml @@ -0,0 +1,2 @@ +ssdp: + - searchTerm: urn:ABB:device:SCU200:1 \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/abb/api.lua b/drivers/ABB/insite-scu200/src/abb/api.lua new file mode 100644 index 0000000000..6524d86567 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/abb/api.lua @@ -0,0 +1,145 @@ +local log = require("log") +local st_utils = require "st.utils" +local json = require "st.json" + +-- Local imports +local config = require("config") +local utils = require("utils") +local RestClient = require "lunchbox.rest" + +-- API for the ABB SCU200 Bridge +local api = {} +api.__index = api + +local SSL_CONFIG = { + mode = "client", + protocol = "any", + verify = "none", + options = "all" +} + +local ADDITIONAL_HEADERS = { + ["Accept"] = "application/json", + ["Content-Type"] = "application/json", +} + +-- Method for getting the base URL +local function get_base_url(bridge_ip) + return "https://" .. bridge_ip .. ":" .. config.REST_API_PORT +end + +-- Method for processing the REST response +local function process_rest_response(response, err, partial) + if err ~= nil then + return response, err, nil + elseif response ~= nil then + local status, decoded_json = pcall(json.decode, response:get_body()) + + if status and response.status == 200 then + log.debug("process_rest_response(): Response = " .. response.status .. " " .. response:get_body()) + + return decoded_json, nil, response.status + elseif status then + log.error("process_rest_response(): Response error = " .. response.status) + + return nil, "response status is not 200 OK", response.status + else + log.error("process_rest_response(): Failed to decode data") + + return nil, "failed to decode data", nil + end + else + return nil, "no response or error received", nil + end +end + +-- Method for creating a retry function +local function retry_fn(retry_attempts) + local count = 0 + + return function() + count = count + 1 + return count < retry_attempts + end +end + +-- Method for performing a GET request +local function do_get(api_instance, path) + log.debug("do_get(): Sending GET request to " .. path) + + return process_rest_response(api_instance.client:get(path, api_instance.headers, retry_fn(5))) +end + +-- Method for performing a POST request +local function do_post(api_instance, path, payload) + log.debug("do_post(): Sending POST request to " .. path .. " with payload " .. json.encode(payload)) + + return process_rest_response(api_instance.client:post(path, payload, api_instance.headers, retry_fn(5))) +end + +-- Method for creating a labeled socket builder +function api.labeled_socket_builder(label) + local socket_builder = utils.labeled_socket_builder(label, SSL_CONFIG) + + return socket_builder +end + +-- Method for creating a new bridge manager +function api.new_bridge_manager(bridge_ip, bridge_dni) + local base_url = get_base_url(bridge_ip) + local socket_builder = api.labeled_socket_builder(bridge_dni) + + return setmetatable( + { + headers = st_utils.deep_copy(ADDITIONAL_HEADERS), + client = RestClient.new(base_url, socket_builder), + base_url = base_url + }, + api + ) +end + +-- Method for getting the thing infos +function api.get_thing_infos(bridge_ip, bridge_dni) + local socket_builder = api.labeled_socket_builder(bridge_dni .. " (thing infos)") + local response, error, status = process_rest_response(RestClient.one_shot_get(get_base_url(bridge_ip) .. "/devices", ADDITIONAL_HEADERS, socket_builder)) + + if not error and status == 200 then + return response + else + log.error("api.get_thing_infos(): Failed to get thing infos, error = " .. error) + return nil + end +end + +-- Method for getting the bridge info +function api.get_bridge_info(bridge_ip, bridge_dni) + local socket_builder = api.labeled_socket_builder(bridge_dni .. " (bridge info)") + local response, error, status = process_rest_response(RestClient.one_shot_get(get_base_url(bridge_ip) .. "/bridge", ADDITIONAL_HEADERS, socket_builder)) + + if not error and status == 200 then + return response + else + log.error("api.get_bridge_info(): Failed to get thing infos, error = " .. error) + return nil + end +end + +-- API methods +function api:get_devices() + return do_get(self, "/devices") +end + +function api:get_device_by_id(id) + return do_get(self, string.format("/devices/%s", id)) +end + +function api:post_device_by_id(id, payload) + return do_post(self, string.format("/devices/%s/control", id), payload) +end + +function api:get_sse_url() + return self.base_url .. "/events" +end + +return api \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/abb/device_manager.lua b/drivers/ABB/insite-scu200/src/abb/device_manager.lua new file mode 100644 index 0000000000..a9de6c0811 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/abb/device_manager.lua @@ -0,0 +1,115 @@ +local log = require("log") + +-- Local imports +local utils = require("utils") +local fields = require("fields") +local api = require("abb.api") +local device_refresher = require("abb.device_refresher") + +-- Device manager methods +local device_manager = {} + +-- Method for checking if connection is valid +function device_manager.is_valid_connection(driver, device, conn_info) + local dni = utils.get_dni_from_device(device) + + if not conn_info then + log.error("device_manager.is_valid_connection(): Failed to find conn_info, dni = " .. dni) + return false + end + + local bridge_ip = utils.get_device_ip_address(device) + local thing_infos = api.get_thing_infos(bridge_ip, dni) + + if thing_infos and thing_infos.devices then + return true + else + log.error("device_manager.is_valid_connection(): Failed to get thing infos, dni = " .. dni) + return false + end +end + +-- Method for getting bridge connection info +function device_manager.get_bridge_connection_info(driver, bridge_dni, bridge_ip) + local bridge_conn_info = api.new_bridge_manager(bridge_ip, bridge_dni) + + if bridge_conn_info == nil then + log.error("device_manager.get_bridge_connection_info(): No bridge connection info") + end + + return bridge_conn_info +end + +-- Method for handling JSON status +function device_manager.handle_device_json(driver, device, device_json) + local dni = utils.get_dni_from_device(device) + if dni == nil then + log.error("device_manager.handle_device_json(): dni is nil, the device has been probably deleted") + return + end + + if not device_json then + log.error("device_manager.handle_device_json(): device_json is nil, dni = " .. dni) + return + end + + log.debug("device_manager.handle_device_json(): dni: " .. dni .. " device_json = " .. utils.dump(device_json)) + + local status = device_json.status + if status ~= nil then + if status == "offline" then + log.info("device_manager.handle_device_json(): status is offline, dni = " .. dni) + + device:offline() + return + elseif status == "online" then + device:online() + end + end + + local values = device_json.values + if values == nil then + log.error("device_manager.handle_device_json(): values is nil, dni = " .. dni) + return + end + + device_refresher.refresh_device(driver, device, values) +end + +-- Method for refreshing device +function device_manager.refresh(driver, device) + local dni = utils.get_dni_from_device(device) + local communication_device = device:get_parent_device() or device + local conn_info = communication_device:get_field(fields.CONN_INFO) + + if not conn_info then + log.warn("device_manager.refresh(): Failed to find conn_info, dni = " .. dni) + return + end + + local response, err, status = conn_info:get_device_by_id(dni) + + if err or status ~= 200 then + status = status or "nil" + log.error("device_manager.refresh(): Failed to get device by id, dni = " .. dni .. ", err = " .. err .. ", status = " .. status) + + device:offline() + + return + end + + device_manager.handle_device_json(driver, device, response) +end + +-- Method for monitoring the connection of the bridge devices +function device_manager.bridge_monitor(driver, device, bridge_info) + local child_devices = device:get_child_list() + + for _, thing_device in ipairs(child_devices) do + device.thread:call_with_delay(0, function() -- Run within bridge thread to use the same connection + device_manager.refresh(driver, thing_device) + end) + end +end + +return device_manager \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/abb/device_refresher.lua b/drivers/ABB/insite-scu200/src/abb/device_refresher.lua new file mode 100644 index 0000000000..628c5b4201 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/abb/device_refresher.lua @@ -0,0 +1,382 @@ +local log = require("log") +local caps = require('st.capabilities') + +-- Local imports +local utils = require("utils") +local config = require("config") +local fields = require('fields') + +-- Controller for refreshing device data +local device_refresher = {} + +local function refresh_current_sensor(driver, device, values) + local dni = utils.get_dni_from_device(device) + log.info("refresh_current_sensor(): Refreshing data of Current Sensor, dni = " .. dni) + + -- Refresh Current Measurement + local current = values.current + + if current ~= nil then + log.trace("refresh_current_sensor(): Refreshing Current Measurement, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.currentMeasurement.current({value=current, unit="A"})) + end + + -- Refresh Active Power + local activePower = values.activePower + + if activePower ~= nil then + log.trace("refresh_current_sensor(): Refreshing Active Power, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.powerMeter.power({value=activePower, unit="W"})) + end + + -- Refresh Active Energy + local activeEnergy = values.activeEnergy + + if activeEnergy ~= nil then + -- Refresh Active Energy + log.trace("refresh_current_sensor(): Refreshing Active Energy, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.energyMeter.energy({value=activeEnergy, unit="kWh"})) + + -- Verify whether the appropriate time have elapsed to report the energy values + local last_energy_report = device:get_field(fields.LAST_ENERGY_REPORT) or 0.0 + + if (os.time() - last_energy_report) >= config.EDGE_CHILD_ENERGY_REPORT_INTERVAL then -- Report the energy consumption/production periodically + local current_consumption_production_report = device:get_latest_state("main", caps.powerConsumptionReport.ID, caps.powerConsumptionReport.powerConsumption.NAME) + + -- Calculate delta consumption/production energy + local delta_consumption_production_report = 0.0 + if current_consumption_production_report ~= nil then + delta_consumption_production_report = math.max((activeEnergy * 1000) - current_consumption_production_report.energy, 0.0) + end + + -- Refresh Power Energy Report + log.trace("refresh_current_sensor(): Refreshing Energy Report, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.powerConsumptionReport.powerConsumption({energy=activeEnergy * 1000, deltaEnergy=delta_consumption_production_report})) + + -- Save date of the last energy report + local current_energy_report = last_energy_report + config.EDGE_CHILD_ENERGY_REPORT_INTERVAL + if (current_energy_report + config.EDGE_CHILD_ENERGY_REPORT_INTERVAL) < os.time() then + current_energy_report = os.time() + end + + device:set_field(fields.LAST_ENERGY_REPORT, current_energy_report, {persist=false}) + else + log.debug("refresh_current_sensor(): " .. config.EDGE_CHILD_ENERGY_REPORT_INTERVAL .. " seconds haven't elapsed yet! Last consumption was at " .. last_energy_report .. ", dni = " .. dni) + end + end + + return true +end + +local function refresh_energy_meter(driver, device, values) + local dni = utils.get_dni_from_device(device) + log.info("refresh_energy_meter(): Refreshing data of Energy Meter, dni = " .. dni) + + -- Refresh Voltage Measurement + local voltage = values.voltage + + if voltage ~= nil then + log.trace("refresh_energy_meter(): Refreshing Voltage Measurement, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.voltageMeasurement.voltage({value=voltage, unit="V"})) + end + + -- Refresh Current Measurement + local current = values.current + + if current ~= nil then + log.trace("refresh_energy_meter(): Refreshing Current Measurement, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.currentMeasurement.current({value=current, unit="A"})) + end + + -- Refresh Active Power + local activePower = values.activePowerTotal + + if activePower ~= nil then + log.trace("refresh_energy_meter(): Refreshing Active Power, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.powerMeter.power({value=activePower, unit="W"})) + end + + -- Refresh Active Energy Net, Import & Export Total + local activeEnergyNetTotal = values.activeEnergyNetTotal + local activeEnergyImportTotal = values.activeEnergyImportTotal + local activeEnergyExportTotal = values.activeEnergyExportTotal + + if activeEnergyNetTotal == nil then + if activeEnergyImportTotal ~= nil and activeEnergyExportTotal ~= nil then + -- If only Import and Export Total are available, calculate Net Total + activeEnergyNetTotal = activeEnergyImportTotal - activeEnergyExportTotal + end + end + + local activeEnergyNetPositive = math.max(activeEnergyNetTotal, 0.0) + local activeEnergyNetNegative = math.min(activeEnergyNetTotal, 0.0) * -1 -- Convert to positive value + + if activeEnergyNetTotal ~= nil and activeEnergyImportTotal ~= nil and activeEnergyExportTotal ~= nil then + -- Refresh Active Energy Net Positive + log.trace("refresh_energy_meter(): Refreshing Active Energy Net Positive, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.energyMeter.energy({value=activeEnergyNetPositive, unit="kWh"})) + + -- Refresh Active Energy Import Total + log.trace("refresh_energy_meter(): Refreshing Active Energy Import Total, dni = " .. dni) + device.profile.components["consumptionMeter"]:emit_event(caps.energyMeter.energy({value=activeEnergyImportTotal, unit="kWh"})) + + -- Refresh Active Energy Export Total + log.trace("refresh_energy_meter(): Refreshing Active Energy Export Total, dni = " .. dni) + device.profile.components["productionMeter"]:emit_event(caps.energyMeter.energy({value=activeEnergyExportTotal, unit="kWh"})) + + -- Refresh Active Energy Net Negative + log.trace("refresh_energy_meter(): Refreshing Active Energy Net Negative, dni = " .. dni) + device.profile.components["surplus"]:emit_event(caps.energyMeter.energy({value=activeEnergyNetNegative, unit="kWh"})) + + -- Verify whether the appropriate time have elapsed to report the energy net, consumption and production + local last_energy_report = device:get_field(fields.LAST_ENERGY_REPORT) or 0.0 + + if (os.time() - last_energy_report) >= config.EDGE_CHILD_ENERGY_REPORT_INTERVAL then -- Report the energy net, consumption and production periodically + local current_net_positive_report = device:get_latest_state("main", caps.powerConsumptionReport.ID, caps.powerConsumptionReport.powerConsumption.NAME) + local current_consumption_report = device:get_latest_state("consumptionMeter", caps.powerConsumptionReport.ID, caps.powerConsumptionReport.powerConsumption.NAME) + local current_production_report = device:get_latest_state("productionMeter", caps.powerConsumptionReport.ID, caps.powerConsumptionReport.powerConsumption.NAME) + local current_net_negative_report = device:get_latest_state("surplus", caps.powerConsumptionReport.ID, caps.powerConsumptionReport.powerConsumption.NAME) + + -- Calculate delta net, consumption and production energy + local delta_net_positive_report = 0.0 + if current_net_positive_report ~= nil then + delta_net_positive_report = math.max((activeEnergyNetPositive * 1000) - current_net_positive_report.energy, 0.0) + end + + local delta_consumption_energy = 0.0 + if current_consumption_report ~= nil then + delta_consumption_energy = math.max((activeEnergyImportTotal * 1000) - current_consumption_report.energy, 0.0) + end + + local delta_production_energy = 0.0 + if current_production_report ~= nil then + delta_production_energy = math.max((activeEnergyExportTotal * 1000) - current_production_report.energy, 0.0) + end + + local delta_net_negative_report = 0.0 + if current_net_negative_report ~= nil then + delta_net_negative_report = math.max((activeEnergyNetNegative * 1000) - current_net_negative_report.energy, 0.0) + end + + -- Refresh Power Net Positive, Consumption, Production & Net Negative Report + log.trace("refresh_energy_meter(): Refreshing Power Net Positive, Consumption, Production & Net Negative Report, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.powerConsumptionReport.powerConsumption({energy=activeEnergyNetPositive * 1000, deltaEnergy=delta_net_positive_report})) + device.profile.components["consumptionMeter"]:emit_event(caps.powerConsumptionReport.powerConsumption({energy=activeEnergyImportTotal * 1000, deltaEnergy=delta_consumption_energy})) + device.profile.components["productionMeter"]:emit_event(caps.powerConsumptionReport.powerConsumption({energy=activeEnergyExportTotal * 1000, deltaEnergy=delta_production_energy})) + device.profile.components["surplus"]:emit_event(caps.powerConsumptionReport.powerConsumption({energy=activeEnergyNetNegative * 1000, deltaEnergy=delta_net_negative_report})) + + -- Save date of the last consumption + local current_energy_report = last_energy_report + config.EDGE_CHILD_ENERGY_REPORT_INTERVAL + if (current_energy_report + config.EDGE_CHILD_ENERGY_REPORT_INTERVAL) < os.time() then + current_energy_report = os.time() + end + + device:set_field(fields.LAST_ENERGY_REPORT, current_energy_report, {persist=false}) + else + log.debug("refresh_energy_meter(): " .. config.EDGE_CHILD_ENERGY_REPORT_INTERVAL .. " seconds haven't elapsed yet! Last consumption was at " .. last_energy_report .. ", dni = " .. dni) + end + end + + return true +end + +local function refresh_auxiliary_contact(driver, device, values) + local dni = utils.get_dni_from_device(device) + log.info("refresh_auxiliary_contact(): Refreshing data of Auxiliary Contact, dni = " .. dni) + + -- Refresh Contact Sensor + local isClosed = values.isClosed + + if isClosed ~= nil then + log.trace("refresh_auxiliary_contact(): Refreshing Switch, dni = " .. dni) + + if isClosed == 1 then + isClosed = true + else + isClosed = false + end + + if isClosed then + device:emit_event(caps.switch.switch.on()) + else + device:emit_event(caps.switch.switch.off()) + end + end + + return true +end + +local function refresh_output_module(driver, device, values) + local dni = utils.get_dni_from_device(device) + log.info("refresh_output_module(): Refreshing data of Output Module, dni = " .. dni) + + -- Refresh Switch + local isClosed = values.isClosed + log.trace("refresh_output_module(): Refreshing Switch, dni = " .. dni) + + if isClosed == 1 then + isClosed = true + else + isClosed = false + end + + if isClosed then + device:emit_event(caps.switch.switch.on()) + else + device:emit_event(caps.switch.switch.off()) + end + + return true +end + +local function refresh_water_meter(driver, device, values) + local dni = utils.get_dni_from_device(device) + log.info("refresh_water_meter(): Refreshing data of Water Meter, dni = " .. dni) + + local unit = values.unit + if unit == nil then + log.error("refresh_water_meter(): The unit of the water meter is not set, dni = " .. dni) + return false + end + + -- Refresh Water Meter: last hour + local lastHourFlow = values.lastHourFlow + + if lastHourFlow ~= nil then + log.trace("refresh_water_meter(): Refreshing Water Meter: last hour, dni = " .. dni) + device:emit_event(caps.waterMeter.lastHour({value=lastHourFlow, unit=unit})) + end + + -- Refresh Water Meter: last 24 hours + local lastTwentyFourHoursFlow = values.lastTwentyFourHoursFlow + + if lastTwentyFourHoursFlow ~= nil then + log.trace("refresh_water_meter(): Refreshing Water Meter: last 24 hours, dni = " .. dni) + device:emit_event(caps.waterMeter.lastTwentyFourHours({value=lastTwentyFourHoursFlow, unit=unit})) + end + + -- Refresh Water Meter: last 7 days + local lastSevenDaysFlow = values.lastSevenDaysFlow + + if lastSevenDaysFlow ~= nil then + log.trace("refresh_water_meter(): Refreshing Water Meter: last 7 days, dni = " .. dni) + device:emit_event(caps.waterMeter.lastSevenDays({value=lastSevenDaysFlow, unit=unit})) + end + + return true +end + +local function refresh_gas_meter(driver, device, values) + local dni = utils.get_dni_from_device(device) + log.info("refresh_gas_meter(): Refreshing data of Gas Meter, dni = " .. dni) + + local gasMeterVolumeUnit = values.gasMeterVolumeUnit + if gasMeterVolumeUnit == nil then + log.error("refresh_gas_meter(): The unit of the gas meter is not set, dni = " .. dni) + return false + end + + -- Correct the unit if necessary + if gasMeterVolumeUnit == "m3" then + gasMeterVolumeUnit = "m^3" + end + + -- Refresh Gas Meter + local gasMeterVolume = values.gasMeterVolume + + if gasMeterVolume ~= nil then + log.trace("refresh_gas_meter(): Refreshing Gas Meter, dni = " .. dni) + device:emit_event(caps.gasMeter.gasMeterVolume({value=gasMeterVolume, unit=gasMeterVolumeUnit})) + end + + return true +end + +local function refresh_usb_energy_meter(driver, device, values) + local dni = utils.get_dni_from_device(device) + log.info("refresh_usb_energy_meter(): Refreshing data of USB Energy Meter, dni = " .. dni) + + -- Refresh Active Power Import Total + local activePowerImportTotal = values.activePowerImportTotal + + if activePowerImportTotal ~= nil then + log.trace("refresh_usb_energy_meter(): Refreshing Active Power Import Total, dni = " .. dni) + device:emit_event(caps.powerMeter.power({value=activePowerImportTotal, unit="W"})) + end + + -- Refresh Active Energy Import Total + local activeEnergyImportTotal = values.activeEnergyImportTotal + + if activeEnergyImportTotal ~= nil then + log.trace("refresh_usb_energy_meter(): Refreshing Active Energy Import Total, dni = " .. dni) + device:emit_event(caps.energyMeter.energy({value=activeEnergyImportTotal, unit="kWh"})) + + -- Verify whether the appropriate time have elapsed to report the energy consumption and production + local last_energy_report = device:get_field(fields.LAST_ENERGY_REPORT) or 0.0 + + if (os.time() - last_energy_report) >= config.EDGE_CHILD_ENERGY_REPORT_INTERVAL then -- Report the energy consumption periodically + local current_consumption_report = device:get_latest_state("main", caps.powerConsumptionReport.ID, caps.powerConsumptionReport.powerConsumption.NAME) + + -- Calculate delta consumption energy + local delta_consumption_energy = 0.0 + if current_consumption_report ~= nil then + delta_consumption_energy = math.max((activeEnergyImportTotal * 1000) - current_consumption_report.energy, 0.0) + end + + -- Refresh Power Consumption Report + log.trace("refresh_usb_energy_meter(): Refreshing Power Consumption Report, dni = " .. dni) + device:emit_event(caps.powerConsumptionReport.powerConsumption({energy=activeEnergyImportTotal * 1000, deltaEnergy=delta_consumption_energy})) + + -- Save date of the last consumption + local current_energy_report = last_energy_report + config.EDGE_CHILD_ENERGY_REPORT_INTERVAL + if (current_energy_report + config.EDGE_CHILD_ENERGY_REPORT_INTERVAL) < os.time() then + current_energy_report = os.time() + end + + device:set_field(fields.LAST_ENERGY_REPORT, current_energy_report, {persist=false}) + else + log.debug("refresh_usb_energy_meter(): " .. config.EDGE_CHILD_ENERGY_REPORT_INTERVAL .. " seconds haven't elapsed yet! Last consumption was at " .. last_energy_report .. ", dni = " .. dni) + end + end + + return true +end + +function device_refresher.refresh_device(driver, device, values) + local dni, device_type = utils.get_dni_from_device(device) + log.info("device_refresher.refresh_device(): Refreshing data of device, dni = " .. dni) + + if device_type == fields.DEVICE_TYPE_BRIDGE then + log.debug("device_refresher.refresh_device(): Cannot refresh bridge device, dni = " .. dni) + return + end + + log.debug("device_refresher.refresh_device(): Provided values: " .. utils.dump(values)) + + local refresh_methods = { + [utils.get_thing_exact_type(config.EDGE_CHILD_CURRENT_SENSOR_TYPE)] = refresh_current_sensor, + [utils.get_thing_exact_type(config.EDGE_CHILD_ENERGY_METER_MODULE_TYPE)] = refresh_energy_meter, + [utils.get_thing_exact_type(config.EDGE_CHILD_AUXILIARY_CONTACT_TYPE)] = refresh_auxiliary_contact, + [utils.get_thing_exact_type(config.EDGE_CHILD_OUTPUT_MODULE_TYPE)] = refresh_output_module, + [utils.get_thing_exact_type(config.EDGE_CHILD_ENERGY_METER_TYPE)] = refresh_energy_meter, + [utils.get_thing_exact_type(config.EDGE_CHILD_WATER_METER_TYPE)] = refresh_water_meter, + [utils.get_thing_exact_type(config.EDGE_CHILD_GAS_METER_TYPE)] = refresh_gas_meter, + [utils.get_thing_exact_type(config.EDGE_CHILD_USB_ENERGY_METER_TYPE)] = refresh_usb_energy_meter + } + + local device_model = utils.get_device_model(device) + if device_model == nil then + log.error("device_refresher.refresh_device(): No device model found for device, dni = " .. dni) + return + end + + local refresh_method = refresh_methods[device_model] + if refresh_method == nil then + log.error("device_refresher.refresh_device(): No refresh method found for device, dni = " .. dni .. ", model = " .. device_model) + return + end + + return refresh_method(driver, device, values) +end + +return device_refresher \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/commands.lua b/drivers/ABB/insite-scu200/src/commands.lua new file mode 100644 index 0000000000..453d0f521f --- /dev/null +++ b/drivers/ABB/insite-scu200/src/commands.lua @@ -0,0 +1,111 @@ +local caps = require('st.capabilities') +local log = require('log') +local json = require('dkjson') + +-- Local imports +local utils = require('utils') +local fields = require('fields') +local config = require("config") +local device_manager = require('abb.device_manager') +local connection_monitor = require('connection_monitor') + +-- Commands handler for the bridge and thing devices +local commands = {} + +-- Method for posting the payload to the device +local function post_payload(device, payload) + local dni = utils.get_dni_from_device(device) + local communication_device = device:get_parent_device() or device + local conn_info = communication_device:get_field(fields.CONN_INFO) + + local _, err, status = conn_info:post_device_by_id(dni, payload) + if not err and status == 200 then + log.info("post_payload(): Success, dni = " .. dni) + + return true + else + status = status or "nil" + log.error("post_payload(): Error, err = " .. err .. ", status = " .. status .. ", dni = " .. dni) + + device:offline() + + return false + end +end + +-- Switch on command +function commands.switch_on(driver, device, cmd) + local dni, _ = utils.get_dni_from_device(device) + log.info("commands.switch_on(): Switching on capablity within dni = " .. dni) + + local device_model = utils.get_device_model(device) + + local payload = nil + local event = nil + if device_model == utils.get_thing_exact_type(config.EDGE_CHILD_OUTPUT_MODULE_TYPE) then + payload = json.encode({capability = cmd.capability, command = cmd.command}) + event = caps.switch.switch.on() + end + + if payload ~= nil and event ~= nil then + local bridge = device:get_parent_device() + + bridge.thread:call_with_delay(0, function() -- Run within bridge thread to use the same connection + local success = post_payload(device, payload) + if success then + device:emit_event(event) + end + end) + end +end + +-- Switch off commands +function commands.switch_off(driver, device, cmd) + local dni, _ = utils.get_dni_from_device(device) + log.info("commands.switch_off(): Switching off capablity within dni = " .. dni) + + local device_model = utils.get_device_model(device) + + local payload = nil + local event = nil + if device_model == utils.get_thing_exact_type(config.EDGE_CHILD_OUTPUT_MODULE_TYPE) then + payload = json.encode({capability = cmd.capability, command = cmd.command}) + event = caps.switch.switch.off() + end + + if payload ~= nil and event ~= nil then + local bridge = device:get_parent_device() + + bridge.thread:call_with_delay(0, function() -- Run within bridge thread to use the same connection + local success = post_payload(device, payload) + if success then + device:emit_event(event) + end + end) + end +end + +-- Refresh command +function commands.refresh(driver, device, cmd) + local dni, device_type = utils.get_dni_from_device(device) + log.info("commands.refresh(): Refresh capability within dni = " .. dni) + + if device_type == fields.DEVICE_TYPE_BRIDGE then + connection_monitor.check_and_update_connection(driver, device) + local child_devices = device:get_child_list() + + for _, thing_device in ipairs(child_devices) do + device_manager.refresh(driver, thing_device) + end + elseif device_type == fields.DEVICE_TYPE_THING then + local bridge = device:get_parent_device() + + if bridge.thread ~= nil then + bridge.thread:call_with_delay(0, function() -- Run within bridge thread to use the same connection + device_manager.refresh(driver, device) + end) + end + end +end + +return commands \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/config.lua b/drivers/ABB/insite-scu200/src/config.lua new file mode 100644 index 0000000000..8470708d9e --- /dev/null +++ b/drivers/ABB/insite-scu200/src/config.lua @@ -0,0 +1,72 @@ +local config = {} + +-- Device Config +config.DEVICE_TYPE = "LAN" + +config.MANUFACTURER = "ABB" + +config.BRIDGE_PROFILE = "abb.scu200.bridge.v1" +config.BRIDGE_TYPE = "SCU200" +config.BRIDGE_VERSION = "1" + +config.BRIDGE_URN = "urn:" .. config.MANUFACTURER .. ":device:" .. config.BRIDGE_TYPE .. ":" .. config.BRIDGE_VERSION + +config.BRIDGE_CONN_MONITOR_INTERVAL = 300 -- 5 minutes + +-- Edge Child Config +config.EDGE_CHILD_TYPE = "EDGE_CHILD" + +config.EDGE_CHILD_CURRENT_SENSOR_TYPE = "CurrentSensor" +config.EDGE_CHILD_ENERGY_METER_MODULE_TYPE = "EnergyMeterModule" +config.EDGE_CHILD_AUXILIARY_CONTACT_TYPE = "AuxiliaryContact" +config.EDGE_CHILD_OUTPUT_MODULE_TYPE = "OutputModule" +config.EDGE_CHILD_ENERGY_METER_TYPE = "EnergyMeter" +config.EDGE_CHILD_WATER_METER_TYPE = "WaterMeter" +config.EDGE_CHILD_GAS_METER_TYPE = "GasMeter" +config.EDGE_CHILD_USB_ENERGY_METER_TYPE = "USBEnergyMeter" + +config.EDGE_CHILD_CURRENT_SENSOR_VERSION = 1 +config.EDGE_CHILD_ENERGY_METER_MODULE_VERSION = 1 +config.EDGE_CHILD_AUXILIARY_CONTACT_VERSION = 1 +config.EDGE_CHILD_OUTPUT_MODULE_VERSION = 1 +config.EDGE_CHILD_ENERGY_METER_VERSION = 1 +config.EDGE_CHILD_WATER_METER_VERSION = 1 +config.EDGE_CHILD_GAS_METER_VERSION = 1 +config.EDGE_CHILD_USB_ENERGY_METER_VERSION = 1 + +config.EDGE_CHILD_CURRENT_SENSOR_CONSUMPTION_PROFILE = "abb.scu200.current-sensor-consumption.v1" +config.EDGE_CHILD_CURRENT_SENSOR_PRODUCTION_PROFILE = "abb.scu200.current-sensor-production.v1" +config.EDGE_CHILD_AUXILIARY_CONTACT_PROFILE = "abb.scu200.auxiliary-contact.v1" +config.EDGE_CHILD_OUTPUT_MODULE_PROFILE = "abb.scu200.output-module.v1" +config.EDGE_CHILD_ENERGY_METER_PROFILE = "abb.scu200.energy-meter.v1" +config.EDGE_CHILD_WATER_METER_PROFILE = "abb.scu200.water-meter.v1" +config.EDGE_CHILD_GAS_METER_PROFILE = "abb.scu200.gas-meter.v1" +config.EDGE_CHILD_USB_ENERGY_METER_PROFILE = "abb.scu200.usb-energy-meter.v1" + +config.EDGE_CHILD_CURRENT_SENSOR_REFRESH_PERIOD = 30 +config.EDGE_CHILD_ENERGY_METER_MODULE_REFRESH_PERIOD = 30 +config.EDGE_CHILD_AUXILIARY_CONTACT_REFRESH_PERIOD = 300 -- 5 minutes +config.EDGE_CHILD_OUTPUT_MODULE_REFRESH_PERIOD = 300 -- 5 minutes +config.EDGE_CHILD_ENERGY_METER_REFRESH_PERIOD = 30 +config.EDGE_CHILD_WATER_METER_REFRESH_PERIOD = 300 -- 5 minutes +config.EDGE_CHILD_GAS_METER_REFRESH_PERIOD = 300 -- 5 minutes +config.EDGE_CHILD_USB_ENERGY_METER_REFRESH_PERIOD = 30 + +config.EDGE_CHILD_ENERGY_REPORT_INTERVAL = 900 -- 15 minutes + +-- REST API Config +config.REST_API_PORT = 1025 + +-- SSDP Config +config.MC_ADDRESS = "239.255.255.250" +config.MC_PORT = 1900 +config.MC_TIMEOUT = 5 +config.MSEARCH = table.concat({ + "M-SEARCH * HTTP/1.1", + "HOST: 239.255.255.250:1900", + "MAN: \"ssdp:discover\"", + "MX: 5", + "ST: " .. config.BRIDGE_URN +}, "\r\n") + +return config diff --git a/drivers/ABB/insite-scu200/src/connection_monitor.lua b/drivers/ABB/insite-scu200/src/connection_monitor.lua new file mode 100644 index 0000000000..bdeb2b9621 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/connection_monitor.lua @@ -0,0 +1,71 @@ +local log = require("log") + +-- Local imports +local fields = require("fields") +local utils = require("utils") +local discovery = require("discovery") +local eventsource_handler = require("eventsource_handler") +local device_manager = require("abb.device_manager") + +-- Connection monitor for the SCU200 Bridge +local connection_monitor = {} + +function connection_monitor.update_connection(driver, device, bridge_ip) + local bridge_dni = utils.get_dni_from_device(device) + log.info("connection_monitor.update_connection(): Update connection for bridge device: " .. bridge_dni) + + local conn_info = device_manager.get_bridge_connection_info(driver, bridge_dni, bridge_ip) + + if device_manager.is_valid_connection(driver, device, conn_info) then + device:set_field(fields.CONN_INFO, conn_info) + eventsource_handler.create_sse(driver, device) + end +end + +local function find_new_connection(driver, device) + local dni = utils.get_dni_from_device(device) + log.info("find_new_connection(): Find new connection for dni = " .. dni) + + local found_devices = discovery.find_devices() + + if found_devices ~= nil then + local found_device = found_devices[dni] + + if found_device then + log.info("find_new_connection(): Found new connection for dni = " .. dni) + + local ip = found_device.ip + + device:set_field(fields.BRIDGE_IPV4, ip, {persist = true}) + connection_monitor.update_connection(driver, device, ip) + end + end +end + +function connection_monitor.check_and_update_connection(driver, device) + local dni = utils.get_dni_from_device(device) + local conn_info = device:get_field(fields.CONN_INFO) + + if not device_manager.is_valid_connection(driver, device, conn_info) then + log.error("connection_monitor.check_and_update_connection(): Disconnected from device. Try to find new connection for dni = " .. dni) + + find_new_connection(driver, device) + end +end + +-- Method for monitoring the connection of the bridge devices +function connection_monitor.monitor_connections(driver) + local device_list = driver:get_devices() + + for _, device in ipairs(device_list) do + if device:get_field(fields.DEVICE_TYPE) == fields.DEVICE_TYPE_BRIDGE then + local dni = utils.get_dni_from_device(device) + log.info("connection_monitor.monitor_connections(): Monitoring connection for bridge device: " .. dni) + + connection_monitor.check_and_update_connection(driver, device) + device_manager.bridge_monitor(driver, device) + end + end +end + +return connection_monitor \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/discovery.lua b/drivers/ABB/insite-scu200/src/discovery.lua new file mode 100644 index 0000000000..3958c48026 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/discovery.lua @@ -0,0 +1,315 @@ +local log = require "log" +local socket = require('socket') +local cosock = require "cosock" + +-- Local imports +local api = require("abb.api") +local config = require("config") +local utils = require("utils") +local fields = require("fields") + +-- Discovery service run within SmartThings app +local discovery = {} + +local joined_bridge = {} +local joined_thing = {} + +-- Method for setting the device fields +function discovery.set_device_fields(driver, device) + local dni = utils.get_dni_from_device(device) + + if joined_bridge[dni] ~= nil then + log.info("discovery.set_device_field(): Setting device field for bridge: " .. dni) + local bridge_cache_value = driver.datastore.bridge_discovery_cache[dni] + + device:set_field(fields.BRIDGE_IPV4, bridge_cache_value.ip, {persist = true}) + device:set_field(fields.DEVICE_TYPE, fields.DEVICE_TYPE_BRIDGE, {persist = true}) + elseif joined_thing[dni] ~= nil then + log.info("discovery.set_device_field(): Setting device field for thing: " .. dni) + local thing_cache_value = driver.datastore.thing_discovery_cache[dni] + + device:set_field(fields.PARENT_BRIDGE_DNI, thing_cache_value.parent_bridge_dni, {persist = true}) + device:set_field(fields.THING_INFO, thing_cache_value.thing_info, {persist = true}) + device:set_field(fields.DEVICE_TYPE, fields.DEVICE_TYPE_THING, {persist = true}) + else + log.warn("discovery.set_device_field(): Could not set device field for unknown device: " .. dni) + end +end + +-- Method for updating the bridge discovery cache +local function update_bridge_discovery_cache(driver, dni, device) + log.info("update_bridge_discovery_cache(): Updating bridge discovery cache: " .. dni) + + driver.datastore.bridge_discovery_cache[dni] = { + ip = device["ip"] + } +end + +-- Method for updating the thing discovery cache +local function update_thing_discovery_cache(driver, thing_dni, parent_bridge_dni, thing_info) + log.info("update_thing_discovery_cache(): Updating thing discovery cache: " .. thing_dni) + + driver.datastore.thing_discovery_cache[thing_dni] = { + parent_bridge_dni = parent_bridge_dni, + thing_info = thing_info, + } +end + +-- Method for trying to add a new bridge +local function try_add_bridge(driver, dni, device) + log.info("try_add_bridge(): Trying to add bridge: " .. dni) + + local bridge_info = api.get_bridge_info(device["ip"], dni) + if bridge_info == nil then + log.error("try_add_bridge(): Failed to get bridge info for bridge: " .. dni) + return false + end + + update_bridge_discovery_cache(driver, dni, device) + + local metadata = { + type = config.DEVICE_TYPE, + device_network_id = dni, + label = bridge_info.name, + profile = config.BRIDGE_PROFILE, + manufacturer = config.MANUFACTURER, + model = config.BRIDGE_TYPE, + vendor_provided_label = config.BRIDGE_TYPE + } + + local success, err = driver:try_create_device(metadata) + + if success then + log.debug("try_add_bridge(): Bridge created: " .. dni) + + return true + else + log.error("try_add_bridge(): Failed to create bridge: " .. dni) + log.debug("try_add_bridge(): Error: " .. err) + + return false + end +end + +-- Method for trying to add a new thing +local function try_add_thing(driver, parent_device, thing_dni, thing_info) + local parent_device_dni = utils.get_dni_from_device(parent_device) + log.info("try_add_thing(): Trying to add thing: " .. thing_dni .. " of type: " .. thing_info.type .. " on bridge: " .. parent_device_dni) + + update_thing_discovery_cache(driver, thing_dni, parent_device_dni, thing_info) + + if thing_info.type == utils.get_thing_exact_type(config.EDGE_CHILD_WATER_METER_TYPE) or thing_info.type == utils.get_thing_exact_type(config.EDGE_CHILD_GAS_METER_TYPE) then + log.warn("try_add_thing(): Not supported thing type: " .. thing_info.type) + return false + elseif thing_info.type == utils.get_thing_exact_type(config.EDGE_CHILD_CURRENT_SENSOR_TYPE) and thing_info.properties.isExport then + log.warn("try_add_thing(): Current sensor with production data is not supported") + return false + end + + local profile_ref = utils.get_thing_profile_ref(thing_info) + if profile_ref == nil then + log.error("try_add_thing(): Failed to get profile reference for thing: " .. thing_dni) + return false + end + + local parent_device_id = utils.get_device_id_from_device(parent_device) + + local metadata = { + type = config.EDGE_CHILD_TYPE, + label = thing_info.name, + vendor_provided_label = thing_info.name, + profile = profile_ref, + manufacturer = config.MANUFACTURER, + model = thing_info.type, + parent_device_id = parent_device_id, + parent_assigned_child_key = thing_info.uuid, + } + + local success, err = driver:try_create_device(metadata) + + if success then + log.debug("try_add_thing(): Thing created: " .. thing_dni) + + return true + else + log.error("try_add_thing(): Failed to create thing: " .. thing_dni) + log.debug("try_add_thing(): Error: " .. err) + + return false + end +end + +-- SSDP Response parser +local function parse_ssdp(data) + local res = {} + + res.status = data:sub(0, data:find('\r\n')) + + for line in data:gmatch("[^\r\n]+") do + local _, _, header, value = string.find(line, "([%w-]+):%s*([%a+-:_ /=?]*)") + + if header ~= nil and value ~= nil then + res[header:lower()] = value + end + end + + return res +end + +-- Method for finding devices +function discovery.find_devices() + log.info("discovery.find_devices(): Finding devices") + + -- Initialize UDP socket + local upnp = cosock.socket.udp() + + upnp:setsockname('*', 0) + upnp:setoption("broadcast", true) + upnp:settimeout(config.MC_TIMEOUT) + + -- Broadcast M-SEARCH request + log.info("discovery.find_devices(): Scanning network...") + + upnp:sendto(config.MSEARCH, config.MC_ADDRESS, config.MC_PORT) + + -- Listen for responses + local devices = {} + local start_time = socket.gettime() + + while (socket.gettime() - start_time) < config.MC_TIMEOUT do + local res = upnp:receivefrom() + + if res ~= nil then + local device = parse_ssdp(res) + local dni = string.match(device["usn"], "^uuid:([a-zA-Z0-9-]+)::" .. config.BRIDGE_URN .. "$") + + if dni ~= nil then + local _, _, device_ip = string.find(device["location"], "https?://(%d+%.%d+%.%d+%.%d+):?%d*/?.*") + device["ip"] = device_ip + + devices[dni] = device + end + end + end + + -- Print found devices + if next(devices) then + for dni, device in pairs(devices) do + log.debug("discovery.find_devices(): Device found: " .. utils.dump(device)) + end + else + log.debug("discovery.find_devices(): No devices found") + end + + -- Close the UDP socket + upnp:close() + + log.debug("discovery.find_devices(): Stop scanning network") + + if devices ~= nil then + return devices + end + + return nil +end + +-- Start the discovery of bridges +local function discover_bridges(driver) + log.info("discover_bridges(): Discovering bridges") + + -- Get the known devices + local known_devices = {} + + for _, device in pairs(driver:get_devices()) do + local dni, device_type = utils.get_dni_from_device(device) + known_devices[dni] = device + + log.debug("discover_bridges(): Known devices: " .. dni .. " with type: " .. device_type) + end + + -- Find new devices + local found_devices = discovery.find_devices() + + if found_devices ~= nil then + for dni, device in pairs(found_devices) do + if not known_devices or not known_devices[dni] then + log.info("discover_bridges(): Found new bridge: " .. dni) + + if not joined_bridge[dni] then + if try_add_bridge(driver, dni, device) then + joined_bridge[dni] = true + end + else + log.debug("discover_bridges(): Bridge already joined: " .. dni) + end + else + log.debug("discover_bridges(): Bridge already added: " .. dni) + end + end + end +end + +-- Start the discovery of things +local function discover_things(driver) + log.info("discover_things(): Discovering things") + + -- Get the known devices + local known_devices = {} + + for _, device in pairs(driver:get_devices()) do + local dni, device_type = utils.get_dni_from_device(device) + known_devices[dni] = device + + log.debug("discover_things(): Known devices: " .. dni .. " with type: " .. device_type) + end + + -- Found new devices + for bridge_dni, bridge_cache_value in pairs(driver.datastore.bridge_discovery_cache) do + local bridge_ip = bridge_cache_value.ip + log.info("discover_things(): Fetching things from bridge: " .. bridge_dni .. " at IP: " .. bridge_ip) + + if known_devices[bridge_dni] ~= nil and known_devices[bridge_dni]:get_field(fields.CONN_INFO) ~= nil then + local thing_infos = api.get_thing_infos(bridge_ip, bridge_dni) + + if thing_infos and thing_infos.devices ~= nil then + for _, thing_info in pairs(thing_infos.devices) do + if thing_info ~= nil then + local thing_dni = thing_info.uuid + + log.info("discover_things(): Found thing: " .. thing_dni .. " on bridge: " .. bridge_dni) + + if thing_dni ~= nil then + if not known_devices[thing_dni] then + if try_add_thing(driver, known_devices[bridge_dni], thing_dni, thing_info) then + joined_thing[thing_dni] = true + end + elseif not joined_thing[thing_dni] then + log.debug("discover_things(): Thing already known: " .. thing_dni) + else + log.debug("discover_things(): Thing already joined: " .. thing_dni) + end + end + end + + cosock.socket.sleep(0.2) + end + end + end + end +end + +-- Main function to start the discovery service +function discovery.start(driver, _, should_continue) + log.info("discovery.start(): Starting discovery") + + while should_continue() do + discover_bridges(driver) + discover_things(driver) + + cosock.socket.sleep(0.2) + end + + log.info("discovery.start(): Ending discovery") +end + +return discovery \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/eventsource_handler.lua b/drivers/ABB/insite-scu200/src/eventsource_handler.lua new file mode 100644 index 0000000000..82e7131b87 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/eventsource_handler.lua @@ -0,0 +1,128 @@ +local log = require('log') +local json = require('dkjson') + +-- Local imports +local EventSource = require "lunchbox.sse.eventsource" +local device_manager = require("abb.device_manager") +local device_refresher = require("abb.device_refresher") +local fields = require "fields" +local utils = require "utils" + +local eventsource_handler = {} + +-- Method for handling an incoming SSE event +function eventsource_handler.handle_sse_event(driver, bridge, msg) + log.debug("eventsource_handler.handle_sse_event(): Handling SSE event (TYPE: " .. msg.type .. ", DATA: " .. msg.data .. ")") + + if msg.type == "valueChanged" then + local data = json.decode(msg.data) + + if data ~= nil and next(data) ~= nil then + -- Find the device + local device = nil + + local child_devices = bridge:get_child_list() + for _, thing_device in ipairs(child_devices) do + local dni = utils.get_dni_from_device(thing_device) + + if dni == data.uuid then + device = thing_device + break + end + end + + if device == nil then + log.warn("eventsource_handler.handle_sse_event(): Failed to find the device with dni: " .. data.uuid) + return + end + + -- Prepare the values + local values = {} + + values[data.attribute.name] = data.attribute.value + + -- Refresh the device + if device_refresher.refresh_device(driver, device, values) then + -- Define online status + device:online() + else + log.error("eventsource_handler.handle_sse_event(): Failed to update the device's values") + + -- Set device as offline + device:offline() + end + else + log.error("eventsource_handler.handle_sse_event(): Failed to decode JSON data: " .. msg.data) + end + elseif msg.type == "noDevices" then + log.info("eventsource_handler.handle_sse_event(): No devices to monitor found") + + eventsource_handler.close_sse(driver, bridge) + elseif msg.type == "refreshConnection" then + log.info("eventsource_handler.handle_sse_event(): Refreshing connection") + + eventsource_handler.close_sse(driver, bridge) + eventsource_handler.create_sse(driver, bridge) + else + log.warn("eventsource_handler.handle_sse_event(): Unknown SSE event type: " .. msg.type) + end +end + +-- Method for creating SSE +function eventsource_handler.create_sse(driver, device) + local dni = utils.get_dni_from_device(device) + log.info("eventsource_handler.create_sse(): Creating SSE for dni: " .. dni) + + local conn_info = device:get_field(fields.CONN_INFO) + + if not device_manager.is_valid_connection(driver, device, conn_info) then + log.error("eventsource_handler.create_sse(): Invalid connection for dni: " .. dni) + return + end + + local sse_url = conn_info:get_sse_url() + if not sse_url then + log.error("eventsource_handler.create_sse(): Failed to get sse_url for dni: " .. dni) + return + end + + log.trace("eventsource_handler.create_sse(): Creating SSE EventSource for " .. dni .. " with sse_url: " .. sse_url) + local eventsource = EventSource.new(sse_url, {}, nil, nil) + + eventsource.onmessage = function(msg) + if msg then + eventsource_handler.handle_sse_event(driver, device, msg) + end + end + + eventsource.onerror = function() + log.error("eventsource_handler.create_sse(): Error in the eventsource for dni: " .. dni) + device:offline() + end + + eventsource.onopen = function(msg) + log.info("eventsource_handler.create_sse(): Eventsource has been opened for dni: " .. dni) + device:online() + end + + local old_eventsource = device:get_field(fields.EVENT_SOURCE) + if old_eventsource then + log.info("eventsource_handler.create_sse(): Eventsource has been closed for dni: " .. dni) + old_eventsource:close() + end + device:set_field(fields.EVENT_SOURCE, eventsource) +end + +-- Method for closing SSE +function eventsource_handler.close_sse(driver, device) + local dni = utils.get_dni_from_device(device) + log.info("eventsource_handler.close_sse(): Closing SSE for dni: " .. dni) + + local eventsource = device:get_field(fields.EVENT_SOURCE) + if eventsource then + log.info("eventsource_handler.close_sse(): Closing eventsource for device: " .. dni) + eventsource:close() + end +end + +return eventsource_handler diff --git a/drivers/ABB/insite-scu200/src/fields.lua b/drivers/ABB/insite-scu200/src/fields.lua new file mode 100644 index 0000000000..7c56019cc1 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/fields.lua @@ -0,0 +1,16 @@ +-- Table of constants used to index in to device store fields +local fields = { + BRIDGE_IPV4 = "bridge_ipv4", + THING_INFO = "thing_info", + CONN_INFO = "conn_info", + PARENT_BRIDGE_DNI = "parent_bridge_dni", + EVENT_SOURCE = "eventsource", + DEVICE_TYPE = "devcie_type", + LAST_ENERGY_REPORT = "last_energy_report", + _INIT = "init", + + DEVICE_TYPE_BRIDGE = "bridge", + DEVICE_TYPE_THING = "thing" +} + +return fields \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/init.lua b/drivers/ABB/insite-scu200/src/init.lua new file mode 100644 index 0000000000..df33d18b1c --- /dev/null +++ b/drivers/ABB/insite-scu200/src/init.lua @@ -0,0 +1,45 @@ +local log = require('log') +local Driver = require('st.driver') +local caps = require('st.capabilities') + +-- Local imports +local discovery = require('discovery') +local commands = require('commands') +local config = require('config') +local lifecycles = require('lifecycles') +local connection_monitor = require('connection_monitor') + +-- Driver definition +local driver = Driver("ABB.SCU200", { + discovery = discovery.start, + lifecycle_handlers = lifecycles, + capability_handlers = { + -- Refresh command handler + [caps.refresh.ID] = { + [caps.refresh.commands.refresh.NAME] = commands.refresh + }, + [caps.switch.ID] = { + [caps.switch.commands.on.NAME] = commands.switch_on, + [caps.switch.commands.off.NAME] = commands.switch_off + } + } +}) + +-- Prepare datastores for bridge and thing discovery caches +if driver.datastore.bridge_discovery_cache == nil then + driver.datastore.bridge_discovery_cache = {} +end + +if driver.datastore.thing_discovery_cache == nil then + driver.datastore.thing_discovery_cache = {} +end + +-- Connection monitoring thread +driver:call_on_schedule(config.BRIDGE_CONN_MONITOR_INTERVAL, connection_monitor.monitor_connections, "SCU200 Bridge connection monitoring thread") + +-- Initialize driver +log.info("Starting driver") + +driver:run() + +log.warn("Exiting driver") \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/lifecycles.lua b/drivers/ABB/insite-scu200/src/lifecycles.lua new file mode 100644 index 0000000000..83cdfa0eb7 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/lifecycles.lua @@ -0,0 +1,87 @@ +local log = require('log') + +-- Local imports +local fields = require('fields') +local utils = require('utils') +local discovery = require('discovery') +local commands = require('commands') +local connection_monitor = require('connection_monitor') +local eventsource_handler = require("eventsource_handler") + +-- Lifecycles handlers for the driver +local lifecycles = {} + +-- Lifecycle handler for a device which has been initialized +function lifecycles.init(driver, device) + local dni, device_type = utils.get_dni_from_device(device) + + -- Verify if the device has already been initialized + if device:get_field(fields._INIT) then + log.info("lifecycles.init(): Device already initialized: " .. dni .. " of type: " .. device_type) + return + end + + log.info("lifecycles.init(): Initializing device: " .. dni .. " of type: " .. device_type) + + if device_type == fields.DEVICE_TYPE_BRIDGE then + if driver.datastore.bridge_discovery_cache[dni] then + log.debug("lifecycles.init(): Setting unsaved bridge fields") + discovery.set_device_fields(driver, device) + end + + local bridge_ip = device:get_field(fields.BRIDGE_IPV4) + + connection_monitor.update_connection(driver, device, bridge_ip) + elseif device_type == fields.DEVICE_TYPE_THING then + if driver.datastore.thing_discovery_cache[dni] then + log.debug("lifecycles.init(): Setting unsaved thing fields") + discovery.set_device_fields(driver, device) + end + + -- Refresh the device manually + commands.refresh(driver, device, nil) + + -- Refresh schedule + local refresh_period = utils.get_thing_refresh_period(device) + + device.thread:call_on_schedule( + refresh_period, + function () + return commands.refresh(driver, device, nil) + end, + "Refresh schedule") + end + + -- Set the device as initialized + device:set_field(fields._INIT, true, {persist = false}) +end + +-- Lifecycle handler for a device which has been added +function lifecycles.added(driver, device) + local dni, device_type = utils.get_dni_from_device(device) + log.info("lifecycles.added(): Adding device: " .. dni .. " of type: " .. device_type) + + -- Force the initialization due to cases where the device is not initialized after being added + lifecycles.init(driver, device) +end + +-- Lifecycle handler for a device which has been removed +function lifecycles.removed(driver, device) + local dni, device_type = utils.get_dni_from_device(device) + log.info("lifecycles.removed(): Removing device: " .. dni .. " of type: " .. device_type) + + if device_type == fields.DEVICE_TYPE_BRIDGE then + log.debug("lifecycles.removed(): Closing SSE for device: " .. dni) + + eventsource_handler.close_sse(driver, device) + elseif device_type == fields.DEVICE_TYPE_THING then + log.debug("lifecycles.removed(): Removing schedules for device: " .. dni) + + -- Remove the schedules to avoid unnecessary CPU processing + for timer in pairs(device.thread.timers) do + device.thread:cancel_timer(timer) + end + end +end + +return lifecycles \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/lunchbox/init.lua b/drivers/ABB/insite-scu200/src/lunchbox/init.lua new file mode 100644 index 0000000000..d454f39e68 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/lunchbox/init.lua @@ -0,0 +1,4 @@ +local RestClient = require "lunchbox.rest" +local EventSource = require "lunchbox.sse.eventsource" + +return {RestClient = RestClient, EventSource = EventSource} \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/lunchbox/rest.lua b/drivers/ABB/insite-scu200/src/lunchbox/rest.lua new file mode 100644 index 0000000000..b7c03525a6 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/lunchbox/rest.lua @@ -0,0 +1,427 @@ +---@class ChunkedResponse : Response +---@field package _received_body boolean +---@field package _parsed_headers boolean +---@field public new fun(status_code: number, socket: table?): ChunkedResponse +---@field public fill_body fun(self: ChunkedResponse): string? +---@field public append_body fun(self: ChunkedResponse, next_chunk_body: string): ChunkedResponse + +local socket = require "cosock.socket" +local utils = require "utils" +local lb_utils = require "lunchbox.util" +local Request = require "luncheon.request" +local Response = require "luncheon.response" --[[@as ChunkedResponse]] +local json = require('dkjson') + +local api_version = require("version").api + +local RestCallStates = { + SEND = "Send", + RECEIVE = "Receive", + RETRY = "Retry", + RECONNECT = "Reconnect", + COMPLETE = "Complete", +} + +local function connect(client) + local port = 80 + local use_ssl = false + + if client.base_url.scheme == "https" then + port = 443 + use_ssl = true + end + + if client.base_url.port ~= port then port = client.base_url.port end + local sock, err = client.socket_builder(client.base_url.host, port, use_ssl) + + if sock == nil then + client.socket = nil + return false, err + end + + client.socket = sock + return true +end + +local function reconnect(client) + if client.socket ~= nil then + client.socket:close() + client.socket = nil + end + return connect(client) +end + +---comment +---@param client RestClient +---@param request Request +---@return integer? bytes_sent +---@return string? err_msg +---@return integer idx +local function send_request(client, request) + if client.socket == nil then + return nil, "no socket available", 0 + end + local payload = request:serialize() + + local bytes, err, idx = nil, nil, 0 + + repeat bytes, err, idx = client.socket:send(payload, idx + 1, #payload) until (bytes == #payload) + or (err ~= nil) + + return bytes, err, idx +end + +local function parse_chunked_response(original_response, sock) + local ChunkedTransferStates = { + EXPECTING_CHUNK_LENGTH = "ExpectingChunkLength", + EXPECTING_BODY_CHUNK = "ExpectingBodyChunk", + } + + local full_response = Response.new(original_response.status, nil) --[[@as ChunkedResponse]] + + for header in original_response.headers:iter() do full_response.headers:append_chunk(header) end + + local original_body, err = original_response:get_body() + if type(original_body) ~= "string" or err ~= nil then + return original_body, (err or "unexpected nil in error position") + end + local next_chunk_bytes = tonumber(original_body, 16) + local next_chunk_body = "" + local bytes_read = 0; + + local state = ChunkedTransferStates.EXPECTING_BODY_CHUNK + + repeat + local pat = nil + local next_recv, next_err, partial = nil, nil, nil + + if state == ChunkedTransferStates.EXPECTING_BODY_CHUNK then + pat = next_chunk_bytes + else + pat = "*l" + end + + next_recv, next_err, partial = sock:receive(pat) + + if next_err ~= nil then + if string.lower(next_err) == "closed" then + if partial ~= nil and #partial >= 1 then + full_response:append_body(partial) + next_chunk_bytes = 0 + else + return nil, next_err + end + else + return nil, ("unexpected error reading chunked transfer: " .. next_err) + end + end + + if next_recv ~= nil and #next_recv >= 1 then + if state == ChunkedTransferStates.EXPECTING_BODY_CHUNK then + bytes_read = bytes_read + #next_recv + next_chunk_body = next_chunk_body .. next_recv + + if bytes_read >= next_chunk_bytes then + full_response = full_response:append_body(next_chunk_body) + next_chunk_body = "" + bytes_read = 0 + + state = ChunkedTransferStates.EXPECTING_CHUNK_LENGTH + end + elseif state == ChunkedTransferStates.EXPECTING_CHUNK_LENGTH then + next_chunk_bytes = tonumber(next_recv, 16) + + state = ChunkedTransferStates.EXPECTING_BODY_CHUNK + end + end + until next_chunk_bytes == 0 + + local _ = sock:receive("*l") -- clear the trailing CRLF + + full_response._received_body = true + full_response._parsed_headers = true + + return full_response +end + +local function recv_additional_response(original_response, sock) + local full_response = Response.new(original_response.status, nil) + local headers = original_response:get_headers() + local content_length_str = headers:get_one("Content-Length") + local content_length = nil + local bytes_read = 0 + if content_length_str then + content_length = math.tointeger(content_length_str) + end + + local next_recv, next_err, partial + + repeat + next_recv, next_err, partial = sock:receive(content_length - bytes_read) + + if next_recv ~= nil and #next_recv >= 1 then + full_response:append_body(next_recv) + bytes_read = bytes_read + #next_recv + end + + if partial ~= nil and #partial >= 1 then + full_response:append_body(partial) + bytes_read = bytes_read + #partial + end + until next_err == "closed" or bytes_read >= content_length + + full_response._received_body = true + full_response._parsed_headers = true + + return full_response +end + +local function handle_response(sock) + if api_version >= 9 then + local response, err = Response.tcp_source(sock) + if err or (not response) then return response, (err or "unknown error") end + return response, response:fill_body() + end + -- called select right before passing in so we receive immediately + local initial_recv, initial_err, partial = Response.source(function() return sock:receive('*l') end) + + local full_response = nil + + if initial_recv ~= nil then + local headers = initial_recv:get_headers() + + if headers:get_one("Content-Length") then + full_response = recv_additional_response(initial_recv, sock) + elseif headers and headers:get_one("Transfer-Encoding") == "chunked" then + local response, err = parse_chunked_response(initial_recv, sock) + if err ~= nil then + return nil, err + end + full_response = response + else + full_response = initial_recv + end + + return full_response + else + return nil, initial_err, partial + end +end + +local function execute_request(client, request, retry_fn) + if not client._active then + return nil, "Called `execute request` on a terminated REST Client", nil + end + + if client.socket == nil then + local success, err = connect(client) + if not success then return nil, err, nil end + end + + local should_retry = retry_fn + + if type(should_retry) ~= "function" then + should_retry = function() return false end + end + + -- send output + local _bytes_sent, send_err, _idx = nil, nil, 0 + -- recv output + local response, recv_err, partial = nil, nil, nil + -- return values + local ret, err = nil, nil + + local backoff = utils.backoff_builder(60, 1, 0.1) + local current_state = RestCallStates.SEND + + repeat + local retry = should_retry() + if current_state == RestCallStates.SEND then + backoff = utils.backoff_builder(60, 1, 0.1) + _bytes_sent, send_err, _idx = send_request(client, request) + + if not send_err then + current_state = RestCallStates.RECEIVE + elseif retry then + if string.lower(send_err) == "closed" or string.lower(send_err):match("broken pipe") then + current_state = RestCallStates.RECONNECT + else + current_state = RestCallStates.RETRY + end + else + ret = nil + err = send_err + current_state = RestCallStates.COMPLETE + end + elseif current_state == RestCallStates.RECEIVE then + response, recv_err, partial = handle_response(client.socket) + + if not recv_err then + ret = response + err = nil + current_state = RestCallStates.COMPLETE + elseif retry then + if string.lower(recv_err) == "closed" or string.lower(recv_err):match("broken pipe") then + current_state = RestCallStates.RECONNECT + else + current_state = RestCallStates.RETRY + end + else + ret = nil + err = recv_err + current_state = RestCallStates.COMPLETE + end + elseif current_state == RestCallStates.RECONNECT then + local success, reconn_err = reconnect(client) + if success then + current_state = RestCallStates.RETRY + elseif not retry then + ret = nil + err = reconn_err + current_state = RestCallStates.COMPLETE + else + socket.sleep(backoff()) + end + elseif current_state == RestCallStates.RETRY then + bytes_sent, send_err, _idx = nil, nil, 0 + response, recv_err, partial = nil, nil, nil + current_state = RestCallStates.SEND + socket.sleep(backoff()) + end + until current_state == RestCallStates.COMPLETE + + return ret, err, partial +end + +---@class RestClient +--- +---@field base_url table `net.url` URL table +---@field socket table `cosock` TCP socket +local RestClient = {} +RestClient.__index = RestClient + +function RestClient.one_shot_get(full_url, additional_headers, socket_builder) + local url_table = lb_utils.force_url_table(full_url) + + local query_params = "" + if url_table.query ~= nil then + query_params = "?" + + for param, value in pairs(url_table.query) do + query_params = query_params .. param .. "=" .. value .. "&" + end + + query_params = query_params:sub(1, -2) + end + + local client = RestClient.new(url_table.scheme .. "://" .. url_table.authority, socket_builder) + local ret, err = client:get(url_table.path .. query_params, additional_headers) + + client:shutdown() + + return ret, err +end + +function RestClient.one_shot_post(full_url, body, additional_headers, socket_builder) + local url_table = lb_utils.force_url_table(full_url) + + local query_params = "" + if url_table.query ~= nil then + query_params = "?" + + for param, value in pairs(url_table.query) do + query_params = query_params .. param .. "=" .. value .. "&" + end + + query_params = query_params:sub(1, -2) + end + + if type(body) == "table" then + body = json.encode(body) + end + + local client = RestClient.new(url_table.scheme .. "://" .. url_table.authority, socket_builder) + local ret, err = client:post(url_table.path .. query_params, body, additional_headers) + + client:shutdown() + + return ret, err +end + +function RestClient:close_socket() + if self.socket ~= nil and self._active then + self.socket:close() + self.socket = nil + end +end + +function RestClient:shutdown() + self:close_socket() + self._active = false +end + +function RestClient:update_base_url(new_url) + if self.socket ~= nil then + self.socket:close() + self.socket = nil + end + + self.base_url = lb_utils.force_url_table(new_url) +end + +function RestClient:get(path, additional_headers, retry_fn) + local request = Request.new("GET", path, nil):add_header( + "user-agent", "smartthings-lua-edge-driver" + ):add_header("host", string.format("%s", self.base_url.host)):add_header( + "connection", "keep-alive" + ) + + if additional_headers ~= nil and type(additional_headers) == "table" then + for k, v in pairs(additional_headers) do request = request:add_header(k, v) end + end + + return execute_request(self, request, retry_fn) +end + +function RestClient:post(path, body_string, additional_headers, retry_fn) + local request = Request.new("POST", path, nil):add_header( + "user-agent", "smartthings-lua-edge-driver" + ):add_header("host", string.format("%s", self.base_url.host)):add_header( + "connection", "keep-alive" + ) + + if additional_headers ~= nil and type(additional_headers) == "table" then + for k, v in pairs(additional_headers) do request = request:add_header(k, v) end + end + + request = request:append_body(body_string) + + return execute_request(self, request, retry_fn) +end + +function RestClient:put(path, body_string, additional_headers, retry_fn) + local request = Request.new("PUT", path, nil):add_header( + "user-agent", "smartthings-lua-edge-driver" + ):add_header("host", string.format("%s", self.base_url.host)):add_header( + "connection", "keep-alive" + ) + + if additional_headers ~= nil and type(additional_headers) == "table" then + for k, v in pairs(additional_headers) do request = request:add_header(k, v) end + end + + request = request:append_body(body_string) + + return execute_request(self, request, retry_fn) +end + +function RestClient.new(base_url, sock_builder) + base_url = lb_utils.force_url_table(base_url) + + if type(sock_builder) ~= "function" then sock_builder = utils.labeled_socket_builder() end + + return + setmetatable({base_url = base_url, socket_builder = sock_builder, socket = nil, _active = true}, RestClient) +end + +return RestClient \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/lunchbox/sse/eventsource.lua b/drivers/ABB/insite-scu200/src/lunchbox/sse/eventsource.lua new file mode 100644 index 0000000000..d9a5930df2 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/lunchbox/sse/eventsource.lua @@ -0,0 +1,523 @@ +local cosock = require "cosock" +local socket = require "cosock.socket" +local ssl = require "cosock.ssl" +local json = require "dkjson" + +local log = require "log" +local util = require "lunchbox.util" +local Request = require "luncheon.request" +local Response = require "luncheon.response" + +--- A pure Lua implementation of the EventSource interface. +--- The EventSource interface represents the client end of an HTTP(S) +--- connection that receives an event stream following the Server-Sent events +--- specification. +--- +--- MDN Documentation for EventSource: https://developer.mozilla.org/en-US/docs/Web/API/EventSource +--- HTML Spec: https://html.spec.whatwg.org/multipage/server-sent-events.html +--- +--- @class EventSource +--- @field public url table A `net.url` table representing the URL for the connection +--- @field public ready_state number Enumeration of the ready states outlined in the spec. +--- @field public onopen function in-line callback for on-open events +--- @field public onmessage function in-line callback for on-message events +--- @field public onerror function in-line callback for on-error events; error callbacks will fire +--- @field package _reconnect boolean flag that says whether or not the client should attempt to reconnect on close. +--- @field package _reconnect_time_millis number The amount of time to wait between reconnects, in millis. Can be sent by the server. +--- @field package _sock_builder function|nil optional. If this function exists, it will be called to create a new TCP socket on connection. +--- @field package _sock table? the TCP socket for the connection +--- @field package _needs_more boolean flag to track whether or not we're still expecting mroe on this source before we dispatch +--- @field package _last_field string the last field the parsing path saw, in case it needs to append more to its value +--- @field package _extra_headers table a table of string:string key-value pairs that will be inserted in to the initial requests's headers. +--- @field package _parse_buffers table inner state, keeps track of the various event stream buffers in between dispatches. +--- @field package _listeners table event listeners attached using the add_event_listener API instead of the inline callbacks. +local EventSource = {} +EventSource.__index = EventSource + +--- The Ready States that an EventSource can be in. We use base 0 to match the specification. +EventSource.ReadyStates = util.read_only { + CONNECTING = 0, -- The connection has not yet been established + OPEN = 1, -- The connection is open + CLOSED = 2 -- The connection has closed +} + +--- The event types supported by this source, patterned after their values in JavaScript. +EventSource.EventTypes = util.read_only { + ON_OPEN = "open", + ON_MESSAGE = "message", + ON_ERROR = "error", +} + +--- Helper function that creates the initial Request to start the stream. +--- @function create_request +--- @local +--- @param url_table table a net.url table +--- @param extra_headers table a set of key/value pairs (strings) to capture any extra HTTP headers needed +--- @param body table a set of key/value pairs (strings) to be sent as the body of the initial POST request. +local function create_request(url_table, extra_headers, body) + local request = Request.new("POST", url_table.path, nil) + :add_header("user-agent", "smartthings-lua-edge-driver") + :add_header("host", string.format("%s", url_table.host)) + :add_header("connection", "keep-alive") + :add_header("accept", "text/event-stream") + :add_header("content-type", "application/json") + + if type(extra_headers) == "table" then + for k, v in pairs(extra_headers) do + request = request:add_header(k, v) + end + end + + local encoded_body = json.encode(body) + request = request:append_body(encoded_body) + + return request +end + +--- Helper function to send the request and kick off the stream. +--- @function send_stream_start_request +--- @local +--- @param payload string the entire string buffer to send +--- @param sock table the TCP socket to send it over +local function send_stream_start_request(payload, sock) + local bytes, err, idx = nil, nil, 0 + + repeat + bytes, err, idx = sock:send(payload, idx + 1, #payload) + until (bytes == #payload) or (err ~= nil) + + if err then + log.error_with({ hub_logs = true }, "send error: " .. err) + end + + return bytes, err, idx +end + +--- Helper function to create an table representing an event from the source's parse buffers. +--- @function make_event +--- @local +--- @param source EventSource +local function make_event(source) + local event_type = nil + + if #source._parse_buffers["event"] > 0 then + event_type = source._parse_buffers["event"] + end + + return { + type = event_type or "message", + data = source._parse_buffers["data"], + origin = source.url.scheme .. "://" .. source.url.host, + lastEventId = source._parse_buffers["id"] + } +end + +--- SSE spec for dispatching an event: +--- https://html.spec.whatwg.org/multipage/server-sent-events.html#dispatchMessage +--- @function dispatch_event +--- @local +--- @param source EventSource +local function dispatch_event(source) + local data_buffer = source._parse_buffers["data"] + local is_blank_line = data_buffer ~= nil and + (#data_buffer == 0) or + data_buffer == "\n" or + data_buffer == "\r" or + data_buffer == "\r\n" + if data_buffer ~= nil and not is_blank_line then + local event = util.read_only(make_event(source)) + + if type(source.onmessage) == "function" then + source.onmessage(event) + end + + for _, listener in ipairs(source._listeners[EventSource.EventTypes.ON_MESSAGE]) do + if type(listener) == "function" then + listener(event) + end + end + end + + source._parse_buffers["event"] = "" + source._parse_buffers["data"] = "" +end + +local valid_fields = util.read_only { + ["event"] = true, + ["data"] = true, + ["id"] = true, + ["retry"] = true +} + +-- An event stream "line" can end in more than one way; from the spec: +-- Lines must be separated by either +-- a U+000D CARRIAGE RETURN U+000A LINE FEED (CRLF) character pair, +-- a single U+000A LINE FEED (LF) character, +-- or a single U+000D CARRIAGE RETURN (CR) character. +-- +-- util.iter_string_lines won't suffice here because: +-- a.) it assumes \n, and +-- b.) it doesn't differentiate between a "line" that ends without a newline and one that does. +-- +-- h/t to github.com/FreeMasen for the suggestions on the efficient implementation of this +local function find_line_endings(chunk) + local r_idx, n_idx = string.find(chunk, "[\r\n]+") + if r_idx == nil or r_idx == n_idx then + -- 1 character or no match + return r_idx, n_idx + end + local slice = string.sub(chunk, r_idx, n_idx) + if slice == "\r\n" then + return r_idx, n_idx + end + -- invalid multi character match, return first character only + return r_idx, r_idx +end + +local function event_lines(chunk) + local remaining = chunk + local line_end, rn_end + local remainder_sent = false + return function() + line_end, rn_end = find_line_endings(remaining) + if not line_end then + if remainder_sent or (not remaining) or #remaining == 0 then + return nil + else + remainder_sent = true + return remaining, false + end + end + local next_line = string.sub(remaining, 1, line_end - 1) + remaining = string.sub(remaining, rn_end + 1) + return next_line, true + end +end +--- SSE spec for interpreting an event stream: +--- https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface +--- @function parse +--- @local +--- @param source EventSource +--- @param recv string the received payload from the last socket receive +local function sse_parse_chunk(source, recv) + for line, complete in event_lines(recv) do + if not source._needs_more and (#line == 0 or (not line:match("([%w%p]+)"))) then -- empty/blank lines indicate dispatch + dispatch_event(source) + elseif source._needs_more then + local append = line + if source._last_field == "data" and complete then append = append .. "\n" end + if complete then source._needs_more = false end + source._parse_buffers[source._last_field] = source._parse_buffers[source._last_field] .. append + else + if line:sub(1, 1) ~= ":" then -- ignore any complete lines that start w/ a colon + local matches = line:gmatch("(%w*)(:*)(.*)") -- colon after field is optional, in that case it's a field w/ no value + + for field, _colon, value in matches do + value = value:gsub("^[^%g]", "", 1) -- trim a single leading space character + + if valid_fields[field] then + source._last_field = field + if field == "retry" then + local new_time = tonumber(value, 10) + if type(new_time) == "number" then + source._reconnect_time_millis = new_time + end + elseif field == "data" then + local append = (value or "") + -- if complete then append = append .. "\n" end + source._parse_buffers[field] = source._parse_buffers[field] .. append + elseif field == "id" then + -- skip ID's if they contain the NULL character + if not string.find(value, '\0') then + source._parse_buffers[field] = value + end + else + source._parse_buffers[field] = value + end + end + source._needs_more = source._needs_more or (not complete) + end + end + end + end +end + +--- Helper function that captures the cyclic logic of the EventSource while in the CONNECTING state. +--- @function connecting_action +--- @local +--- @param source EventSource +local function connecting_action(source) + if not source._sock then + if type(source._sock_builder) == "function" then + source._sock = source._sock_builder() + else + source._sock, err = socket.tcp() + if err ~= nil then return nil, err end + + _, err = source._sock:settimeout(60) + if err ~= nil then return nil, err end + + _, err = source._sock:connect(source.url.host, source.url.port) + if err ~= nil then return nil, err end + + _, err = source._sock:setoption("keepalive", true) + if err ~= nil then return nil, err end + + if source.url.scheme == "https" then + source._sock, err = ssl.wrap(source._sock, { + mode = "client", + protocol = "any", + verify = "none", + options = "all" + }) + if err ~= nil then return nil, err end + + _, err = source._sock:dohandshake() + if err ~= nil then return nil, err end + end + end + end + + local request = create_request(source.url, source._extra_headers, source._body) + + local last_event_id = source._parse_buffers["id"] + + if last_event_id ~= nil and #last_event_id > 0 then + request = request:add_header("Last-Event-ID", last_event_id) + end + + local _, err, _ = send_stream_start_request(request:serialize(), source._sock) + + if err ~= nil then + return nil, err + end + + local response + response, err = Response.tcp_source(source._sock) + + if not response or err ~= nil then + return nil, err or "nil response from Response.tcp_source" + end + + if response.status ~= 200 then + return nil, "Server responded with status other than 200 OK", { response.status, response.status_msg } + end + + local headers, err = response:get_headers() + if err ~= nil then + return nil, err + end + local content_type = string.lower((headers and headers:get_one('content-type') or "none")) + if not content_type:find("text/event-stream", 1, true) then + local err_msg = "Expected content type of text/event-stream in response headers, received: " .. content_type + return nil, err_msg + end + + source.ready_state = EventSource.ReadyStates.OPEN + + if type(source.onopen) == "function" then + source.onopen() + end + + for _, listener in ipairs(source._listeners[EventSource.EventTypes.ON_OPEN]) do + if type(listener) == "function" then + listener() + end + end +end +--- Helper function that captures the cyclic logic of the EventSource while in the OPEN state. +--- @function open_action +--- @local +--- @param source EventSource +local function open_action(source) + local recv, err, partial = source._sock:receive('*l') + + if err then + --- connection is fine but there was nothing + --- to be read from the other end so we just + --- early return. + if err == "timeout" or err == "wantread" then + return + else + --- real error, close the connection. + source._sock:close() + source._sock = nil + source.ready_state = EventSource.ReadyStates.CLOSED + return nil, err, partial + end + end + + -- the number of bytes to read per the chunked encoding spec + local recv_as_num = tonumber(recv, 16) + + if recv_as_num ~= nil and recv_as_num == 0 then + return -- the stream has ended + end + + if recv_as_num ~= nil then + recv, err, partial = source._sock:receive(recv_as_num) + if err then + if err == "timeout" or err == "wantread" then + return + else + --- real error, close the connection. + source._sock:close() + source._sock = nil + source.ready_state = EventSource.ReadyStates.CLOSED + return nil, err, partial + end + end + + local _, err, partial = source._sock:receive('*l') -- clear the final line + + if err then + if err == "timeout" or err == "wantread" then + return + else + --- real error, close the connection. + source._sock:close() + source._sock = nil + source.ready_state = EventSource.ReadyStates.CLOSED + return nil, err, partial + end + end + sse_parse_chunk(source, recv) + else + local recv_dbg = recv or "" + if #recv_dbg == 0 then + recv_dbg = "" + end + recv_dbg = recv_dbg:gsub("\r\n", ""):gsub("\n", ""):gsub("\r", "") + log.error_with({ hub_logs = true }, string.format("Received %s while expecting a chunked encoding payload length (hex number)\n", recv_dbg)) + end +end + +--- Helper function that captures the cyclic logic of the EventSource while in the CLOSED state. +--- @function closed_action +--- @local +--- @param source EventSource +local function closed_action(source) + if source._sock ~= nil then + source._sock:close() + source._sock = nil + end + + if source._reconnect then + if type(source.onerror) == "function" then + source.onerror() + end + + for _, listener in ipairs(source._listeners[EventSource.EventTypes.ON_ERROR]) do + if type(listener) == "function" then + listener() + end + end + + local sleep_time_secs = source._reconnect_time_millis / 1000.0 + socket.sleep(sleep_time_secs) + + source.ready_state = EventSource.ReadyStates.CONNECTING + end +end + +local state_actions = { + [EventSource.ReadyStates.CONNECTING] = connecting_action, + [EventSource.ReadyStates.OPEN] = open_action, + [EventSource.ReadyStates.CLOSED] = closed_action +} + +--- Create a new EventSource. The only required parameter is the URL, which can +--- be a string or a net.url table. The string form will be converted to a net.url table. +--- +--- @param url string|table a string or a net.url table representing the complete URL (minimally a scheme/host/path, port optional) for the event stream. +--- @param extra_headers table|nil an optional table of key-value pairs (strings) to be added to the initial POST request +--- @param body table|nil an optional table of key-value pairs (strings) to be sent as the body of the initial POST request +--- @param sock_builder function|nil an optional function to be used to create the TCP socket for the stream. If nil, a set of defaults will be used to create a new TCP socket. +--- @return EventSource a new EventSource +function EventSource.new(url, extra_headers, body, sock_builder) + local url_table = util.force_url_table(url) + + if not url_table.port then + if url_table.scheme == "http" then + url_table.port = 80 + elseif url_table.scheme == "https" then + url_table.port = 443 + end + end + + local sock = nil + + if type(sock_builder) == "function" then + sock = sock_builder() + end + + local source = setmetatable({ + url = url_table, + ready_state = EventSource.ReadyStates.CONNECTING, + onopen = nil, + onmessage = nil, + onerror = nil, + _needs_more = false, + _last_field = nil, + _reconnect = true, + _reconnect_time_millis = 15 * 1000, + _sock_builder = sock_builder, + _sock = sock, + _extra_headers = extra_headers, + _body = body, + _parse_buffers = { + ["data"] = "", + ["id"] = "", + ["event"] = "", + }, + _listeners = { + [EventSource.EventTypes.ON_OPEN] = {}, + [EventSource.EventTypes.ON_MESSAGE] = {}, + [EventSource.EventTypes.ON_ERROR] = {} + }, + }, EventSource) + + cosock.spawn(function() + local st_utils = require "st.utils" + while true do + if source.ready_state == EventSource.ReadyStates.CLOSED and not source._reconnect then + return + end + local _, action_err, partial = state_actions[source.ready_state](source) + if action_err ~= nil then + if action_err ~= "timeout" or action_err ~= "wantread" then + log.error_with({ hub_logs = true }, "Event Source Coroutine State Machine error: " .. action_err) + if partial ~= nil and #partial > 0 then + log.error_with({ hub_logs = true }, st_utils.stringify_table(partial, "\tReceived Partial", true)) + end + source.ready_state = EventSource.ReadyStates.CLOSED + end + end + end + end) + + return source +end + +--- Close the event source, signalling that a reconnect is not desired +function EventSource:close() + self._reconnect = false + if self._sock ~= nil then + self._sock:close() + end + self._sock = nil + self.ready_state = EventSource.ReadyStates.CLOSED +end + +--- Add a callback to the event source +---@param listener_type string One of "message", "open", or "error" +---@param listener function the callback to be called in case of an event. Open and Error events have no payload. The message event will have a single argument, a table. +function EventSource:add_event_listener(listener_type, listener) + local list = self._listeners[listener_type] + + if list then + table.insert(list, listener) + end +end + +return EventSource \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/lunchbox/util.lua b/drivers/ABB/insite-scu200/src/lunchbox/util.lua new file mode 100644 index 0000000000..6eb94407eb --- /dev/null +++ b/drivers/ABB/insite-scu200/src/lunchbox/util.lua @@ -0,0 +1,46 @@ +local net_url = require "net.url" + +local util = {} + +util.force_url_table = function(url) + if type(url) ~= "table" then url = net_url.parse(url) end + + if not url.port then + if url.scheme == "http" then + url.port = 80 + elseif url.scheme == "https" then + url.port = 443 + end + end + + return url +end + +util.read_only = function(tbl) + if type(tbl) == "table" then + local proxy = {} + local mt = { -- create metatable + __index = tbl, + __newindex = function(t, k, v) error("attempt to update a read-only table", 2) end, + } + setmetatable(proxy, mt) + return proxy + else + return tbl + end +end + +util.iter_string_lines = function(str) + if str:sub(-1) ~= "\n" then str = str .. "\n" end + + return str:gmatch("(.-)\n") +end + +util.copy_data = function(tbl) + local ret = {} + for k, v in pairs(tbl) do ret[k] = v end + + return ret +end + +return util \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/utils.lua b/drivers/ABB/insite-scu200/src/utils.lua new file mode 100644 index 0000000000..139b5503c5 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/utils.lua @@ -0,0 +1,219 @@ +local log = require("log") +local socket = require "cosock.socket" +local ssl = require "cosock.ssl" + +-- Local imports +local config = require("config") +local fields = require("fields") + +-- Utility functions for the SmartThings edge driver +local utils = {} + +-- Get the device id from the device +function utils.get_dni_from_device(device) + if device.parent_assigned_child_key then + local thing_dni = device.parent_assigned_child_key + + return thing_dni, fields.DEVICE_TYPE_THING + else + local bridge_dni = device.device_network_id + + return bridge_dni, fields.DEVICE_TYPE_BRIDGE + end +end + +-- Get the device ID +function utils.get_device_id_from_device(device) + return device.st_store.id +end + +-- Get the device model +function utils.get_device_model(device) + local thing_info = device:get_field(fields.THING_INFO) + + if thing_info == nil then + return nil + end + + return thing_info.type +end + +-- Get the device IP address +function utils.get_device_ip_address(device) + local _, device_type = utils.get_dni_from_device(device) + + if device_type == fields.DEVICE_TYPE_BRIDGE then + return device:get_field(fields.BRIDGE_IPV4) + else + local bridge = device:get_parent_device() + + return bridge:get_field(fields.BRIDGE_IPV4) + end +end + +-- Method for getting edge child device version by type +function utils.get_edge_child_device_version(edge_child_device_type) + local edge_child_device_versions = { + [config.EDGE_CHILD_CURRENT_SENSOR_TYPE] = config.EDGE_CHILD_CURRENT_SENSOR_VERSION, + [config.EDGE_CHILD_ENERGY_METER_MODULE_TYPE] = config.EDGE_CHILD_ENERGY_METER_MODULE_VERSION, + [config.EDGE_CHILD_AUXILIARY_CONTACT_TYPE] = config.EDGE_CHILD_AUXILIARY_CONTACT_VERSION, + [config.EDGE_CHILD_OUTPUT_MODULE_TYPE] = config.EDGE_CHILD_OUTPUT_MODULE_VERSION, + [config.EDGE_CHILD_ENERGY_METER_TYPE] = config.EDGE_CHILD_ENERGY_METER_VERSION, + [config.EDGE_CHILD_WATER_METER_TYPE] = config.EDGE_CHILD_WATER_METER_VERSION, + [config.EDGE_CHILD_GAS_METER_TYPE] = config.EDGE_CHILD_GAS_METER_VERSION, + [config.EDGE_CHILD_USB_ENERGY_METER_TYPE] = config.EDGE_CHILD_USB_ENERGY_METER_VERSION + } + + return edge_child_device_versions[edge_child_device_type] +end + +-- Method for getting the thing exact type +function utils.get_thing_exact_type(edge_child_device_type) + local device_version = utils.get_edge_child_device_version(edge_child_device_type) + + if device_version == nil then + return nil + end + + return config.MANUFACTURER .. "_" .. config.BRIDGE_TYPE .. "_" .. edge_child_device_type .. "_" .. device_version +end + +-- Method for getting the thing profile reference +function utils.get_thing_profile_ref(thing_info) + if thing_info.type == utils.get_thing_exact_type(config.EDGE_CHILD_CURRENT_SENSOR_TYPE) then + if thing_info.properties.isExport then + return config.EDGE_CHILD_CURRENT_SENSOR_PRODUCTION_PROFILE + else + return config.EDGE_CHILD_CURRENT_SENSOR_CONSUMPTION_PROFILE + end + end + + local thing_profiles = { + [utils.get_thing_exact_type(config.EDGE_CHILD_ENERGY_METER_MODULE_TYPE)] = config.EDGE_CHILD_ENERGY_METER_PROFILE, + [utils.get_thing_exact_type(config.EDGE_CHILD_AUXILIARY_CONTACT_TYPE)] = config.EDGE_CHILD_AUXILIARY_CONTACT_PROFILE, + [utils.get_thing_exact_type(config.EDGE_CHILD_OUTPUT_MODULE_TYPE)] = config.EDGE_CHILD_OUTPUT_MODULE_PROFILE, + [utils.get_thing_exact_type(config.EDGE_CHILD_ENERGY_METER_TYPE)] = config.EDGE_CHILD_ENERGY_METER_PROFILE, + [utils.get_thing_exact_type(config.EDGE_CHILD_WATER_METER_TYPE)] = config.EDGE_CHILD_WATER_METER_PROFILE, + [utils.get_thing_exact_type(config.EDGE_CHILD_GAS_METER_TYPE)] = config.EDGE_CHILD_GAS_METER_PROFILE, + [utils.get_thing_exact_type(config.EDGE_CHILD_USB_ENERGY_METER_TYPE)] = config.EDGE_CHILD_USB_ENERGY_METER_PROFILE + } + + return thing_profiles[thing_info.type] +end + +-- Method for getting the thing refresh period +function utils.get_thing_refresh_period(device) + local device_model = utils.get_device_model(device) + + local thing_refresh_periods = { + [utils.get_thing_exact_type(config.EDGE_CHILD_CURRENT_SENSOR_TYPE)] = config.EDGE_CHILD_CURRENT_SENSOR_REFRESH_PERIOD, + [utils.get_thing_exact_type(config.EDGE_CHILD_ENERGY_METER_MODULE_TYPE)] = config.EDGE_CHILD_ENERGY_METER_MODULE_REFRESH_PERIOD, + [utils.get_thing_exact_type(config.EDGE_CHILD_AUXILIARY_CONTACT_TYPE)] = config.EDGE_CHILD_AUXILIARY_CONTACT_REFRESH_PERIOD, + [utils.get_thing_exact_type(config.EDGE_CHILD_OUTPUT_MODULE_TYPE)] = config.EDGE_CHILD_OUTPUT_MODULE_REFRESH_PERIOD, + [utils.get_thing_exact_type(config.EDGE_CHILD_ENERGY_METER_TYPE)] = config.EDGE_CHILD_ENERGY_METER_REFRESH_PERIOD, + [utils.get_thing_exact_type(config.EDGE_CHILD_WATER_METER_TYPE)] = config.EDGE_CHILD_WATER_METER_REFRESH_PERIOD, + [utils.get_thing_exact_type(config.EDGE_CHILD_GAS_METER_TYPE)] = config.EDGE_CHILD_GAS_METER_REFRESH_PERIOD, + [utils.get_thing_exact_type(config.EDGE_CHILD_USB_ENERGY_METER_TYPE)] = config.EDGE_CHILD_USB_ENERGY_METER_REFRESH_PERIOD + } + + return thing_refresh_periods[device_model] +end + +-- Method for dumping a table to string +function utils.dump(o) + if type(o) == "table" then + local s = '{' + + for k,v in pairs(o) do + if type(k) ~= "number" then k = '"'..k..'"' end + s = s .. ' ['..k..'] = ' .. utils.dump(v) .. ',' + end + + return s .. '} ' + else + return tostring(o) + end +end + +-- Method for building a exponential backoff time value generator +function utils.backoff_builder(max, inc, rand) + local count = 0 + inc = inc or 1 + + return function() + local randval = 0 + if rand then + randval = math.random() * rand * 2 - rand + end + + local base = inc * (2 ^ count - 1) + count = count + 1 + + -- ensure base backoff (not including random factor) is less than max + if max then base = math.min(base, max) end + + -- ensure total backoff is >= 0 + return math.max(base + randval, 0) + end +end + +-- Method for creating a labeled socket +function utils.labeled_socket_builder(label, ssl_config) + label = (label or "") + if #label > 0 then + label = label .. " " + end + + if not ssl_config then + ssl_config = { mode = "client", protocol = "any", verify = "none", options = "all" } + end + + local function make_socket(host, port, wrap_ssl) + log.info("utils.labeled_socket_builder(): Creating TCP socket for REST Connection: " .. label) + local _ = nil + local sock, err = socket.tcp() + + if err ~= nil or (not sock) then + return nil, (err or "unknown error creating TCP socket") + end + + log.debug("utils.labeled_socket_builder(): Setting TCP socket timeout for REST Connection: " .. label) + _, err = sock:settimeout(60) + if err ~= nil then + return nil, "settimeout error: " .. err + end + + log.debug("utils.labeled_socket_builder(): Connecting TCP socket for REST Connection: " .. label) + _, err = sock:connect(host, port) + if err ~= nil then + return nil, "Connect error: " .. err + end + + log.debug("utils.labeled_socket_builder(): Set Keepalive for TCP socket for REST Connection: " .. label) + _, err = sock:setoption("keepalive", true) + if err ~= nil then + return nil, "Setoption error: " .. err + end + + if wrap_ssl then + log.debug("utils.labeled_socket_builder(): Creating SSL wrapper for REST Connection: " .. label) + sock, err = ssl.wrap(sock, ssl_config) + if err ~= nil then + return nil, "SSL wrap error: " .. err + end + + log.debug("utils.labeled_socket_builder(): Performing SSL handshake for REST Connection: " .. label) + _, err = sock:dohandshake() + if err ~= nil then + return nil, "Error with SSL handshake: " .. err + end + end + + log.info("utils.labeled_socket_builder(): Successfully created TCP connection: " .. label) + return sock, err + end + + return make_socket +end + +return utils \ No newline at end of file diff --git a/drivers/Aqara/aqara-lock/profiles/aqara-lock-battery.yml b/drivers/Aqara/aqara-lock/profiles/aqara-lock-battery.yml index 0af3282ddb..0fbe7a718b 100644 --- a/drivers/Aqara/aqara-lock/profiles/aqara-lock-battery.yml +++ b/drivers/Aqara/aqara-lock/profiles/aqara-lock-battery.yml @@ -6,6 +6,8 @@ components: version: 1 - id: battery version: 1 + - id: batteryLevel + version: 1 - id: lockAlarm version: 1 - id: remoteControlStatus diff --git a/drivers/Aqara/aqara-lock/src/init.lua b/drivers/Aqara/aqara-lock/src/init.lua index 0d7cc77c4f..e9eb32c9b4 100644 --- a/drivers/Aqara/aqara-lock/src/init.lua +++ b/drivers/Aqara/aqara-lock/src/init.lua @@ -25,6 +25,10 @@ local SUPPORTED_ALARM_VALUES = { "damaged", "forcedOpeningAttempt", "unableToLoc local SERIAL_NUM_TX = "serial_num_tx" local SERIAL_NUM_RX = "serial_num_rx" local SEQ_NUM = "seq_num" +local THRESHOLD_BATTERY = { + ["aqara.lock.akr011"] = { low = 47, dryout = 28 }, -- K100 + ["aqara.lock.akr001"] = { low = 47, dryout = 31 } -- L100 +} local function my_secret_data_handler(driver, device, secret_info) if secret_info.secret_kind ~= "aqara" then return end @@ -75,16 +79,19 @@ local function device_init(self, device) device:emit_event(capabilities.lock.supportedUnlockDirections({ "fromInside", "fromOutside" }, { visibility = { displayed = false } })) device:emit_event(capabilities.battery.type("AA")) + device:emit_event(capabilities.batteryLevel.type("AA")) local battery_quantity = 8 if device:get_model() == "aqara.lock.akr001" then battery_quantity = 6 end device:emit_event(capabilities.battery.quantity(battery_quantity)) + device:emit_event(capabilities.batteryLevel.quantity(battery_quantity)) end local function device_added(self, device) remoteControlShow(device) device:emit_event(Battery.battery(100)) + device:emit_event(capabilities.batteryLevel.battery("normal")) device:emit_event(LockAlarm.alarm.clear({ visibility = { displayed = false } })) device:emit_event(antiLockStatus.antiLockStatus("unknown", { visibility = { displayed = false } })) device:emit_event(Lock.lock.locked()) @@ -158,8 +165,18 @@ local function event_door_handler(driver, device, evt_name, evt_value) end end +local function calc_battery_level(model, level) + local batteryLevel = "normal" + if level < THRESHOLD_BATTERY[model].dryout then + batteryLevel = "critical" + elseif level < THRESHOLD_BATTERY[model].low then + batteryLevel = "warning" + end + return batteryLevel +end local function event_battery_handler(driver, device, evt_name, evt_value) device:emit_event(Battery.battery(evt_value)) + device:emit_event(capabilities.batteryLevel.battery(calc_battery_level(device:get_model(), evt_value))) end local function event_abnormal_status_handler(driver, device, evt_name, evt_value) diff --git a/drivers/Aqara/aqara-lock/src/test/test_aqara_lock.lua b/drivers/Aqara/aqara-lock/src/test/test_aqara_lock.lua index f127a16d0c..8fcec06b5f 100644 --- a/drivers/Aqara/aqara-lock/src/test/test_aqara_lock.lua +++ b/drivers/Aqara/aqara-lock/src/test/test_aqara_lock.lua @@ -13,6 +13,7 @@ test.add_package_capability("lockCredentialInfo.yaml") local lockAlarm = capabilities["lockAlarm"] test.add_package_capability("lockAlarm.yaml") local Battery = capabilities.battery +local BatteryLevel = capabilities.batteryLevel local Lock = capabilities.lock local PRI_CLU = 0xFCC0 @@ -45,7 +46,9 @@ local function test_init() test.socket.capability:__expect_send(mock_device:generate_test_message("main", Lock.supportedUnlockDirections({"fromInside", "fromOutside"}, { visibility = { displayed = false } }))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", Battery.type("AA"))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", BatteryLevel.type("AA"))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", Battery.quantity(8))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", BatteryLevel.quantity(8))) test.mock_device.add_test_device(mock_device) end test.set_test_init_function(test_init) @@ -59,6 +62,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", remoteControlStatus.remoteControlEnabled('false', { visibility = { displayed = false } }))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", Battery.battery(100))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", BatteryLevel.battery("normal"))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", lockAlarm.alarm.clear({ visibility = { displayed = false } }))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", @@ -76,6 +80,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", remoteControlStatus.remoteControlEnabled('true', { visibility = { displayed = false } }))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", Battery.battery(100))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", BatteryLevel.battery("normal"))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", lockAlarm.alarm.clear({ visibility = { displayed = false } }))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", diff --git a/drivers/SinuxSoft/britzyhub/README.md b/drivers/SinuxSoft/britzyhub/README.md new file mode 100644 index 0000000000..510501f7cb --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/README.md @@ -0,0 +1,9 @@ +# BritzyHub Matter Edge Driver + +Edge driver for registering BritzyHub Matter dedicated Matter devices. + +--- + +## Reference + +- Use CLI.md for detailed SmartThings CLI command summaries diff --git a/drivers/SinuxSoft/britzyhub/config.yml b/drivers/SinuxSoft/britzyhub/config.yml new file mode 100644 index 0000000000..21551f1849 --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/config.yml @@ -0,0 +1,6 @@ +name: 'BritzyHub Matter' +packageKey: 'britzyhub-matter' +permissions: + matter: {} +description: "SmartThings Edge driver for BritzyHub Matter devices." +vendorSupportInformation: "https://sinux.kr" \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/fingerprints.yml b/drivers/SinuxSoft/britzyhub/fingerprints.yml new file mode 100644 index 0000000000..904b544db8 --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/fingerprints.yml @@ -0,0 +1,16 @@ +matterGeneric: + - id: "elevator" + deviceLabel: Elevator + deviceTypes: + - id: 0xFF02 + deviceProfileName: elevator + - id: "gas-valve" + deviceLabel: Gas Valve + deviceTypes: + - id: 0xFF01 + deviceProfileName: gas-valve + - id: "vent" + deviceLabel: Vent + deviceTypes: + - id: 0xFF03 + deviceProfileName: vent \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/profiles/elevator.yml b/drivers/SinuxSoft/britzyhub/profiles/elevator.yml new file mode 100644 index 0000000000..a120b501dc --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/profiles/elevator.yml @@ -0,0 +1,10 @@ +name: elevator +components: + - id: main + capabilities: + - id: elevatorCall + version: 1 + - id: refresh + version: 1 + categories: + - name: Elevator \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/profiles/gas-valve.yml b/drivers/SinuxSoft/britzyhub/profiles/gas-valve.yml new file mode 100644 index 0000000000..30f18c453b --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/profiles/gas-valve.yml @@ -0,0 +1,10 @@ +name: gas-valve +components: + - id: main + capabilities: + - id: safetyValve + version: 1 + - id: refresh + version: 1 + categories: + - name: GasValve \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/profiles/vent.yml b/drivers/SinuxSoft/britzyhub/profiles/vent.yml new file mode 100644 index 0000000000..2c956b307f --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/profiles/vent.yml @@ -0,0 +1,19 @@ +name: vent +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: fanMode + version: 1 + config: + values: + - key: "fanMode.value" + enabledValues: + - low + - medium + - high + - id: refresh + version: 1 + categories: + - name: Vent \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/src/elevator/init.lua b/drivers/SinuxSoft/britzyhub/src/elevator/init.lua new file mode 100644 index 0000000000..51aa1d8e0e --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/src/elevator/init.lua @@ -0,0 +1,91 @@ +-- SinuxSoft (c) 2025 +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local log = require "log" + +local elevator_cap = capabilities.elevatorCall +local onoff_cluster = clusters.OnOff + +local ELEVATOR_DEVICE_TYPE_ID = 0xFF02 + +local function on_off_attr_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, elevator_cap.callStatus.called()) + else + device:emit_event_for_endpoint(ib.endpoint_id, elevator_cap.callStatus.standby()) + end +end + +local function handle_elevator_call(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = onoff_cluster.server.commands.On(device, endpoint_id) + device:send(req) +end + +local function find_default_endpoint(device, cluster) + local eps = device:get_endpoints(cluster) + table.sort(eps) + for _, ep in ipairs(eps) do + if ep ~= 0 then return ep end + end + log.warn(string.format("No endpoint found, using default %d", device.MATTER_DEFAULT_ENDPOINT)) + return device.MATTER_DEFAULT_ENDPOINT +end + +local function component_to_endpoint(device, component_name, cluster_id) + return find_default_endpoint(device, clusters.OnOff.ID) +end + +local function device_init(driver, device) + device:subscribe() + device:set_component_to_endpoint_fn(component_to_endpoint) +end + +local function info_changed(driver, device, event, args) + device:add_subscribed_attribute(onoff_cluster.attributes.OnOff) + device:subscribe() +end + +local function is_matter_elevator(opts, driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == ELEVATOR_DEVICE_TYPE_ID then + return true + end + end + end + return false +end + +local elevator_handler = { + NAME = "Elevator Handler", + can_handle = is_matter_elevator, + lifecycle_handlers = { + init = device_init, + infoChanged = info_changed, + }, + matter_handlers = { + attr = { + [onoff_cluster.ID] = { + [onoff_cluster.attributes.OnOff.ID] = on_off_attr_handler, + } + } + }, + capability_handlers = { + [elevator_cap.ID] = { + [elevator_cap.commands.call.NAME] = handle_elevator_call, + } + }, + supported_capabilities = { + elevator_cap, + }, + subscribed_attributes = { + [elevator_cap.ID] = { + onoff_cluster.attributes.OnOff + } + }, +} + +return elevator_handler \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/src/gas-valve/init.lua b/drivers/SinuxSoft/britzyhub/src/gas-valve/init.lua new file mode 100644 index 0000000000..52dd3cafb6 --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/src/gas-valve/init.lua @@ -0,0 +1,91 @@ +-- SinuxSoft (c) 2025 +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local log = require "log" + +local valve_cap = capabilities.safetyValve +local onoff_cluster = clusters.OnOff + +local GAS_VALVE_DEVICE_TYPE_ID = 0xFF01 + +local function on_off_attr_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, valve_cap.valve.open()) + else + device:emit_event_for_endpoint(ib.endpoint_id, valve_cap.valve.closed()) + end +end + +local function handle_valve_close(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = onoff_cluster.server.commands.Off(device, endpoint_id) + device:send(req) +end + +local function find_default_endpoint(device, cluster) + local eps = device:get_endpoints(cluster) + table.sort(eps) + for _, ep in ipairs(eps) do + if ep ~= 0 then return ep end + end + log.warn(string.format("No endpoint found, using default %d", device.MATTER_DEFAULT_ENDPOINT)) + return device.MATTER_DEFAULT_ENDPOINT +end + +local function component_to_endpoint(device, component_name, cluster_id) + return find_default_endpoint(device, onoff_cluster.ID) +end + +local function device_init(driver, device) + device:subscribe() + device:set_component_to_endpoint_fn(component_to_endpoint) +end + +local function info_changed(driver, device, event, args) + device:add_subscribed_attribute(onoff_cluster.attributes.OnOff) + device:subscribe() +end + +local function is_matter_gas_valve(opts, driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == GAS_VALVE_DEVICE_TYPE_ID then + return true + end + end + end + return false +end + +local gas_valve_handler = { + NAME = "Gas Valve Handler", + can_handle = is_matter_gas_valve, + lifecycle_handlers = { + init = device_init, + infoChanged = info_changed, + }, + matter_handlers = { + attr = { + [onoff_cluster.ID] = { + [onoff_cluster.attributes.OnOff.ID] = on_off_attr_handler, + } + } + }, + capability_handlers = { + [valve_cap.ID] = { + [valve_cap.commands.close.NAME] = handle_valve_close, + } + }, + supported_capabilities = { + valve_cap, + }, + subscribed_attributes = { + [valve_cap.ID] = { + onoff_cluster.attributes.OnOff + } + }, +} + +return gas_valve_handler \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/src/init.lua b/drivers/SinuxSoft/britzyhub/src/init.lua new file mode 100644 index 0000000000..60f3cb90ce --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/src/init.lua @@ -0,0 +1,16 @@ +-- SinuxSoft (c) 2025 +-- Licensed under the Apache License, Version 2.0 + +local MatterDriver = require "st.matter.driver" +local log = require "log" + +local matter_driver = MatterDriver("britzyhub-matter", { + sub_drivers = { + require ("elevator"), + require ("gas-valve"), + require ("vent"), + } +}) + +log.info_with({hub_logs=true}, string.format("Starting %s driver, with dispatcher: %s", matter_driver.NAME, matter_driver.matter_dispatcher)) +matter_driver:run() \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/src/vent/init.lua b/drivers/SinuxSoft/britzyhub/src/vent/init.lua new file mode 100644 index 0000000000..93e11f40c6 --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/src/vent/init.lua @@ -0,0 +1,164 @@ +-- SinuxSoft (c) 2025 +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local log = require "log" + +local VENTILATOR_DEVICE_TYPE_ID = 0xFF03 + +local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" + +local subscribed_attributes = { + [capabilities.switch.ID] = { + clusters.OnOff.attributes.OnOff + }, + [capabilities.fanMode.ID] = { + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, +} + +local function map_enum_value(map, value, default) + return map[value] or default +end + +local function find_default_endpoint(device, cluster) + local eps = device:get_endpoints(cluster) + table.sort(eps) + for _, v in ipairs(eps) do + if v ~= 0 then + return v + end + end + log.warn(string.format("No endpoint found, using default %d", device.MATTER_DEFAULT_ENDPOINT)) + return device.MATTER_DEFAULT_ENDPOINT +end + +local function component_to_endpoint(device, component_name, cluster_id) + local component_to_endpoint_map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) + if component_to_endpoint_map ~= nil and component_to_endpoint_map[component_name] ~= nil then + return component_to_endpoint_map[component_name] + end + if not cluster_id then return device.MATTER_DEFAULT_ENDPOINT end + return find_default_endpoint(device, cluster_id) +end + +local endpoint_to_component = function(device, endpoint_id) + local component_to_endpoint_map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) + if component_to_endpoint_map ~= nil then + for comp, ep in pairs(component_to_endpoint_map) do + if ep == endpoint_id then + return comp + end + end + end + return "main" +end + +local function on_off_attr_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.on()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) + end +end + +local AC_FAN_MODE_MAP = { + [clusters.FanControl.attributes.FanMode.LOW] = capabilities.fanMode.fanMode.low(), + [clusters.FanControl.attributes.FanMode.MEDIUM] = capabilities.fanMode.fanMode.medium(), + [clusters.FanControl.attributes.FanMode.HIGH] = capabilities.fanMode.fanMode.high(), +} + +local function fan_mode_handler(driver, device, ib, response) + local cap = capabilities.fanMode + if device:supports_capability_by_id(cap.ID) then + local event = map_enum_value(AC_FAN_MODE_MAP, ib.data.value, cap.fanMode.low()) + device:emit_event_for_endpoint(ib.endpoint_id, event) + end +end + +local function handle_switch_on(driver, device, cmd) + local ep = component_to_endpoint(device, cmd.component, clusters.OnOff.ID) + device:send(clusters.OnOff.server.commands.On(device, ep)) +end + +local function handle_switch_off(driver, device, cmd) + local ep = component_to_endpoint(device, cmd.component, clusters.OnOff.ID) + device:send(clusters.OnOff.server.commands.Off(device, ep)) +end + +local function set_fan_mode(driver, device, cmd) + local args = cmd.args.fanMode + local mode_map = { + low = clusters.FanControl.attributes.FanMode.LOW, + medium = clusters.FanControl.attributes.FanMode.MEDIUM, + high = clusters.FanControl.attributes.FanMode.HIGH, + } + local ep = component_to_endpoint(device, cmd.component, clusters.FanControl.ID) + local mode = mode_map[args] or clusters.FanControl.attributes.FanMode.OFF + device:send(clusters.FanControl.attributes.FanMode:write(device, ep, mode)) +end + +local function device_init(driver, device) + device:subscribe() + device:set_component_to_endpoint_fn(component_to_endpoint) + device:set_endpoint_to_component_fn(endpoint_to_component) +end + +local function info_changed(driver, device, event, args) + for cap_id, attributes in pairs(subscribed_attributes) do + if device:supports_capability_by_id(cap_id) then + for _, attr in ipairs(attributes) do + device:add_subscribed_attribute(attr) + end + end + end + device:subscribe() +end + +local function is_matter_ventilator(opts, driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == VENTILATOR_DEVICE_TYPE_ID then + return true + end + end + end + return false +end + +local ventilator_handler = { + NAME = "Ventilator Handler", + can_handle = is_matter_ventilator, + lifecycle_handlers = { + init = device_init, + infoChanged = info_changed, + }, + matter_handlers = { + attr = { + [clusters.OnOff.ID] = { + [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, + }, + [clusters.FanControl.ID] = { + [clusters.FanControl.attributes.FanMode.ID] = fan_mode_handler, + }, + } + }, + capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = handle_switch_on, + [capabilities.switch.commands.off.NAME] = handle_switch_off, + }, + [capabilities.fanMode.ID] = { + [capabilities.fanMode.commands.setFanMode.NAME] = set_fan_mode, + }, + }, + supported_capabilities = { + capabilities.switch, + capabilities.fanMode, + }, + subscribed_attributes = subscribed_attributes +} + +return ventilator_handler \ No newline at end of file diff --git a/drivers/SmartThings/hub/profiles/v4-hub.yml b/drivers/SmartThings/hub/profiles/v4-hub.yml index f7c85625b6..b1b882c8b6 100644 --- a/drivers/SmartThings/hub/profiles/v4-hub.yml +++ b/drivers/SmartThings/hub/profiles/v4-hub.yml @@ -18,6 +18,8 @@ components: version: 1 - id: sec.wifiConfiguration version: 1 + - id: sec.appliedHubGroupMemberState + version: 1 categories: - name: Hub categoryType: manufacturer diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/init.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/init.lua index c7057f82e4..fff466fea1 100644 --- a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/init.lua +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local ActivatedCarbonFilterMonitoringServerAttributes = require "ActivatedCarbonFilterMonitoring.server.attributes" local ActivatedCarbonFilterMonitoringTypes = require "ActivatedCarbonFilterMonitoring.types" diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua index ef3fb00a6a..9f34e51dbf 100644 --- a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -123,4 +113,3 @@ end setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) return AttributeList - diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua index dd6fe1ecfb..71170e772f 100644 --- a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(ChangeIndication, {__call = ChangeIndication.new_value, __index = ChangeIndication.base_type}) return ChangeIndication - diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/init.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/init.lua index 2ff8e6e89a..80e6e6b86a 100644 --- a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/init.lua b/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/init.lua index 9a6be67486..382197bc60 100644 --- a/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/init.lua +++ b/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local DishwasherAlarmServerAttributes = require "DishwasherAlarm.server.attributes" local DishwasherAlarmTypes = require "DishwasherAlarm.types" diff --git a/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/types/init.lua b/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/types/init.lua index d5c4bba1fb..3ddec58dcc 100644 --- a/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/DishwasherMode/init.lua b/drivers/SmartThings/matter-appliance/src/DishwasherMode/init.lua index 913f001fdb..0bb22e1884 100644 --- a/drivers/SmartThings/matter-appliance/src/DishwasherMode/init.lua +++ b/drivers/SmartThings/matter-appliance/src/DishwasherMode/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local DishwasherModeServerAttributes = require "DishwasherMode.server.attributes" local DishwasherModeServerCommands = require "DishwasherMode.server.commands" diff --git a/drivers/SmartThings/matter-appliance/src/DishwasherMode/types/init.lua b/drivers/SmartThings/matter-appliance/src/DishwasherMode/types/init.lua index dd07b2f752..34d510160e 100644 --- a/drivers/SmartThings/matter-appliance/src/DishwasherMode/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/DishwasherMode/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/init.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/init.lua index 21795104b7..b6df5d554c 100644 --- a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/init.lua +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local HepaFilterMonitoringServerAttributes = require "HepaFilterMonitoring.server.attributes" local HepaFilterMonitoringTypes = require "HepaFilterMonitoring.types" diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/AttributeList.lua index c4f817e428..6fdd2e5350 100644 --- a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/AttributeList.lua +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/AttributeList.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -123,4 +113,3 @@ end setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) return AttributeList - diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua index 06a80153e3..b205026e1b 100644 --- a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(ChangeIndication, {__call = ChangeIndication.new_value, __index = ChangeIndication.base_type}) return ChangeIndication - diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/init.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/init.lua index 77aca088ff..f4eea9ee26 100644 --- a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/init.lua b/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/init.lua index 63bf4168e7..c6ff01850b 100644 --- a/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/init.lua +++ b/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local LaundryWasherControlsServerAttributes = require "LaundryWasherControls.server.attributes" local LaundryWasherControlsTypes = require "LaundryWasherControls.types" diff --git a/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/types/init.lua b/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/types/init.lua index e8a2f0ac53..4ba9f25726 100644 --- a/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/init.lua b/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/init.lua index aa030e58cc..e64bde94a0 100644 --- a/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/init.lua +++ b/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local LaundryWasherModeServerAttributes = require "LaundryWasherMode.server.attributes" local LaundryWasherModeServerCommands = require "LaundryWasherMode.server.commands" diff --git a/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/types/init.lua b/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/types/init.lua index 057fe27dbe..d201d4ba36 100644 --- a/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/init.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/init.lua index 697aa3fd05..5c333dea1e 100644 --- a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/init.lua +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local MicrowaveOvenControlServerAttributes = require "MicrowaveOvenControl.server.attributes" local MicrowaveOvenControlServerCommands = require "MicrowaveOvenControl.server.commands" @@ -81,4 +85,4 @@ setmetatable(MicrowaveOvenControl.commands, command_helper_mt) setmetatable(MicrowaveOvenControl, {__index = cluster_base}) -return MicrowaveOvenControl \ No newline at end of file +return MicrowaveOvenControl diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/types/init.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/types/init.lua index 29c27b80ae..1d11195ee2 100644 --- a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/init.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/init.lua index fdbbf4c443..dca8ff3ed5 100644 --- a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/init.lua +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local MicrowaveOvenModeServerAttributes = require "MicrowaveOvenMode.server.attributes" local MicrowaveOvenModeTypes = require "MicrowaveOvenMode.types" diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/init.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/init.lua index 412d69cabd..e67593141b 100644 --- a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/OperationalState/init.lua b/drivers/SmartThings/matter-appliance/src/OperationalState/init.lua index 5239cc1cfe..7be274fb8d 100644 --- a/drivers/SmartThings/matter-appliance/src/OperationalState/init.lua +++ b/drivers/SmartThings/matter-appliance/src/OperationalState/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local OperationalStateServerAttributes = require "OperationalState.server.attributes" local OperationalStateServerCommands = require "OperationalState.server.commands" diff --git a/drivers/SmartThings/matter-appliance/src/OperationalState/types/init.lua b/drivers/SmartThings/matter-appliance/src/OperationalState/types/init.lua index 8c735ed06a..18bbca0824 100644 --- a/drivers/SmartThings/matter-appliance/src/OperationalState/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/OperationalState/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/OvenMode/init.lua b/drivers/SmartThings/matter-appliance/src/OvenMode/init.lua index 76e1817dc1..4fc7c11376 100644 --- a/drivers/SmartThings/matter-appliance/src/OvenMode/init.lua +++ b/drivers/SmartThings/matter-appliance/src/OvenMode/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local OvenModeServerAttributes = require "OvenMode.server.attributes" local OvenModeServerCommands = require "OvenMode.server.commands" diff --git a/drivers/SmartThings/matter-appliance/src/OvenMode/types/init.lua b/drivers/SmartThings/matter-appliance/src/OvenMode/types/init.lua index 8bd0777339..b3565aaa2b 100644 --- a/drivers/SmartThings/matter-appliance/src/OvenMode/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/OvenMode/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/init.lua b/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/init.lua index a03452193c..1c9610b520 100644 --- a/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/init.lua +++ b/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local RefrigeratorAlarmServerAttributes = require "RefrigeratorAlarm.server.attributes" local RefrigeratorAlarmTypes = require "RefrigeratorAlarm.types" diff --git a/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/types/init.lua b/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/types/init.lua index 111371cb52..d8ebdc1ed3 100644 --- a/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/init.lua b/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/init.lua index f85f5e4e31..345b7b57b1 100644 --- a/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/init.lua +++ b/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local RefrigeratorAndTemperatureControlledCabinetModeServerAttributes = require "RefrigeratorAndTemperatureControlledCabinetMode.server.attributes" local RefrigeratorAndTemperatureControlledCabinetModeServerCommands = require "RefrigeratorAndTemperatureControlledCabinetMode.server.commands" diff --git a/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/types/init.lua b/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/types/init.lua index d03e0d01d7..0079c8e25e 100644 --- a/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/TemperatureControl/init.lua b/drivers/SmartThings/matter-appliance/src/TemperatureControl/init.lua index 2d453d8662..2ab1c94056 100644 --- a/drivers/SmartThings/matter-appliance/src/TemperatureControl/init.lua +++ b/drivers/SmartThings/matter-appliance/src/TemperatureControl/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local TemperatureControlServerAttributes = require "TemperatureControl.server.attributes" local TemperatureControlServerCommands = require "TemperatureControl.server.commands" diff --git a/drivers/SmartThings/matter-appliance/src/TemperatureControl/types/init.lua b/drivers/SmartThings/matter-appliance/src/TemperatureControl/types/init.lua index feeafbc78b..53c77d6ff9 100644 --- a/drivers/SmartThings/matter-appliance/src/TemperatureControl/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/TemperatureControl/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/common-utils.lua b/drivers/SmartThings/matter-appliance/src/common-utils.lua index 1250d40912..d35ee2b6fc 100644 --- a/drivers/SmartThings/matter-appliance/src/common-utils.lua +++ b/drivers/SmartThings/matter-appliance/src/common-utils.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" diff --git a/drivers/SmartThings/matter-appliance/src/embedded-cluster-utils.lua b/drivers/SmartThings/matter-appliance/src/embedded-cluster-utils.lua index 52d9909dff..5a5a89af73 100644 --- a/drivers/SmartThings/matter-appliance/src/embedded-cluster-utils.lua +++ b/drivers/SmartThings/matter-appliance/src/embedded-cluster-utils.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.matter.clusters" local utils = require "st.utils" diff --git a/drivers/SmartThings/matter-appliance/src/init.lua b/drivers/SmartThings/matter-appliance/src/init.lua index d107a48fc5..044da51e7c 100644 --- a/drivers/SmartThings/matter-appliance/src/init.lua +++ b/drivers/SmartThings/matter-appliance/src/init.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local MatterDriver = require "st.matter.driver" local capabilities = require "st.capabilities" @@ -307,15 +296,7 @@ local matter_driver_template = { capabilities.fanSpeedPercent, capabilities.windMode }, - sub_drivers = { - require("matter-cook-top"), - require("matter-dishwasher"), - require("matter-extractor-hood"), - require("matter-laundry"), - require("matter-microwave-oven"), - require("matter-oven"), - require("matter-refrigerator") - } + sub_drivers = require("sub_drivers"), } local matter_driver = MatterDriver("matter-appliance", matter_driver_template) diff --git a/drivers/SmartThings/matter-appliance/src/lazy_load_subdriver.lua b/drivers/SmartThings/matter-appliance/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..a04740d267 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/lazy_load_subdriver.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + local MatterDriver = require "st.matter.driver" + local version = require "version" + if version.api >= 16 then + return MatterDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return MatterDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/matter-appliance/src/matter-cook-top/can_handle.lua b/drivers/SmartThings/matter-appliance/src/matter-cook-top/can_handle.lua new file mode 100644 index 0000000000..d2899d279a --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-cook-top/can_handle.lua @@ -0,0 +1,21 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_cook_top_device(opts, driver, device, ...) + local common_utils = require "common-utils" + local COOK_TOP_DEVICE_TYPE_ID = 0x0078 + local OVEN_DEVICE_ID = 0x007B + + local cook_top_eps = common_utils.get_endpoints_for_dt(device, COOK_TOP_DEVICE_TYPE_ID) + local oven_eps = common_utils.get_endpoints_for_dt(device, OVEN_DEVICE_ID) + -- we want to skip lifecycle events in cases where the device is an oven with a composed cook-top device + if (#oven_eps > 0) and opts.dispatcher_class == "DeviceLifecycleDispatcher" then + return false + end + if #cook_top_eps > 0 then + return true, require("matter-cook-top") + end + return false +end + +return is_cook_top_device diff --git a/drivers/SmartThings/matter-appliance/src/matter-cook-top/init.lua b/drivers/SmartThings/matter-appliance/src/matter-cook-top/init.lua index 0a8f9f3182..d0ce48dc18 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-cook-top/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-cook-top/init.lua @@ -1,97 +1,73 @@ --- 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.matter.clusters" -local common_utils = require "common-utils" -local embedded_cluster_utils = require "embedded-cluster-utils" -local version = require "version" - -if version.api < 10 then - clusters.TemperatureControl = require "TemperatureControl" -end - -local COOK_SURFACE_DEVICE_TYPE_ID = 0x0077 -local COOK_TOP_DEVICE_TYPE_ID = 0x0078 -local OVEN_DEVICE_ID = 0x007B - -local function table_contains(tab, val) - for _, tab_val in ipairs(tab) do - if tab_val == val then - return true - end - end - return false -end - -local function device_added(driver, device) - local cook_surface_endpoints = common_utils.get_endpoints_for_dt(device, COOK_SURFACE_DEVICE_TYPE_ID) - local componentToEndpointMap = { - ["cookSurfaceOne"] = cook_surface_endpoints[1], - ["cookSurfaceTwo"] = cook_surface_endpoints[2] - } - device:set_field(common_utils.COMPONENT_TO_ENDPOINT_MAP, componentToEndpointMap, { persist = true }) -end - -local function do_configure(driver, device) - local cook_surface_endpoints = common_utils.get_endpoints_for_dt(device, COOK_SURFACE_DEVICE_TYPE_ID) - - local tl_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, - { feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_LEVEL }) - - local profile_name - if #cook_surface_endpoints > 0 then - profile_name = "cook-surface-one" - if table_contains(tl_eps, cook_surface_endpoints[1]) then - profile_name = profile_name .. "-tl" - end - - -- we only support up to two cook surfaces - if #cook_surface_endpoints > 1 then - profile_name = profile_name .. "-cook-surface-two" - if table_contains(tl_eps, cook_surface_endpoints[2]) then - profile_name = profile_name .. "-tl" - end - end - end - - if profile_name then - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({ profile = profile_name }) - end -end - -local function is_cook_top_device(opts, driver, device, ...) - local cook_top_eps = common_utils.get_endpoints_for_dt(device, COOK_TOP_DEVICE_TYPE_ID) - local oven_eps = common_utils.get_endpoints_for_dt(device, OVEN_DEVICE_ID) - -- we want to skip lifecycle events in cases where the device is an oven with a composed cook-top device - if (#oven_eps > 0) and opts.dispatcher_class == "DeviceLifecycleDispatcher" then - return false - end - if #cook_top_eps > 0 then - return true - end - return false -end - --- Matter Handlers -- -local matter_cook_top_handler = { - NAME = "matter-cook-top", - lifecycle_handlers = { - added = device_added, - doConfigure = do_configure - }, - can_handle = is_cook_top_device -} - -return matter_cook_top_handler +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local clusters = require "st.matter.clusters" +local common_utils = require "common-utils" +local embedded_cluster_utils = require "embedded-cluster-utils" +local version = require "version" + +if version.api < 10 then + clusters.TemperatureControl = require "TemperatureControl" +end + +local COOK_SURFACE_DEVICE_TYPE_ID = 0x0077 + +local function table_contains(tab, val) + for _, tab_val in ipairs(tab) do + if tab_val == val then + return true + end + end + return false +end + +local function device_added(driver, device) + local cook_surface_endpoints = common_utils.get_endpoints_for_dt(device, COOK_SURFACE_DEVICE_TYPE_ID) + local componentToEndpointMap = { + ["cookSurfaceOne"] = cook_surface_endpoints[1], + ["cookSurfaceTwo"] = cook_surface_endpoints[2] + } + device:set_field(common_utils.COMPONENT_TO_ENDPOINT_MAP, componentToEndpointMap, { persist = true }) +end + +local function do_configure(driver, device) + local cook_surface_endpoints = common_utils.get_endpoints_for_dt(device, COOK_SURFACE_DEVICE_TYPE_ID) + + local tl_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, + { feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_LEVEL }) + + local profile_name + if #cook_surface_endpoints > 0 then + profile_name = "cook-surface-one" + if table_contains(tl_eps, cook_surface_endpoints[1]) then + profile_name = profile_name .. "-tl" + end + + -- we only support up to two cook surfaces + if #cook_surface_endpoints > 1 then + profile_name = profile_name .. "-cook-surface-two" + if table_contains(tl_eps, cook_surface_endpoints[2]) then + profile_name = profile_name .. "-tl" + end + end + end + + if profile_name then + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({ profile = profile_name }) + end +end + + +-- Matter Handlers -- +local matter_cook_top_handler = { + NAME = "matter-cook-top", + lifecycle_handlers = { + added = device_added, + doConfigure = do_configure + }, + can_handle = require("matter-cook-top.can_handle"), +} + +return matter_cook_top_handler diff --git a/drivers/SmartThings/matter-appliance/src/matter-dishwasher/can_handle.lua b/drivers/SmartThings/matter-appliance/src/matter-dishwasher/can_handle.lua new file mode 100644 index 0000000000..37226f3f3e --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-dishwasher/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local function is_matter_dishwasher(opts, driver, device) + local DISHWASHER_DEVICE_TYPE_ID = 0x0075 + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == DISHWASHER_DEVICE_TYPE_ID then + return true, require("matter-dishwasher") + end + end + end + return false +end + +return is_matter_dishwasher diff --git a/drivers/SmartThings/matter-appliance/src/matter-dishwasher/init.lua b/drivers/SmartThings/matter-appliance/src/matter-dishwasher/init.lua index 96339fafc5..2e01afe7fb 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-dishwasher/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-dishwasher/init.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -18,8 +7,6 @@ local common_utils = require "common-utils" local embedded_cluster_utils = require "embedded-cluster-utils" local version = require "version" -local DISHWASHER_DEVICE_TYPE_ID = 0x0075 - if version.api < 10 then clusters.DishwasherAlarm = require "DishwasherAlarm" clusters.DishwasherMode = require "DishwasherMode" @@ -36,16 +23,6 @@ local OPERATIONAL_STATE_COMMAND_MAP = { local SUPPORTED_DISHWASHER_MODES = "__supported_dishwasher_modes" -local function is_matter_dishwasher(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == DISHWASHER_DEVICE_TYPE_ID then - return true - end - end - end - return false -end -- Lifecycle Handlers -- local function do_configure(driver, device) @@ -267,7 +244,7 @@ local matter_dishwasher_handler = { [capabilities.operationalState.commands.resume.NAME] = handle_operational_state_resume } }, - can_handle = is_matter_dishwasher + can_handle = require("matter-dishwasher.can_handle"), } return matter_dishwasher_handler diff --git a/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/can_handle.lua b/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/can_handle.lua new file mode 100644 index 0000000000..d8ec2f9afd --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_matter_extractor_hood(opts, driver, device) + local EXTRACTOR_HOOD_DEVICE_TYPE_ID = 0x007A + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == EXTRACTOR_HOOD_DEVICE_TYPE_ID then + return true, require("matter-extractor-hood") + end + end + end + return false +end + +return is_matter_extractor_hood diff --git a/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/init.lua b/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/init.lua index 57eef3ac94..f83681e726 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/init.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -77,17 +67,6 @@ local function do_configure(driver, device) end -- Matter Handlers -- -local function is_matter_extractor_hood(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == EXTRACTOR_HOOD_DEVICE_TYPE_ID then - return true - end - end - end - return false -end - local function fan_mode_handler(driver, device, ib, response) if ib.data.value == clusters.FanControl.attributes.FanMode.OFF then device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode.off()) @@ -309,7 +288,7 @@ local matter_extractor_hood_handler = { [capabilities.windMode.commands.setWindMode.NAME] = set_wind_mode } }, - can_handle = is_matter_extractor_hood + can_handle = require("matter-extractor-hood.can_handle"), } return matter_extractor_hood_handler diff --git a/drivers/SmartThings/matter-appliance/src/matter-laundry/can_handle.lua b/drivers/SmartThings/matter-appliance/src/matter-laundry/can_handle.lua new file mode 100644 index 0000000000..dd71f12e9e --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-laundry/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_matter_laundry_device(opts, driver, device) + local LAUNDRY_WASHER_DEVICE_TYPE_ID = 0x0073 + local LAUNDRY_DRYER_DEVICE_TYPE_ID = 0x007C + local LAUNDRY_DEVICE_TYPE_ID= "__laundry_device_type_id" + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == LAUNDRY_WASHER_DEVICE_TYPE_ID or dt.device_type_id == LAUNDRY_DRYER_DEVICE_TYPE_ID then + device:set_field(LAUNDRY_DEVICE_TYPE_ID, dt.device_type_id, {persist = true}) + return dt.device_type_id, require("matter-laundry") + end + end + end + return false +end + +return is_matter_laundry_device diff --git a/drivers/SmartThings/matter-appliance/src/matter-laundry/init.lua b/drivers/SmartThings/matter-appliance/src/matter-laundry/init.lua index 8c49291da0..26b9efb606 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-laundry/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-laundry/init.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -47,23 +37,12 @@ local SUPPORTED_LAUNDRY_WASHER_MODES = "__supported_laundry_washer_modes" local SUPPORTED_LAUNDRY_WASHER_SPIN_SPEEDS = "__supported_laundry_spin_speeds" local SUPPORTED_LAUNDRY_WASHER_RINSES = "__supported_laundry_washer_rinses" -local function is_matter_laundry_device(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == LAUNDRY_WASHER_DEVICE_TYPE_ID or dt.device_type_id == LAUNDRY_DRYER_DEVICE_TYPE_ID then - device:set_field(LAUNDRY_DEVICE_TYPE_ID, dt.device_type_id, {persist = true}) - return dt.device_type_id - end - end - end - return false -end - -- Lifecycle Handlers -- local function do_configure(driver, device) local tn_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, {feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_NUMBER}) local tl_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, {feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_LEVEL}) - local device_type = is_matter_laundry_device({}, driver, device) + local is_matter_laundry_device = require("matter-laundry.can_handle") + local device_type, _ = is_matter_laundry_device({}, driver, device) local profile_name = "laundry" if (device_type == LAUNDRY_WASHER_DEVICE_TYPE_ID) then profile_name = profile_name.."-washer" @@ -316,7 +295,7 @@ local matter_laundry_handler = { [capabilities.operationalState.commands.resume.NAME] = handle_operational_state_resume } }, - can_handle = is_matter_laundry_device + can_handle = require("matter-laundry.can_handle"), } return matter_laundry_handler diff --git a/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/can_handle.lua b/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/can_handle.lua new file mode 100644 index 0000000000..ebdda29863 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_matter_mircowave_oven(opts, driver, device) + local MICROWAVE_OVEN_DEVICE_TYPE_ID = 0x0079 + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == MICROWAVE_OVEN_DEVICE_TYPE_ID then + return true, require("matter-microwave-oven") + end + end + end + return false +end + +return is_matter_mircowave_oven diff --git a/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/init.lua b/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/init.lua index 6ebd6a4c69..342723656a 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/init.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -34,7 +24,6 @@ local OPERATIONAL_STATE_COMMAND_MAP = { [clusters.OperationalState.commands.Resume.ID] = "resume", } -local MICROWAVE_OVEN_DEVICE_TYPE_ID = 0x0079 local DEFAULT_COOKING_MODE = 0 local DEFAULT_COOKING_TIME = 30 local MICROWAVE_OVEN_SUPPORTED_MODES_KEY = "__microwave_oven_supported_modes__" @@ -45,16 +34,6 @@ local function device_init(driver, device) device:send(clusters.MicrowaveOvenControl.attributes.MaxCookTime:read(device, device.MATTER_DEFAULT_ENDPOINT)) end -local function is_matter_mircowave_oven(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == MICROWAVE_OVEN_DEVICE_TYPE_ID then - return true - end - end - end - return false -end local function get_last_set_cooking_parameters(device) local cookingTime = device:get_latest_state("main", capabilities.cookTime.ID, capabilities.cookTime.cookTime.NAME) or DEFAULT_COOKING_TIME @@ -271,7 +250,7 @@ local matter_microwave_oven = { capabilities.mode, capabilities.cookTime }, - can_handle = is_matter_mircowave_oven + can_handle = require("matter-microwave-oven.can_handle"), } return matter_microwave_oven diff --git a/drivers/SmartThings/matter-appliance/src/matter-oven/can_handle.lua b/drivers/SmartThings/matter-appliance/src/matter-oven/can_handle.lua new file mode 100644 index 0000000000..a5bb61f9b8 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-oven/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_oven_device(opts, driver, device) + local common_utils = require "common-utils" + local OVEN_DEVICE_ID = 0x007B + local oven_eps = common_utils.get_endpoints_for_dt(device, OVEN_DEVICE_ID) + if #oven_eps > 0 then + return true, require("matter-oven") + end + return false +end + +return is_oven_device diff --git a/drivers/SmartThings/matter-appliance/src/matter-oven/init.lua b/drivers/SmartThings/matter-appliance/src/matter-oven/init.lua index 9847f24191..6c31504a6a 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-oven/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-oven/init.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -28,18 +18,10 @@ end local SUPPORTED_OVEN_MODES_MAP = "__supported_oven_modes_map_key_" -local OVEN_DEVICE_ID = 0x007B local COOK_SURFACE_DEVICE_TYPE_ID = 0x0077 local COOK_TOP_DEVICE_TYPE_ID = 0x0078 local TCC_DEVICE_TYPE_ID = 0x0071 -local function is_oven_device(opts, driver, device) - local oven_eps = common_utils.get_endpoints_for_dt(device, OVEN_DEVICE_ID) - if #oven_eps > 0 then - return true - end - return false -end -- Lifecycle Handlers -- local function device_added(driver, device) @@ -141,7 +123,7 @@ local matter_oven_handler = { [capabilities.temperatureSetpoint.commands.setTemperatureSetpoint.NAME] = handle_temperature_setpoint } }, - can_handle = is_oven_device + can_handle = require("matter-oven.can_handle"), } return matter_oven_handler diff --git a/drivers/SmartThings/matter-appliance/src/matter-refrigerator/can_handle.lua b/drivers/SmartThings/matter-appliance/src/matter-refrigerator/can_handle.lua new file mode 100644 index 0000000000..376b32cc2d --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-refrigerator/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_matter_refrigerator(opts, driver, device) + local REFRIGERATOR_DEVICE_TYPE_ID = 0x0070 + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == REFRIGERATOR_DEVICE_TYPE_ID then + return true, require("matter-refrigerator") + end + end + end + return false +end + +return is_matter_refrigerator diff --git a/drivers/SmartThings/matter-appliance/src/matter-refrigerator/init.lua b/drivers/SmartThings/matter-appliance/src/matter-refrigerator/init.lua index ec57c61505..c48412e849 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-refrigerator/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-refrigerator/init.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -18,7 +8,6 @@ local common_utils = require "common-utils" local embedded_cluster_utils = require "embedded-cluster-utils" local version = require "version" -local REFRIGERATOR_DEVICE_TYPE_ID = 0x0070 local TEMPERATURE_CONTROLLED_CABINET_DEVICE_TYPE_ID = 0x0071 if version.api < 10 then @@ -29,17 +18,6 @@ end local SUPPORTED_REFRIGERATOR_TCC_MODES_MAP = "__supported_refrigerator_tcc_modes_map" -local function is_matter_refrigerator(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == REFRIGERATOR_DEVICE_TYPE_ID then - return true - end - end - end - return false -end - -- Lifecycle Handlers -- local function device_added(driver, device) local cabinet_eps = {} @@ -182,7 +160,7 @@ local matter_refrigerator_handler = { [capabilities.temperatureSetpoint.commands.setTemperatureSetpoint.NAME] = handle_temperature_setpoint } }, - can_handle = is_matter_refrigerator + can_handle = require("matter-refrigerator.can_handle"), } return matter_refrigerator_handler diff --git a/drivers/SmartThings/matter-appliance/src/sub_drivers.lua b/drivers/SmartThings/matter-appliance/src/sub_drivers.lua new file mode 100644 index 0000000000..e0ad824520 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/sub_drivers.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("matter-cook-top"), + lazy_load_if_possible("matter-dishwasher"), + lazy_load_if_possible("matter-extractor-hood"), + lazy_load_if_possible("matter-laundry"), + lazy_load_if_possible("matter-microwave-oven"), + lazy_load_if_possible("matter-oven"), + lazy_load_if_possible("matter-refrigerator"), +} +return sub_drivers diff --git a/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua b/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua index 1fffd78597..4a65f8e882 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -70,34 +60,29 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.TemperatureMeasurement.attributes.MeasuredValue, clusters.TemperatureControl.attributes.SelectedTemperatureLevel, clusters.TemperatureControl.attributes.SupportedTemperatureLevels } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ profile = "cook-surface-one-tl-cook-surface-two-tl" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) -test.register_coroutine_test( - "Verify device profile update", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) - mock_device:expect_metadata_update({ profile = "cook-surface-one-tl-cook-surface-two-tl" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end -) - test.register_coroutine_test( "Assert component to endpoint map", function() @@ -199,4 +184,4 @@ test.register_message_test( } ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua b/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua index f35a88c012..f446d1149b 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(6) @@ -57,6 +47,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.DishwasherMode.attributes.CurrentMode, @@ -71,7 +63,6 @@ local function test_init() clusters.TemperatureControl.attributes.SelectedTemperatureLevel, clusters.TemperatureControl.attributes.SupportedTemperatureLevels } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -79,12 +70,13 @@ local function test_init() end end test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) test.socket.matter:__expect_send({mock_device.id, read_req}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) mock_device:expect_metadata_update({ profile = "dishwasher-tn-tl" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end diff --git a/drivers/SmartThings/matter-appliance/src/test/test_extractor_hood.lua b/drivers/SmartThings/matter-appliance/src/test/test_extractor_hood.lua index 8d02396191..b8d18a3ab4 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_extractor_hood.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_extractor_hood.lua @@ -1,21 +1,10 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -86,6 +75,8 @@ local mock_device_onoff = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local subscribed_attributes = { [capabilities.fanMode.ID] = { clusters.FanControl.attributes.FanModeSequence, @@ -117,16 +108,16 @@ local function test_init() end end end - - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) mock_device:expect_metadata_update({ profile = "extractor-hood-hepa-ac-wind" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_onoff() + test.disable_startup_messages() local cluster_subscribe_list = { clusters.FanControl.attributes.FanModeSequence, clusters.FanControl.attributes.FanMode, @@ -135,15 +126,16 @@ local function test_init_onoff() clusters.FanControl.attributes.WindSetting, clusters.OnOff.attributes.OnOff } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_onoff) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) + subscribe_request:merge(cluster:subscribe(mock_device_onoff)) end end - test.socket.matter:__expect_send({mock_device_onoff.id, subscribe_request}) test.mock_device.add_test_device(mock_device_onoff) test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "init" }) + test.socket.matter:__expect_send({mock_device_onoff.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "doConfigure"}) mock_device_onoff:expect_metadata_update({ profile = "extractor-hood-wind-light" }) mock_device_onoff:expect_metadata_update({ provisioning_state = "PROVISIONED" }) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua b/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua index 7695c51d48..9049b5f64e 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(6) @@ -56,6 +46,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LaundryWasherMode.attributes.CurrentMode, @@ -76,8 +68,9 @@ local function test_init() end end test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_laundry_washer.lua b/drivers/SmartThings/matter-appliance/src/test/test_laundry_washer.lua index 966d1f7d17..6f7ef3ee5c 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_laundry_washer.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_laundry_washer.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(6) @@ -56,6 +46,8 @@ local mock_device_washer = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_washer) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LaundryWasherMode.attributes.CurrentMode, @@ -69,7 +61,6 @@ local function test_init() clusters.TemperatureControl.attributes.SelectedTemperatureLevel, clusters.TemperatureControl.attributes.SupportedTemperatureLevels } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request_washer = cluster_subscribe_list[1]:subscribe(mock_device_washer) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -77,8 +68,9 @@ local function test_init() end end test.socket.matter:__expect_send({ mock_device_washer.id, subscribe_request_washer }) - test.mock_device.add_test_device(mock_device_washer) test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "init" }) + test.socket.matter:__expect_send({ mock_device_washer.id, subscribe_request_washer }) test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "doConfigure"}) local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_matter_appliance_rpc_5.lua b/drivers/SmartThings/matter-appliance/src/test/test_matter_appliance_rpc_5.lua index 57ec49cfbf..d8faa1e5ee 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_matter_appliance_rpc_5.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_matter_appliance_rpc_5.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(5) @@ -268,6 +258,8 @@ local mock_device_refrigerator = test.mock_device.build_test_matter_device({ }) local function test_init_dishwasher() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_dishwasher) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.DishwasherMode.attributes.CurrentMode, @@ -282,7 +274,6 @@ local function test_init_dishwasher() clusters.TemperatureControl.attributes.SelectedTemperatureLevel, clusters.TemperatureControl.attributes.SupportedTemperatureLevels } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_dishwasher) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -290,11 +281,21 @@ local function test_init_dishwasher() end end test.socket.matter:__expect_send({ mock_device_dishwasher.id, subscribe_request }) - test.mock_device.add_test_device(mock_device_dishwasher) test.socket.device_lifecycle:__queue_receive({ mock_device_dishwasher.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dishwasher.id, "init" }) + test.socket.matter:__expect_send({ mock_device_dishwasher.id, subscribe_request }) + local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() + read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) + test.socket.matter:__expect_send({mock_device_dishwasher.id, read_req}) + test.socket.device_lifecycle:__queue_receive({ mock_device_dishwasher.id, "doConfigure"}) + mock_device_dishwasher:expect_metadata_update({ profile = "dishwasher-tn-tl" }) + mock_device_dishwasher:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end -local function test_init_washer_dryer() +local function test_init_dryer() + test.disable_startup_messages() + test.socket.matter:__set_channel_ordering("relaxed") + test.mock_device.add_test_device(mock_device_dryer) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LaundryWasherMode.attributes.CurrentMode, @@ -308,29 +309,62 @@ local function test_init_washer_dryer() clusters.TemperatureControl.attributes.SelectedTemperatureLevel, clusters.TemperatureControl.attributes.SupportedTemperatureLevels } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_dryer) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device_dryer)) end end - local subscribe_request_washer = cluster_subscribe_list[1]:subscribe(mock_device_washer) + test.socket.matter:__expect_send({ mock_device_dryer.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dryer.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dryer.id, "init" }) + test.socket.matter:__expect_send({ mock_device_dryer.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dryer.id, "doConfigure"}) + local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() + read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) + test.socket.matter:__expect_send({mock_device_dryer.id, read_req}) + mock_device_dryer:expect_metadata_update({ profile = "laundry-dryer-tn-tl" }) + mock_device_dryer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_washer() + test.disable_startup_messages() + test.socket.matter:__set_channel_ordering("relaxed") + test.mock_device.add_test_device(mock_device_washer) + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LaundryWasherMode.attributes.CurrentMode, + clusters.LaundryWasherMode.attributes.SupportedModes, + clusters.OperationalState.attributes.OperationalState, + clusters.OperationalState.attributes.OperationalError, + clusters.OperationalState.attributes.AcceptedCommandList, + clusters.TemperatureControl.attributes.TemperatureSetpoint, + clusters.TemperatureControl.attributes.MaxTemperature, + clusters.TemperatureControl.attributes.MinTemperature, + clusters.TemperatureControl.attributes.SelectedTemperatureLevel, + clusters.TemperatureControl.attributes.SupportedTemperatureLevels + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_washer) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then - subscribe_request_washer:merge(cluster:subscribe(mock_device_washer)) + subscribe_request:merge(cluster:subscribe(mock_device_washer)) end end - test.socket.matter:__expect_send({ mock_device_dryer.id, subscribe_request }) - test.socket.matter:__expect_send({ mock_device_washer.id, subscribe_request_washer }) - test.mock_device.add_test_device(mock_device_dryer) - test.mock_device.add_test_device(mock_device_washer) - test.socket.device_lifecycle:__queue_receive({ mock_device_dryer.id, "added" }) + test.socket.matter:__expect_send({ mock_device_washer.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "added" }) - test.set_rpc_version(5) + test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "init" }) + test.socket.matter:__expect_send({ mock_device_washer.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "doConfigure"}) + local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() + read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) + test.socket.matter:__expect_send({mock_device_washer.id, read_req}) + mock_device_washer:expect_metadata_update({ profile = "laundry-washer-tn-tl" }) + mock_device_washer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_oven() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_oven) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.TemperatureMeasurement.attributes.MeasuredValue, @@ -342,7 +376,6 @@ local function test_init_oven() clusters.OvenMode.attributes.CurrentMode, clusters.OvenMode.attributes.SupportedModes, } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_oven) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -350,12 +383,16 @@ local function test_init_oven() end end test.socket.matter:__expect_send({ mock_device_oven.id, subscribe_request }) - test.mock_device.add_test_device(mock_device_oven) test.socket.device_lifecycle:__queue_receive({ mock_device_oven.id, "added" }) - test.set_rpc_version(5) + test.socket.device_lifecycle:__queue_receive({ mock_device_oven.id, "init" }) + test.socket.matter:__expect_send({ mock_device_oven.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_oven.id, "doConfigure"}) + mock_device_oven:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_refrigerator() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_refrigerator) local cluster_subscribe_list = { clusters.RefrigeratorAlarm.attributes.State, clusters.RefrigeratorAndTemperatureControlledCabinetMode.attributes.CurrentMode, @@ -365,7 +402,6 @@ local function test_init_refrigerator() clusters.TemperatureControl.attributes.MinTemperature, clusters.TemperatureMeasurement.attributes.MeasuredValue } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_refrigerator) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -373,14 +409,19 @@ local function test_init_refrigerator() end end test.socket.matter:__expect_send({ mock_device_refrigerator.id, subscribe_request }) - test.mock_device.add_test_device(mock_device_refrigerator) test.socket.device_lifecycle:__queue_receive({ mock_device_refrigerator.id, "added" }) - test.set_rpc_version(5) + test.socket.device_lifecycle:__queue_receive({ mock_device_refrigerator.id, "init" }) + test.socket.matter:__expect_send({ mock_device_refrigerator.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_refrigerator.id, "doConfigure"}) + local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() + read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) + test.socket.matter:__expect_send({mock_device_refrigerator.id, read_req}) + mock_device_refrigerator:expect_metadata_update({ profile = "refrigerator-freezer-tn-tl" }) + mock_device_refrigerator:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for dishwasher", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_dishwasher.id, @@ -420,7 +461,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for dishwasher, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_dishwasher.id, @@ -460,7 +500,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for laundry washer", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_washer.id, @@ -495,12 +534,11 @@ test.register_coroutine_test( { mock_device_washer.id, clusters.TemperatureControl.commands.SetTemperature(mock_device_washer, washer_ep, 28 * 100, nil) } ) end, - { test_init = test_init_washer_dryer } + { test_init = test_init_washer } ) test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for laundry washer, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_washer.id, @@ -535,12 +573,11 @@ test.register_coroutine_test( { mock_device_washer.id, clusters.TemperatureControl.commands.SetTemperature(mock_device_washer, washer_ep, 50 * 100, nil) } ) end, - { test_init = test_init_washer_dryer } + { test_init = test_init_washer } ) test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for laundry dryer", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_dryer.id, @@ -575,12 +612,11 @@ test.register_coroutine_test( { mock_device_dryer.id, clusters.TemperatureControl.commands.SetTemperature(mock_device_dryer, dryer_ep, 40 * 100, nil) } ) end, - { test_init = test_init_washer_dryer } + { test_init = test_init_dryer } ) test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for laundry dryer, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_dryer.id, @@ -615,12 +651,11 @@ test.register_coroutine_test( { mock_device_dryer.id, clusters.TemperatureControl.commands.SetTemperature(mock_device_dryer, dryer_ep, 40 * 100, nil) } ) end, - { test_init = test_init_washer_dryer } + { test_init = test_init_dryer } ) test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for oven", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_oven.id, @@ -660,7 +695,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for oven, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_oven.id, @@ -700,7 +734,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for refrigerator endpoint", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_refrigerator.id, @@ -740,7 +773,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for refrigerator endpoint, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_refrigerator.id, @@ -780,7 +812,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for freezer endpoint", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_refrigerator.id, @@ -820,7 +851,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for freezer endpoint, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_refrigerator.id, diff --git a/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua b/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua index 3d7008b036..624f1751f6 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -54,6 +44,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OperationalState.attributes.OperationalState, clusters.OperationalState.attributes.OperationalError, @@ -63,17 +55,19 @@ local function test_init() clusters.MicrowaveOvenControl.attributes.MaxCookTime, clusters.MicrowaveOvenControl.attributes.CookTime } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) test.socket.matter:__expect_send({ mock_device.id, clusters.MicrowaveOvenControl.attributes.MaxCookTime:read( mock_device, APPLICATION_ENDPOINT) }) - test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function init_supported_microwave_oven_modes() diff --git a/drivers/SmartThings/matter-appliance/src/test/test_oven.lua b/drivers/SmartThings/matter-appliance/src/test/test_oven.lua index 6af4556c6a..76a23a0f44 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_oven.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_oven.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(6) @@ -112,6 +102,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.TemperatureMeasurement.attributes.MeasuredValue, @@ -123,7 +115,6 @@ local function test_init() clusters.OvenMode.attributes.CurrentMode, clusters.OvenMode.attributes.SupportedModes, } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -131,8 +122,11 @@ local function test_init() end end test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua b/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua index 1ef5734d0f..0ac94856e3 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(6) @@ -67,6 +57,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.RefrigeratorAlarm.attributes.State, clusters.RefrigeratorAndTemperatureControlledCabinetMode.attributes.CurrentMode, @@ -76,7 +68,6 @@ local function test_init() clusters.TemperatureControl.attributes.MinTemperature, clusters.TemperatureMeasurement.attributes.MeasuredValue } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -84,8 +75,9 @@ local function test_init() end end test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) diff --git a/drivers/SmartThings/matter-button/src/test/test_matter_button_parent_child.lua b/drivers/SmartThings/matter-button/src/test/test_matter_button_parent_child.lua index 7655c29e1e..a9d2629803 100644 --- a/drivers/SmartThings/matter-button/src/test/test_matter_button_parent_child.lua +++ b/drivers/SmartThings/matter-button/src/test/test_matter_button_parent_child.lua @@ -74,6 +74,7 @@ local CLUSTER_SUBSCRIBE_LIST ={ } local function test_init() + test.set_rpc_version(0) local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end diff --git a/drivers/SmartThings/matter-button/src/test/test_matter_multi_button.lua b/drivers/SmartThings/matter-button/src/test/test_matter_multi_button.lua index 42d97c109d..2ecf083ba7 100644 --- a/drivers/SmartThings/matter-button/src/test/test_matter_multi_button.lua +++ b/drivers/SmartThings/matter-button/src/test/test_matter_multi_button.lua @@ -73,35 +73,47 @@ local CLUSTER_SUBSCRIBE_LIST ={ clusters.Switch.server.events.MultiPressComplete, } +-- All messages queued and expectations set are done before the driver is actually run local function test_init() + -- we dont want the integration test framework to generate init/doConfigure, we are doing that here + -- so we can set the proper expectations on those events. + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) -- make sure the cache is populated + + -- added results in a profile update + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + mock_device:expect_metadata_update({ profile = "4-button-battery" }) + + -- init results in subscription interaction local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - mock_device:expect_metadata_update({ profile = "4-button-battery" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + --doConfigure sets the provisioing state to provisioned + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + -- simulate the profile change update taking affect and the device info changing local device_info_copy = utils.deep_copy(mock_device.raw_st_data) device_info_copy.profile.id = "4-buttons-battery" local device_info_json = dkjson.encode(device_info_copy) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 50)}) test.socket.capability:__expect_send(mock_device:generate_test_message("button4", button_attr.pushed({state_change = false}))) end test.set_test_init_function(test_init) +-- this one is failing because it expects added or test.register_message_test( "Handle single press sequence, no hold", { { diff --git a/drivers/SmartThings/matter-energy/src/init.lua b/drivers/SmartThings/matter-energy/src/init.lua index 69c28f8638..51d361753a 100644 --- a/drivers/SmartThings/matter-energy/src/init.lua +++ b/drivers/SmartThings/matter-energy/src/init.lua @@ -34,21 +34,24 @@ if version.api < 12 then end local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -local SUPPORTED_EVSE_MODES_MAP = "__supported_evse_modes_map" -local SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP = "__supported_device_energy_management_modes_map" -local RECURRING_REPORT_POLL_TIMER = "__recurring_report_poll_timer" -local RECURRING_POLL_TIMER = "__recurring_poll_timer" -local LAST_REPORTED_TIME = "__last_reported_time" -local POWER_CONSUMPTION_REPORT_TIME_INTERVAL = "__pcr_time_interval" -local DEVICE_REPORTED_TIME_INTERVAL_CONSIDERED = "__timer_interval_considered" +local SUPPORTED_EVSE_MODES = "__supported_evse_modes" +local SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES = "__supported_device_energy_management_modes" + +local CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" +local LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" +local LAST_EXPORTED_REPORT_TIMESTAMP = "__last_exported_report_timestamp" +local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds + -- total in case there are multiple electrical sensors local TOTAL_CUMULATIVE_ENERGY_IMPORTED = "__total_cumulative_energy_imported" local TOTAL_CUMULATIVE_ENERGY_EXPORTED = "__total_cumulative_energy_exported" local TOTAL_ACTIVE_POWER = "__total_active_power" -local TIMER_REPEAT = (1 * 60) -- 1 minute -local REPORT_TIMEOUT = (15 * 60) -- Report the value each 15 minutes -local MAX_REPORT_TIMEOUT = (30 * 60) +local updated_fields = { + { current_field_name = "__supported_evse_modes_map", updated_field_name = nil }, + { current_field_name = "__supported_device_energy_management_modes_map", updated_field_name = nil } +} + local MAX_CHARGING_CURRENT_CONSTRAINT = 80000 -- In v1.3 release of stack, this check for 80 A is performed. local EVSE_DEVICE_TYPE_ID = 0x050C @@ -57,7 +60,6 @@ local BATTERY_STORAGE_DEVICE_TYPE_ID = 0x0018 local ELECTRICAL_SENSOR_DEVICE_TYPE_ID = 0x0510 local DEVICE_ENERGY_MANAGEMENT_DEVICE_TYPE_ID = 0x050D - local function get_endpoints_for_dt(device, device_type) local endpoints = {} for _, ep in ipairs(device.endpoints) do @@ -102,6 +104,25 @@ local function component_to_endpoint(device, component) end end +local function get_field_for_endpoint(device, field, endpoint) + return device:get_field(string.format("%s_%d", field, endpoint)) +end + +local function set_field_for_endpoint(device, field, endpoint, value, additional_params) + device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) +end + +local function check_field_name_updates(device) + for _, field in ipairs(updated_fields) do + if device:get_field(field.current_field_name) then + if field.updated_field_name ~= nil then + device:set_field(field.updated_field_name, device:get_field(field.current_field_name), {persist = true}) + end + device:set_field(field.current_field_name, nil) + end + end +end + local function time_zone_offset() return os.difftime(os.time(), os.time(os.date("!*t", os.time()))) end @@ -135,17 +156,17 @@ local function tbl_contains(array, value) end local get_total = function(map) + local total_value = 0 if type(map) == "table" then - local total_value = 0 for _, value in pairs(map) do if type(value) == "number" then total_value = total_value + value end end - return total_value else log.debug("get_total: 'map' should be of type table") end + return total_value end -- MAPS -- @@ -193,145 +214,31 @@ local BATTERY_CHARGING_STATE_MAP = { [clusters.PowerSource.types.BatChargeStateEnum.IS_AT_FULL_CHARGE] = capabilities.chargingState.chargingState.fullyCharged, } --- Matter Handlers -local function read_cumulative_energy(device) - local cumul_imp_eps = embedded_cluster_utils.get_endpoints( - device, clusters.ElectricalEnergyMeasurement.ID, - { feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.IMPORTED_ENERGY } - ) - if cumul_imp_eps and #cumul_imp_eps > 0 then - local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(device) - device:send(read_req) - end - - -- read energy exported only in case of Solar Power / Battery Storage device. - local solar_power_eps = get_endpoints_for_dt(device, SOLAR_POWER_DEVICE_TYPE_ID) or {} - local battery_storage_eps = get_endpoints_for_dt(device, BATTERY_STORAGE_DEVICE_TYPE_ID) or {} - local eps_to_read = {} - utils.merge(eps_to_read, battery_storage_eps) - utils.merge(eps_to_read, solar_power_eps) - if #eps_to_read > 0 then - local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(device, eps_to_read[1]) - for i, ep in ipairs(eps_to_read) do - if i > 1 then - read_req:merge(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(device, ep)) - end - end - device:send(read_req) - end -end - -local function create_poll_schedule(device) - local poll_timer = device:get_field(RECURRING_POLL_TIMER) - if poll_timer ~= nil then - return - end - - local cumul_eps = embedded_cluster_utils.get_endpoints(device, - clusters.ElectricalEnergyMeasurement.ID, - { feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY }) or {} - if #cumul_eps == 0 then - return - end - read_cumulative_energy(device) - -- Read cumulative energy imported/exported attributes every minute - local timer = device.thread:call_on_schedule(TIMER_REPEAT, function() - read_cumulative_energy(device) - end, "polling_schedule_timer") - - device:set_field(RECURRING_POLL_TIMER, timer) -end - -local report_energy_to_app = function(device, comp, energy_map, startTime, endTime) - local component = device.profile.components[comp] - local total_cumulative_energy = get_total(energy_map) or 0 - - -- Calculate the energy consumed between the start and the end time - local previousTotalConsumptionWh = device:get_latest_state( - comp, capabilities.powerConsumptionReport.ID, capabilities.powerConsumptionReport.powerConsumption.NAME - ) or { energy = 0 } - local deltaEnergyWh = math.max(total_cumulative_energy - previousTotalConsumptionWh.energy, 0.0) - - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - device:emit_component_event(component, capabilities.powerConsumptionReport.powerConsumption({ - start = startTime, - ["end"] = endTime, - deltaEnergy = deltaEnergyWh, - energy = total_cumulative_energy - })) -end - -local function create_poll_report_schedule(device) - local polling_schedule_timer = device:get_field(RECURRING_REPORT_POLL_TIMER) - if polling_schedule_timer ~= nil then - return - end - - -- The powerConsumption report needs to be updated at least every 15 minutes in order to be included in SmartThings Energy - local pcr_interval = device:get_field(POWER_CONSUMPTION_REPORT_TIME_INTERVAL) or REPORT_TIMEOUT - - local timer = device.thread:call_on_schedule(pcr_interval, function() - local current_time = os.time() - local last_time = device:get_field(LAST_REPORTED_TIME) or 0 - local cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED) - local cumulative_energy_exported = device:get_field(TOTAL_CUMULATIVE_ENERGY_EXPORTED) - device:set_field(LAST_REPORTED_TIME, current_time, { persist = true }) - local startTime = epoch_to_iso8601(last_time) - local endTime = epoch_to_iso8601(current_time - 1) - - if cumulative_energy_imported ~= nil then - local evse_eps = get_endpoints_for_dt(device, EVSE_DEVICE_TYPE_ID) or {} - local comp_id = "importedEnergy" - if #evse_eps > 0 then - comp_id = "main" - end - report_energy_to_app(device, comp_id, cumulative_energy_imported, startTime, endTime) - end - - -- If energy exported is set, it must be for Solar Power / Battery Storage Device. - if cumulative_energy_exported ~= nil then - report_energy_to_app(device, "exportedEnergy", cumulative_energy_exported, startTime, endTime) - end - end, "polling_report_schedule_timer") - - device:set_field(RECURRING_REPORT_POLL_TIMER, timer) -end - -local function create_poll_schedules_for_cumulative_energy_reports(device) - if not device:supports_capability(capabilities.powerConsumptionReport) then - return - end - create_poll_schedule(device) - create_poll_report_schedule(device) -end - -local function delete_reporting_timer(device) - local reporting_poll_timer = device:get_field(RECURRING_REPORT_POLL_TIMER) - if reporting_poll_timer ~= nil then - device.thread:cancel_timer(reporting_poll_timer) - device:set_field(RECURRING_REPORT_POLL_TIMER, nil) - end -end - -local function delete_poll_schedules(device) - local poll_timer = device:get_field(RECURRING_POLL_TIMER) - if poll_timer ~= nil then - device.thread:cancel_timer(poll_timer) - device:set_field(RECURRING_POLL_TIMER, nil) - end - delete_reporting_timer(device) -end - -- Lifecycle Handlers -- local function device_init(driver, device) + check_field_name_updates(device) device:subscribe() device:set_endpoint_to_component_fn(endpoint_to_component) device:set_component_to_endpoint_fn(component_to_endpoint) - create_poll_schedules_for_cumulative_energy_reports(device) - local current_time = os.time() - local current_time_iso8601 = epoch_to_iso8601(current_time) -- emit current time by default - device:emit_event(capabilities.evseChargingSession.targetEndTime(current_time_iso8601)) + local evse_eps = get_endpoints_for_dt(device, EVSE_DEVICE_TYPE_ID) or {} + if #evse_eps > 0 then + local current_time = os.time() + local current_time_iso8601 = epoch_to_iso8601(current_time) + device:emit_event(capabilities.evseChargingSession.targetEndTime(current_time_iso8601)) + end + + -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. + -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. + local electrical_energy_measurement_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) + if #electrical_energy_measurement_eps > 0 then + local cumulative_energy_eps = embedded_cluster_utils.get_endpoints( + device, + clusters.ElectricalEnergyMeasurement.ID, + {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY} + ) + if #cumulative_energy_eps == 0 then device:set_field(CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = false}) end + end end local function device_added(driver, device) @@ -381,11 +288,10 @@ local function info_changed(driver, device) end end device:subscribe() - create_poll_schedules_for_cumulative_energy_reports(device) end local function device_removed(driver, device) - delete_poll_schedules(device) + device.log.info("device removed") end -- Matter Handlers -- @@ -504,7 +410,6 @@ local function power_mode_handler(driver, device, ib, response) end local function energy_evse_supported_modes_attr_handler(driver, device, ib, response) - local supportedEvseModesMap = device:get_field(SUPPORTED_EVSE_MODES_MAP) or {} local supportedEvseModes = {} for _, mode in ipairs(ib.data.elements) do if version.api < 11 then @@ -512,8 +417,7 @@ local function energy_evse_supported_modes_attr_handler(driver, device, ib, resp end table.insert(supportedEvseModes, mode.elements.label.value) end - supportedEvseModesMap[ib.endpoint_id] = supportedEvseModes - device:set_field(SUPPORTED_EVSE_MODES_MAP, supportedEvseModesMap, { persist = true }) + set_field_for_endpoint(device, SUPPORTED_EVSE_MODES, ib.endpoint_id, supportedEvseModes, { persist = true }) local event = capabilities.mode.supportedModes(supportedEvseModes, { visibility = { displayed = false } }) device:emit_event_for_endpoint(ib.endpoint_id, event) event = capabilities.mode.supportedArguments(supportedEvseModes, { visibility = { displayed = false } }) @@ -521,10 +425,7 @@ local function energy_evse_supported_modes_attr_handler(driver, device, ib, resp end local function energy_evse_mode_attr_handler(driver, device, ib, response) - device.log.info(string.format("energy_evse_modes_attr_handler currentMode: %s", ib.data.value)) - - local supportedEvseModesMap = device:get_field(SUPPORTED_EVSE_MODES_MAP) or {} - local supportedEvseModes = supportedEvseModesMap[ib.endpoint_id] or {} + local supportedEvseModes = get_field_for_endpoint(device, SUPPORTED_EVSE_MODES, ib.endpoint_id) or {} local currentMode = ib.data.value for i, mode in ipairs(supportedEvseModes) do if i - 1 == currentMode then @@ -535,7 +436,6 @@ local function energy_evse_mode_attr_handler(driver, device, ib, response) end local function device_energy_mgmt_supported_modes_attr_handler(driver, device, ib, response) - local supportedDeviceEnergyMgmtModesMap = device:get_field(SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP) or {} local supportedDeviceEnergyMgmtModes = {} for _, mode in ipairs(ib.data.elements) do if version.api < 12 then @@ -543,8 +443,7 @@ local function device_energy_mgmt_supported_modes_attr_handler(driver, device, i end table.insert(supportedDeviceEnergyMgmtModes, mode.elements.label.value) end - supportedDeviceEnergyMgmtModesMap[ib.endpoint_id] = supportedDeviceEnergyMgmtModes - device:set_field(SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP, supportedDeviceEnergyMgmtModesMap, { persist = true }) + set_field_for_endpoint(device, SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES, ib.endpoint_id, supportedDeviceEnergyMgmtModes, { persist = true }) local event = capabilities.mode.supportedModes(supportedDeviceEnergyMgmtModes, { visibility = { displayed = false } }) device:emit_event_for_endpoint(ib.endpoint_id, event) event = capabilities.mode.supportedArguments(supportedDeviceEnergyMgmtModes, { visibility = { displayed = false } }) @@ -552,10 +451,7 @@ local function device_energy_mgmt_supported_modes_attr_handler(driver, device, i end local function device_energy_mgmt_mode_attr_handler(driver, device, ib, response) - device.log.info(string.format("device_energy_mgmt_mode_attr_handler currentMode: %s", ib.data.value)) - - local supportedDeviceEnergyMgmtModesMap = device:get_field(SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP) or {} - local supportedDeviceEnergyMgmtModes = supportedDeviceEnergyMgmtModesMap[ib.endpoint_id] or {} + local supportedDeviceEnergyMgmtModes = get_field_for_endpoint(device, SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES, ib.endpoint_id) or {} local currentMode = ib.data.value for i, mode in ipairs(supportedDeviceEnergyMgmtModes) do if i - 1 == currentMode then @@ -565,93 +461,88 @@ local function device_energy_mgmt_mode_attr_handler(driver, device, ib, response end end -local function report_energy_meter(device, energy_map_id) - --report energyMeter for Solar Power and Battery Storage devices only. - local battery_storage_eps = get_endpoints_for_dt(device, BATTERY_STORAGE_DEVICE_TYPE_ID) or {} - local solar_power_eps = get_endpoints_for_dt(device, SOLAR_POWER_DEVICE_TYPE_ID) or {} - local energy_map = device:get_field(energy_map_id) or {} - local total_energy = get_total(energy_map) or 0 +local function report_power_consumption_to_st_energy(device, component, latest_total_imported_energy_wh) + local current_time = os.time() - if #battery_storage_eps > 0 then - local component = device.profile.components["importedEnergy"] - if energy_map_id == TOTAL_CUMULATIVE_ENERGY_EXPORTED then - component = device.profile.components["exportedEnergy"] - end - device:emit_component_event(component, capabilities.energyMeter.energy({value = total_energy, unit = "Wh"})) + local last_report_timestamp_field = component.id == "exportedEnergy" and LAST_EXPORTED_REPORT_TIMESTAMP or LAST_IMPORTED_REPORT_TIMESTAMP + local last_time = device:get_field(last_report_timestamp_field) or 0 + -- Ensure that the previous report was sent at least 15 minutes ago + if MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then return end - -- energyMeter in Solar Power devices must report exported energy only. - if #solar_power_eps > 0 and energy_map_id == TOTAL_CUMULATIVE_ENERGY_EXPORTED then - device:emit_event(capabilities.energyMeter.energy({value = total_energy, unit = "Wh"})) + device:set_field(last_report_timestamp_field, current_time, { persist = true }) + + -- Calculate the energy delta between reports + local energy_delta_wh = 0.0 + local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, + capabilities.powerConsumptionReport.powerConsumption.NAME) + if previous_imported_report and previous_imported_report.energy then + energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) end + + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + device:emit_component_event(component, capabilities.powerConsumptionReport.powerConsumption({ + start = epoch_to_iso8601(last_time), + ["end"] = epoch_to_iso8601(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) end -local function cumulative_energy_handler(energy_map_id) - return function(driver, device, ib, response) - if ib.data then - if version.api < 11 then - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct:augment_type(ib.data) - end - local endpoint_id = string.format(ib.endpoint_id) - local cumulative_energy_Wh = utils.round(ib.data.elements.energy.value / 1000) -- convert mWh to Wh - local total_cumulative_energy = device:get_field(energy_map_id) or {} - - -- in case there are multiple electrical sensors store them in a table. - total_cumulative_energy[endpoint_id] = cumulative_energy_Wh - device:set_field(energy_map_id, total_cumulative_energy, { persist = true }) - report_energy_meter(device, energy_map_id) +local function get_component_for_energy_reports(device, cumulative_import_or_export_field) + local energyMeter_component, powerConsumption_component + if cumulative_import_or_export_field == TOTAL_CUMULATIVE_ENERGY_EXPORTED then -- this is an export report + energyMeter_component = "exportedEnergy" + powerConsumption_component = "exportedEnergy" + if #get_endpoints_for_dt(device, SOLAR_POWER_DEVICE_TYPE_ID) > 0 then + energyMeter_component = "main" + powerConsumption_component = "exportedEnergy" + end + else + energyMeter_component = "main" + powerConsumption_component = "main" + if #get_endpoints_for_dt(device, BATTERY_STORAGE_DEVICE_TYPE_ID) > 0 then + energyMeter_component = "importedEnergy" + powerConsumption_component = "importedEnergy" + elseif #get_endpoints_for_dt(device, SOLAR_POWER_DEVICE_TYPE_ID) > 0 then + energyMeter_component = "N/A" -- do not send cumulative import reports for solar power end end + return energyMeter_component, powerConsumption_component end - -local function periodic_energy_handler(energy_map_id) +local function energy_report_handler_factory(is_cumulative_report, cumulative_import_or_export_field) return function(driver, device, ib, response) - local endpoint_id = ib.endpoint_id - local cumul_eps = embedded_cluster_utils.get_endpoints(device, - clusters.ElectricalEnergyMeasurement.ID, - { feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY }) - - if ib.data then - if version.api < 11 then - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct:augment_type(ib.data) - end + if not ib.data then return + elseif version.api < 11 then clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct:augment_type(ib.data) end - local start_timestamp = ib.data.elements.start_timestamp.value or 0 - local end_timestamp = ib.data.elements.end_timestamp.value or 0 - - local device_reporting_time_interval = end_timestamp - start_timestamp - if not device:get_field(DEVICE_REPORTED_TIME_INTERVAL_CONSIDERED) and device_reporting_time_interval > REPORT_TIMEOUT then - -- This is a one time setup in order to consider a larger time interval if the interval the device chooses to report is greater than 15 minutes. - device_reporting_time_interval = utils.clamp_value(device_reporting_time_interval, REPORT_TIMEOUT, MAX_REPORT_TIMEOUT) - device:set_field(DEVICE_REPORTED_TIME_INTERVAL_CONSIDERED, true, {persist=true}) - device:set_field(POWER_CONSUMPTION_REPORT_TIME_INTERVAL, device_reporting_time_interval, {persist = true}) - delete_reporting_timer(device) - create_poll_report_schedule(device) - end + local endpoint_id = string.format(ib.endpoint_id) + local total_cumulative_energy = device:get_field(cumulative_import_or_export_field) or {} + local energy_Wh = utils.round(ib.data.elements.energy.value / 1000) -- convert mWh to Wh - if tbl_contains(cumul_eps, endpoint_id) then - -- Since cluster in this endpoint supports both CUME & PERE features, we will prefer - -- cumulative_energy_handler to handle the energy report for this endpoint over periodic_energy_handler. - return + if not is_cumulative_report then + if device:get_field(CUMULATIVE_REPORTS_NOT_SUPPORTED) ~= true then + return -- if this is a periodic report and cumulative reports ARE supported by the device, ignore the report altogether. end + energy_Wh = energy_Wh + (total_cumulative_energy[endpoint_id] or 0) -- handle the periodic report + end + total_cumulative_energy[endpoint_id] = energy_Wh -- in the case that there are multiple electrical sensors, store them in a table. + device:set_field(cumulative_import_or_export_field, total_cumulative_energy, { persist = true }) - endpoint_id = string.format(ib.endpoint_id) - local energy_Wh = utils.round(ib.data.elements.energy.value / 1000) -- convert mWh to Wh - local total_cumulative_energy = device:get_field(energy_map_id) or {} - - -- in case there are multiple electrical sensors store them in a table. - total_cumulative_energy[endpoint_id] = total_cumulative_energy[endpoint_id] or 0 - total_cumulative_energy[endpoint_id] = total_cumulative_energy[endpoint_id] + energy_Wh - device:set_field(energy_map_id, total_cumulative_energy, { persist = true }) - report_energy_meter(device, energy_map_id) + local summed_total_energy = get_total(total_cumulative_energy) + local energyMeter_component, powerConsumption_component = get_component_for_energy_reports(device, cumulative_import_or_export_field) + if device.profile.components[energyMeter_component] and device:supports_capability(capabilities.energyMeter) then + device:emit_component_event(device.profile.components[energyMeter_component], capabilities.energyMeter.energy({value = summed_total_energy, unit = "Wh"})) + end + if device.profile.components[powerConsumption_component] and device:supports_capability(capabilities.powerConsumptionReport) then + report_power_consumption_to_st_energy(device, device.profile.components[powerConsumption_component], summed_total_energy) end end end local function active_power_handler(driver, device, ib, response) - local battery_storage_eps = get_endpoints_for_dt(device, SOLAR_POWER_DEVICE_TYPE_ID) or {} - local solar_power_eps = get_endpoints_for_dt(device, BATTERY_STORAGE_DEVICE_TYPE_ID) or {} + local battery_storage_eps = get_endpoints_for_dt(device, BATTERY_STORAGE_DEVICE_TYPE_ID) or {} + local solar_power_eps = get_endpoints_for_dt(device, SOLAR_POWER_DEVICE_TYPE_ID) or {} -- Consider only Solar Power / Battery Storage devices and sum up in case there are multiple endpoints. if (tbl_contains(solar_power_eps, ib.endpoint_id) or tbl_contains(battery_storage_eps, ib.endpoint_id)) and ib.data.value then local endpoint_id = string.format(ib.endpoint_id) @@ -726,8 +617,7 @@ local function handle_set_mode_command(driver, device, cmd) local set_mode_handlers = { ["main"] = function( ... ) local ep = component_to_endpoint(device, cmd.component) - local supportedEvseModesMap = device:get_field(SUPPORTED_EVSE_MODES_MAP) - local supportedEvseModes = supportedEvseModesMap[ep] or {} + local supportedEvseModes = get_field_for_endpoint(device, SUPPORTED_EVSE_MODES, ep) or {} for i, mode in ipairs(supportedEvseModes) do if cmd.args.mode == mode then device:send(clusters.EnergyEvseMode.commands.ChangeToMode(device, ep, i - 1)) @@ -738,8 +628,7 @@ local function handle_set_mode_command(driver, device, cmd) end, ["deviceEnergyManagement"] = function( ... ) local ep = component_to_endpoint(device, cmd.component) - local supportedDeviceEnergyMgmtModesMap = device:get_field(SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP) - local supportedDeviceEnergyMgmtModes = supportedDeviceEnergyMgmtModesMap[ep] or {} + local supportedDeviceEnergyMgmtModes = get_field_for_endpoint(device, SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES, ep) or {} for i, mode in ipairs(supportedDeviceEnergyMgmtModes) do if cmd.args.mode == mode then device:send(clusters.DeviceEnergyManagementMode.commands.ChangeToMode(device, ep, i - 1)) @@ -788,10 +677,10 @@ matter_driver_template = { [clusters.DeviceEnergyManagementMode.attributes.CurrentMode.ID] = device_energy_mgmt_mode_attr_handler, }, [clusters.ElectricalEnergyMeasurement.ID] = { - [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = cumulative_energy_handler(TOTAL_CUMULATIVE_ENERGY_IMPORTED), - [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = periodic_energy_handler(TOTAL_CUMULATIVE_ENERGY_IMPORTED), - [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported.ID] = cumulative_energy_handler(TOTAL_CUMULATIVE_ENERGY_EXPORTED), - [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported.ID] = periodic_energy_handler(TOTAL_CUMULATIVE_ENERGY_EXPORTED), + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = energy_report_handler_factory(true, TOTAL_CUMULATIVE_ENERGY_IMPORTED), + [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = energy_report_handler_factory(false, TOTAL_CUMULATIVE_ENERGY_IMPORTED), + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported.ID] = energy_report_handler_factory(true, TOTAL_CUMULATIVE_ENERGY_EXPORTED), + [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported.ID] = energy_report_handler_factory(false, TOTAL_CUMULATIVE_ENERGY_EXPORTED), }, [clusters.PowerSource.ID] = { [clusters.PowerSource.attributes.BatPercentRemaining.ID] = battery_percent_remaining_attr_handler, diff --git a/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua b/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua index 453858d7c2..5b5734acf5 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua @@ -62,6 +62,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.ElectricalPowerMeasurement.attributes.ActivePower, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, @@ -69,27 +71,18 @@ local function test_init() clusters.PowerSource.attributes.BatPercentRemaining, clusters.PowerSource.attributes.BatChargeState } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) - - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, BATTERY_STORAGE_EP) - }) - + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) @@ -198,23 +191,12 @@ test.register_coroutine_test( test.register_coroutine_test( "Ensure the total cumulative energy exported powerConsumption for both endpoints is reported every 15 minutes", function() - test.socket.matter:__set_channel_ordering("relaxed") - test.socket.capability:__set_channel_ordering("relaxed") - - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) - - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, BATTERY_STORAGE_EP) - }) + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, BATTERY_STORAGE_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --100Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) --100Wh test.socket.capability:__expect_send( mock_device:generate_test_message("importedEnergy", @@ -223,57 +205,47 @@ test.register_coroutine_test( })) ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("importedEnergy", + capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 100 + }) + ) + ) + test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyExported:build_test_report_data(mock_device, BATTERY_STORAGE_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 300000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --300Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 400000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) --400Wh test.socket.capability:__expect_send( mock_device:generate_test_message("exportedEnergy", capabilities.energyMeter.energy({ - value = 300, unit = "Wh" + value = 400, unit = "Wh" })) ) - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - test.socket.capability:__expect_send( mock_device:generate_test_message("exportedEnergy", capabilities.powerConsumptionReport.powerConsumption({ - energy = 300, - deltaEnergy = 300, - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" - })) - ) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("importedEnergy", - capabilities.powerConsumptionReport.powerConsumption({ - energy = 100, - deltaEnergy = 100, start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" - })) + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 400 + }) + ) ) test.wait_for_events() - - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) - - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, BATTERY_STORAGE_EP) - }) + test.mock_time.advance_time(2000) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, BATTERY_STORAGE_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 200000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --200Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 200000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) --200Wh test.socket.capability:__expect_send( mock_device:generate_test_message("importedEnergy", @@ -281,10 +253,20 @@ test.register_coroutine_test( value = 200, unit = "Wh" }))) + test.socket.capability:__expect_send( + mock_device:generate_test_message("importedEnergy", + capabilities.powerConsumptionReport.powerConsumption({ + energy = 200, + deltaEnergy = 0.0, + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:48:20Z" + })) + ) + test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyExported:build_test_report_data(mock_device, BATTERY_STORAGE_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 400000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --400Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 400000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) --400Wh test.socket.capability:__expect_send( mock_device:generate_test_message("exportedEnergy", @@ -293,36 +275,19 @@ test.register_coroutine_test( })) ) - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - test.socket.capability:__expect_send( mock_device:generate_test_message("exportedEnergy", capabilities.powerConsumptionReport.powerConsumption({ energy = 400, - deltaEnergy = 100, - start = "1970-01-01T00:15:00Z", - ["end"] = "1970-01-01T00:29:59Z" + deltaEnergy = 0.0, + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:48:20Z" })) ) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("importedEnergy", - capabilities.powerConsumptionReport.powerConsumption({ - energy = 200, - deltaEnergy = 100, - start = "1970-01-01T00:15:00Z", - ["end"] = "1970-01-01T00:29:59Z" - })) - ) - - test.wait_for_events() end, { test_init = function() test_init() - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "create_poll_report_schedule") - test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") end } ) diff --git a/drivers/SmartThings/matter-energy/src/test/test_evse.lua b/drivers/SmartThings/matter-energy/src/test/test_evse.lua index ad9db31150..6add71bde9 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_evse.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_evse.lua @@ -75,6 +75,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.EnergyEvse.attributes.State, clusters.EnergyEvse.attributes.SupplyState, @@ -90,19 +92,22 @@ local function test_init() clusters.DeviceEnergyManagementMode.attributes.CurrentMode, clusters.DeviceEnergyManagementMode.attributes.SupportedModes, } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.evseChargingSession.targetEndTime("1970-01-01T00:00:00Z"))) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ profile = "evse-power-meas" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-energy/src/test/test_evse_energy_meas.lua b/drivers/SmartThings/matter-energy/src/test/test_evse_energy_meas.lua index 91332241b7..610c443625 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_evse_energy_meas.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_evse_energy_meas.lua @@ -75,6 +75,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.EnergyEvse.attributes.State, clusters.EnergyEvse.attributes.SupplyState, @@ -89,24 +91,22 @@ local function test_init() clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.evseChargingSession.targetEndTime("1970-01-01T00:00:00Z"))) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ profile = "evse-energy-meas" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) @@ -120,92 +120,34 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "Ensure timers are created for the device", - function() - local poll_timer = mock_device:get_field("__recurring_poll_timer") - assert(poll_timer ~= nil, "poll_timer should exist") - - local report_poll_timer = mock_device:get_field("__recurring_report_poll_timer") - assert(report_poll_timer ~= nil, "report_poll_timer should exist") - end -) - -test.register_coroutine_test( - "Ensure timers are created for the device", - function() - test.socket.matter:__set_channel_ordering("relaxed") - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "removed" }) - test.wait_for_events() - - local poll_timer = mock_device:get_field("__recurring_poll_timer") - assert(poll_timer == nil, "poll_timer should not exist") - - local report_poll_timer = mock_device:get_field("__recurring_report_poll_timer") - assert(report_poll_timer == nil, "report_poll_timer should not exist") - end -) - -test.register_coroutine_test( - "Ensure that every 60 seconds the driver reads the CumulativeEnergyImported attribute for both endpoints", - function() - test.mock_time.advance_time(60) - test.socket.matter:__set_channel_ordering("relaxed") - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) - test.wait_for_events() - end, - { - test_init = function() - test_init() - test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") - end - } -) - -test.register_coroutine_test( - "Ensure the total accumulated powerConsumption for both endpoints is reported every 15 minutes", + "Ensure the total accumulated powerConsumption for both endpoints is reported", function() - test.socket.matter:__set_channel_ordering("relaxed") - test.socket.capability:__set_channel_ordering("relaxed") - - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) - test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, ELECTRICAL_SENSOR_EP_ONE, clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --100Wh + test.wait_for_events() + test.mock_time.advance_time(901) + test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, ELECTRICAL_SENSOR_EP_TWO, clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 150000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --150Wh - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ energy = 250, - deltaEnergy = 250, + deltaEnergy = 0.0, start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" + ["end"] = "1970-01-01T00:15:00Z" })) ) - - test.wait_for_events() end, { test_init = function() test_init() - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "create_poll_report_schedule") - test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") end } ) diff --git a/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua b/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua index 049b364d6f..c7446af44b 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua @@ -71,28 +71,25 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.ElectricalPowerMeasurement.attributes.ActivePower, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_ONE) - read_req:merge(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_TWO)) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.socket.matter:__expect_send({ - mock_device.id, - read_req - }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) @@ -129,65 +126,14 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "Ensure timers are created for the device and terminated on removed", + "Ensure the total cumulative energy exported powerConsumption for both endpoints is reported", function() - test.socket.matter:__set_channel_ordering("relaxed") - local poll_timer = mock_device:get_field("__recurring_poll_timer") - assert(poll_timer ~= nil, "poll_timer should not exist") - - local report_poll_timer = mock_device:get_field("__recurring_report_poll_timer") - assert(report_poll_timer ~= nil, "report_poll_timer should exist") - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "removed" }) - test.wait_for_events() - - local poll_timer = mock_device:get_field("__recurring_poll_timer") - assert(poll_timer == nil, "poll_timer should not exist") - - local report_poll_timer = mock_device:get_field("__recurring_report_poll_timer") - assert(report_poll_timer == nil, "report_poll_timer should not exist") - end -) - -test.register_coroutine_test( - "Ensure that every 60 seconds the driver reads the CumulativeEnergyExported attribute for both endpoints", - function() - test.mock_time.advance_time(60) - test.socket.matter:__set_channel_ordering("relaxed") - local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_ONE) - read_req:merge(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_TWO)) - test.socket.matter:__expect_send({ - mock_device.id, - read_req - }) - test.wait_for_events() - end, - { - test_init = function() - test_init() - test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") - end - } -) - -test.register_coroutine_test( - "Ensure the total cumulative energy exported powerConsumption for both endpoints is reported every 15 minutes", - function() - test.socket.matter:__set_channel_ordering("relaxed") - test.socket.capability:__set_channel_ordering("relaxed") - - local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_ONE) - read_req:merge(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_TWO)) - - test.socket.matter:__expect_send({ - mock_device.id, - read_req - }) + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyExported:build_test_report_data(mock_device, SOLAR_POWER_EP_ONE, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --100Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) --100Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -196,37 +142,32 @@ test.register_coroutine_test( })) ) - test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes - .CumulativeEnergyExported:build_test_report_data(mock_device, - SOLAR_POWER_EP_TWO, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 150000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --150Wh - - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.energyMeter.energy({ - value = 250, unit = "Wh" - })) - ) - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - test.socket.capability:__expect_send( mock_device:generate_test_message("exportedEnergy", capabilities.powerConsumptionReport.powerConsumption({ - energy = 250, - deltaEnergy = 250, + energy = 100, + deltaEnergy = 0.0, start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" - })) + ["end"] = "1970-01-01T00:15:00Z" + }) + ) ) - test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes + .CumulativeEnergyExported:build_test_report_data(mock_device, + SOLAR_POWER_EP_TWO, + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 150000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) --150Wh + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.energyMeter.energy({ + value = 250, unit = "Wh" + })) + ) end, { test_init = function() test_init() - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "create_poll_report_schedule") - test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") end } ) @@ -237,7 +178,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyExported:build_test_report_data(mock_device, SOLAR_POWER_EP_ONE, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --100Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) --100Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -249,7 +190,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, SOLAR_POWER_EP_ONE, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --100Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) --100Wh end ) diff --git a/drivers/SmartThings/matter-hrap/config.yml b/drivers/SmartThings/matter-hrap/config.yml new file mode 100644 index 0000000000..21d79bc8ec --- /dev/null +++ b/drivers/SmartThings/matter-hrap/config.yml @@ -0,0 +1,6 @@ +name: 'Matter HRAP' +packageKey: 'matter-hrap' +permissions: + matter: {} +description: "SmartThings driver for Matter HRAP devices" +vendorSupportInformation: "https://support.smartthings.com" diff --git a/drivers/SmartThings/matter-hrap/fingerprints.yml b/drivers/SmartThings/matter-hrap/fingerprints.yml new file mode 100644 index 0000000000..7b768709c2 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/fingerprints.yml @@ -0,0 +1,11 @@ +matterGeneric: + - id: "matter/network-manager" + deviceLabel: Matter Network Infrastructure Manager + deviceTypes: + - id: 0x0090 # NIM + deviceProfileName: network-infrastructure-manager + - id: "matter/thread-border-router" + deviceLabel: Matter Thread Border Router + deviceTypes: + - id: 0x0091 # TBR + deviceProfileName: thread-border-router diff --git a/drivers/SmartThings/matter-hrap/profiles/network-infrastructure-manager.yml b/drivers/SmartThings/matter-hrap/profiles/network-infrastructure-manager.yml new file mode 100644 index 0000000000..725676c502 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/profiles/network-infrastructure-manager.yml @@ -0,0 +1,16 @@ +name: network-infrastructure-manager +components: +- id: main + capabilities: + - id: threadBorderRouter + version: 1 + - id: threadNetwork + version: 1 + - id: wifiInformation + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Networking diff --git a/drivers/SmartThings/matter-hrap/profiles/thread-border-router.yml b/drivers/SmartThings/matter-hrap/profiles/thread-border-router.yml new file mode 100644 index 0000000000..b879dd2e76 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/profiles/thread-border-router.yml @@ -0,0 +1,14 @@ +name: thread-border-router +components: +- id: main + capabilities: + - id: threadBorderRouter + version: 1 + - id: threadNetwork + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Networking diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/DatasetResponse.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/DatasetResponse.lua new file mode 100644 index 0000000000..0cf8facc53 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/DatasetResponse.lua @@ -0,0 +1,103 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local DatasetResponse = {} + +DatasetResponse.NAME = "DatasetResponse" +DatasetResponse.ID = 0x0002 +DatasetResponse.field_defs = { + { + name = "dataset", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.OctetString1", + }, +} + +function DatasetResponse:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + base_type_obj.elements = elems +end + +function DatasetResponse:build_test_command_response(device, endpoint_id, dataset, interaction_status) + local function init(self, device, endpoint_id, dataset) + local out = {} + local args = {dataset} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = DatasetResponse, + __tostring = DatasetResponse.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + false, + true + ) + end + local self_request = init(self, device, endpoint_id, dataset) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + self_request.info_blocks[1].tlv, + interaction_status + ) +end + +function DatasetResponse:init() + return nil +end + +function DatasetResponse:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function DatasetResponse:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(DatasetResponse, {__call = DatasetResponse.init}) + +return DatasetResponse diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/init.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/init.lua new file mode 100644 index 0000000000..3c8bee49fa --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/init.lua @@ -0,0 +1,23 @@ +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("ThreadBorderRouterManagement.client.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local ThreadBorderRouterManagementClientCommands = {} + +function ThreadBorderRouterManagementClientCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ThreadBorderRouterManagementClientCommands, command_mt) + +return ThreadBorderRouterManagementClientCommands + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/init.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/init.lua new file mode 100644 index 0000000000..3b3485d03d --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/init.lua @@ -0,0 +1,148 @@ +local cluster_base = require "st.matter.cluster_base" +local ThreadBorderRouterManagementServerAttributes = require "ThreadBorderRouterManagement.server.attributes" +local ThreadBorderRouterManagementServerCommands = require "ThreadBorderRouterManagement.server.commands" +local ThreadBorderRouterManagementClientCommands = require "ThreadBorderRouterManagement.client.commands" +local ThreadBorderRouterManagementTypes = require "ThreadBorderRouterManagement.types" + +local ThreadBorderRouterManagement = {} + +ThreadBorderRouterManagement.ID = 0x0452 +ThreadBorderRouterManagement.NAME = "ThreadBorderRouterManagement" +ThreadBorderRouterManagement.server = {} +ThreadBorderRouterManagement.client = {} +ThreadBorderRouterManagement.server.attributes = ThreadBorderRouterManagementServerAttributes:set_parent_cluster(ThreadBorderRouterManagement) +ThreadBorderRouterManagement.server.commands = ThreadBorderRouterManagementServerCommands:set_parent_cluster(ThreadBorderRouterManagement) +ThreadBorderRouterManagement.client.commands = ThreadBorderRouterManagementClientCommands:set_parent_cluster(ThreadBorderRouterManagement) +ThreadBorderRouterManagement.types = ThreadBorderRouterManagementTypes + +function ThreadBorderRouterManagement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "BorderRouterName", + [0x0001] = "BorderAgentID", + [0x0002] = "ThreadVersion", + [0x0003] = "InterfaceEnabled", + [0x0004] = "ActiveDatasetTimestamp", + [0x0005] = "PendingDatasetTimestamp", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function ThreadBorderRouterManagement:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "GetActiveDatasetRequest", + [0x0001] = "GetPendingDatasetRequest", + [0x0003] = "SetActiveDatasetRequest", + [0x0004] = "SetPendingDatasetRequest", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +function ThreadBorderRouterManagement:get_client_command_by_id(command_id) + local client_id_map = { + [0x0002] = "DatasetResponse", + } + if client_id_map[command_id] ~= nil then + return self.client.commands[client_id_map[command_id]] + end + return nil +end + +ThreadBorderRouterManagement.attribute_direction_map = { + ["BorderRouterName"] = "server", + ["BorderAgentID"] = "server", + ["ThreadVersion"] = "server", + ["InterfaceEnabled"] = "server", + ["ActiveDatasetTimestamp"] = "server", + ["PendingDatasetTimestamp"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +do + local has_aliases, aliases = pcall(require, "st.matter.clusters.aliases.ThreadBorderRouterManagement.server.attributes") + if has_aliases then + for alias, _ in pairs(aliases) do + ThreadBorderRouterManagement.attribute_direction_map[alias] = "server" + end + end +end + +ThreadBorderRouterManagement.command_direction_map = { + ["GetActiveDatasetRequest"] = "server", + ["GetPendingDatasetRequest"] = "server", + ["SetActiveDatasetRequest"] = "server", + ["SetPendingDatasetRequest"] = "server", + ["DatasetResponse"] = "client", +} + +do + local has_aliases, aliases = pcall(require, "st.matter.clusters.aliases.ThreadBorderRouterManagement.server.commands") + if has_aliases then + for alias, _ in pairs(aliases) do + ThreadBorderRouterManagement.command_direction_map[alias] = "server" + end + end +end + +do + local has_aliases, aliases = pcall(require, "st.matter.clusters.aliases.ThreadBorderRouterManagement.client.commands") + if has_aliases then + for alias, _ in pairs(aliases) do + ThreadBorderRouterManagement.command_direction_map[alias] = "client" + end + end +end + +ThreadBorderRouterManagement.FeatureMap = ThreadBorderRouterManagement.types.Feature + +function ThreadBorderRouterManagement.are_features_supported(feature, feature_map) + if (ThreadBorderRouterManagement.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ThreadBorderRouterManagement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ThreadBorderRouterManagement.NAME)) + end + return ThreadBorderRouterManagement[direction].attributes[key] +end +ThreadBorderRouterManagement.attributes = {} +setmetatable(ThreadBorderRouterManagement.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = ThreadBorderRouterManagement.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, ThreadBorderRouterManagement.NAME)) + end + return ThreadBorderRouterManagement[direction].commands[key] +end +ThreadBorderRouterManagement.commands = {} +setmetatable(ThreadBorderRouterManagement.commands, command_helper_mt) + +local event_helper_mt = {} +event_helper_mt.__index = function(self, key) + return ThreadBorderRouterManagement.server.events[key] +end +ThreadBorderRouterManagement.events = {} +setmetatable(ThreadBorderRouterManagement.events, event_helper_mt) + +setmetatable(ThreadBorderRouterManagement, {__index = cluster_base}) + +return ThreadBorderRouterManagement + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ActiveDatasetTimestamp.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ActiveDatasetTimestamp.lua new file mode 100644 index 0000000000..b0dc67b590 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ActiveDatasetTimestamp.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ActiveDatasetTimestamp = { + ID = 0x0004, + NAME = "ActiveDatasetTimestamp", + base_type = require "st.matter.data_types.Uint64", +} + +function ActiveDatasetTimestamp:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function ActiveDatasetTimestamp:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ActiveDatasetTimestamp:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ActiveDatasetTimestamp:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ActiveDatasetTimestamp:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function ActiveDatasetTimestamp:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(ActiveDatasetTimestamp, {__call = ActiveDatasetTimestamp.new_value, __index = ActiveDatasetTimestamp.base_type}) +return ActiveDatasetTimestamp + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/BorderRouterName.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/BorderRouterName.lua new file mode 100644 index 0000000000..33fd11c111 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/BorderRouterName.lua @@ -0,0 +1,69 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local BorderRouterName = { + ID = 0x0000, + NAME = "BorderRouterName", + base_type = require "st.matter.data_types.UTF8String1", +} + +function BorderRouterName:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function BorderRouterName:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + + +function BorderRouterName:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function BorderRouterName:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function BorderRouterName:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function BorderRouterName:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(BorderRouterName, {__call = BorderRouterName.new_value, __index = BorderRouterName.base_type}) +return BorderRouterName + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/InterfaceEnabled.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/InterfaceEnabled.lua new file mode 100644 index 0000000000..c2676bfaf0 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/InterfaceEnabled.lua @@ -0,0 +1,69 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local InterfaceEnabled = { + ID = 0x0003, + NAME = "InterfaceEnabled", + base_type = require "st.matter.data_types.Boolean", +} + +function InterfaceEnabled:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function InterfaceEnabled:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + + +function InterfaceEnabled:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function InterfaceEnabled:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function InterfaceEnabled:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function InterfaceEnabled:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(InterfaceEnabled, {__call = InterfaceEnabled.new_value, __index = InterfaceEnabled.base_type}) +return InterfaceEnabled + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ThreadVersion.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ThreadVersion.lua new file mode 100644 index 0000000000..6630c863e9 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ThreadVersion.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ThreadVersion = { + ID = 0x0002, + NAME = "ThreadVersion", + base_type = require "st.matter.data_types.Uint16", +} + +function ThreadVersion:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function ThreadVersion:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ThreadVersion:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ThreadVersion:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ThreadVersion:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function ThreadVersion:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(ThreadVersion, {__call = ThreadVersion.new_value, __index = ThreadVersion.base_type}) +return ThreadVersion + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/init.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/init.lua new file mode 100644 index 0000000000..96cad47fdd --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/init.lua @@ -0,0 +1,24 @@ +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("ThreadBorderRouterManagement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local ThreadBorderRouterManagementServerAttributes = {} + +function ThreadBorderRouterManagementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ThreadBorderRouterManagementServerAttributes, attr_mt) + +return ThreadBorderRouterManagementServerAttributes + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/GetActiveDatasetRequest.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/GetActiveDatasetRequest.lua new file mode 100644 index 0000000000..b63982575e --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/GetActiveDatasetRequest.lua @@ -0,0 +1,79 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local GetActiveDatasetRequest = {} + +GetActiveDatasetRequest.NAME = "GetActiveDatasetRequest" +GetActiveDatasetRequest.ID = 0x0000 +GetActiveDatasetRequest.field_defs = { +} + +function GetActiveDatasetRequest:init(device, endpoint_id) + local out = {} + local args = {} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = GetActiveDatasetRequest, + __tostring = GetActiveDatasetRequest.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function GetActiveDatasetRequest:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function GetActiveDatasetRequest:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + base_type_obj.elements = elems +end + +function GetActiveDatasetRequest:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(GetActiveDatasetRequest, {__call = GetActiveDatasetRequest.init}) + +return GetActiveDatasetRequest diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/init.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/init.lua new file mode 100644 index 0000000000..ec75068d8e --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/init.lua @@ -0,0 +1,28 @@ +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("ThreadBorderRouterManagement.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local ThreadBorderRouterManagementServerCommands = {} + +function ThreadBorderRouterManagementServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ThreadBorderRouterManagementServerCommands, command_mt) + +local status, aliases = pcall(require, "st.matter.clusters.aliases.ThreadBorderRouterManagement.server.commands") +if status then + aliases:add_to_class(ThreadBorderRouterManagementServerCommands) +end + +return ThreadBorderRouterManagementServerCommands + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/Feature.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/Feature.lua new file mode 100644 index 0000000000..c8116cd481 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/Feature.lua @@ -0,0 +1,54 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.PAN_CHANGE = 0x0001 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + PAN_CHANGE = 0x0001, +} + +Feature.is_pan_change_set = function(self) + return (self.value & self.PAN_CHANGE) ~= 0 +end + +Feature.set_pan_change = function(self) + if self.value ~= nil then + self.value = self.value | self.PAN_CHANGE + else + self.value = self.PAN_CHANGE + end +end + +Feature.unset_pan_change = function(self) + self.value = self.value & (~self.PAN_CHANGE & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.PAN_CHANGE + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_pan_change_set = Feature.is_pan_change_set, + set_pan_change = Feature.set_pan_change, + unset_pan_change = Feature.unset_pan_change, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/init.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/init.lua new file mode 100644 index 0000000000..3aece4eef2 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/init.lua @@ -0,0 +1,15 @@ +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("ThreadBorderRouterManagement.types." .. key) + end + return types_mt.__types_cache[key] +end + +local ThreadBorderRouterManagementTypes = {} + +setmetatable(ThreadBorderRouterManagementTypes, types_mt) + +return ThreadBorderRouterManagementTypes + diff --git a/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/init.lua b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/init.lua new file mode 100644 index 0000000000..061ee5854f --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/init.lua @@ -0,0 +1,58 @@ +local cluster_base = require "st.matter.cluster_base" +local WiFiNetworkManagementServerAttributes = require "WiFiNetworkManagement.server.attributes" + +local WiFiNetworkManagement = {} + +WiFiNetworkManagement.ID = 0x0451 +WiFiNetworkManagement.NAME = "WiFiNetworkManagement" +WiFiNetworkManagement.server = {} +WiFiNetworkManagement.client = {} +WiFiNetworkManagement.server.attributes = WiFiNetworkManagementServerAttributes:set_parent_cluster(WiFiNetworkManagement) + +function WiFiNetworkManagement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "Ssid", + [0x0001] = "PassphraseSurrogate", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +WiFiNetworkManagement.attribute_direction_map = { + ["Ssid"] = "server", + ["PassphraseSurrogate"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +do + local has_aliases, aliases = pcall(require, "st.matter.clusters.aliases.WiFiNetworkManagement.server.attributes") + if has_aliases then + for alias, _ in pairs(aliases) do + WiFiNetworkManagement.attribute_direction_map[alias] = "server" + end + end +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = WiFiNetworkManagement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, WiFiNetworkManagement.NAME)) + end + return WiFiNetworkManagement[direction].attributes[key] +end +WiFiNetworkManagement.attributes = {} +setmetatable(WiFiNetworkManagement.attributes, attribute_helper_mt) + +setmetatable(WiFiNetworkManagement, {__index = cluster_base}) + +return WiFiNetworkManagement + diff --git a/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/Ssid.lua b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/Ssid.lua new file mode 100644 index 0000000000..e7a2a43fb5 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/Ssid.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local Ssid = { + ID = 0x0000, + NAME = "Ssid", + base_type = require "st.matter.data_types.OctetString1", +} + +function Ssid:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function Ssid:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function Ssid:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function Ssid:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Ssid:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function Ssid:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(Ssid, {__call = Ssid.new_value, __index = Ssid.base_type}) +return Ssid + diff --git a/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/init.lua b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/init.lua new file mode 100644 index 0000000000..de0b55c8b1 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/init.lua @@ -0,0 +1,24 @@ +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("WiFiNetworkManagement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local WiFiNetworkManagementServerAttributes = {} + +function WiFiNetworkManagementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(WiFiNetworkManagementServerAttributes, attr_mt) + +return WiFiNetworkManagementServerAttributes + diff --git a/drivers/SmartThings/matter-hrap/src/embedded-cluster-utils.lua b/drivers/SmartThings/matter-hrap/src/embedded-cluster-utils.lua new file mode 100644 index 0000000000..ca36cd7562 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/embedded-cluster-utils.lua @@ -0,0 +1,62 @@ +-- 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.matter.clusters" +local utils = require "st.utils" +local version = require "version" + +if version.api < 13 then + clusters.ThreadBorderRouterManagement = require "ThreadBorderRouterManagement" +end + +local embedded_cluster_utils = {} + +local embedded_clusters_api_13 = { + [clusters.ThreadBorderRouterManagement.ID] = clusters.ThreadBorderRouterManagement, +} + +function embedded_cluster_utils.get_endpoints(device, cluster_id, opts) + -- If using older lua libs and need to check for an embedded cluster feature, + -- we must use the embedded cluster definitions here + if version.api < 13 and embedded_clusters_api_13[cluster_id] ~= nil then + local embedded_cluster = embedded_clusters_api_13[cluster_id] + if not opts then opts = {} end + if utils.table_size(opts) > 1 then + device.log.warn_with({hub_logs = true}, "Invalid options for get_endpoints") + return + end + local clus_has_features = function(clus, feature_bitmap) + if not feature_bitmap or not clus then return false end + return embedded_cluster.are_features_supported(feature_bitmap, clus.feature_map) + end + local eps = {} + for _, ep in ipairs(device.endpoints) do + for _, clus in ipairs(ep.clusters) do + if ((clus.cluster_id == cluster_id) + and (opts.feature_bitmap == nil or clus_has_features(clus, opts.feature_bitmap)) + and ((opts.cluster_type == nil and clus.cluster_type == "SERVER" or clus.cluster_type == "BOTH") + or (opts.cluster_type == clus.cluster_type)) + or (cluster_id == nil)) then + table.insert(eps, ep.endpoint_id) + if cluster_id == nil then break end + end + end + end + return eps + else + return device:get_endpoints(cluster_id, opts) + end +end + +return embedded_cluster_utils diff --git a/drivers/SmartThings/matter-hrap/src/init.lua b/drivers/SmartThings/matter-hrap/src/init.lua new file mode 100644 index 0000000000..df05e7ce83 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/init.lua @@ -0,0 +1,217 @@ +-- 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 MatterDriver = require "st.matter.driver" +local data_types = require "st.matter.data_types" +local im = require "st.matter.interaction_model" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local lustre_utils = require "lustre.utils" +local st_utils = require "st.utils" +local version = require "version" +local log = require "log" + +local CURRENT_ACTIVE_DATASET_TIMESTAMP = "__CURRENT_ACTIVE_DATASET_TIMESTAMP" +local GET_ACTIVE_DATASET_RETRY_ATTEMPTS = "__GET_ACTIVE_DATASET_RETRY_ATTEMPTS" + +-- Include driver-side definitions when lua libs api version is <13 +if version.api < 13 then + clusters.ThreadBorderRouterManagement = require "ThreadBorderRouterManagement" + clusters.WiFiNetworkManagement = require "WiFiNetworkManagement" +end + + +--[[ ATTRIBUTE HANDLERS ]]-- + +local function border_router_name_attribute_handler(driver, device, ib) + -- per the spec, the recommended attribute format is ._meshcop._udp. This logic removes the MeshCoP suffix IFF it is present + local meshCop_name = ib.data.value + local terminal_display_char = (string.find(meshCop_name, "._meshcop._udp") or 64) - 1 -- where 64-1=63, the maximum allowed length for BorderRouterName + local display_name = string.sub(meshCop_name, 1, terminal_display_char) + device:emit_event_for_endpoint(ib.endpoint, capabilities.threadBorderRouter.borderRouterName({ value = display_name })) +end + +local function ssid_attribute_handler(driver, device, ib) + if (ib.data.value == string.char(data_types.Null.ID) or ib.data.value == nil) then -- Matter TLV-encoded NULL or Lua-encoded NULL + device.log.info("No primary Wi-Fi network is available") + return + end + local valid_utf8, utf8_err = lustre_utils.validate_utf8(ib.data.value) + if valid_utf8 then + device:emit_event_for_endpoint(ib.endpoint, capabilities.wifiInformation.ssid({ value = ib.data.value })) + else + device.log.info("UTF-8 validation of SSID failed: Error: '"..utf8_err.."'") + end +end + +local function thread_interface_enabled_attribute_handler(driver, device, ib) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint, capabilities.threadBorderRouter.threadInterfaceState("enabled")) + else + device:emit_event_for_endpoint(ib.endpoint, capabilities.threadBorderRouter.threadInterfaceState("disabled")) + end +end + +-- Spec uses TLV encoding of Thread Version, which should be mapped to a more human-readable name +local VERSION_TLV_MAP = { + [1] = "1.0.0", + [2] = "1.1.0", + [3] = "1.2.0", + [4] = "1.3.0", + [5] = "1.4.0", +} + +local function thread_version_attribute_handler(driver, device, ib) + local version_name = VERSION_TLV_MAP[ib.data.value] + if version_name then + device:emit_event_for_endpoint(ib.endpoint, capabilities.threadBorderRouter.threadVersion({ value = version_name })) + else + device.log.warn("The received TLV-encoded Thread version does not have a provided mapping to a human-readable version format") + end +end + +local function active_dataset_timestamp_handler(driver, device, ib) + if not ib.data.value then + device.log.info("No Thread operational dataset configured") + elseif ib.data.value ~= device:get_field(CURRENT_ACTIVE_DATASET_TIMESTAMP) then + device:send(clusters.ThreadBorderRouterManagement.server.commands.GetActiveDatasetRequest(device, ib.endpoint_id)) + device:set_field(CURRENT_ACTIVE_DATASET_TIMESTAMP, ib.data.value, { persist = true }) + end +end + + +--[[ COMMAND HANDLERS ]]-- + +local threadNetwork = capabilities.threadNetwork +local TLV_TYPE_ATTR_MAP = { + [0] = threadNetwork.channel, + [1] = threadNetwork.panId, + [2] = threadNetwork.extendedPanId, + [3] = threadNetwork.networkName, + -- [4] intentionally omitted, refers to the MeshCoP PSKc + [5] = threadNetwork.networkKey, +} + +local function dataset_response_handler(driver, device, ib) + if not ib.info_block.data.elements.dataset then + device.log.debug_with({ hub_logs = true }, "Received empty Thread operational dataset") + return + end + + local operational_dataset_length = ib.info_block.data.elements.dataset.byte_length + local spec_defined_max_dataset_length = 254 + if operational_dataset_length > spec_defined_max_dataset_length then + device.log.error_with({ hub_logs = true }, "Received Thread operational dataset is too long") + return + end + + -- parse dataset + local operational_dataset = ib.info_block.data.elements.dataset.value + local cur_byte = 1 + while cur_byte + 1 <= operational_dataset_length do + local tlv_type = string.byte(operational_dataset, cur_byte) + local tlv_length = string.byte(operational_dataset, cur_byte + 1) + if (cur_byte + 1 + tlv_length) > operational_dataset_length then + device.log.error_with({ hub_logs = true }, "Received Thread operational dataset has a malformed TLV encoding") + return + end + local tlv_mapped_attr = TLV_TYPE_ATTR_MAP[tlv_type] + if tlv_mapped_attr then + -- extract the value from a TLV-encoded message. Message format: byte tag + byte length + length byte value + local tlv_value = operational_dataset:sub(cur_byte + 2, cur_byte + 1 + tlv_length) + -- format data as required by threadNetwork attribute properties + if tlv_mapped_attr == threadNetwork.channel or tlv_mapped_attr == threadNetwork.panId then + tlv_value = st_utils.deserialize_int(tlv_value, tlv_length) + elseif tlv_mapped_attr ~= threadNetwork.networkName then + tlv_value = st_utils.bytes_to_hex_string(tlv_value) + end + device:emit_event(tlv_mapped_attr({ value = tlv_value })) + end + cur_byte = cur_byte + 2 + tlv_length + end +end + +local function get_active_dataset_response_handler(driver, device, ib) + if ib.status == im.InteractionResponse.Status.FAILURE then + -- per spec, on a GetActiveDatasetRequest failure, a failure response is sent over the same command. + -- on failure, retry the read up to 3 times before failing out. + local retries_attempted = device:get_field(GET_ACTIVE_DATASET_RETRY_ATTEMPTS) or 0 + if retries_attempted < 3 then + device.log.error_with({ hub_logs = true }, "Failed to retrieve Thread operational dataset. Retrying " .. retries_attempted + 1 .. "/3") + device:set_field(GET_ACTIVE_DATASET_RETRY_ATTEMPTS, retries_attempted + 1) + else + -- do not retry again, but reset the count to 0. + device:set_field(GET_ACTIVE_DATASET_RETRY_ATTEMPTS, 0) + end + elseif ib.status == im.InteractionResponse.Status.UNSUPPORTED_ACCESS then + device.log.error_with({ hub_logs = true }, + "Failed to retrieve Thread operational dataset, since the GetActiveDatasetRequest command was not executed over CASE" + ) + end +end + + +--[[ LIFECYCLE HANDLERS ]]-- + +local function device_init(driver, device) + device:subscribe() +end + + +--[[ MATTER DRIVER TEMPLATE ]]-- + +local matter_driver_template = { + lifecycle_handlers = { + init = device_init, + }, + matter_handlers = { + attr = { + [clusters.WiFiNetworkManagement.ID] = { + [clusters.WiFiNetworkManagement.attributes.Ssid.ID] = ssid_attribute_handler, + }, + [clusters.ThreadBorderRouterManagement.ID] = { + [clusters.ThreadBorderRouterManagement.attributes.BorderRouterName.ID] = border_router_name_attribute_handler, + [clusters.ThreadBorderRouterManagement.attributes.ThreadVersion.ID] = thread_version_attribute_handler, + [clusters.ThreadBorderRouterManagement.attributes.InterfaceEnabled.ID] = thread_interface_enabled_attribute_handler, + [clusters.ThreadBorderRouterManagement.attributes.ActiveDatasetTimestamp.ID] = active_dataset_timestamp_handler, + } + }, + cmd_response = { + [clusters.ThreadBorderRouterManagement.ID] = { + [clusters.ThreadBorderRouterManagement.client.commands.DatasetResponse.ID] = dataset_response_handler, + [clusters.ThreadBorderRouterManagement.server.commands.GetActiveDatasetRequest.ID] = get_active_dataset_response_handler, + } + } + }, + subscribed_attributes = { + [capabilities.threadBorderRouter.ID] = { + clusters.ThreadBorderRouterManagement.attributes.ActiveDatasetTimestamp, + clusters.ThreadBorderRouterManagement.attributes.BorderRouterName, + clusters.ThreadBorderRouterManagement.attributes.InterfaceEnabled, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion, + }, + [capabilities.wifiInformation.ID] = { + clusters.WiFiNetworkManagement.attributes.Ssid, + } + }, + supported_capabilities = { + capabilities.threadBorderRouter, + capabilities.threadNetwork, + capabilities.wifiInformation, + } +} + +local matter_driver = MatterDriver("matter-hrap", matter_driver_template) +log.info_with({hub_logs=true}, string.format("Starting %s driver, with dispatcher: %s", matter_driver.NAME, matter_driver.matter_dispatcher)) +matter_driver:run() diff --git a/drivers/SmartThings/matter-hrap/src/test/test_thread_border_router_network.lua b/drivers/SmartThings/matter-hrap/src/test/test_thread_border_router_network.lua new file mode 100644 index 0000000000..0467e19de3 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/test/test_thread_border_router_network.lua @@ -0,0 +1,342 @@ +-- 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 data_types = require "st.matter.data_types" +local capabilities = require "st.capabilities" + +local clusters = require "st.matter.clusters" +clusters.ThreadBorderRouterManagement = require "ThreadBorderRouterManagement" +clusters.WifiNetworkMangement = require "WiFiNetworkManagement" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("network-infrastructure-manager.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1,} -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.ThreadBorderRouterManagement.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.WifiNetworkMangement.ID, cluster_type = "SERVER", feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0090, device_type_revision = 1,} -- Network Infrastructure Manager + } + } + } +}) + +local cluster_subscribe_list = { + clusters.ThreadBorderRouterManagement.attributes.ActiveDatasetTimestamp, + clusters.ThreadBorderRouterManagement.attributes.BorderRouterName, + clusters.ThreadBorderRouterManagement.attributes.InterfaceEnabled, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion, + clusters.WifiNetworkMangement.attributes.Ssid, +} + +local function test_init() + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "ThreadVersion should display the correct stringified version", + function() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion:build_test_report_data( + mock_device, 1, 3 + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadBorderRouter.threadVersion({ value = "1.2.0" })) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion:build_test_report_data( + mock_device, 1, 4 + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadBorderRouter.threadVersion({ value = "1.3.0" })) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion:build_test_report_data( + mock_device, 1, 5 + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadBorderRouter.threadVersion({ value = "1.4.0" })) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion:build_test_report_data( + mock_device, 1, 6 + ) + }) + end +) + +test.register_message_test( + "InterfaceEnabled should correctly display enabled or disabled", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.InterfaceEnabled:build_test_report_data(mock_device, 1, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.threadBorderRouter.threadInterfaceState("enabled")) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.InterfaceEnabled:build_test_report_data(mock_device, 1, false) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.threadBorderRouter.threadInterfaceState("disabled")) + } + } +) + +test.register_message_test( + "BorderRouterName should correctly display the given name", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.BorderRouterName:build_test_report_data(mock_device, 1, "john foo._meshcop._udp") + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.threadBorderRouter.borderRouterName({ value = "john foo"})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.BorderRouterName:build_test_report_data(mock_device, 1, "jane bar._meshcop._udp") + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.threadBorderRouter.borderRouterName({ value = "jane bar"})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.BorderRouterName:build_test_report_data(mock_device, 1, "john foo no suffix") + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.threadBorderRouter.borderRouterName({ value = "john foo no suffix"})) + }, + } +) + +test.register_message_test( + "wifiInformation capability should correctly display the Ssid", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.WifiNetworkMangement.attributes.Ssid:build_test_report_data(mock_device, 1, "test name for ssid!") + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.wifiInformation.ssid({ value = "test name for ssid!" })) + } + } +) + +test.register_message_test( + "Null-valued ssid (TLV 0x14) should correctly fail", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.WifiNetworkMangement.attributes.Ssid:build_test_report_data(mock_device, 1, string.char(data_types.Null.ID)) + } + } + } +) + +test.register_message_test( + "Ssid inputs using non-UTF8 encoding should not display an Ssid", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.WifiNetworkMangement.attributes.Ssid:build_test_report_data(mock_device, 1, string.char(0xC0)) -- 0xC0 never appears in utf8 + } + } + } +) + +local hex_dataset = [[ +0E 08 00 00 68 87 D0 B2 00 00 00 03 00 00 18 35 +06 00 04 00 1F FF C0 02 08 25 31 25 A9 B2 16 7F +35 07 08 FD 6E D1 57 02 B4 CD BF 05 10 33 AF 36 +F8 13 8E 8F F9 50 6D 67 22 9B FD F2 40 03 0D 53 +54 2D 35 30 33 32 30 30 31 31 39 36 01 02 D9 78 +04 10 E2 29 D8 2A 84 B2 7D A1 AC 8D D8 71 64 AC +66 7F 0C 04 02 A0 FF F8 +]] + +local serializable_hex_dataset = hex_dataset:gsub("%s+", ""):gsub("..", function(cc) + return string.char(tonumber(cc, 16)) +end) + +test.register_coroutine_test( + "Thread DatasetResponse parsing should emit the correct capability events on an ActiveDatasetTimestamp update. Else, nothing should happen", + function() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.server.attributes.ActiveDatasetTimestamp:build_test_report_data( + mock_device, + 1, + 1 + ) + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ThreadBorderRouterManagement.server.commands.GetActiveDatasetRequest(mock_device, 1), + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.client.commands.DatasetResponse:build_test_command_response( + mock_device, + 1, + serializable_hex_dataset + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.channel({ value = 24 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.extendedPanId({ value = "253125a9b2167f35" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.networkKey({ value = "33af36f8138e8ff9506d67229bfdf240" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.networkName({ value = "ST-5032001196" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.panId({ value = 55672 })) + ) + test.wait_for_events() + + -- after some amount of time, a device init occurs or we re-subscribe for other reasons. + -- Since no change to the ActiveDatasetTimestamp has occurred, no re-read should occur + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.server.attributes.ActiveDatasetTimestamp:build_test_report_data( + mock_device, + 1, + 1 + ) + }) + test.wait_for_events() + + -- after some more amount of time, a device init occurs or we re-subscribe for other reasons. + -- This time, their ActiveDatasetTimestamp has updated, so we should re-read the operational dataset. + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.server.attributes.ActiveDatasetTimestamp:build_test_report_data( + mock_device, + 1, + 2 + ) + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ThreadBorderRouterManagement.server.commands.GetActiveDatasetRequest(mock_device, 1), + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.client.commands.DatasetResponse:build_test_command_response( + mock_device, + 1, + serializable_hex_dataset + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.channel({ value = 24 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.extendedPanId({ value = "253125a9b2167f35" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.networkKey({ value = "33af36f8138e8ff9506d67229bfdf240" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.networkName({ value = "ST-5032001196" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.panId({ value = 55672 })) + ) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-lock/capabilities/lockAlarm.yml b/drivers/SmartThings/matter-lock/capabilities/lockAlarm.yml deleted file mode 100644 index c10f387dbf..0000000000 --- a/drivers/SmartThings/matter-lock/capabilities/lockAlarm.yml +++ /dev/null @@ -1,27 +0,0 @@ -id: lockAlarm -version: 1 -status: proposed -name: Lock Alarm -ephemeral: false -attributes: - alarm: - schema: - type: object - properties: - value: - type: string - enum: - - clear - - lockFactoryReset - - damaged - - forcedOpeningAttempt - - unableToLockTheDoor - - notClosedForALongTime - - highTemperature - - attemptsExceeded - - physicalImpact - additionalProperties: false - required: - - value - enumCommands: [] -commands: {} \ No newline at end of file diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 7ad02e6ac7..3efd33b364 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -10,6 +10,16 @@ matterManufacturer: vendorId: 0x115F productId: 0x2801 deviceProfileName: lock-user-pin + - id: "4447/10247" + deviceLabel: Aqara Smart Lock U200 Lite + vendorId: 0x115F + productId: 0x2807 + deviceProfileName: lock-battery + - id: "4447/10346" + deviceLabel: Aqara Smart Lock U200 US + vendorId: 0x115F + productId: 0x286A + deviceProfileName: lock-user-pin #Eufy - id: "5427/1" deviceLabel: eufy Smart Lock E31 @@ -46,6 +56,27 @@ matterManufacturer: vendorId: 0x1533 productId: 0x0012 deviceProfileName: lock-user-pin-battery + - id: "5427/22" + deviceLabel: eufy FamiLock E32 + vendorId: 0x1533 + productId: 0x0016 + deviceProfileName: lock-user-pin-battery + - id: 5427/20 + deviceLabel: eufy FamiLock E40 + vendorId: 0x1533 + productId: 0x0014 + deviceProfileName: lock-user-pin-battery + #Kwikset + - id: "5153/66" + deviceLabel: Kwikset Halo Select Plus + vendorId: 0x1421 + productId: 0x0042 + deviceProfileName: lock-user-pin-battery + - id: "5153/129" + deviceLabel: Kwikset Aura Reach + vendorId: 0x1421 + productId: 0x0081 + deviceProfileName: lock-user-pin-battery #Level - id: "4767/1" deviceLabel: Level Lock Plus (Matter) @@ -61,12 +92,12 @@ matterManufacturer: deviceLabel: Level Lock Pro vendorId: 0x129F productId: 0x0004 - deviceProfileName: lock-nocodes-notamper + deviceProfileName: lock-nocodes-notamper-batteryLevel - id: "4767/5" deviceLabel: Level Lock (Matter) vendorId: 0x129F productId: 0x0005 - deviceProfileName: lock-nocodes-notamper + deviceProfileName: lock-nocodes-notamper-batteryLevel #Nuki - id: "4957/161" deviceLabel: Nuki Smart Lock Ultra @@ -99,6 +130,17 @@ matterManufacturer: vendorId: 0x147F productId: 0x0001 deviceProfileName: lock-user-pin-battery + - id: "5247/8" + deviceLabel: ULTRALOQ Bolt Smart Matter Door Lock + vendorId: 0x147F + productId: 0x0008 + deviceProfileName: lock-user-pin-battery + #Yale + - id: "4125/33040" + deviceLabel: Yale Lock with Matter + vendorId: 0x101D + productId: 0x8110 + deviceProfileName: lock-user-pin-schedule-battery matterGeneric: - id: "matter/door-lock" deviceLabel: Matter Door Lock diff --git a/drivers/SmartThings/matter-lock/profiles/base-lock-batteryLevel.yml b/drivers/SmartThings/matter-lock/profiles/base-lock-batteryLevel.yml index 257a3c6e3f..093c741219 100755 --- a/drivers/SmartThings/matter-lock/profiles/base-lock-batteryLevel.yml +++ b/drivers/SmartThings/matter-lock/profiles/base-lock-batteryLevel.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: base-lock-batteryLevel components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/base-lock-nobattery.yml b/drivers/SmartThings/matter-lock/profiles/base-lock-nobattery.yml index 769e955219..c223facfbf 100755 --- a/drivers/SmartThings/matter-lock/profiles/base-lock-nobattery.yml +++ b/drivers/SmartThings/matter-lock/profiles/base-lock-nobattery.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: base-lock-nobattery components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/base-lock.yml b/drivers/SmartThings/matter-lock/profiles/base-lock.yml index 094aa523cc..7d69a5c8e4 100755 --- a/drivers/SmartThings/matter-lock/profiles/base-lock.yml +++ b/drivers/SmartThings/matter-lock/profiles/base-lock.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: base-lock components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-batteryLevel.yml b/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-batteryLevel.yml index f946fd27cc..83b8c097f7 100755 --- a/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-batteryLevel.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-batteryLevel.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-lockalarm-batteryLevel components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-nobattery.yml b/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-nobattery.yml index 96641392bf..135a1e1986 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-nobattery.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-nobattery.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-lockalarm-nobattery components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-lockalarm.yml b/drivers/SmartThings/matter-lock/profiles/lock-lockalarm.yml index 007d01aa2f..ed0dd126e6 100755 --- a/drivers/SmartThings/matter-lock/profiles/lock-lockalarm.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-lockalarm.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-lockalarm components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml b/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml new file mode 100644 index 0000000000..3d9e68b44c --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml @@ -0,0 +1,134 @@ +name: lock-modular-embedded-unlatch +components: +- id: main + capabilities: + - id: lock + version: 1 + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + optional: true + - id: lockCredentials + version: 1 + optional: true + - id: lockSchedules + version: 1 + optional: true + - id: lockAliro + version: 1 + optional: true + - id: battery + version: 1 + optional: true + - id: batteryLevel + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock +deviceConfig: + dashboard: + states: + - component: main + capability: lock + version: 1 + actions: + - component: main + capability: lock + version: 1 + visibleCondition: { + "capability": "lock", + "version": "1", + "component": "main", + "value": "lock.value", + "operator": "DOES_NOT_EQUAL", + "operand": "unlatched" + } + detailView: + - component: main + capability: lock + version: 1 + values: + - key: lock.value + alternatives: + - key: locked + type: inactive + value: '{{i18n.attributes.lock.i18n.value.locked.label}}' + - key: unlocked + value: '{{i18n.attributes.lock.i18n.value.unlocked.label}}' + - key: unlatched + value: '{{i18n.attributes.lock.i18n.value.unlatched.label}}' + - key: not fully locked + value: '{{i18n.attributes.lock.i18n.value.not fully locked.label}}' + patch: + - op: add + path: /1 + value: + capability: lock + version: 1 + component: main + label: '{{i18n.commands.unlatch.label}}' + displayType: pushButton + pushButton: + command: unlatch + - component: main + capability: lockAlarm + version: 1 + - component: main + capability: remoteControlStatus + version: 1 + - component: main + capability: battery + version: 1 + - component: main + capability: batteryLevel + version: 1 + automation: + conditions: + - component: main + capability: lock + version: 1 + values: + - key: lock.value + alternatives: + - key: locked + type: inactive + value: '{{i18n.attributes.lock.i18n.value.locked.label}}' + - key: unlocked + value: '{{i18n.attributes.lock.i18n.value.unlocked.label}}' + - key: unlatched + value: '{{i18n.attributes.lock.i18n.value.unlatched.label}}' + - key: not fully locked + value: '{{i18n.attributes.lock.i18n.value.not fully locked.label}}' + - component: main + capability: lockAlarm + version: 1 + - component: main + capability: remoteControlStatus + version: 1 + - component: main + capability: battery + version: 1 + - component: main + capability: batteryLevel + version: 1 + actions: + - component: main + capability: lock + version: 1 + values: + - key: '{{enumCommands}}' + alternatives: + - key: lock + type: inactive + value: '{{i18n.commands.lock.label}}' + - key: unlock + value: '{{i18n.commands.unlock.label}}' + - key: unlatch + value: '{{i18n.commands.unlatch.label}}' diff --git a/drivers/SmartThings/matter-lock/profiles/lock-modular.yml b/drivers/SmartThings/matter-lock/profiles/lock-modular.yml new file mode 100644 index 0000000000..3a8a53bf70 --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-modular.yml @@ -0,0 +1,34 @@ +name: lock-modular +components: +- id: main + capabilities: + - id: lock + version: 1 + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + optional: true + - id: lockCredentials + version: 1 + optional: true + - id: lockSchedules + version: 1 + optional: true + - id: lockAliro + version: 1 + optional: true + - id: battery + version: 1 + optional: true + - id: batteryLevel + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock diff --git a/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper-batteryLevel.yml b/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper-batteryLevel.yml index f55fbe012c..abd55420fd 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper-batteryLevel.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper-batteryLevel.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-nocodes-notamper-batteryLevel components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper.yml b/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper.yml index 2371c84732..20dec32602 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-nocodes-notamper components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-without-codes-batteryLevel.yml b/drivers/SmartThings/matter-lock/profiles/lock-without-codes-batteryLevel.yml index 15ce853cfd..2d577cd19b 100755 --- a/drivers/SmartThings/matter-lock/profiles/lock-without-codes-batteryLevel.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-without-codes-batteryLevel.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-without-codes-batteryLevel components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-without-codes-nobattery.yml b/drivers/SmartThings/matter-lock/profiles/lock-without-codes-nobattery.yml index 8de4bb1a5c..059b65e9f9 100755 --- a/drivers/SmartThings/matter-lock/profiles/lock-without-codes-nobattery.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-without-codes-nobattery.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-without-codes-nobattery components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-without-codes.yml b/drivers/SmartThings/matter-lock/profiles/lock-without-codes.yml index 772b742f20..86bc5e45d3 100755 --- a/drivers/SmartThings/matter-lock/profiles/lock-without-codes.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-without-codes.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-without-codes components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock-batteryLevel.yml b/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock-batteryLevel.yml index fc3371b31c..cf342d9548 100644 --- a/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock-batteryLevel.yml +++ b/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock-batteryLevel.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: nonfunctional-lock-batteryLevel components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock.yml b/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock.yml index 1990cbc025..e662780d55 100644 --- a/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock.yml +++ b/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: nonfunctional-lock components: - id: main diff --git a/drivers/SmartThings/matter-lock/src/DoorLock/init.lua b/drivers/SmartThings/matter-lock/src/DoorLock/init.lua index b9675ebf77..20cf12d0ea 100644 --- a/drivers/SmartThings/matter-lock/src/DoorLock/init.lua +++ b/drivers/SmartThings/matter-lock/src/DoorLock/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local DoorLockServerAttributes = require "DoorLock.server.attributes" local DoorLockServerCommands = require "DoorLock.server.commands" @@ -255,4 +259,4 @@ setmetatable(DoorLock.events, event_helper_mt) setmetatable(DoorLock, {__index = cluster_base}) -return DoorLock \ No newline at end of file +return DoorLock diff --git a/drivers/SmartThings/matter-lock/src/DoorLock/types/init.lua b/drivers/SmartThings/matter-lock/src/DoorLock/types/init.lua index 461d914f84..f4b2939d61 100644 --- a/drivers/SmartThings/matter-lock/src/DoorLock/types/init.lua +++ b/drivers/SmartThings/matter-lock/src/DoorLock/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) @@ -11,4 +14,4 @@ local DoorLockTypes = {} setmetatable(DoorLockTypes, types_mt) -return DoorLockTypes \ No newline at end of file +return DoorLockTypes diff --git a/drivers/SmartThings/matter-lock/src/init.lua b/drivers/SmartThings/matter-lock/src/init.lua index 6133b74e45..b3403863ec 100755 --- a/drivers/SmartThings/matter-lock/src/init.lua +++ b/drivers/SmartThings/matter-lock/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local MatterDriver = require "st.matter.driver" local clusters = require "st.matter.clusters" @@ -717,9 +707,7 @@ local matter_lock_driver = { capabilities.battery, capabilities.batteryLevel, }, - sub_drivers = { - require("new-matter-lock"), - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { init = device_init, added = device_added, diff --git a/drivers/SmartThings/matter-lock/src/lazy_load_subdriver.lua b/drivers/SmartThings/matter-lock/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..a04740d267 --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/lazy_load_subdriver.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + local MatterDriver = require "st.matter.driver" + local version = require "version" + if version.api >= 16 then + return MatterDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return MatterDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/matter-lock/src/lock_utils.lua b/drivers/SmartThings/matter-lock/src/lock_utils.lua index 5d92c55afa..94e95c196f 100644 --- a/drivers/SmartThings/matter-lock/src/lock_utils.lua +++ b/drivers/SmartThings/matter-lock/src/lock_utils.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local lock_utils = { -- Lock device field names @@ -40,7 +29,17 @@ local lock_utils = { SCHEDULE_END_HOUR = "scheduleEndHour", SCHEDULE_END_MINUTE = "scheduleEndMinute", SCHEDULE_LOCAL_START_TIME = "scheduleLocalStartTime", - SCHEDULE_LOCAL_END_TIME = "scheduleLocalEndTime" + SCHEDULE_LOCAL_END_TIME = "scheduleLocalEndTime", + VERIFICATION_KEY = "verificationKey", + GROUP_ID = "groupId", + GROUP_RESOLVING_KEY = "groupResolvingKey", + ISSUER_KEY = "issuerKey", + ISSUER_KEY_INDEX = "issuerKeyIndex", + ENDPOINT_KEY = "endpointKey", + ENDPOINT_KEY_INDEX = "endpointKeyIndex", + ENDPOINT_KEY_TYPE = "endpointKeyType", + DEVICE_KEY_ID = "deviceKeyId", + COMMAND_REQUEST_ID = "commandRequestId" } local capabilities = require "st.capabilities" local json = require "st.json" diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/can_handle.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/can_handle.lua new file mode 100644 index 0000000000..7bbb45ed44 --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_new_matter_lock_products(opts, driver, device) + local device_lib = require "st.device" + if device.network_type ~= device_lib.NETWORK_TYPE_MATTER then + return false + end + local FINGERPRINTS = require("new-matter-lock.fingerprints") + for _, p in ipairs(FINGERPRINTS) do + if device.manufacturer_info.vendor_id == p[1] and + device.manufacturer_info.product_id == p[2] then + return true, require("new-matter-lock") + end + end + return false +end + +return is_new_matter_lock_products diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua new file mode 100644 index 0000000000..799c20b9ae --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua @@ -0,0 +1,36 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local NEW_MATTER_LOCK_PRODUCTS = { + {0x115f, 0x2802}, -- AQARA, U200 + {0x115f, 0x2801}, -- AQARA, U300 + {0x115f, 0x2807}, -- AQARA, U200 Lite + {0x115f, 0x2804}, -- AQARA, U400 + {0x115f, 0x286A}, -- AQARA, U200 US + {0x147F, 0x0001}, -- U-tec + {0x147F, 0x0008}, -- Ultraloq, Bolt Smart Matter Door Lock + {0x144F, 0x4002}, -- Yale, Linus Smart Lock L2 + {0x101D, 0x8110}, -- Yale, New Lock + {0x1533, 0x0001}, -- eufy, E31 + {0x1533, 0x0002}, -- eufy, E30 + {0x1533, 0x0003}, -- eufy, C34 + {0x1533, 0x000F}, -- eufy, FamiLock S3 Max + {0x1533, 0x0010}, -- eufy, FamiLock S3 + {0x1533, 0x0011}, -- eufy, FamiLock E34 + {0x1533, 0x0012}, -- eufy, FamiLock E35 + {0x1533, 0x0016}, -- eufy, FamiLock E32 + {0x1533, 0x0014}, -- eufy, FamiLock E40 + {0x135D, 0x00B1}, -- Nuki, Smart Lock Pro + {0x135D, 0x00B2}, -- Nuki, Smart Lock + {0x135D, 0x00C1}, -- Nuki, Smart Lock + {0x135D, 0x00A1}, -- Nuki, Smart Lock + {0x135D, 0x00B0}, -- Nuki, Smart Lock + {0x15F2, 0x0001}, -- Viomi, AiSafety Smart Lock E100 + {0x158B, 0x0001}, -- Deasino, DS-MT01 + {0x10E1, 0x2002}, -- VDA + {0x1421, 0x0042}, -- Kwikset Halo Select Plus + {0x1421, 0x0081}, -- Kwikset Aura Reach + {0x1236, 0xa538}, -- Schlage Sense Pro +} + +return NEW_MATTER_LOCK_PRODUCTS diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index 70e72cf61f..11aa2b0884 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -1,18 +1,7 @@ --- Copyright 2024 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 device_lib = require "st.device" +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local im = require "st.matter.interaction_model" @@ -27,35 +16,49 @@ end local DoorLock = clusters.DoorLock local PowerSource = clusters.PowerSource -local INITIAL_COTA_INDEX = 1 +local INITIAL_CREDENTIAL_INDEX = 1 local ALL_INDEX = 0xFFFE local MIN_EPOCH_S = 0 local MAX_EPOCH_S = 0xffffffff local THIRTY_YEARS_S = 946684800 -- 1970-01-01T00:00:00 ~ 2000-01-01T00:00:00 -local NEW_MATTER_LOCK_PRODUCTS = { - {0x115f, 0x2802}, -- AQARA, U200 - {0x115f, 0x2801}, -- AQARA, U300 - {0x115f, 0x2807}, -- AQARA, U200 Lite - {0x147F, 0x0001}, -- U-tec - {0x144F, 0x4002}, -- Yale, Linus Smart Lock L2 - {0x101D, 0x8110}, -- Yale, New Lock - {0x1533, 0x0001}, -- eufy, E31 - {0x1533, 0x0002}, -- eufy, E30 - {0x1533, 0x0003}, -- eufy, C34 - {0x1533, 0x000F}, -- eufy, FamiLock S3 Max - {0x1533, 0x0010}, -- eufy, FamiLock S3 - {0x1533, 0x0011}, -- eufy, FamiLock E34 - {0x1533, 0x0012}, -- eufy, FamiLock E35 - {0x135D, 0x00B1}, -- Nuki, Smart Lock Pro - {0x135D, 0x00B2}, -- Nuki, Smart Lock - {0x135D, 0x00C1}, -- Nuki, Smart Lock - {0x135D, 0x00A1}, -- Nuki, Smart Lock - {0x135D, 0x00B0}, -- Nuki, Smart Lock - {0x10E1, 0x2002} -- VDA +local MODULAR_PROFILE_UPDATED = "__MODULAR_PROFILE_UPDATED" + +local RESPONSE_STATUS_MAP = { + [DoorLock.types.DlStatus.SUCCESS] = "success", + [DoorLock.types.DlStatus.FAILURE] = "failure", + [DoorLock.types.DlStatus.DUPLICATE] = "duplicate", + [DoorLock.types.DlStatus.OCCUPIED] = "occupied", + [DoorLock.types.DlStatus.INVALID_FIELD] = "invalidCommand", + [DoorLock.types.DlStatus.RESOURCE_EXHAUSTED] = "resourceExhausted", + [DoorLock.types.DlStatus.NOT_FOUND] = "failure" +} + +local WEEK_DAY_MAP = { + ["Sunday"] = 1, + ["Monday"] = 2, + ["Tuesday"] = 4, + ["Wednesday"] = 8, + ["Thursday"] = 16, + ["Friday"] = 32, + ["Saturday"] = 64, +} + +local ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP = { + ["evictableEndpointKey"] = DoorLock.types.CredentialTypeEnum.ALIRO_EVICTABLE_ENDPOINT_KEY, + ["nonEvictableEndpointKey"] = DoorLock.types.CredentialTypeEnum.ALIRO_NON_EVICTABLE_ENDPOINT_KEY +} + + +local battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_LEVEL = "BATTERY_LEVEL", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" } -local PROFILE_BASE_NAME = "__profile_base_name" +local profiling_data = { + BATTERY_SUPPORT = "__BATTERY_SUPPORT", +} local subscribed_attributes = { [capabilities.lock.ID] = { @@ -77,6 +80,17 @@ local subscribed_attributes = { DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser, DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser }, + [capabilities.lockAliro.ID] = { + DoorLock.attributes.AliroReaderVerificationKey, + DoorLock.attributes.AliroReaderGroupIdentifier, + DoorLock.attributes.AliroReaderGroupSubIdentifier, + DoorLock.attributes.AliroExpeditedTransactionSupportedProtocolVersions, + DoorLock.attributes.AliroGroupResolvingKey, + DoorLock.attributes.AliroSupportedBLEUWBProtocolVersions, + DoorLock.attributes.AliroBLEAdvertisingVersion, + DoorLock.attributes.NumberOfAliroCredentialIssuerKeysSupported, + DoorLock.attributes.NumberOfAliroEndpointKeysSupported, + }, [capabilities.battery.ID] = { PowerSource.attributes.BatPercentRemaining }, @@ -97,18 +111,6 @@ local subscribed_events = { } } -local function is_new_matter_lock_products(opts, driver, device) - if device.network_type ~= device_lib.NETWORK_TYPE_MATTER then - return false - end - for _, p in ipairs(NEW_MATTER_LOCK_PRODUCTS) do - if device.manufacturer_info.vendor_id == p[1] and - device.manufacturer_info.product_id == p[2] then - return true - end - end - return false -end local function find_default_endpoint(device, cluster) local res = device.MATTER_DEFAULT_ENDPOINT @@ -148,15 +150,68 @@ local function device_init(driver, device) local function device_added(driver, device) device:emit_event(capabilities.lockAlarm.alarm.clear({state_change = true})) + local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) + if #battery_feature_eps > 0 then + device:send(clusters.PowerSource.attributes.AttributeList:read(device)) + else + device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.NO_BATTERY, { persist = true }) + end end -local function do_configure(driver, device) +local function match_profile_modular(driver, device) + local enabled_optional_component_capability_pairs = {} + local main_component_capabilities = {} + local modular_profile_name = "lock-modular" + for _, device_ep in pairs(device.endpoints) do + for _, ep_cluster in pairs(device_ep.clusters) do + if ep_cluster.cluster_id == DoorLock.ID then + local clus_has_feature = function(feature_bitmap) + return DoorLock.are_features_supported(feature_bitmap, ep_cluster.feature_map) + end + if clus_has_feature(DoorLock.types.Feature.USER) then + table.insert(main_component_capabilities, capabilities.lockUsers.ID) + end + if clus_has_feature(DoorLock.types.Feature.PIN_CREDENTIAL) then + table.insert(main_component_capabilities, capabilities.lockCredentials.ID) + end + if clus_has_feature(DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES) or + clus_has_feature(DoorLock.types.Feature.YEAR_DAY_ACCESS_SCHEDULES) then + table.insert(main_component_capabilities, capabilities.lockSchedules.ID) + end + if clus_has_feature(DoorLock.types.Feature.UNBOLT) then + device:emit_event(capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) + device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + modular_profile_name = "lock-modular-embedded-unlatch" -- use the embedded config specified in this profile for devices supporting "unlatch" + else + device:emit_event(capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) + device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + end + if clus_has_feature(DoorLock.types.Feature.ALIRO_PROVISIONING) then + table.insert(main_component_capabilities, capabilities.lockAliro.ID) + end + break + end + end + end + + local supported_battery_type = device:get_field(profiling_data.BATTERY_SUPPORT) + if supported_battery_type == battery_support.BATTERY_LEVEL then + table.insert(main_component_capabilities, capabilities.batteryLevel.ID) + elseif supported_battery_type == battery_support.BATTERY_PERCENTAGE then + table.insert(main_component_capabilities, capabilities.battery.ID) + end + + table.insert(enabled_optional_component_capability_pairs, {"main", main_component_capabilities}) + device:try_update_metadata({profile = modular_profile_name, optional_component_capabilities = enabled_optional_component_capability_pairs}) + device:set_field(MODULAR_PROFILE_UPDATED, true) +end + +local function match_profile_switch(driver, device) local user_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.USER}) local pin_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.PIN_CREDENTIAL}) local week_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES}) local year_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.YEAR_DAY_ACCESS_SCHEDULES}) local unbolt_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.UNBOLT}) - local battery_eps = device:get_endpoints(PowerSource.ID, {feature_bitmap = PowerSource.types.PowerSourceFeature.BATTERY}) local profile_name = "lock" if #user_eps > 0 then @@ -174,21 +229,23 @@ local function do_configure(driver, device) else device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) end - if #battery_eps > 0 then - device:set_field(PROFILE_BASE_NAME, profile_name, {persist = true}) - local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - req:merge(clusters.PowerSource.attributes.AttributeList:read()) - device:send(req) - else - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) + + local supported_battery_type = device:get_field(profiling_data.BATTERY_SUPPORT) + if supported_battery_type == battery_support.BATTERY_LEVEL then + profile_name = profile_name .. "-batteryLevel" + elseif supported_battery_type == battery_support.BATTERY_PERCENTAGE then + profile_name = profile_name .. "-battery" end + + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) end local function info_changed(driver, device, event, args) - if device.profile.id == args.old_st_store.profile.id then + if device.profile.id == args.old_st_store.profile.id and not device:get_field(MODULAR_PROFILE_UPDATED) then return end + device:set_field(MODULAR_PROFILE_UPDATED, nil) for cap_id, attributes in pairs(subscribed_attributes) do if device:supports_capability_by_id(cap_id) then for _, attr in ipairs(attributes) do @@ -208,8 +265,35 @@ local function info_changed(driver, device, event, args) device:emit_event(capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) -- lockJammed is madatory end +local function profiling_data_still_required(device) + for _, field in pairs(profiling_data) do + if device:get_field(field) == nil then + return true -- data still required if a field is nil + end + end + return false +end + +local function match_profile(driver, device) + if profiling_data_still_required(device) then return end + + if version.api >= 15 and version.rpc >= 9 then + match_profile_modular(driver, device) + else + match_profile_switch(driver, device) + end +end + +local function do_configure(driver, device) + match_profile(driver, device) +end + +local function driver_switched(driver, device) + match_profile(driver, device) +end + -- This function check busy_state and if busy_state is false, set it to true(current time) -local function check_busy_state(device) +local function is_busy_state_set(device) local c_time = os.time() local busy_state = device:get_field(lock_utils.BUSY_STATE) or false if busy_state == false or c_time - busy_state > 10 then @@ -318,7 +402,7 @@ local function set_cota_credential(device, credential_index) end -- Check Busy State - if check_busy_state(device) == true then + if is_busy_state_set(device) then device.log.debug("delaying setting COTA credential since a credential is currently being set") device.thread:call_with_delay(2, function(t) set_cota_credential(device, credential_index) @@ -365,7 +449,7 @@ local function apply_cota_credentials_if_absent(device) -- delay needed to allow test to override the random credential data device.thread:call_with_delay(0, function(t) -- Attempt to set cota credential at the lowest index - set_cota_credential(device, INITIAL_COTA_INDEX) + set_cota_credential(device, INITIAL_CREDENTIAL_INDEX) end) end) end @@ -393,31 +477,134 @@ local function max_year_schedule_of_user_handler(driver, device, ib, response) device:emit_event(capabilities.lockSchedules.yearDaySchedulesPerUser(ib.data.value, {visibility = {displayed = false}})) end +---------------- +-- Aliro Util -- +---------------- +local function hex_string_to_octet_string(hex_string) + if hex_string == nil then + return nil + end + local octet_string = "" + for i = 1, #hex_string, 2 do + local hex = hex_string:sub(i, i + 1) + octet_string = octet_string .. string.char(tonumber(hex, 16)) + end + return octet_string +end + +----------------------------------- +-- Aliro Reader Verification Key -- +----------------------------------- +local function aliro_reader_verification_key_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.readerVerificationKey( + utils.bytes_to_hex_string(ib.data.value), {visibility = {displayed = false}} + )) + end +end + +----------------------------------- +-- Aliro Reader Group Identifier -- +----------------------------------- +local function aliro_reader_group_id_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.readerGroupIdentifier( + utils.bytes_to_hex_string(ib.data.value), + {visibility = {displayed = false}} + )) + end +end + +------------------------------------------------------------- +-- Aliro Expedited Transaction Supported Protocol Versions -- +------------------------------------------------------------- +local function aliro_group_resolving_key_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.groupResolvingKey( + utils.bytes_to_hex_string(ib.data.value), + {visibility = {displayed = false}} + )) + end +end + +------------------------------- +-- Aliro Group Resolving Key -- +------------------------------- +local function aliro_protocol_versions_handler(driver, device, ib, response) + if ib.data.elements == nil then + return + end + local protocol_versions = {} + for i, element in ipairs(ib.data.elements) do + local version = string.format("%s.%s", element.value:byte(1), element.value:byte(2)) + table.insert(protocol_versions, version); + end + device:emit_event(capabilities.lockAliro.expeditedTransactionProtocolVersions(protocol_versions, {visibility = {displayed = false}})) +end + +----------------------------------------------- +-- Aliro Supported BLE UWB Protocol Versions -- +----------------------------------------------- +local function aliro_supported_ble_uwb_protocol_versions_handler(driver, device, ib, response) + if ib.data.elements == nil then + return + end + local protocol_versions = {} + for i, element in ipairs(ib.data.elements) do + local version = string.format("%s.%s", element.value:byte(1), element.value:byte(2)) + table.insert(protocol_versions, version); + end + device:emit_event(capabilities.lockAliro.bleUWBProtocolVersions(protocol_versions, {visibility = {displayed = false}})) +end + +----------------------------------- +-- Aliro BLE Advertising Version -- +----------------------------------- +local function aliro_ble_advertising_version_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.bleAdvertisingVersion(string.format("%s", ib.data.value), {visibility = {displayed = false}})) + end +end + +------------------------------------------------------ +-- Number Of Aliro Credential Issuer Keys Supported -- +------------------------------------------------------ +local function max_aliro_credential_issuer_key_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.maxCredentialIssuerKeys(ib.data.value, {visibility = {displayed = false}})) + end +end + +--------------------------------------------- +-- Number Of Aliro Endpoint Keys Supported -- +--------------------------------------------- +local function max_aliro_endpoint_key_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.maxEndpointKeys(ib.data.value, {visibility = {displayed = false}})) + end +end + --------------------------------- -- Power Source Attribute List -- --------------------------------- local function handle_power_source_attribute_list(driver, device, ib, response) - local support_battery_percentage = false - local support_battery_level = false for _, attr in ipairs(ib.data.elements) do - -- Re-profile the device if BatPercentRemaining (Attribute ID 0x0C) is present. + -- mark if the device if BatPercentRemaining (Attribute ID 0x0C) or + -- BatChargeLevel (Attribute ID 0x0E) is present and try profiling. if attr.value == 0x0C then - support_battery_percentage = true - end - if attr.value == 0x0E then - support_battery_level = true - end - end - local profile_name = device:get_field(PROFILE_BASE_NAME) - if profile_name ~= nil then - if support_battery_percentage then - profile_name = profile_name .. "-battery" - elseif support_battery_level then - profile_name = profile_name .. "-batteryLevel" + device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_PERCENTAGE, { persist = true }) + match_profile(driver, device) + return + elseif attr.value == 0x0E then + device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_LEVEL, { persist = true }) + match_profile(driver, device) + return end - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) end + + -- neither BatChargeLevel nor BatPercentRemaining were found. Re-profiling without battery. + device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.NO_BATTERY, { persist = true }) + match_profile(driver, device) end ------------------------------- @@ -497,7 +684,7 @@ end ---------------- -- User Table -- ---------------- -local function add_user_to_table(device, userIdx, usrType) +local function add_user_to_table(device, userIdx, userName, userType) -- Get latest user table local user_table = utils.deep_copy(device:get_latest_state( "main", @@ -507,11 +694,11 @@ local function add_user_to_table(device, userIdx, usrType) )) -- Add new entry to table - table.insert(user_table, {userIndex = userIdx, userType = usrType}) + table.insert(user_table, {userIndex = userIdx, userName = userName, userType = userType}) device:emit_event(capabilities.lockUsers.users(user_table, {visibility = {displayed = false}})) end -local function update_user_in_table(device, userIdx, usrType) +local function update_user_in_table(device, userIdx, userName, userType) -- Get latest user table local user_table = utils.deep_copy(device:get_latest_state( "main", @@ -531,7 +718,8 @@ local function update_user_in_table(device, userIdx, usrType) -- Update user entry if i ~= 0 then - user_table[i].userType = usrType + user_table[i].userType = userType + user_table[i].userName = userName device:emit_event(capabilities.lockUsers.users(user_table, {visibility = {displayed = false}})) end end @@ -564,6 +752,40 @@ end ---------------------- -- Credential Table -- ---------------------- +local function has_credentials(device, userIdx) + -- Get latest credential table + local cred_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockCredentials.ID, + capabilities.lockCredentials.credentials.NAME, + {} + )) + + -- Find credential + for index, entry in pairs(cred_table) do + if entry.userIndex == userIdx then + return true + end + end + + -- Get latest Aliro credential table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + -- Find Aliro credential + for index, entry in pairs(aliro_table) do + if entry.userIndex == userIdx then + return true + end + end + + return false +end + local function add_credential_to_table(device, userIdx, credIdx, credType) -- Get latest credential table local cred_table = utils.deep_copy(device:get_latest_state( @@ -582,7 +804,7 @@ local function delete_credential_from_table(device, credIdx) -- If Credential Index is ALL_INDEX, remove all entries from the table if credIdx == ALL_INDEX then device:emit_event(capabilities.lockCredentials.credentials({}, {visibility = {displayed = false}})) - return + return ALL_INDEX end -- Get latest credential table @@ -594,7 +816,7 @@ local function delete_credential_from_table(device, credIdx) )) -- Delete an entry from credential table - local userIdx = 0 + local userIdx = nil for index, entry in pairs(cred_table) do if entry.credentialIndex == credIdx then table.remove(cred_table, index) @@ -635,16 +857,6 @@ end ----------------------------- -- Week Day Schedule Table -- ----------------------------- -local WEEK_DAY_MAP = { - ["Sunday"] = 1, - ["Monday"] = 2, - ["Tuesday"] = 4, - ["Wednesday"] = 8, - ["Thursday"] = 16, - ["Friday"] = 32, - ["Saturday"] = 64, -} - local function add_week_schedule_to_table(device, userIdx, scheduleIdx, schedule) -- Get latest week day schedule table local week_schedule_table = utils.deep_copy(device:get_latest_state( @@ -898,6 +1110,76 @@ local function delete_year_schedule_from_table_as_user(device, userIdx) device:emit_event(capabilities.lockSchedules.yearDaySchedules(new_year_schedule_table, {visibility = {displayed = false}})) end +---------------------------- +-- Aliro Credential Table -- +---------------------------- +local function add_aliro_to_table(device, userIdx, keyIdx, keyType, keyId) + -- Get latest aliro table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + -- Add new entry to table + table.insert(aliro_table, {userIndex = userIdx, keyIndex = keyIdx, keyType = keyType, keyId = keyId}) + device:emit_event(capabilities.lockAliro.credentials(aliro_table, {visibility = {displayed = false}})) +end + +local function delete_aliro_from_table(device, userIdx, keyType, keyId) + -- Get latest aliro table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + -- Delete an entry from aliro table + if keyType == "issuerKey" then + for i, entry in pairs(aliro_table) do + if entry.userIndex == userIdx then + table.remove(aliro_table, i) + break + end + end + else + for i, entry in pairs(aliro_table) do + if entry.userIndex == userIdx and entry.keyId == keyId then + table.remove(aliro_table, i) + break + end + end + end + device:emit_event(capabilities.lockAliro.credentials(aliro_table, {visibility = {displayed = false}})) +end + +local function delete_aliro_from_table_as_user(device, userIdx) + -- If User Index is ALL_INDEX, remove all entry from the table + if userIdx == ALL_INDEX then + device:emit_event(capabilities.lockAliro.credentials({}, {visibility = {displayed = false}})) + return + end + + -- Get latest credential table + local aliro_table = device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME + ) or {} + local new_aliro_table = {} + + -- Re-create credential table + for index, entry in pairs(aliro_table) do + if entry.userIndex ~= userIdx then + table.insert(new_aliro_table, entry) + end + end + + device:emit_event(capabilities.lockAliro.credentials(new_aliro_table, {visibility = {displayed = false}})) +end + -------------- -- Add User -- -------------- @@ -908,32 +1190,26 @@ local function handle_add_user(driver, device, command) local userType = command.args.userType -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end -- Save values to field device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, INITIAL_COTA_INDEX, {persist = true}) + device:set_field(lock_utils.USER_INDEX, INITIAL_CREDENTIAL_INDEX, {persist = true}) device:set_field(lock_utils.USER_NAME, userName, {persist = true}) device:set_field(lock_utils.USER_TYPE, userType, {persist = true}) -- Get available user index local ep = device:component_to_endpoint(command.component) - device:send(DoorLock.server.commands.GetUser(device, ep, INITIAL_COTA_INDEX)) + device:send(DoorLock.server.commands.GetUser(device, ep, INITIAL_CREDENTIAL_INDEX)) end ----------------- @@ -944,33 +1220,28 @@ local function handle_update_user(driver, device, command) local cmdName = "updateUser" local userIdx = command.args.userIndex local userName = command.args.userName - local userType = command.args.lockUserType + local userType = command.args.userType local userTypeMatter = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER if userType == "guest" then userTypeMatter = DoorLock.types.UserTypeEnum.SCHEDULE_RESTRICTED_USER end -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end -- Save values to field device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.USER_NAME, userName, {persist = true}) device:set_field(lock_utils.USER_TYPE, userType, {persist = true}) -- Send command @@ -1004,19 +1275,14 @@ local function get_user_response_handler(driver, device, ib, response) end if status ~= "success" then -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = status } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end @@ -1055,19 +1321,14 @@ local function get_user_response_handler(driver, device, ib, response) ) elseif userIdx >= maxUser then -- There's no available user index -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = "resourceExhausted" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) else -- Check next user index device:send(DoorLock.server.commands.GetUser(device, ep, userIdx + 1)) @@ -1081,6 +1342,7 @@ local function set_user_response_handler(driver, device, ib, response) -- Get result local cmdName = device:get_field(lock_utils.COMMAND_NAME) local userIdx = device:get_field(lock_utils.USER_INDEX) + local userName = device:get_field(lock_utils.USER_NAME) local userType = device:get_field(lock_utils.USER_TYPE) local status = "success" if ib.status == DoorLock.types.DlStatus.FAILURE then @@ -1094,28 +1356,23 @@ local function set_user_response_handler(driver, device, ib, response) -- Update User in table if status == "success" then if cmdName == "addUser" then - add_user_to_table(device, userIdx, userType) + add_user_to_table(device, userIdx, userName, userType) elseif cmdName == "updateUser" then - update_user_in_table(device, userIdx, userType) + update_user_in_table(device, userIdx, userName, userType) end else device.log.warn(string.format("Failed to set user: %s", status)) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = status } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end @@ -1128,20 +1385,14 @@ local function handle_delete_user(driver, device, command) local userIdx = command.args.userIndex -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1162,20 +1413,14 @@ local function handle_delete_all_users(driver, device, command) local cmdName = "deleteAllUsers" -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1206,6 +1451,7 @@ local function clear_user_response_handler(driver, device, ib, response) if status == "success" then delete_user_from_table(device, userIdx) delete_credential_from_table_as_user(device, userIdx) + delete_aliro_from_table_as_user(device, userIdx) delete_week_schedule_from_table_as_user(device, userIdx) delete_year_schedule_from_table_as_user(device, userIdx) else @@ -1213,18 +1459,14 @@ local function clear_user_response_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = status } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - }) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end @@ -1245,25 +1487,19 @@ local function handle_add_credential(driver, device, command) end local credential = { credential_type = DoorLock.types.CredentialTypeEnum.PIN, - credential_index = INITIAL_COTA_INDEX + credential_index = INITIAL_CREDENTIAL_INDEX } local credData = command.args.credentialData -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1271,7 +1507,7 @@ local function handle_add_credential(driver, device, command) device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) device:set_field(lock_utils.USER_TYPE, userType, {persist = true}) - device:set_field(lock_utils.CRED_INDEX, INITIAL_COTA_INDEX, {persist = true}) + device:set_field(lock_utils.CRED_INDEX, INITIAL_CREDENTIAL_INDEX, {persist = true}) device:set_field(lock_utils.CRED_DATA, credData, {persist = true}) -- Send command @@ -1304,20 +1540,14 @@ local function handle_update_credential(driver, device, command) local credData = command.args.credentialData -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1341,19 +1571,10 @@ local function handle_update_credential(driver, device, command) ) end ------------------------------ --- Set Credential Response -- ------------------------------ -local RESPONSE_STATUS_MAP = { - [DoorLock.types.DlStatus.FAILURE] = "failure", - [DoorLock.types.DlStatus.DUPLICATE] = "duplicate", - [DoorLock.types.DlStatus.OCCUPIED] = "occupied", - [DoorLock.types.DlStatus.INVALID_FIELD] = "invalidCommand", - [DoorLock.types.DlStatus.RESOURCE_EXHAUSTED] = "resourceExhausted", - [DoorLock.types.DlStatus.NOT_FOUND] = "failure" -} - -local function set_credential_response_handler(driver, device, ib, response) +--------------------------------- +-- Set Pin Credential Response -- +--------------------------------- +local function set_pin_response_handler(driver, device, ib, response) if ib.status ~= im.InteractionResponse.Status.SUCCESS then device.log.error("Failed to set credential for device") return @@ -1367,9 +1588,10 @@ local function set_credential_response_handler(driver, device, ib, response) local userIdx = device:get_field(lock_utils.USER_INDEX) local userType = device:get_field(lock_utils.USER_TYPE) local credIdx = device:get_field(lock_utils.CRED_INDEX) - local status = "success" local elements = ib.info_block.data.elements - if elements.status.value == DoorLock.types.DlStatus.SUCCESS then + local status = RESPONSE_STATUS_MAP[elements.status.value] + + if status == "success" then -- Don't save user and credential for COTA if cmdName == "addCota" then device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) @@ -1378,7 +1600,7 @@ local function set_credential_response_handler(driver, device, ib, response) -- If user is added also, update User table if userIdx == nil then - add_user_to_table(device, elements.user_index.value, userType) + add_user_to_table(device, elements.user_index.value, nil, userType) end -- Update Credential table @@ -1388,20 +1610,15 @@ local function set_credential_response_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, credentialIndex = credIdx, statusCode = status } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) -- If User Type is Guest and device support schedule, add default schedule local week_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES}) @@ -1431,33 +1648,20 @@ local function set_credential_response_handler(driver, device, ib, response) return end - -- Update commandResult - status = RESPONSE_STATUS_MAP[elements.status.value] + -- In the case DlStatus returns Occupied, this means the current credential index is in use, + -- so we must try the next one. If there is not a next index (i.e. it is nil), + -- we should mark this as "resourceExhausted" and stop attempting to set the credentials. device.log.warn(string.format("Failed to set credential: %s", status)) - - -- Set commandResult to error status - if status == "duplicate" and cmdName == "addCota" then - generate_cota_cred_for_device(device) - device.thread:call_with_delay(0, function(t) set_cota_credential(device, credIdx) end) - return - elseif status ~= "occupied" then - local result = { + if status == "occupied" and elements.next_credential_index.value == nil then + local command_result_info = { commandName = cmdName, - statusCode = status + statusCode = "resourceExhausted" -- No more available credential index } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - return - end - - if elements.next_credential_index.value ~= nil then + elseif status == "occupied" then -- Get parameters local credIdx = elements.next_credential_index.value local credential = { @@ -1490,27 +1694,237 @@ local function set_credential_response_handler(driver, device, ib, response) userTypeMatter -- User Type ) ) + elseif status == "duplicate" and cmdName == "addCota" then + generate_cota_cred_for_device(device) + device.thread:call_with_delay(0, function(t) set_cota_credential(device, credIdx) end) else - local result = { + local command_result_info = { commandName = cmdName, - statusCode = "resourceExhausted" -- No more available credential index + statusCode = status } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end end ------------------------ --- Delete Credential -- ------------------------ -local function handle_delete_credential(driver, device, command) +----------------------------------- +-- Set Aliro Credential Response -- +----------------------------------- +local function set_issuer_key_response_handler(driver, device, ib, response) + local cmdName = "setIssuerKey" + local userIdx = device:get_field(lock_utils.USER_INDEX) + local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + local issuerKeyIndex = device:get_field(lock_utils.ISSUER_KEY_INDEX) + local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) + local elements = ib.info_block.data.elements + local status = RESPONSE_STATUS_MAP[elements.status.value] + + if status == "success" then + -- Delete field data + device:set_field(lock_utils.ISSUER_KEY, nil, {persist = true}) + + -- If user is added also, update User table + if userIdx == nil then + userIdx = elements.user_index.value + add_user_to_table(device, userIdx, nil, "adminMember") + end + + -- Update Aliro table + add_aliro_to_table(device, userIdx, issuerKeyIndex, "issuerKey", nil) + + -- Update commandResult + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + requestId = reqId, + statusCode = status + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + return + end + + -- In the case DlStatus returns Occupied, this means the current credential index is in use, + -- so we must try the next one. If there is not a next index (i.e. it is nil), + -- we should mark this as "resourceExhausted" and stop attempting to set the credentials. + device.log.warn(string.format("Failed to set credential: %s", status)) + if status == "occupied" and elements.next_credential_index.value == nil then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + requestId = reqId, + statusCode = "resourceExhausted" -- No more available credential index + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + elseif status == "occupied" then + -- Get parameters + if userIdx ~= nil then + userType = nil + end + local credIdx = elements.next_credential_index.value + local credType = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY + local credData = device:get_field(lock_utils.ISSUER_KEY) + local credential = { + credential_type = credType, + credential_index = credIdx + } + + -- Save values to field + device:set_field(lock_utils.ISSUER_KEY_INDEX, credIdx, {persist = true}) + + -- Send command + local ep = find_default_endpoint(device, DoorLock.ID) + device:send( + DoorLock.server.commands.SetCredential( + device, ep, + DoorLock.types.DataOperationTypeEnum.ADD, + credential, -- Credential + hex_string_to_octet_string(credData), -- Credential Data + userIdx, -- User Index + nil, -- User Status + userType -- User Type + ) + ) + else + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + requestId = reqId, + statusCode = status + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + end +end + +local function set_endpoint_key_response_handler(driver, device, ib, response) + local cmdName = "setEndpointKey" + local userIdx = device:get_field(lock_utils.USER_INDEX) + local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + local keyId = device:get_field(lock_utils.DEVICE_KEY_ID) + local keyType = device:get_field(lock_utils.ENDPOINT_KEY_TYPE) + local endpointKeyIndex = device:get_field(lock_utils.ENDPOINT_KEY_INDEX) + local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) + local elements = ib.info_block.data.elements + local status = RESPONSE_STATUS_MAP[elements.status.value] + + if status == "success" then + -- Delete field data + device:set_field(lock_utils.ENDPOINT_KEY, nil, {persist = true}) + + -- If user is added also, update User table + if userIdx == nil then + userIdx = elements.user_index.value + add_user_to_table(device, userIdx, nil, "adminMember") + end + + -- Update Aliro table + add_aliro_to_table(device, userIdx, endpointKeyIndex, keyType, keyId) + + -- Update commandResult + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + keyId = keyId, + requestId = reqId, + statusCode = status + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + return + end + + -- In the case DlStatus returns Occupied, this means the current credential index is in use, + -- so we must try the next one. If there is not a next index (i.e. it is nil), + -- we should mark this as "resourceExhausted" and stop attempting to set the credentials. + device.log.warn(string.format("Failed to set credential: %s", status)) + + if status == "occupied" and elements.next_credential_index.value == nil then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + keyId = keyId, + requestId = reqId, + statusCode = "resourceExhausted" -- No more available credential index + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + elseif status == "occupied" then + -- Get parameters + if userIdx ~= nil then + userType = nil + end + local credIdx = elements.next_credential_index.value + local credType = ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP[keyType] + local credData = device:get_field(lock_utils.ENDPOINT_KEY) + local credential = { + credential_type = credType, + credential_index = credIdx + } + + -- Save values to field + device:set_field(lock_utils.ENDPOINT_KEY_INDEX, credIdx, {persist = true}) + + -- Send command + local ep = find_default_endpoint(device, DoorLock.ID) + device:send( + DoorLock.server.commands.SetCredential( + device, ep, + DoorLock.types.DataOperationTypeEnum.ADD, + credential, -- Credential + hex_string_to_octet_string(credData), -- Credential Data + userIdx, -- User Index + nil, -- User Status + userType -- User Type + ) + ) + else + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + keyId = keyId, + requestId = reqId, + statusCode = status + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + end +end + +local function set_credential_response_handler(driver, device, ib, response) + if ib.status ~= im.InteractionResponse.Status.SUCCESS then + device.log.error("Failed to set credential for device") + return + end + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + if cmdName == "addCredential" or cmdName == "updateCredential" or cmdName == "addCota" then + set_pin_response_handler(driver, device, ib, response) + elseif cmdName == "setIssuerKey" then + set_issuer_key_response_handler(driver, device, ib, response) + elseif cmdName == "setEndpointKey" then + set_endpoint_key_response_handler(driver, device, ib, response) + end +end + +----------------------- +-- Delete Credential -- +----------------------- +local function handle_delete_credential(driver, device, command) -- Get parameters local cmdName = "deleteCredential" local credIdx = command.args.credentialIndex @@ -1520,20 +1934,14 @@ local function handle_delete_credential(driver, device, command) } -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1558,20 +1966,14 @@ local function handle_delete_all_credentials(driver, device, command) } -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1588,42 +1990,71 @@ end -- Clear Credential Response -- ------------------------------- local function clear_credential_response_handler(driver, device, ib, response) - -- Get result local cmdName = device:get_field(lock_utils.COMMAND_NAME) - local credIdx = device:get_field(lock_utils.CRED_INDEX) - local status = "success" - if ib.status == DoorLock.types.DlStatus.FAILURE then - status = "failure" - elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then - status = "invalidCommand" + if cmdName ~= "deleteCredential" and cmdName ~= "clearEndpointKey" and + cmdName ~= "clearIssuerKey" and cmdName ~= "deleteAllCredentials" then + return end + local status = RESPONSE_STATUS_MAP[ib.status] or "success" + local command_result_info = { commandName = cmdName, statusCode = status } -- default command result + local userIdx = device:get_field(lock_utils.USER_INDEX) + local all_user_credentials_removed = false - -- Delete User in table - local userIdx = 0 - if status == "success" then + if (cmdName == "deleteCredential" or cmdName == "deleteAllCredentials") and status == "success" then + -- Get result from data saved in relevant, associated fields + local credIdx = device:get_field(lock_utils.CRED_INDEX) + + -- find userIdx associated with credIdx, don't use lock utils field in this case userIdx = delete_credential_from_table(device, credIdx) - if userIdx == 0 then - userIdx = nil + if userIdx ~= nil then + all_user_credentials_removed = not has_credentials(device, userIdx) end - else - device.log.warn(string.format("Failed to clear credential: %s", status)) + + -- set unique command result fields + command_result_info.userIndex = userIdx + command_result_info.credentialIndex = credIdx + elseif cmdName == "clearIssuerKey" and status == "success" then + -- Get result from data saved in relevant, associated fields + local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) + + delete_aliro_from_table(device, userIdx, "issuerKey", nil) + all_user_credentials_removed = not has_credentials(device, userIdx) + + -- set unique command result fields + command_result_info.userIndex = userIdx + command_result_info.requestId = reqId + elseif cmdName == "clearEndpointKey" and status == "success" then + -- Get result from data saved in relevant, associated fields + local deviceKeyId = device:get_field(lock_utils.DEVICE_KEY_ID) + local keyType = device:get_field(lock_utils.ENDPOINT_KEY_TYPE) + local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) + + delete_aliro_from_table(device, userIdx, keyType, deviceKeyId) + all_user_credentials_removed = not has_credentials(device, userIdx) + + -- set unique command result fields + command_result_info.userIndex = userIdx + command_result_info.keyId = deviceKeyId + command_result_info.requestId = reqId + end + + -- user data if credentials were removed + if all_user_credentials_removed then + delete_user_from_table(device, userIdx) + delete_week_schedule_from_table_as_user(device, userIdx) + delete_year_schedule_from_table_as_user(device, userIdx) end -- Update commandResult - local result = { - commandName = cmdName, - userIndex = userIdx, - credentialIndex = credIdx, - statusCode = status - } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + if cmdName == "deleteCredential" or cmdName == "deleteAllCredentials" then + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + else + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + end device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end @@ -1648,20 +2079,14 @@ local function handle_set_week_day_schedule(driver, device, command) local endMinute = schedule.endMinute -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1726,20 +2151,15 @@ local function set_week_day_schedule_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, scheduleIndex = scheduleIdx, statusCode = status } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end @@ -1753,20 +2173,14 @@ local function handle_clear_week_day_schedule(driver, device, command) local userIdx = command.args.userIndex -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1803,20 +2217,15 @@ local function clear_week_day_schedule_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, scheduleIndex = scheduleIdx, statusCode = status } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end @@ -1860,20 +2269,14 @@ local function handle_set_year_day_schedule(driver, device, command) local localEndTime = command.args.schedule.localEndTime -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1929,20 +2332,15 @@ local function set_year_day_schedule_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, scheduleIndex = scheduleIdx, statusCode = status } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end @@ -1956,20 +2354,14 @@ local function handle_clear_year_day_schedule(driver, device, command) local userIdx = command.args.userIndex -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -2078,6 +2470,354 @@ local function handle_refresh(driver, device, command) device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end +local function handle_set_reader_config(driver, device, command) + local cmdName = "setReaderConfig" + local signingKey = command.args.signingKey + local verificationKey = command.args.verificationKey + local groupId = command.args.groupId + local groupResolvingKey = nil + local aliro_ble_uwb_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.ALIROBLEUWB}) + if #aliro_ble_uwb_eps > 0 then + groupResolvingKey = command.args.groupResolvingKey + end + + -- Check busy state + if is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.VERIFICATION_KEY, verificationKey, {persist = true}) + device:set_field(lock_utils.GROUP_ID, groupId, {persist = true}) + device:set_field(lock_utils.GROUP_RESOLVING_KEY, groupResolvingKey, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send( + DoorLock.server.commands.SetAliroReaderConfig( + device, ep, + hex_string_to_octet_string(signingKey), + hex_string_to_octet_string(verificationKey), + hex_string_to_octet_string(groupId), -- Group identification + hex_string_to_octet_string(groupResolvingKey) -- Group resolving key + ) + ) +end + +local function set_aliro_reader_config_handler(driver, device, ib, response) + -- Get result + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + local verificationKey = device:get_field(lock_utils.VERIFICATION_KEY) + local groupId = device:get_field(lock_utils.GROUP_ID) + local groupResolvingKey = device:get_field(lock_utils.GROUP_RESOLVING_KEY) + + local status = "success" + if ib.status == DoorLock.types.DlStatus.FAILURE then + status = "failure" + elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then + status = "invalidCommand" + elseif ib.status == DoorLock.types.DlStatus.SUCCESS then + if verificationKey ~= nil then + device:emit_event(capabilities.lockAliro.readerVerificationKey( + verificationKey, + { + state_change = true, + visibility = {displayed = false} + } + )) + end + if groupId ~= nil then + device:emit_event(capabilities.lockAliro.readerGroupIdentifier( + groupId, + { + state_change = true, + visibility = {displayed = false} + } + )) + end + if groupResolvingKey ~= nil then + device:emit_event(capabilities.lockAliro.groupResolvingKey( + groupResolvingKey, + { + state_change = true, + visibility = {displayed = false} + } + )) + end + end + + -- Update commandResult + local command_result_info = { + commandName = cmdName, + statusCode = status + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) +end + +local function handle_set_card_id(driver, device, command) + if command.args.cardId ~= nil then + device:emit_event(capabilities.lockAliro.cardId(command.args.cardId, {visibility = {displayed = false}})) + end +end + +local function handle_set_issuer_key(driver, device, command) + -- Get parameters + local cmdName = "setIssuerKey" + local userIdx = command.args.userIndex + local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + local issuerKey = command.args.issuerKey + local reqId = command.args.requestId + local credential = { + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, + credential_index = INITIAL_CREDENTIAL_INDEX + } + + -- Adjustment + if userIdx == 0 then + userIdx = nil + else + userType = nil + end + + -- Check busy state + if is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + requestId = reqId, + statusCode = "busy" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.ISSUER_KEY, issuerKey, {persist = true}) + device:set_field(lock_utils.ISSUER_KEY_INDEX, INITIAL_CREDENTIAL_INDEX, {persist = true}) + device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send( + DoorLock.server.commands.SetCredential( + device, ep, + DoorLock.types.DataOperationTypeEnum.ADD, -- Data Operation Type: Add(0), Modify(2) + credential, -- Credential + hex_string_to_octet_string(issuerKey), -- Credential Data + userIdx, -- User Index + nil, -- User Status + userType -- User Type + ) + ) +end + +local function handle_clear_issuer_key(driver, device, command) + -- Get parameters + local cmdName = "clearIssuerKey" + local userIdx = command.args.userIndex + local reqId = command.args.requestId + + -- Check busy state + if is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + requestId = reqId, + statusCode = "busy" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) + + -- Get latest aliro table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + -- Find issuer key index + for index, entry in pairs(aliro_table) do + if entry.userIndex == userIdx and entry.keyType == "issuerKey" then + -- Set parameters + local credential = { + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, + credential_index = entry.keyIndex, + } + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) + break + end + end +end + +local function handle_set_endpoint_key(driver, device, command) + -- Get parameters + local cmdName = "setEndpointKey" + local userIdx = command.args.userIndex + local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + local keyId = command.args.keyId + local keyType = command.args.keyType + local endpointKey = command.args.endpointKey + local reqId = command.args.requestId + local dataOpType = DoorLock.types.DataOperationTypeEnum.ADD -- Data Operation Type: Add(0), Modify(2) + local endpointKeyIndex = INITIAL_CREDENTIAL_INDEX + + -- Min user index of commandResult is 1 + -- 0 should convert to nil before busy check + if userIdx == 0 then + userIdx = nil + end + + -- Check busy state + if is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + keyId = keyId, + requestId = reqId, + statusCode = "busy" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Adjustment + if userIdx ~= nil then + userType = nil + + -- Get latest aliro table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + -- Find existing endpoint key + for index, entry in pairs(aliro_table) do + if (entry.keyType == "evictableEndpointKey" or entry.keyType == "nonEvictableEndpointKey") and entry.keyId == keyId then + dataOpType = DoorLock.types.DataOperationTypeEnum.MODIFY + endpointKeyIndex = entry.keyIndex + delete_aliro_from_table(device, userIdx, keyType, keyId) + break + end + end + end + + local credential = { + credential_type = ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP[keyType], + credential_index = endpointKeyIndex + } + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.DEVICE_KEY_ID, keyId, {persist = true}) + device:set_field(lock_utils.ENDPOINT_KEY_TYPE, keyType, {persist = true}) + device:set_field(lock_utils.ENDPOINT_KEY, endpointKey, {persist = true}) + device:set_field(lock_utils.ENDPOINT_KEY_INDEX, endpointKeyIndex, {persist = true}) + device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send( + DoorLock.server.commands.SetCredential( + device, ep, + dataOpType, -- Data Operation Type: Add(0), Modify(2) + credential, -- Credential + hex_string_to_octet_string(endpointKey), -- Credential Data + userIdx, -- User Index + nil, -- User Status + userType -- User Type + ) + ) +end + +local function handle_clear_endpoint_key(driver, device, command) + -- Get parameters + local cmdName = "clearEndpointKey" + local userIdx = command.args.userIndex + local keyId = command.args.keyId + local keyType = command.args.keyType + local reqId = command.args.requestId + + -- Check busy state + if is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + keyId = keyId, + requestId = reqId, + statusCode = "busy" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.DEVICE_KEY_ID, keyId, {persist = true}) + device:set_field(lock_utils.ENDPOINT_KEY_TYPE, keyType, {persist = true}) + device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) + + -- Get latest aliro table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + local ep = device:component_to_endpoint(command.component) + if keyId == nil then + return + else + -- Find aliro credential + for index, entry in pairs(aliro_table) do + if entry.userIndex == userIdx and entry.keyId == keyId and entry.keyType == keyType then + -- Set parameters + local credential = { + credential_type = ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP[keyType], + credential_index = entry.keyIndex, + } + -- Send command + device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) + break + end + end + end +end + local new_matter_lock_handler = { NAME = "New Matter Lock Handler", lifecycle_handlers = { @@ -2085,6 +2825,7 @@ local new_matter_lock_handler = { added = device_added, doConfigure = do_configure, infoChanged = info_changed, + driverSwitched = driver_switched }, matter_handlers = { attr = { @@ -2098,6 +2839,14 @@ local new_matter_lock_handler = { [DoorLock.attributes.RequirePINforRemoteOperation.ID] = require_remote_pin_handler, [DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser.ID] = max_week_schedule_of_user_handler, [DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser.ID] = max_year_schedule_of_user_handler, + [DoorLock.attributes.AliroReaderVerificationKey.ID] = aliro_reader_verification_key_handler, + [DoorLock.attributes.AliroReaderGroupIdentifier.ID] = aliro_reader_group_id_handler, + [DoorLock.attributes.AliroExpeditedTransactionSupportedProtocolVersions.ID] = aliro_protocol_versions_handler, + [DoorLock.attributes.AliroGroupResolvingKey.ID] = aliro_group_resolving_key_handler, + [DoorLock.attributes.AliroSupportedBLEUWBProtocolVersions.ID] = aliro_supported_ble_uwb_protocol_versions_handler, + [DoorLock.attributes.AliroBLEAdvertisingVersion.ID] = aliro_ble_advertising_version_handler, + [DoorLock.attributes.NumberOfAliroCredentialIssuerKeysSupported.ID] = max_aliro_credential_issuer_key_handler, + [DoorLock.attributes.NumberOfAliroEndpointKeysSupported.ID] = max_aliro_endpoint_key_handler, }, [PowerSource.ID] = { [PowerSource.attributes.AttributeList.ID] = handle_power_source_attribute_list, @@ -2121,6 +2870,7 @@ local new_matter_lock_handler = { [DoorLock.server.commands.SetWeekDaySchedule.ID] = set_week_day_schedule_handler, [DoorLock.server.commands.ClearWeekDaySchedule.ID] = clear_week_day_schedule_handler, [DoorLock.server.commands.SetYearDaySchedule.ID] = set_year_day_schedule_handler, + [DoorLock.server.commands.SetAliroReaderConfig.ID] = set_aliro_reader_config_handler, }, }, }, @@ -2150,6 +2900,14 @@ local new_matter_lock_handler = { [capabilities.lockSchedules.commands.setYearDaySchedule.NAME] = handle_set_year_day_schedule, [capabilities.lockSchedules.commands.clearYearDaySchedules.NAME] = handle_clear_year_day_schedule, }, + [capabilities.lockAliro.ID] = { + [capabilities.lockAliro.commands.setReaderConfig.NAME] = handle_set_reader_config, + [capabilities.lockAliro.commands.setCardId.NAME] = handle_set_card_id, + [capabilities.lockAliro.commands.setIssuerKey.NAME] = handle_set_issuer_key, + [capabilities.lockAliro.commands.clearIssuerKey.NAME] = handle_clear_issuer_key, + [capabilities.lockAliro.commands.setEndpointKey.NAME] = handle_set_endpoint_key, + [capabilities.lockAliro.commands.clearEndpointKey.NAME] = handle_clear_endpoint_key, + }, [capabilities.refresh.ID] = {[capabilities.refresh.commands.refresh.NAME] = handle_refresh} }, supported_capabilities = { @@ -2160,7 +2918,7 @@ local new_matter_lock_handler = { capabilities.battery, capabilities.batteryLevel }, - can_handle = is_new_matter_lock_products + can_handle = require("new-matter-lock.can_handle"), } return new_matter_lock_handler diff --git a/drivers/SmartThings/matter-lock/src/sub_drivers.lua b/drivers/SmartThings/matter-lock/src/sub_drivers.lua new file mode 100644 index 0000000000..0f5d1f76d2 --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("new-matter-lock"), +} +return sub_drivers diff --git a/drivers/SmartThings/matter-lock/src/test/test_aqara_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_aqara_matter_lock.lua index b64324d8f7..c5da1b600e 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_aqara_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_aqara_matter_lock.lua @@ -1,20 +1,10 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" +test.set_rpc_version(0) local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" @@ -41,7 +31,8 @@ local mock_device = test.mock_device.build_test_matter_device({ cluster_id = clusters.DoorLock.ID, cluster_type = "SERVER", cluster_revision = 1, - feature_map = 0x0001, --u32 bitmap + feature_map = clusters.DoorLock.types.Feature.PIN_CREDENTIAL | + clusters.DoorLock.types.Feature.USER } }, device_types = { @@ -52,6 +43,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + -- subscribe request local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(clusters.DoorLock.attributes.OperatingMode:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device)) @@ -62,8 +55,22 @@ local function test_init() subscribe_request:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.events.LockUserChange:subscribe(mock_device)) - test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + -- add test device test.mock_device.add_test_device(mock_device) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + mock_device:expect_metadata_update({ profile = "lock-user-pin" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-lock/src/test/test_bridged_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_bridged_matter_lock.lua index 3c95216dc5..eb8ec9fa98 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_bridged_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_bridged_matter_lock.lua @@ -1,20 +1,10 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local mock_device_record = { @@ -41,58 +31,75 @@ local mock_device_record = { local mock_device = test.mock_device.build_test_matter_device(mock_device_record) local mock_device_record_level = { - profile = t_utils.get_profile_definition("lock-nocodes-notamper-batteryLevel.yml"), - manufacturer_info = {vendor_id = 0x129F, product_id = 0x0001}, -- Level Lock Plus - endpoints = { - { - endpoint_id = 2, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - device_type_id = 0x0016, device_type_revision = 1, -- RootNode - } + profile = t_utils.get_profile_definition("lock-nocodes-notamper-batteryLevel.yml"), + manufacturer_info = {vendor_id = 0x129F, product_id = 0x0001}, -- Level Lock Plus + endpoints = { + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, }, - { - endpoint_id = 10, - clusters = { - {cluster_id = clusters.DoorLock.ID, cluster_type = "SERVER", feature_map = 0x0000}, - }, + device_types = { + device_type_id = 0x0016, device_type_revision = 1, -- RootNode + } + }, + { + endpoint_id = 10, + clusters = { + {cluster_id = clusters.DoorLock.ID, cluster_type = "SERVER", feature_map = 0x0000}, }, }, + }, } local mock_device_level = test.mock_device.build_test_matter_device(mock_device_record_level) local function test_init() - local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device) - subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm:subscribe(mock_device)) - subscribe_request:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_device)) - subscribe_request:merge(clusters.DoorLock.events.LockUserChange:subscribe(mock_device)) - test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes("[]", {visibility = {displayed = false}})) + ) + local req = clusters.DoorLock.attributes.MaxPINCodeLength:read(mock_device, 10) + req:merge(clusters.DoorLock.attributes.MinPINCodeLength:read(mock_device, 10)) + req:merge(clusters.DoorLock.attributes.NumberOfPINUsersSupported:read(mock_device, 10)) + test.socket.matter:__expect_send({mock_device.id, req}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device) + subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm:subscribe(mock_device)) + subscribe_request:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_device)) + subscribe_request:merge(clusters.DoorLock.events.LockUserChange:subscribe(mock_device)) + test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + test.mock_device.add_test_device(mock_device) + - local subscribe_request_level = clusters.DoorLock.attributes.LockState:subscribe(mock_device_level) - test.socket["matter"]:__expect_send({mock_device_level.id, subscribe_request_level}) - test.mock_device.add_test_device(mock_device_level) + test.mock_device.add_test_device(mock_device_level) + test.socket.device_lifecycle:__queue_receive({ mock_device_level.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_level.id, "init" }) + local subscribe_request_level = clusters.DoorLock.attributes.LockState:subscribe(mock_device_level) + test.socket["matter"]:__expect_send({mock_device_level.id, subscribe_request_level}) end test.set_test_init_function(test_init) test.register_coroutine_test( - "doConfigure lifecycle event for base-lock-nobattery", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "base-lock-nobattery" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end + "doConfigure lifecycle event for base-lock-nobattery", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "base-lock-nobattery" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end ) test.register_coroutine_test( - "doConfigure lifecycle event for Level Lock Plus profile", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device_level.id, "doConfigure" }) - mock_device_level:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end + "doConfigure lifecycle event for Level Lock Plus profile", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_level.id, "doConfigure" }) + mock_device_level:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end ) test.run_registered_tests() diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua index 7701d5a90c..8fd0cc0574 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua @@ -1,20 +1,9 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" @@ -43,12 +32,21 @@ local mock_device_record = { local mock_device = test.mock_device.build_test_matter_device(mock_device_record) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) + ) + mock_device:expect_metadata_update({ profile = "lock-without-codes" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_device)) test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_battery.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_battery.lua index 6aff133939..a8f7506798 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_battery.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_battery.lua @@ -1,20 +1,10 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local uint32 = require "st.matter.data_types.Uint32" @@ -67,28 +57,41 @@ local mock_device_no_battery_record = { local mock_device_no_battery = test.mock_device.build_test_matter_device(mock_device_no_battery_record) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) + ) + mock_device:expect_metadata_update({ profile = "lock-without-codes" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.events.LockUserChange:subscribe(mock_device)) test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() - test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) + test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) end test.set_test_init_function(test_init) local function test_init_no_battery() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_no_battery) + test.socket.device_lifecycle:__queue_receive({ mock_device_no_battery.id, "added" }) + test.socket.capability:__expect_send( + mock_device_no_battery:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) + ) + mock_device_no_battery:expect_metadata_update({ profile = "lock-without-codes-nobattery" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_no_battery.id, "init" }) local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device_no_battery) - subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) + subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device_no_battery)) subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm:subscribe(mock_device_no_battery)) subscribe_request:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_device_no_battery)) subscribe_request:merge(clusters.DoorLock.events.LockUserChange:subscribe(mock_device_no_battery)) test.socket["matter"]:__expect_send({mock_device_no_battery.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_no_battery) test.socket.device_lifecycle:__queue_receive({ mock_device_no_battery.id, "doConfigure" }) mock_device_no_battery:expect_metadata_update({ profile = "base-lock-nobattery" }) mock_device_no_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_batteryLevel.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_batteryLevel.lua index 22f9ccd29a..990bff8b60 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_batteryLevel.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_batteryLevel.lua @@ -1,20 +1,9 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" @@ -44,10 +33,15 @@ local mock_device = test.mock_device.build_test_matter_device(mock_device_record local function test_init() - local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device) - subscribe_request:merge(clusters.PowerSource.attributes.BatChargeLevel:subscribe(mock_device)) - test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device) + subscribe_request:merge(clusters.PowerSource.attributes.BatChargeLevel:subscribe(mock_device)) + test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) @@ -84,20 +78,20 @@ test.register_message_test( message = mock_device:generate_test_message("main", capabilities.batteryLevel.battery.warning()), }, { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.PowerSource.attributes.BatChargeLevel:build_test_report_data( - mock_device, 10, clusters.PowerSource.types.BatChargeLevelEnum.OK - ), - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.batteryLevel.battery.normal()), + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.PowerSource.attributes.BatChargeLevel:build_test_report_data( + mock_device, 10, clusters.PowerSource.types.BatChargeLevelEnum.OK + ), }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.batteryLevel.battery.normal()), + }, } ) diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_codes.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_codes.lua index c672410296..a3e05c9634 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_codes.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_codes.lua @@ -1,40 +1,16 @@ --- Copyright 2022 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. - --- Copyright 2022 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. --- Mock out globals +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local test = require "integration_test" local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local json = require "st.json" local clusters = require "st.matter.clusters" local DoorLock = clusters.DoorLock local im = require "st.matter.interaction_model" local types = DoorLock.types + local mock_device_record = { profile = t_utils.get_profile_definition("base-lock.yml"), manufacturer_info = {vendor_id = 0xcccc, product_id = 0x1}, @@ -56,7 +32,7 @@ local mock_device_record = { cluster_type = "SERVER", feature_map = 0x0101, -- PIN & USR }, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0}, }, }, }, @@ -64,13 +40,18 @@ local mock_device_record = { local mock_device = test.mock_device.build_test_matter_device(mock_device_record) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device)) subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "base-lock-nobattery" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) @@ -189,11 +170,11 @@ local function init_code_slot(slot_number, name, device) ) local credential = DoorLock.types.DlCredential( - { + { credential_type = DoorLock.types.DlCredentialType.PIN, credential_index = slot_number, } - ) + ) test.socket.matter:__expect_send( { device.id, diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua index 6303ec7bcd..c41d911881 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua @@ -1,34 +1,9 @@ --- Copyright 2022 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. - --- Copyright 2022 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. --- Mock out globals +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local test = require "integration_test" local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local json = require "st.json" local clusters = require "st.matter.clusters" @@ -64,6 +39,9 @@ local mock_device_record = { local mock_device = test.mock_device.build_test_matter_device(mock_device_record) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device)) @@ -71,7 +49,9 @@ local function test_init() subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) test.socket.matter:__expect_send({mock_device.id, DoorLock.attributes.RequirePINforRemoteOperation:read(mock_device, 10)}) - test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_modular.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_modular.lua new file mode 100644 index 0000000000..c4c226fbbe --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_modular.lua @@ -0,0 +1,644 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" + +local DoorLock = clusters.DoorLock + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock.yml"), + manufacturer_info = { + vendor_id = 0x147F, + product_id = 0x0001, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = DoorLock.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0x0, + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = 10 + }, + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + +local mock_device_unlatch = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock-unlatch.yml"), + manufacturer_info = { + vendor_id = 0x147F, + product_id = 0x0001, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = DoorLock.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0x1000, -- UNLATCH + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = 10 + }, + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + +local mock_device_user_pin = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock-user-pin.yml"), + manufacturer_info = { + vendor_id = 0x147F, + product_id = 0x0001, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = DoorLock.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0x0181, -- PIN & USR & COTA + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = 10 + }, + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + +local mock_device_user_pin_schedule_unlatch = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock-user-pin-schedule-unlatch.yml"), + manufacturer_info = { + vendor_id = 0x147F, + product_id = 0x0001, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = DoorLock.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0x1591, -- PIN & USR & COTA & WDSCH & YDSCH & UNLATCH + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = 10 + }, + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + +local mock_device_modular = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock-modular-embedded-unlatch.yml"), + manufacturer_info = { + vendor_id = 0x147F, + product_id = 0x0001, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = DoorLock.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0x1591, -- PIN & USR & COTA & WDSCH & YDSCH & UNLATCH + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = 10 + }, + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + + +local function test_init() + test.disable_startup_messages() + -- subscribe request + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) + -- add test device + test.mock_device.add_test_device(mock_device) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_unlatch() + test.disable_startup_messages() + -- subscribe request + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_unlatch) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_unlatch)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_unlatch)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_unlatch)) + -- add test device, handle initial subscribe + test.mock_device.add_test_device(mock_device_unlatch) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "added" }) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.matter:__expect_send({mock_device_unlatch.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "init" }) + test.socket.matter:__expect_send({mock_device_unlatch.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) + mock_device_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_user_pin() + test.disable_startup_messages() + -- subscribe request + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_user_pin) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.attributes.NumberOfPINUsersSupported:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.attributes.MaxPINCodeLength:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.attributes.MinPINCodeLength:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.attributes.RequirePINforRemoteOperation:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device_user_pin)) + -- add test device + test.mock_device.add_test_device(mock_device_user_pin) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "added" }) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.matter:__expect_send({mock_device_user_pin.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "init" }) + test.socket.matter:__expect_send({mock_device_user_pin.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) + mock_device_user_pin:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_user_pin_schedule_unlatch() + test.disable_startup_messages() + -- subscribe request + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_user_pin_schedule_unlatch) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.attributes.NumberOfPINUsersSupported:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.attributes.MaxPINCodeLength:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.attributes.MinPINCodeLength:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.attributes.RequirePINforRemoteOperation:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device_user_pin_schedule_unlatch)) + -- add test device + test.mock_device.add_test_device(mock_device_user_pin_schedule_unlatch) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "added" }) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.matter:__expect_send({mock_device_user_pin_schedule_unlatch.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "init" }) + test.socket.matter:__expect_send({mock_device_user_pin_schedule_unlatch.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) + mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_modular() + test.disable_startup_messages() + -- subscribe request + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_modular) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_modular)) + -- add test device + test.mock_device.add_test_device(mock_device_modular) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_modular.id, "added" }) + test.socket.capability:__expect_send( + mock_device_modular:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.matter:__expect_send({mock_device_modular.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device_modular.id, "init" }) + test.socket.matter:__expect_send({mock_device_modular.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_modular.id, "doConfigure" }) + mock_device_modular:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Test lock profile change when attributes related to BAT feature is not available.", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + mock_device:expect_metadata_update({ profile = "lock-modular", optional_component_capabilities = {{"main", {}}} }) + end +) + +test.register_coroutine_test( + "Test modular lock profile change when BatChargeLevel attribute is available", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + mock_device:expect_metadata_update({ profile = "lock-modular", optional_component_capabilities = {{"main", {"batteryLevel"}}} }) + end +) + +test.register_coroutine_test( + "Test modular lock profile change when BatChargeLevel and BatPercentRemaining attributes are available", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(12), -- BatPercentRemaining + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + mock_device:expect_metadata_update({ profile = "lock-modular", optional_component_capabilities = {{"main", {"battery"}}} }) + end +) + +test.register_coroutine_test( + "Test modular lock profile change with unlatch when attributes related to BAT feature is not available.", + function() + test.socket.matter:__queue_receive( + { + mock_device_unlatch.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_unlatch, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + mock_device_unlatch:expect_metadata_update({ profile = "lock-modular-embedded-unlatch", optional_component_capabilities = {{"main", {}}} }) + end, + { test_init = test_init_unlatch } +) + +test.register_coroutine_test( + "Test lock-unlatch profile change with unlatch when BatChargeLevel attribute is available", + function() + test.socket.matter:__queue_receive( + { + mock_device_unlatch.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_unlatch, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + mock_device_unlatch:expect_metadata_update({ profile = "lock-modular-embedded-unlatch", optional_component_capabilities = {{"main", {"batteryLevel"}}} }) + end, + { test_init = test_init_unlatch } +) + +test.register_coroutine_test( + "Test modular lock profile change with unlatch when BatChargeLevel and BatPercentRemaining attributes are available", + function() + test.socket.matter:__queue_receive( + { + mock_device_unlatch.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_unlatch, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(12), -- BatPercentRemaining + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + mock_device_unlatch:expect_metadata_update({ profile = "lock-modular-embedded-unlatch", optional_component_capabilities = {{"main", {"battery"}}} }) + end, + { test_init = test_init_unlatch } +) + +test.register_coroutine_test( + "Test lock-user-pin profile change when BatChargeLevel and BatPercentRemaining attributes are available", + function() + test.socket.matter:__queue_receive( + { + mock_device_user_pin.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_user_pin, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(12), -- BatPercentRemaining + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + mock_device_user_pin:expect_metadata_update({ profile = "lock-modular", optional_component_capabilities = {{"main", {"lockUsers", "lockCredentials", "battery"}}} }) + end, + { test_init = test_init_user_pin } +) + +test.register_coroutine_test( + "Test modular lock profile change with user, pin. schedule, and unlatch supported when BatChargeLevel and BatPercentRemaining attributes are available", + function() + test.socket.matter:__queue_receive( + { + mock_device_user_pin_schedule_unlatch.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_user_pin_schedule_unlatch, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(12), -- BatPercentRemaining + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + mock_device_user_pin_schedule_unlatch:expect_metadata_update({ profile = "lock-modular-embedded-unlatch", optional_component_capabilities = {{"main", {"lockUsers", "lockCredentials", "lockSchedules", "battery"}}} }) + + end, + { test_init = test_init_user_pin_schedule_unlatch } +) + +test.register_coroutine_test( + "Test modular lock profile update (modular to modular) with user, pin. schedule, and unlatch supported. Ensure infoChanged updates subscription", + function() + test.socket.matter:__queue_receive( + { + mock_device_modular.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_modular, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(12), -- BatPercentRemaining + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + test.socket.capability:__expect_send( + mock_device_modular:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device_modular:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + mock_device_modular:expect_metadata_update({ profile = "lock-modular-embedded-unlatch", optional_component_capabilities = {{"main", {"lockUsers", "lockCredentials", "lockSchedules", "battery"}}} }) + + local updated_device_profile = t_utils.get_profile_definition("lock-modular-embedded-unlatch.yml", + {enabled_optional_capabilities = {{ "main", {"lockUsers", "lockCredentials", "lockSchedules", "battery"}}, + },} + ) + updated_device_profile.id = "00000000-1111-2222-3333-000000000010" + + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_device_modular:generate_info_changed({ profile = updated_device_profile })) + + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_modular) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.attributes.NumberOfPINUsersSupported:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.attributes.MaxPINCodeLength:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.attributes.MinPINCodeLength:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.attributes.RequirePINforRemoteOperation:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device_modular)) + subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device_modular)) + test.socket.matter:__expect_send({mock_device_modular.id, subscribe_request}) + test.socket.capability:__expect_send( + mock_device_modular:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.capability:__expect_send( + mock_device_modular:generate_test_message("main", capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) + ) + end, + { test_init = test_init_modular } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_unlatch.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_unlatch.lua index 2e390f4d34..642ca3bf7a 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_unlatch.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_unlatch.lua @@ -1,20 +1,10 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" +test.set_rpc_version(0) local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" local DoorLock = clusters.DoorLock @@ -54,28 +44,32 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + -- subscribe_request local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device)) subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) - test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + -- add test device, handle initial subscribe test.mock_device.add_test_device(mock_device) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockAlarm.alarm("clear", {state_change = true})) + ) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + mock_device:expect_metadata_update({ profile = "lock-unlatch" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) -test.register_coroutine_test( - "Assert profile applied over doConfigure", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "lock-unlatch" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - end -) - test.register_coroutine_test( "Handle received OperatingMode(Normal, Vacation) from Matter device.", function() diff --git a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua index 9659c9484d..738248fd8e 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua @@ -1,20 +1,10 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" +test.set_rpc_version(0) local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" local DoorLock = clusters.DoorLock @@ -55,6 +45,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + -- subscribe request local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device)) subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device)) @@ -67,24 +59,26 @@ local function test_init() subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device)) - test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + -- add test device, handle initial subscribe test.mock_device.add_test_device(mock_device) + test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + mock_device:expect_metadata_update({ profile = "lock-user-pin" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) -test.register_coroutine_test( - "Assert profile applied over doConfigure", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "lock-user-pin" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - end -) - test.register_coroutine_test( "Handle received OperatingMode(Normal, Vacation) from Matter device.", function() @@ -966,7 +960,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockUsers.users({{userIndex = 1, userType = "adminMember"}}, {visibility={displayed=false}}) + capabilities.lockUsers.users({{userIndex = 1, userName="Guest1", userType = "adminMember"}}, {visibility={displayed=false}}) ) ) test.socket.capability:__expect_send( @@ -1488,7 +1482,6 @@ test.register_coroutine_test( ), } ) - -- test.wait_for_events() end ) @@ -1740,11 +1733,29 @@ test.register_coroutine_test( capabilities.lockCredentials.credentials({}, {visibility={displayed=false}}) ) ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.weekDaySchedules({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.yearDaySchedules({}, {visibility={displayed=false}}) + ) + ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.lockCredentials.commandResult( - {commandName="deleteAllCredentials", credentialIndex=65534, statusCode="success"}, + {commandName="deleteAllCredentials", userIndex=65534, credentialIndex=65534, statusCode="success"}, {state_change=true, visibility={displayed=false}} ) ) diff --git a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua index 603f056c06..03a51a3150 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua @@ -1,20 +1,10 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" +test.set_rpc_version(0) local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local clusters = require "st.matter.clusters" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" @@ -174,24 +164,52 @@ local mock_device_user_pin_schedule_unlatch = test.mock_device.build_test_matter }) local function test_init() + test.disable_startup_messages() + -- subscribe request local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device)) subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) - test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + -- add test device, handle initial subscribe test.mock_device.add_test_device(mock_device) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_unlatch() + test.disable_startup_messages() + -- subscribe request local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_unlatch) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_unlatch)) subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_unlatch)) subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_unlatch)) - test.socket["matter"]:__expect_send({mock_device_unlatch.id, subscribe_request}) + -- add test device, handle initial subscribe test.mock_device.add_test_device(mock_device_unlatch) + test.socket.matter:__expect_send({mock_device_unlatch.id, subscribe_request}) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "added" }) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.matter:__expect_send({mock_device_unlatch.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "init" }) + test.socket.matter:__expect_send({mock_device_unlatch.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) + mock_device_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_user_pin() + test.disable_startup_messages() + -- subscribe request local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_user_pin) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_user_pin)) subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device_user_pin)) @@ -202,11 +220,24 @@ local function test_init_user_pin() subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_user_pin)) subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_user_pin)) subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device_user_pin)) - test.socket["matter"]:__expect_send({mock_device_user_pin.id, subscribe_request}) + -- add test device, handle initial subscribe test.mock_device.add_test_device(mock_device_user_pin) + test.socket.matter:__expect_send({mock_device_user_pin.id, subscribe_request}) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "added" }) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.matter:__expect_send({mock_device_user_pin.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "init" }) + test.socket.matter:__expect_send({mock_device_user_pin.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) + mock_device_user_pin:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_user_pin_schedule_unlatch() + test.disable_startup_messages() + -- subscribe request local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_user_pin_schedule_unlatch) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_user_pin_schedule_unlatch)) subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device_user_pin_schedule_unlatch)) @@ -219,8 +250,19 @@ local function test_init_user_pin_schedule_unlatch() subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_user_pin_schedule_unlatch)) subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_user_pin_schedule_unlatch)) subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device_user_pin_schedule_unlatch)) - test.socket["matter"]:__expect_send({mock_device_user_pin_schedule_unlatch.id, subscribe_request}) + -- add test device, handle initial subscribe test.mock_device.add_test_device(mock_device_user_pin_schedule_unlatch) + test.socket.matter:__expect_send({mock_device_user_pin_schedule_unlatch.id, subscribe_request}) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "added" }) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.matter:__expect_send({mock_device_user_pin_schedule_unlatch.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "init" }) + test.socket.matter:__expect_send({mock_device_user_pin_schedule_unlatch.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) + mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) @@ -228,18 +270,6 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Test lock profile change when attributes related to BAT feature is not available.", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device.id, @@ -257,6 +287,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) mock_device:expect_metadata_update({ profile = "lock" }) end ) @@ -264,18 +297,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock profile change when BatChargeLevel attribute is available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device.id, @@ -294,6 +315,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) mock_device:expect_metadata_update({ profile = "lock-batteryLevel" }) end ) @@ -301,18 +325,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock profile change when BatChargeLevel and BatPercentRemaining attributes are available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device.id, @@ -332,6 +344,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) mock_device:expect_metadata_update({ profile = "lock-battery" }) end ) @@ -339,18 +354,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-unlatch profile change when attributes related to BAT feature is not available.", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) - mock_device_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_unlatch.id, @@ -368,6 +371,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) mock_device_unlatch:expect_metadata_update({ profile = "lock-unlatch" }) end, { test_init = test_init_unlatch } @@ -376,18 +382,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-unlatch profile change when BatChargeLevel attribute is available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) - mock_device_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_unlatch.id, @@ -406,6 +400,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) mock_device_unlatch:expect_metadata_update({ profile = "lock-unlatch-batteryLevel" }) end, { test_init = test_init_unlatch } @@ -414,18 +411,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-unlatch profile change when BatChargeLevel and BatPercentRemaining attributes are available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) - mock_device_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_unlatch.id, @@ -445,6 +430,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) mock_device_unlatch:expect_metadata_update({ profile = "lock-unlatch-battery" }) end, { test_init = test_init_unlatch } @@ -453,18 +441,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin profile change when attributes related to BAT feature is not available.", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) - mock_device_user_pin:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin.id, @@ -482,6 +458,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) mock_device_user_pin:expect_metadata_update({ profile = "lock-user-pin" }) end, { test_init = test_init_user_pin } @@ -490,18 +469,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin profile change when BatChargeLevel attribute is available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) - mock_device_user_pin:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin.id, @@ -520,6 +487,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) mock_device_user_pin:expect_metadata_update({ profile = "lock-user-pin-batteryLevel" }) end, { test_init = test_init_user_pin } @@ -528,18 +498,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin profile change when BatChargeLevel and BatPercentRemaining attributes are available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) - mock_device_user_pin:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin.id, @@ -559,6 +517,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) mock_device_user_pin:expect_metadata_update({ profile = "lock-user-pin-battery" }) end, { test_init = test_init_user_pin } @@ -567,18 +528,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin-schedule-unlatch profile change when attributes related to BAT feature is not available.", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) - mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin_schedule_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin_schedule_unlatch.id, @@ -596,6 +545,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) mock_device_user_pin_schedule_unlatch:expect_metadata_update({ profile = "lock-user-pin-schedule-unlatch" }) end, { test_init = test_init_user_pin_schedule_unlatch } @@ -604,18 +556,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin-schedule-unlatch profile change when BatChargeLevel attribute is available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) - mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin_schedule_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin_schedule_unlatch.id, @@ -634,6 +574,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) mock_device_user_pin_schedule_unlatch:expect_metadata_update({ profile = "lock-user-pin-schedule-unlatch-batteryLevel" }) end, { test_init = test_init_user_pin_schedule_unlatch } @@ -642,18 +585,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin-schedule-unlatch profile change when BatChargeLevel and BatPercentRemaining attributes are available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) - mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin_schedule_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin_schedule_unlatch.id, @@ -673,6 +604,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) mock_device_user_pin_schedule_unlatch:expect_metadata_update({ profile = "lock-user-pin-schedule-unlatch-battery" }) end, { test_init = test_init_user_pin_schedule_unlatch } diff --git a/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua b/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua index 5444652b29..cf5bc44648 100644 --- a/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua +++ b/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua @@ -15,7 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -49,286 +48,283 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) - local function test_init() - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel - } - test.socket.matter:__set_channel_ordering("relaxed") - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.disable_startup_messages() test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device) + subscribe_request:merge(clusters.LevelControl.attributes.CurrentLevel:subscribe(mock_device)) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) test.register_message_test( - "Mute and unmute commands should send the appropriate commands", + "Mute and unmute commands should send the appropriate commands", + { { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioMute", component = "main", command = "mute", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.Off(mock_device, 10) - } - }, - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioMute", component = "main", command = "unmute", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, 10) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 10, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioMute.mute.unmuted()) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 10, false) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioMute.mute.muted()) - } + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioMute", component = "main", command = "mute", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.Off(mock_device, 10) } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioMute", component = "main", command = "unmute", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, 10) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 10, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioMute.mute.unmuted()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 10, false) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioMute.mute.muted()) + } + } ) test.register_message_test( - "Set mute command should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioMute", component = "main", command = "setMute", args = { "muted" } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.Off(mock_device, 10) - } - }, - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioMute", component = "main", command = "setMute", args = { "unmuted" } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, 10) - } - } + "Set mute command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioMute", component = "main", command = "setMute", args = { "muted" } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.Off(mock_device, 10) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioMute", component = "main", command = "setMute", args = { "unmuted" } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, 10) + } } + } ) test.register_message_test( - "Set volume command should send the appropriate commands", + "Set volume command should send the appropriate commands", + { { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioVolume", component = "main", command = "setVolume", args = { 20 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) - } + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioVolume", component = "main", command = "setVolume", args = { 20 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) } + } ) test.register_message_test( - "Volume up/down command should send the appropriate commands", + "Volume up/down command should send the appropriate commands", + { { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioVolume", component = "main", command = "setVolume", args = { 20 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50 ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) - }, - -- volume up - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioVolume", component = "main", command = "volumeUp", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(25/100.0 * 254), 0, 0, 0) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 63 ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(25)) - }, - -- volume down - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioVolume", component = "main", command = "volumeDown", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50 ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) - }, - } + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioVolume", component = "main", command = "setVolume", args = { 20 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50 ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) + }, + -- volume up + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioVolume", component = "main", command = "volumeUp", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(25/100.0 * 254), 0, 0, 0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 63 ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(25)) + }, + -- volume down + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioVolume", component = "main", command = "volumeDown", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50 ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) + }, + } ) local function refresh_commands(dev) @@ -338,25 +334,24 @@ local function refresh_commands(dev) end test.register_message_test( - "Handle received refresh.", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "refresh", component = "main", command = "refresh", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - refresh_commands(mock_device) - } - }, - } + "Handle received refresh.", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + refresh_commands(mock_device) + } + }, + } ) - test.run_registered_tests() diff --git a/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua b/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua index 52cde71ed3..ebae93ca53 100644 --- a/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua +++ b/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua @@ -15,7 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -84,34 +83,88 @@ local mock_device_variable_speed = test.mock_device.build_test_matter_device({ } }) +local supported_key_codes = { + "UP", + "DOWN", + "LEFT", + "RIGHT", + "SELECT", + "BACK", + "EXIT", + "MENU", + "SETTINGS", + "HOME", + "NUMBER0", + "NUMBER1", + "NUMBER2", + "NUMBER3", + "NUMBER4", + "NUMBER5", + "NUMBER6", + "NUMBER7", + "NUMBER8", + "NUMBER9" +} local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.MediaPlayback.attributes.CurrentState } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - print(i) - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - print(subscribe_request) - end + subscribe_request:merge(cluster_subscribe_list[2]:subscribe(mock_device)) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.mediaPlayback.supportedPlaybackCommands({"play", "pause", "stop"}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.mediaTrackControl.supportedTrackControlCommands({"previousTrack", "nextTrack"}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.keypadInput.supportedKeyCodes(supported_key_codes) + ) + ) + + test.mock_device.add_test_device(mock_device_variable_speed) + test.socket.device_lifecycle:__queue_receive({ mock_device_variable_speed.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_variable_speed.id, "init" }) subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_variable_speed) - for i, cluster in ipairs(cluster_subscribe_list) do - print(i) - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_variable_speed)) - end - print(subscribe_request) - end + subscribe_request:merge(cluster_subscribe_list[2]:subscribe(mock_device_variable_speed)) test.socket.matter:__expect_send({mock_device_variable_speed.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_variable_speed) + + test.socket.device_lifecycle:__queue_receive({ mock_device_variable_speed.id, "doConfigure" }) + mock_device_variable_speed:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + test.socket.capability:__expect_send( + mock_device_variable_speed:generate_test_message( + "main", capabilities.mediaPlayback.supportedPlaybackCommands({"play", "pause", "stop", "rewind", "fastForward"}) + ) + ) + test.socket.capability:__expect_send( + mock_device_variable_speed:generate_test_message( + "main", capabilities.mediaTrackControl.supportedTrackControlCommands({"previousTrack", "nextTrack"}) + ) + ) + test.socket.capability:__expect_send( + mock_device_variable_speed:generate_test_message( + "main", capabilities.keypadInput.supportedKeyCodes(supported_key_codes) + ) + ) end test.set_test_init_function(test_init) @@ -557,28 +610,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.keypadInput.supportedKeyCodes({ - "UP", - "DOWN", - "LEFT", - "RIGHT", - "SELECT", - "BACK", - "EXIT", - "MENU", - "SETTINGS", - "HOME", - "NUMBER0", - "NUMBER1", - "NUMBER2", - "NUMBER3", - "NUMBER4", - "NUMBER5", - "NUMBER6", - "NUMBER7", - "NUMBER8", - "NUMBER9", - }) + capabilities.keypadInput.supportedKeyCodes(supported_key_codes) ) ) @@ -608,28 +640,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device_variable_speed:generate_test_message( "main", - capabilities.keypadInput.supportedKeyCodes({ - "UP", - "DOWN", - "LEFT", - "RIGHT", - "SELECT", - "BACK", - "EXIT", - "MENU", - "SETTINGS", - "HOME", - "NUMBER0", - "NUMBER1", - "NUMBER2", - "NUMBER3", - "NUMBER4", - "NUMBER5", - "NUMBER6", - "NUMBER7", - "NUMBER8", - "NUMBER9", - }) + capabilities.keypadInput.supportedKeyCodes(supported_key_codes) ) ) diff --git a/drivers/SmartThings/matter-rvc/capabilities/robotCleanerOperatingState.yml b/drivers/SmartThings/matter-rvc/capabilities/robotCleanerOperatingState.yml new file mode 100644 index 0000000000..d6cea7f3f2 --- /dev/null +++ b/drivers/SmartThings/matter-rvc/capabilities/robotCleanerOperatingState.yml @@ -0,0 +1,108 @@ +name: Robot Cleaner Operating State +status: live +attributes: + operatingState: + schema: + type: object + additionalProperties: false + properties: + value: + type: string + enum: + - stopped + - running + - paused + - seekingCharger + - charging + - docked + - unableToStartOrResume + - unableToCompleteOperation + - commandInvalidInState + - failedToFindChargingDock + - stuck + - dustBinMissing + - dustBinFull + - waterTankEmpty + - waterTankMissing + - waterTankLidOpen + - mopCleaningPadMissing + required: + - value + enumCommands: + - command: start + value: running + - command: pause + value: paused + - command: goHome + value: seekingCharger + actedOnBy: + - start + - pause + - goHome + supportedOperatingStates: + schema: + type: object + additionalProperties: false + properties: + value: + type: array + items: + type: string + enum: + - stopped + - running + - paused + - seekingCharger + - charging + - docked + - unableToStartOrResume + - unableToCompleteOperation + - commandInvalidInState + - failedToFindChargingDock + - stuck + - dustBinMissing + - dustBinFull + - waterTankEmpty + - waterTankMissing + - waterTankLidOpen + - mopCleaningPadMissing + required: + - value + supportedCommands: + schema: + type: object + additionalProperties: false + properties: + value: + type: array + items: + type: string + enum: + - start + - pause + - goHome + supportedOperatingStateCommands: + schema: + type: object + additionalProperties: false + properties: + value: + type: array + items: + type: string + enum: + - start + - pause + - goHome +commands: + start: + arguments: [] + name: start + pause: + arguments: [] + name: pause + goHome: + arguments: [] + name: goHome +id: robotCleanerOperatingState +version: 1 diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml index 15c766fbdb..c15783f6d2 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml @@ -5,6 +5,8 @@ components: capabilities: - id: robotCleanerOperatingState version: 1 + - id: mode + version: 1 - id: serviceArea version: 1 - id: refresh @@ -13,119 +15,6 @@ components: version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner - - id: cleanMode - label: Clean mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: main - capability: serviceArea - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list \ No newline at end of file +metadata: + mnmn: SmartThingsEdge + vid: generic-rvc diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml index a83c7801a0..33af113030 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml @@ -5,122 +5,14 @@ components: capabilities: - id: robotCleanerOperatingState version: 1 + - id: mode + version: 1 - id: firmwareUpdate version: 1 - id: refresh version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner - - id: cleanMode - label: Clean mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list +metadata: + mnmn: SmartThingsEdge + vid: generic-rvc diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml index 8f5ca7bc21..560eda12fa 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml @@ -13,75 +13,3 @@ components: version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: main - capability: serviceArea - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list \ No newline at end of file diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc.yml b/drivers/SmartThings/matter-rvc/profiles/rvc.yml index 7246590f4e..84bed73495 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc.yml @@ -11,72 +11,3 @@ components: version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list diff --git a/drivers/SmartThings/matter-rvc/src/RvcOperationalState/init.lua b/drivers/SmartThings/matter-rvc/src/RvcOperationalState/init.lua index b58f105694..3be13eaef6 100644 --- a/drivers/SmartThings/matter-rvc/src/RvcOperationalState/init.lua +++ b/drivers/SmartThings/matter-rvc/src/RvcOperationalState/init.lua @@ -39,6 +39,7 @@ function RvcOperationalState:get_server_command_by_id(command_id) [0x0001] = "Stop", [0x0002] = "Start", [0x0003] = "Resume", + [0x0080] = "GoHome", } if server_id_map[command_id] ~= nil then return self.server.commands[server_id_map[command_id]] @@ -74,6 +75,7 @@ RvcOperationalState.command_direction_map = { ["Stop"] = "server", ["Start"] = "server", ["Resume"] = "server", + ["GoHome"] = "server", ["OperationalCommandResponse"] = "client", } diff --git a/drivers/SmartThings/matter-rvc/src/RvcOperationalState/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-rvc/src/RvcOperationalState/server/attributes/AcceptedCommandList.lua new file mode 100644 index 0000000000..dd92f54543 --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/RvcOperationalState/server/attributes/AcceptedCommandList.lua @@ -0,0 +1,75 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AcceptedCommandList = { + ID = 0xFFF9, + NAME = "AcceptedCommandList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +function AcceptedCommandList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, AcceptedCommandList.element_type) + end +end + +function AcceptedCommandList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function AcceptedCommandList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AcceptedCommandList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AcceptedCommandList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) +return AcceptedCommandList + diff --git a/drivers/SmartThings/matter-rvc/src/RvcOperationalState/server/commands/GoHome.lua b/drivers/SmartThings/matter-rvc/src/RvcOperationalState/server/commands/GoHome.lua new file mode 100644 index 0000000000..4328914a1a --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/RvcOperationalState/server/commands/GoHome.lua @@ -0,0 +1,79 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local GoHome = {} + +GoHome.NAME = "GoHome" +GoHome.ID = 0x0080 +GoHome.field_defs = { +} + +function GoHome:init(device, endpoint_id) + local out = {} + local args = {} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = GoHome, + __tostring = GoHome.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function GoHome:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function GoHome:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + base_type_obj.elements = elems +end + +function GoHome:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(GoHome, {__call = GoHome.init}) + +return GoHome diff --git a/drivers/SmartThings/matter-rvc/src/init.lua b/drivers/SmartThings/matter-rvc/src/init.lua index c8fae5aaad..1e52f321ee 100644 --- a/drivers/SmartThings/matter-rvc/src/init.lua +++ b/drivers/SmartThings/matter-rvc/src/init.lua @@ -16,9 +16,6 @@ local MatterDriver = require "st.matter.driver" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local area_type = require "Global.types.AreaTypeTag" -local landmark = require "Global.types.LandmarkTag" - local embedded_cluster_utils = require "embedded_cluster_utils" -- Include driver-side definitions when lua libs api version is < 10 @@ -35,10 +32,23 @@ if version.api < 13 then clusters.Global = require "Global" end -local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" local RUN_MODE_SUPPORTED_MODES = "__run_mode_supported_modes" +local CURRENT_RUN_MODE = "__current_run_mode" local CLEAN_MODE_SUPPORTED_MODES = "__clean_mode_supported_modes" -local OPERATING_STATE_SUPPORTED_COMMANDS = "__operating_state_supported_commands" +local SERVICE_AREA_PROFILED = "__SERVICE_AREA_PROFILED" + +local clus_op_enum = clusters.OperationalState.types.OperationalStateEnum +local clus_rvc_op_enum = clusters.RvcOperationalState.types.OperationalStateEnum +local cap_op_enum = capabilities.robotCleanerOperatingState.operatingState +local cap_op_cmds = capabilities.robotCleanerOperatingState.commands +local OPERATING_STATE_MAP = { + [clus_op_enum.STOPPED] = cap_op_enum.stopped, + [clus_op_enum.RUNNING] = cap_op_enum.running, + [clus_op_enum.PAUSED] = cap_op_enum.paused, + [clus_rvc_op_enum.SEEKING_CHARGER] = cap_op_enum.seekingCharger, + [clus_rvc_op_enum.CHARGING] = cap_op_enum.charging, + [clus_rvc_op_enum.DOCKED] = cap_op_enum.docked +} local subscribed_attributes = { [capabilities.mode.ID] = { @@ -48,6 +58,7 @@ local subscribed_attributes = { clusters.RvcCleanMode.attributes.CurrentMode }, [capabilities.robotCleanerOperatingState.ID] = { + clusters.RvcOperationalState.attributes.OperationalStateList, clusters.RvcOperationalState.attributes.OperationalState, clusters.RvcOperationalState.attributes.OperationalError }, @@ -57,32 +68,19 @@ local subscribed_attributes = { } } -local function component_to_endpoint(device, component) - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} - if map[component] then - return map[component] - else - return device.MATTER_DEFAULT_ENDPOINT +local function find_default_endpoint(device, cluster_id) + local eps = device:get_endpoints(cluster_id) + table.sort(eps) + for _, v in ipairs(eps) do + if v ~= 0 then --0 is the matter RootNode endpoint + return v + end end + device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) + return device.MATTER_DEFAULT_ENDPOINT end -local function device_added(driver, device) - local run_mode_eps = device:get_endpoints(clusters.RvcRunMode.ID) or {} - local clean_mode_eps = device:get_endpoints(clusters.RvcCleanMode.ID) or {} - local component_to_endpoint_map = { - ["main"] = run_mode_eps[1], - ["runMode"] = run_mode_eps[1], - ["cleanMode"] = clean_mode_eps[1] - } - device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, {persist = true}) -end - -local function device_init(driver, device) - device:subscribe() - device:set_component_to_endpoint_fn(component_to_endpoint) -end - -local function do_configure(driver, device) +local function match_profile(driver, device) local clean_mode_eps = device:get_endpoints(clusters.RvcCleanMode.ID) or {} local service_area_eps = embedded_cluster_utils.get_endpoints(device, clusters.ServiceArea.ID) or {} @@ -96,6 +94,31 @@ local function do_configure(driver, device) device.log.info_with({hub_logs = true}, string.format("Updating device profile to %s.", profile_name)) device:try_update_metadata({profile = profile_name}) +end + +local function device_init(driver, device) + device:subscribe() + + -- comp/ep map functionality removed 9/5/25. + device:set_field("__component_to_endpoint_map", nil) + + if not device:get_field(SERVICE_AREA_PROFILED) then + if #device:get_endpoints(clusters.ServiceArea.ID) > 0 then + match_profile(driver, device) + end + device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) + end +end + +local function do_configure(driver, device) + match_profile(driver, device) + device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) + device:send(clusters.RvcOperationalState.attributes.AcceptedCommandList:read()) +end + +local function driver_switched(driver, device) + match_profile(driver, device) + device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) device:send(clusters.RvcOperationalState.attributes.AcceptedCommandList:read()) end @@ -107,7 +130,11 @@ end -- Helper functions -- local function supports_rvc_operational_state(device, command_name) - local supported_op_commands = device:get_field(OPERATING_STATE_SUPPORTED_COMMANDS) or {} + local supported_op_commands = device:get_latest_state( + "main", + capabilities.robotCleanerOperatingState.ID, + capabilities.robotCleanerOperatingState.supportedCommands.NAME + ) or {} for _, cmd in ipairs(supported_op_commands) do if cmd == command_name then return true @@ -123,8 +150,6 @@ local function can_send_state_command(device, command_name, current_state, curre end local set_mode = capabilities.mode.commands.setMode.NAME - local cap_op_cmds = capabilities.robotCleanerOperatingState.commands - local cap_op_enum = capabilities.robotCleanerOperatingState.operatingState if command_name ~= set_mode and supports_rvc_operational_state(device, command_name) == false then return false end @@ -162,7 +187,7 @@ local function can_send_state_command(device, command_name, current_state, curre return false end -local function update_supported_arguments(device, current_run_mode, current_state) +local function update_supported_arguments(device, ep, current_run_mode, current_state) device.log.info(string.format("update_supported_arguments: %s, %s", current_run_mode, current_state)) if current_run_mode == nil or current_state == nil then return @@ -173,15 +198,7 @@ local function update_supported_arguments(device, current_run_mode, current_stat local event = capabilities.robotCleanerOperatingState.supportedOperatingStateCommands( {}, {visibility = {displayed = false}} ) - device:emit_component_event(device.profile.components["main"], event) - -- Set runMode to empty - event = capabilities.mode.supportedArguments({}, {visibility = {displayed = false}}) - device:emit_component_event(device.profile.components["runMode"], event) - -- Set cleanMode to empty - local component = device.profile.components["cleanMode"] - if component ~= nil then - device:emit_component_event(component, event) - end + device:emit_event_for_endpoint(ep, event) return end @@ -200,10 +217,7 @@ local function update_supported_arguments(device, current_run_mode, current_stat end -- Set Supported Operating State Commands - local cap_op_cmds = capabilities.robotCleanerOperatingState.commands - local cap_op_enum = capabilities.robotCleanerOperatingState.operatingState local supported_op_commands = {} - if can_send_state_command(device, cap_op_cmds.goHome.NAME, current_state, nil) == true then table.insert(supported_op_commands, cap_op_cmds.goHome.NAME) end @@ -216,42 +230,7 @@ local function update_supported_arguments(device, current_run_mode, current_stat local event = capabilities.robotCleanerOperatingState.supportedOperatingStateCommands( supported_op_commands, {visibility = {displayed = false}} ) - device:emit_component_event(device.profile.components["main"], event) - - -- Check whether non-idle mode can be selected or not - local can_be_non_idle = false - if current_tag == clusters.RvcRunMode.types.ModeTag.IDLE and - (current_state == cap_op_enum.stopped.NAME or current_state == cap_op_enum.paused.NAME or - current_state == cap_op_enum.docked.NAME or current_state == cap_op_enum.charging.NAME) then - can_be_non_idle = true - end - - -- Set supported run arguments - local supported_arguments = {} -- For generic plugin - for _, mode in ipairs(supported_run_modes) do - if mode.tag == clusters.RvcRunMode.types.ModeTag.IDLE or can_be_non_idle == true then - table.insert(supported_arguments, mode.label) - end - end - - -- Send event to set supported run arguments - local component = device.profile.components["runMode"] - local event = capabilities.mode.supportedArguments(supported_arguments, {visibility = {displayed = false}}) - device:emit_component_event(component, event) - - -- Set supported clean arguments - local supported_clean_modes = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} - supported_arguments = {} - for _, mode in ipairs(supported_clean_modes) do - table.insert(supported_arguments, mode.label) - end - - -- Send event to set supported clean modes - local component = device.profile.components["cleanMode"] - if component ~= nil then - local event = capabilities.mode.supportedArguments(supported_arguments, {visibility = {displayed = false}}) - device:emit_component_event(component, event) - end + device:emit_event_for_endpoint(ep, event) end -- Matter Handlers -- @@ -282,23 +261,14 @@ local function run_mode_supported_mode_handler(driver, device, ib, response) end device:set_field(RUN_MODE_SUPPORTED_MODES, supported_modes_id_tag, { persist = true }) - -- Update Supported Modes - local component = device.profile.components["runMode"] - local event = capabilities.mode.supportedModes(supported_modes, {visibility = {displayed = false}}) - device:emit_component_event(component, event) - -- Update Supported Arguments - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) local current_state = device:get_latest_state( "main", capabilities.robotCleanerOperatingState.ID, capabilities.robotCleanerOperatingState.operatingState.NAME ) - update_supported_arguments(device, current_run_mode, current_state) + update_supported_arguments(device, ib.endpoint_id, current_run_mode, current_state) end local function run_mode_current_mode_handler(driver, device, ib, response) @@ -318,8 +288,7 @@ local function run_mode_current_mode_handler(driver, device, ib, response) end -- Set current mode - local component = device.profile.components["runMode"] - device:emit_component_event(component, capabilities.mode.mode(current_run_mode)) + device:set_field(CURRENT_RUN_MODE, current_run_mode, { persist = true }) -- Update supported mode local current_state = device:get_latest_state( @@ -327,11 +296,10 @@ local function run_mode_current_mode_handler(driver, device, ib, response) capabilities.robotCleanerOperatingState.ID, capabilities.robotCleanerOperatingState.operatingState.NAME ) - update_supported_arguments(device, current_run_mode, current_state) + update_supported_arguments(device, ib.endpoint_id, current_run_mode, current_state) end local function clean_mode_supported_mode_handler(driver, device, ib, response) - device.log.info("clean_mode_supported_mode_handler") local supported_modes = {} local supported_modes_id = {} for _, mode in ipairs(ib.data.elements) do @@ -343,11 +311,10 @@ local function clean_mode_supported_mode_handler(driver, device, ib, response) end device:set_field(CLEAN_MODE_SUPPORTED_MODES, supported_modes_id, { persist = true }) - local component = device.profile.components["cleanMode"] local event = capabilities.mode.supportedModes(supported_modes, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) event = capabilities.mode.supportedArguments(supported_modes, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end local function clean_mode_current_mode_handler(driver, device, ib, response) @@ -356,8 +323,7 @@ local function clean_mode_current_mode_handler(driver, device, ib, response) local supported_clean_mode = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} for _, mode in ipairs(supported_clean_mode) do if mode.id == mode_id then - local component = device.profile.components["cleanMode"] - device:emit_component_event(component, capabilities.mode.mode(mode.label)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.mode.mode(mode.label)) break end end @@ -365,31 +331,16 @@ end local function rvc_operational_state_attr_handler(driver, device, ib, response) device.log.info(string.format("rvc_operational_state_attr_handler operationalState: %s", ib.data.value)) - local clus_op_enum = clusters.OperationalState.types.OperationalStateEnum - local clus_rvc_op_enum = clusters.RvcOperationalState.types.OperationalStateEnum - local cap_op_enum = capabilities.robotCleanerOperatingState.operatingState - local OPERATING_STATE_MAP = { - [clus_op_enum.STOPPED] = cap_op_enum.stopped, - [clus_op_enum.RUNNING] = cap_op_enum.running, - [clus_op_enum.PAUSED] = cap_op_enum.paused, - [clus_rvc_op_enum.SEEKING_CHARGER] = cap_op_enum.seekingCharger, - [clus_rvc_op_enum.CHARGING] = cap_op_enum.charging, - [clus_rvc_op_enum.DOCKED] = cap_op_enum.docked - } if ib.data.value ~= clus_op_enum.ERROR then device:emit_event_for_endpoint(ib.endpoint_id, OPERATING_STATE_MAP[ib.data.value]()) end -- Supported Mode update - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) if ib.data.value ~= clus_op_enum.ERROR then - update_supported_arguments(device, current_run_mode, OPERATING_STATE_MAP[ib.data.value].NAME) + update_supported_arguments(device, ib.endpoint_id, current_run_mode, OPERATING_STATE_MAP[ib.data.value].NAME) else - update_supported_arguments(device, current_run_mode, "Error") + update_supported_arguments(device, ib.endpoint_id, current_run_mode, "Error") end end @@ -426,9 +377,21 @@ local function rvc_operational_error_attr_handler(driver, device, ib, response) end end +local function rvc_operational_state_list_attr_handler(driver, device, ib, response) + local supportedOperatingState = {} + for _, state in ipairs(ib.data.elements) do + clusters.RvcOperationalState.types.OperationalStateStruct:augment_type(state) + if OPERATING_STATE_MAP[state.elements.operational_state_id.value] ~= nil then + table.insert(supportedOperatingState, OPERATING_STATE_MAP[state.elements.operational_state_id.value].NAME) + end + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.robotCleanerOperatingState.supportedOperatingStates( + supportedOperatingState, {visibility = {displayed = false}} + )) +end + local function handle_rvc_operational_state_accepted_command_list(driver, device, ib, response) device.log.info("handle_rvc_operational_state_accepted_command_list") - local cap_op_cmds = capabilities.robotCleanerOperatingState.commands local OP_COMMAND_MAP = { [clusters.RvcOperationalState.commands.Pause.ID] = cap_op_cmds.pause, [clusters.RvcOperationalState.commands.Resume.ID] = cap_op_cmds.start, @@ -438,14 +401,12 @@ local function handle_rvc_operational_state_accepted_command_list(driver, device for _, attr in ipairs(ib.data.elements) do table.insert(supportedOperatingStateCommands, OP_COMMAND_MAP[attr.value].NAME) end - device:set_field(OPERATING_STATE_SUPPORTED_COMMANDS, supportedOperatingStateCommands, { persist = true }) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.robotCleanerOperatingState.supportedCommands( + supportedOperatingStateCommands, {visibility = {displayed = false}} + )) -- Get current run mode, current tag, current operating state - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) local current_tag = 0xFFFF local supported_run_modes = device:get_field(RUN_MODE_SUPPORTED_MODES) or {} for _, mode in ipairs(supported_run_modes) do @@ -459,7 +420,6 @@ local function handle_rvc_operational_state_accepted_command_list(driver, device capabilities.robotCleanerOperatingState.ID, capabilities.robotCleanerOperatingState.operatingState.NAME ) - local cap_op_enum = capabilities.robotCleanerOperatingState.operatingState if current_state ~= cap_op_enum.stopped.NAME and current_state ~= cap_op_enum.running.NAME and current_state ~= cap_op_enum.paused.NAME and current_state ~= cap_op_enum.seekingCharger.NAME and current_state ~= cap_op_enum.charging.NAME and current_state ~= cap_op_enum.docked.NAME then @@ -467,7 +427,6 @@ local function handle_rvc_operational_state_accepted_command_list(driver, device end -- Set Supported Operating State Commands - local cap_op_cmds = capabilities.robotCleanerOperatingState.commands local supported_op_commands = {} if can_send_state_command(device, cap_op_cmds.goHome.NAME, current_state, current_tag) == true then table.insert(supported_op_commands, cap_op_cmds.goHome.NAME) @@ -481,7 +440,7 @@ local function handle_rvc_operational_state_accepted_command_list(driver, device local event = capabilities.robotCleanerOperatingState.supportedOperatingStateCommands( supported_op_commands, {visibility = {displayed = false}} ) - device:emit_component_event(device.profile.components["main"], event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end local function upper_to_camelcase(name) @@ -512,23 +471,22 @@ local function rvc_service_area_supported_areas_handler(driver, device, ib, resp if location_info.location_name.value ~= "" then area_name = location_info.location_name.value elseif location_info.floor_number.value ~= nil and location_info.area_type.value ~= nil then - area_name = location_info.floor_number.value .. "F " .. upper_to_camelcase(string.gsub(area_type.pretty_print(location_info.area_type),"AreaTypeTag: ","")) + area_name = location_info.floor_number.value .. "F " .. upper_to_camelcase(string.gsub(clusters.Global.types.AreaTypeTag.pretty_print(location_info.area_type),"AreaTypeTag: ","")) elseif location_info.floor_number.value ~= nil then area_name = location_info.floor_number.value .. "F" elseif location_info.area_type.value ~= nil then - area_name = upper_to_camelcase(string.gsub(area_type.pretty_print(location_info.area_type),"AreaTypeTag: ","")) + area_name = upper_to_camelcase(string.gsub(clusters.Global.types.AreaTypeTag.pretty_print(location_info.area_type),"AreaTypeTag: ","")) end end if area_name == "" then - area_name = upper_to_camelcase(string.gsub(landmark.pretty_print(landmark_info.landmark_tag),"LandmarkTag: ","")) + area_name = upper_to_camelcase(string.gsub(clusters.Global.types.LandmarkTag.pretty_print(landmark_info.landmark_tag),"LandmarkTag: ","")) end table.insert(supported_areas, {["areaId"] = area_id, ["areaName"] = area_name}) end -- Update Supported Areas - local component = device.profile.components["main"] local event = capabilities.serviceArea.supportedAreas(supported_areas, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end -- In case selected area is not in supportedarea then should i add to supported area or remove from selectedarea @@ -538,9 +496,19 @@ local function rvc_service_area_selected_areas_handler(driver, device, ib, respo table.insert(selected_areas, areaId.value) end - local component = device.profile.components["main"] + if next(selected_areas) == nil then + local supported_areas = device:get_latest_state( + "main", + capabilities.serviceArea.ID, + capabilities.serviceArea.supportedAreas.NAME + ) + for i, area in ipairs(supported_areas or {}) do + table.insert(selected_areas, area.areaId) + end + end + local event = capabilities.serviceArea.selectedAreas(selected_areas, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end local function robot_cleaner_areas_selection_response_handler(driver, device, ib, response) @@ -555,23 +523,18 @@ local function robot_cleaner_areas_selection_response_handler(driver, device, ib else device.log.error(string.format("robot_cleaner_areas_selection_response_handler: %s, %s",status.pretty_print(status),status_text)) local selectedAreas = device:get_latest_state("main", capabilities.serviceArea.ID, capabilities.serviceArea.selectedAreas.NAME) - local component = device.profile.components["main"] local event = capabilities.serviceArea.selectedAreas(selectedAreas, {state_change = true}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end end -- Capability Handlers -- local function handle_robot_cleaner_operating_state_start(driver, device, cmd) device.log.info("handle_robot_cleaner_operating_state_start") - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) -- Get current run mode, current tag, current operating state - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) local current_tag = 0xFFFF local supported_run_modes = device:get_field(RUN_MODE_SUPPORTED_MODES) or {} for _, mode in ipairs(supported_run_modes) do @@ -585,19 +548,17 @@ local function handle_robot_cleaner_operating_state_start(driver, device, cmd) capabilities.robotCleanerOperatingState.ID, capabilities.robotCleanerOperatingState.operatingState.NAME ) - local cap_op_enum = capabilities.robotCleanerOperatingState.operatingState if current_state ~= cap_op_enum.stopped.NAME and current_state ~= cap_op_enum.running.NAME and current_state ~= cap_op_enum.paused.NAME and current_state ~= cap_op_enum.seekingCharger.NAME and current_state ~= cap_op_enum.charging.NAME and current_state ~= cap_op_enum.docked.NAME then current_state = "Error" end - local cap_op_cmds = capabilities.robotCleanerOperatingState.commands if can_send_state_command(device, cap_op_cmds.start.NAME, current_state, current_tag) == true then device:send(clusters.RvcOperationalState.commands.Resume(device, endpoint_id)) elseif can_send_state_command(device, capabilities.mode.commands.setMode.NAME, current_state, current_tag) == true then for _, mode in ipairs(supported_run_modes) do - endpoint_id = device:component_to_endpoint("runMode") + endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) if mode.tag == clusters.RvcRunMode.types.ModeTag.CLEANING then device:send(clusters.RvcRunMode.commands.ChangeToMode(device, endpoint_id, mode.id)) return @@ -608,37 +569,26 @@ end local function handle_robot_cleaner_operating_state_pause(driver, device, cmd) device.log.info("handle_robot_cleaner_operating_state_pause") - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) device:send(clusters.RvcOperationalState.commands.Pause(device, endpoint_id)) end local function handle_robot_cleaner_operating_state_go_home(driver, device, cmd) device.log.info("handle_robot_cleaner_operating_state_go_home") - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) device:send(clusters.RvcOperationalState.commands.GoHome(device, endpoint_id)) end local function handle_robot_cleaner_mode(driver, device, cmd) device.log.info(string.format("handle_robot_cleaner_mode component: %s, mode: %s", cmd.component, cmd.args.mode)) - local endpoint_id = device:component_to_endpoint(cmd.component) - if cmd.component == "runMode" then - local supported_modes = device:get_field(RUN_MODE_SUPPORTED_MODES) or {} - for _, mode in ipairs(supported_modes) do - if cmd.args.mode == mode.label then - device.log.info(string.format("mode.label: %s, mode.id: %s", mode.label, mode.id)) - device:send(clusters.RvcRunMode.commands.ChangeToMode(device, endpoint_id, mode.id)) - return - end - end - elseif cmd.component == "cleanMode" then - local supported_modes = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} - for _, mode in ipairs(supported_modes) do - if cmd.args.mode == mode.label then - device.log.info(string.format("mode.label: %s, mode.id: %s", mode.label, mode.id)) - device:send(clusters.RvcCleanMode.commands.ChangeToMode(device, endpoint_id, mode.id)) - return - end + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) + local supported_modes = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} + for _, mode in ipairs(supported_modes) do + if cmd.args.mode == mode.label then + device.log.info(string.format("mode.label: %s, mode.id: %s", mode.label, mode.id)) + device:send(clusters.RvcCleanMode.commands.ChangeToMode(device, endpoint_id, mode.id)) + return end end end @@ -651,7 +601,7 @@ local function handle_robot_cleaner_areas_selection(driver, device, cmd) for i, areaId in ipairs(cmd.args.areas) do table.insert(selectAreas, uint32_dt(areaId)) end - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) if cmd.component == "main" then device:send(clusters.ServiceArea.commands.SelectAreas(device, endpoint_id, selectAreas)) end @@ -660,9 +610,9 @@ end local matter_rvc_driver = { lifecycle_handlers = { init = device_init, - added = device_added, doConfigure = do_configure, infoChanged = info_changed, + driverSwitched = driver_switched }, matter_handlers = { attr = { @@ -675,6 +625,7 @@ local matter_rvc_driver = { [clusters.RvcCleanMode.attributes.CurrentMode.ID] = clean_mode_current_mode_handler, }, [clusters.RvcOperationalState.ID] = { + [clusters.RvcOperationalState.attributes.OperationalStateList.ID] = rvc_operational_state_list_attr_handler, [clusters.RvcOperationalState.attributes.OperationalState.ID] = rvc_operational_state_attr_handler, [clusters.RvcOperationalState.attributes.OperationalError.ID] = rvc_operational_error_attr_handler, [clusters.RvcOperationalState.attributes.AcceptedCommandList.ID] = handle_rvc_operational_state_accepted_command_list, diff --git a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua index 1fb2afb752..e8d91fd1ca 100644 --- a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua +++ b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua @@ -17,6 +17,7 @@ local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" local version = require "version" +test.add_package_capability("robotCleanerOperatingState.yml") if version.api < 10 then clusters.RvcCleanMode = require "RvcCleanMode" @@ -31,6 +32,7 @@ if version.api < 13 then end local APPLICATION_ENDPOINT = 10 +local SERVICE_AREA_PROFILED = "__SERVICE_AREA_PROFILED" local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("rvc-clean-mode-service-area.yml"), @@ -64,16 +66,20 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + mock_device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local subscribed_attributes = { [capabilities.mode.ID] = { - clusters.RvcRunMode.attributes.SupportedModes, - clusters.RvcRunMode.attributes.CurrentMode, - clusters.RvcCleanMode.attributes.SupportedModes, - clusters.RvcCleanMode.attributes.CurrentMode, + clusters.RvcRunMode.attributes.SupportedModes, + clusters.RvcRunMode.attributes.CurrentMode, + clusters.RvcCleanMode.attributes.SupportedModes, + clusters.RvcCleanMode.attributes.CurrentMode, }, [capabilities.robotCleanerOperatingState.ID] = { - clusters.RvcOperationalState.attributes.OperationalState, - clusters.RvcOperationalState.attributes.OperationalError + clusters.RvcOperationalState.attributes.OperationalStateList, + clusters.RvcOperationalState.attributes.OperationalState, + clusters.RvcOperationalState.attributes.OperationalError }, [capabilities.serviceArea.ID] = { clusters.ServiceArea.attributes.SupportedAreas, @@ -90,9 +96,14 @@ local function test_init() end end end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ profile = "rvc-clean-mode-service-area" }) + test.socket.matter:__expect_send({mock_device.id, clusters.RvcOperationalState.attributes.AcceptedCommandList:read()}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) @@ -112,8 +123,6 @@ local RUN_MODES = { CLEANING_MODE, } -local RUN_MODE_LABELS = { RUN_MODES[1].label, RUN_MODES[2].label, RUN_MODES[3].label } - local CLEAN_MODE_1 = { label = "Clean Mode 1", mode = 0, mode_tags = { modeTagStruct({ mfg_code = 0x1E1E, value = 1 }) } } local CLEAN_MODE_2 = { label = "Clean Mode 2", mode = 1, mode_tags = { modeTagStruct({ mfg_code = 0x1E1E, value = 2 }) } } @@ -136,12 +145,6 @@ local function supported_run_mode_init() } ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedModes(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end local function supported_clean_mode_init() @@ -156,13 +159,13 @@ local function supported_clean_mode_init() }) test.socket.capability:__expect_send( mock_device:generate_test_message( - "cleanMode", + "main", capabilities.mode.supportedModes(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) ) ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "cleanMode", + "main", capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) ) ) @@ -191,6 +194,19 @@ local function operating_state_init() SUPPORTED_OPERATIONAL_STATE_COMMAND ) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.robotCleanerOperatingState.supportedCommands( + { + capabilities.robotCleanerOperatingState.commands.pause.NAME, + capabilities.robotCleanerOperatingState.commands.start.NAME, + capabilities.robotCleanerOperatingState.commands.goHome.NAME + }, + {visibility = {displayed = false}} + ) + ) + ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -216,7 +232,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On changing the run mode to a mode with an IDLE tag, supportedArgument must be set to the appropriate value", function() + "On changing the run mode to a mode with an IDLE tag, supportedOperatingStateCommands must be set to the appropriate value", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -228,12 +244,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -246,23 +256,11 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) test.register_coroutine_test( - "On changing the run mode to a mode with an CLEANING tag, supportedArgument must be set to the appropriate value", function() + "On changing the run mode to a mode with an CLEANING tag, supportedOperatingStateCommands must be set to the appropriate value", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -274,12 +272,6 @@ test.register_coroutine_test( CLEANING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = CLEANING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -289,23 +281,11 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) test.register_coroutine_test( - "On changing the run mode to a mode with an MAPPING tag, supportedArgument must be set to the appropriate value", function() + "On changing the run mode to a mode with an MAPPING tag, supportedOperatingStateCommands must be set to the appropriate value", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -317,12 +297,6 @@ test.register_coroutine_test( MAPPING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = MAPPING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -332,18 +306,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -363,7 +325,7 @@ test.register_coroutine_test( }) test.socket.capability:__expect_send( mock_device:generate_test_message( - "cleanMode", + "main", capabilities.mode.mode({value = cleanMode.label}) ) ) @@ -371,24 +333,6 @@ test.register_coroutine_test( end ) -test.register_coroutine_test( - "On changing the rvc run mode, appropriate RvcRunMode command must be sent to the device", function() - supported_run_mode_init() - operating_state_init() - test.wait_for_events() - for _, runMode in ipairs(RUN_MODES) do - test.socket.capability:__queue_receive({ - mock_device.id, - { capability = "mode", component = "runMode", command = "setMode", args = { runMode.label } } - }) - test.socket.matter:__expect_send({ - mock_device.id, - clusters.RvcRunMode.server.commands.ChangeToMode(mock_device, APPLICATION_ENDPOINT, runMode.mode) - }) - end - end -) - test.register_coroutine_test( "On changing the rvc clean mode, appropriate RvcCleanMode command must be sent to the device", function() supported_clean_mode_init() @@ -397,7 +341,7 @@ test.register_coroutine_test( for _, cleanMode in ipairs(CLEAN_MODES) do test.socket.capability:__queue_receive({ mock_device.id, - { capability = "mode", component = "cleanMode", command = "setMode", args = { cleanMode.label } } + { capability = "mode", component = "main", command = "setMode", args = { cleanMode.label } } }) test.socket.matter:__expect_send({ mock_device.id, @@ -408,7 +352,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On receive the Start Command, supportedArgument must be set to the appropriate value", function() + "On receive the start Command of the capability, ChangeToMode command must be sent to the device", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -421,12 +365,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -439,18 +377,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, @@ -464,7 +390,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On receive the goHome Command, supportedArgument must be set to the appropriate value", function() + "On receive the goHome Command of the capability, GoHome command must be sent to the device", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -477,12 +403,6 @@ test.register_coroutine_test( CLEANING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = CLEANING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -492,18 +412,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({IDLE_MODE.label}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, @@ -517,7 +425,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On receive the pause Command, supportedArgument must be set to the appropriate value", function() + "On receive the pause Command of the capability, Pause command must be sent to the device", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -530,12 +438,6 @@ test.register_coroutine_test( CLEANING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = CLEANING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -545,18 +447,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({IDLE_MODE.label}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -583,18 +473,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, @@ -620,12 +498,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -638,18 +510,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -673,18 +533,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -701,12 +549,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -719,18 +561,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -757,18 +587,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -785,12 +603,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -803,18 +615,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -841,18 +641,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -869,12 +657,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -887,18 +669,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -922,18 +692,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -950,12 +708,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -968,18 +720,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -1003,18 +743,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -1031,12 +759,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -1049,18 +771,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -1078,18 +788,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments({}, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalError:build_test_report_data( diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 3753e8b23b..4295c87a93 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -10,6 +10,11 @@ matterManufacturer: vendorId: 0x115F productId: 0x2003 deviceProfileName: motion-illuminance-battery + - id: "4447/8197" + deviceLabel: Presence Multi-Sensor FP300 + vendorId: 0x115F + productId: 0x2005 + deviceProfileName: presence-illuminance-temperature-humidity-battery #Bosch - id: 4617/12309 deviceLabel: "Door/window contact II [M]" @@ -69,6 +74,42 @@ matterManufacturer: vendorId: 0x120B productId: 0x1007 deviceProfileName: co + - id: "4619/4104" + deviceLabel: Smart water leakage sensor + vendorId: 0x120B + productId: 0x1008 + deviceProfileName: leak-battery + - id: "4619/4192" + deviceLabel: Smart Humidity&Temperature Sensor + vendorId: 0x120B + productId: 0x1060 + deviceProfileName: temperature-humidity-battery + # Ikea + - id: "4476/32773" + deviceLabel: TIMMERFLOTTE temp/hmd sensor + vendorId: 0x117C + productId: 0x8005 + deviceProfileName: temperature-humidity-battery + - id: "4476/32774" + deviceLabel: KLIPPBOK Matter water leak sensor smart + vendorId: 0x117C + productId: 0x8006 + deviceProfileName: leak-battery + - id: "4476/12288" + deviceLabel: MYGGSPRAY wrlss mtn sensor + vendorId: 0x117C + productId: 0x3000 + deviceProfileName: motion-illuminance-battery + - id: "4476/12289" + deviceLabel: ALPSTUGA Matter air quality sensor smart + vendorId: 0x117C + productId: 0x3001 + deviceProfileName: aqs-modular-temp-humidity + - id: "4476/32775" + deviceLabel: MYGGBETT Matter door/window sensor smart + vendorId: 0x117C + productId: 0x8007 + deviceProfileName: contact-battery # Legrand - id: "Legrand/Netatmo/Smart-2-in-1-Sensor" deviceLabel: Netatmo Smart 2-in-1 Sensor @@ -102,12 +143,22 @@ matterManufacturer: vendorId: 0x1547 productId: 0x03ED deviceProfileName: contact-battery + - id: "5447/1002" + deviceLabel: Sense by MACO | Window TT + vendorId: 0x1547 + productId: 0x03EA + deviceProfileName: contact-battery # Meross - id: "4933/16897" deviceLabel: Smart Presence Sensor vendorId: 0x1345 productId: 0x4201 deviceProfileName: motion-illuminance + - id: "4933/16898" + deviceLabel: Smart Presence Sensor (Thread) + vendorId: 0x1345 + productId: 0x4202 + deviceProfileName: motion-illuminance-battery # Neo - id: "4991/1122" deviceLabel: Door Sensor @@ -158,6 +209,17 @@ matterManufacturer: vendorId: 0x102E productId: 0x2030 deviceProfileName: motion-illuminance-battery + # Sensereo + - id: "5526/1" + deviceLabel: Sensereo Matter Smoke Alarm MS1 + vendorId: 0x1596 + productId: 0x0001 + deviceProfileName: smoke-battery + - id: "5526/2" + deviceLabel: MSC-1 + vendorId: 0x1596 + productId: 0x0002 + deviceProfileName: smoke-co-comeas-battery # Siterwell - id: "4736/847" deviceLabel: Siterwell Door Window Sensor @@ -261,6 +323,13 @@ matterGeneric: deviceTypes: - id: 0x0076 # Smoke CO Alarm deviceProfileName: smoke-co + - id: "matter/smoke-temp-humidity/sensor" + deviceLabel: Matter Smoke/Temp/Humidity Sensor + deviceTypes: + - id: 0x0076 # Smoke CO Alarm + - id: 0x0307 # Humidity Sensor + - id: 0x0302 # Temperature Sensor + deviceProfileName: smoke-temp-humidity-battery - id: "matter/rain/sensor" deviceLabel: Matter Rain Sensor deviceTypes: diff --git a/drivers/SmartThings/matter-sensor/profiles/freeze.yml b/drivers/SmartThings/matter-sensor/profiles/freeze.yml new file mode 100644 index 0000000000..111fc4c71a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/freeze.yml @@ -0,0 +1,18 @@ +name: freeze +components: +- id: main + capabilities: + - id: temperatureAlarm + version: 1 + config: + values: + - key: "temperatureAlarm.value" + enabledValues: + - cleared + - freeze + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: WaterFreezeDetector diff --git a/drivers/SmartThings/matter-sensor/profiles/leak.yml b/drivers/SmartThings/matter-sensor/profiles/leak.yml new file mode 100644 index 0000000000..a2f6d07727 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/leak.yml @@ -0,0 +1,12 @@ +name: leak +components: +- id: main + capabilities: + - id: waterSensor + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: LeakSensor diff --git a/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery-illuminance.yml b/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery-illuminance.yml index b6c30d3421..3bac085178 100644 --- a/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery-illuminance.yml +++ b/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery-illuminance.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: matter-motion-battery-illuminance components: - id: main diff --git a/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery.yml b/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery.yml index 0e2b57c71e..66ea49c033 100644 --- a/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery.yml +++ b/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: matter-motion-battery components: - id: main diff --git a/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel-illuminance.yml b/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel-illuminance.yml index 87c7fdf701..f9f97b3360 100644 --- a/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel-illuminance.yml +++ b/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel-illuminance.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: matter-motion-batteryLevel-illuminance components: - id: main diff --git a/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel.yml b/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel.yml index afc752557f..fa497ba042 100644 --- a/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel.yml +++ b/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: matter-motion-batteryLevel components: - id: main diff --git a/drivers/SmartThings/matter-sensor/profiles/rain.yml b/drivers/SmartThings/matter-sensor/profiles/rain.yml new file mode 100644 index 0000000000..c55b5d489f --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/rain.yml @@ -0,0 +1,12 @@ +name: rain +components: +- id: main + capabilities: + - id: rainSensor + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RainSensor diff --git a/drivers/SmartThings/matter-sensor/profiles/smoke-temp-humidity-battery.yml b/drivers/SmartThings/matter-sensor/profiles/smoke-temp-humidity-battery.yml new file mode 100644 index 0000000000..cadbe9eb8a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/smoke-temp-humidity-battery.yml @@ -0,0 +1,25 @@ +name: smoke-temp-humidity-battery +components: +- id: main + capabilities: + - id: smokeDetector + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmokeDetector +preferences: + - preferenceId: tempOffset + explicit: true + - preferenceId: humidityOffset + explicit: true \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/init.lua b/drivers/SmartThings/matter-sensor/src/AirQuality/init.lua deleted file mode 100644 index 78e77fce51..0000000000 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/init.lua +++ /dev/null @@ -1,59 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local AirQualityServerAttributes = require "AirQuality.server.attributes" -local AirQualityTypes = require "AirQuality.types" - -local AirQuality = {} - -AirQuality.ID = 0x005B -AirQuality.NAME = "AirQuality" -AirQuality.server = {} -AirQuality.client = {} -AirQuality.server.attributes = AirQualityServerAttributes:set_parent_cluster(AirQuality) -AirQuality.types = AirQualityTypes - -function AirQuality:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "AirQuality", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - --- Attribute Mapping -AirQuality.attribute_direction_map = { - ["AirQuality"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -AirQuality.FeatureMap = AirQuality.types.Feature - -function AirQuality.are_features_supported(feature, feature_map) - if (AirQuality.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = AirQuality.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, AirQuality.NAME)) - end - return AirQuality[direction].attributes[key] -end -AirQuality.attributes = {} -setmetatable(AirQuality.attributes, attribute_helper_mt) - -setmetatable(AirQuality, {__index = cluster_base}) - -return AirQuality - diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AcceptedCommandList.lua deleted file mode 100644 index 6a8d95df1d..0000000000 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AcceptedCommandList.lua +++ /dev/null @@ -1,75 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local AcceptedCommandList = { - ID = 0xFFF9, - NAME = "AcceptedCommandList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - -function AcceptedCommandList:augment_type(data_type_obj) - for i, v in ipairs(data_type_obj.elements) do - data_type_obj.elements[i] = data_types.validate_or_build_type(v, AcceptedCommandList.element_type) - end -end - -function AcceptedCommandList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function AcceptedCommandList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AcceptedCommandList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AcceptedCommandList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function AcceptedCommandList:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function AcceptedCommandList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) -return AcceptedCommandList - diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AirQuality.lua b/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AirQuality.lua deleted file mode 100644 index f92f43543a..0000000000 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AirQuality.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local AirQuality = { - ID = 0x0000, - NAME = "AirQuality", - base_type = require "AirQuality.types.AirQualityEnum", -} - -function AirQuality:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function AirQuality:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AirQuality:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AirQuality:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function AirQuality:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function AirQuality:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(AirQuality, {__call = AirQuality.new_value, __index = AirQuality.base_type}) -return AirQuality - diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AttributeList.lua deleted file mode 100644 index 93e96817e6..0000000000 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AttributeList.lua +++ /dev/null @@ -1,75 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local AttributeList = { - ID = 0xFFFB, - NAME = "AttributeList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - -function AttributeList:augment_type(data_type_obj) - for i, v in ipairs(data_type_obj.elements) do - data_type_obj.elements[i] = data_types.validate_or_build_type(v, AttributeList.element_type) - end -end - -function AttributeList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function AttributeList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AttributeList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AttributeList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function AttributeList:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function AttributeList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) -return AttributeList - diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/EventList.lua b/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/EventList.lua deleted file mode 100644 index 69155cd7ca..0000000000 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/EventList.lua +++ /dev/null @@ -1,75 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local EventList = { - ID = 0xFFFA, - NAME = "EventList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - -function EventList:augment_type(data_type_obj) - for i, v in ipairs(data_type_obj.elements) do - data_type_obj.elements[i] = data_types.validate_or_build_type(v, EventList.element_type) - end -end - -function EventList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function EventList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function EventList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function EventList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function EventList:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function EventList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(EventList, {__call = EventList.new_value, __index = EventList.base_type}) -return EventList - diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/init.lua deleted file mode 100644 index aef3b476a9..0000000000 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("AirQuality.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local AirQualityServerAttributes = {} - -function AirQualityServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(AirQualityServerAttributes, attr_mt) - -return AirQualityServerAttributes - diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/types/AirQualityEnum.lua b/drivers/SmartThings/matter-sensor/src/AirQuality/types/AirQualityEnum.lua deleted file mode 100644 index 317a42dc9b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/types/AirQualityEnum.lua +++ /dev/null @@ -1,45 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local AirQualityEnum = {} --- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility --- with how types were handled in api < 10. -local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) -new_mt.__index.pretty_print = function(self) - local name_lookup = { - [self.UNKNOWN] = "UNKNOWN", - [self.GOOD] = "GOOD", - [self.FAIR] = "FAIR", - [self.MODERATE] = "MODERATE", - [self.POOR] = "POOR", - [self.VERY_POOR] = "VERY_POOR", - [self.EXTREMELY_POOR] = "EXTREMELY_POOR", - } - return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) -end -new_mt.__tostring = new_mt.__index.pretty_print - -new_mt.__index.UNKNOWN = 0x00 -new_mt.__index.GOOD = 0x01 -new_mt.__index.FAIR = 0x02 -new_mt.__index.MODERATE = 0x03 -new_mt.__index.POOR = 0x04 -new_mt.__index.VERY_POOR = 0x05 -new_mt.__index.EXTREMELY_POOR = 0x06 - -AirQualityEnum.UNKNOWN = 0x00 -AirQualityEnum.GOOD = 0x01 -AirQualityEnum.FAIR = 0x02 -AirQualityEnum.MODERATE = 0x03 -AirQualityEnum.POOR = 0x04 -AirQualityEnum.VERY_POOR = 0x05 -AirQualityEnum.EXTREMELY_POOR = 0x06 - -AirQualityEnum.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(AirQualityEnum, new_mt) - -return AirQualityEnum - diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/AirQuality/types/Feature.lua deleted file mode 100644 index 86b90ce627..0000000000 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/types/Feature.lua +++ /dev/null @@ -1,120 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.FAIR = 0x0001 -Feature.MODERATE = 0x0002 -Feature.VERY_POOR = 0x0004 -Feature.EXTREMELY_POOR = 0x0008 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - FAIR = 0x0001, - MODERATE = 0x0002, - VERY_POOR = 0x0004, - EXTREMELY_POOR = 0x0008, -} - -Feature.is_fair_set = function(self) - return (self.value & self.FAIR) ~= 0 -end - -Feature.set_fair = function(self) - if self.value ~= nil then - self.value = self.value | self.FAIR - else - self.value = self.FAIR - end -end - -Feature.unset_fair = function(self) - self.value = self.value & (~self.FAIR & self.BASE_MASK) -end - -Feature.is_moderate_set = function(self) - return (self.value & self.MODERATE) ~= 0 -end - -Feature.set_moderate = function(self) - if self.value ~= nil then - self.value = self.value | self.MODERATE - else - self.value = self.MODERATE - end -end - -Feature.unset_moderate = function(self) - self.value = self.value & (~self.MODERATE & self.BASE_MASK) -end - -Feature.is_very_poor_set = function(self) - return (self.value & self.VERY_POOR) ~= 0 -end - -Feature.set_very_poor = function(self) - if self.value ~= nil then - self.value = self.value | self.VERY_POOR - else - self.value = self.VERY_POOR - end -end - -Feature.unset_very_poor = function(self) - self.value = self.value & (~self.VERY_POOR & self.BASE_MASK) -end - -Feature.is_extremely_poor_set = function(self) - return (self.value & self.EXTREMELY_POOR) ~= 0 -end - -Feature.set_extremely_poor = function(self) - if self.value ~= nil then - self.value = self.value | self.EXTREMELY_POOR - else - self.value = self.EXTREMELY_POOR - end -end - -Feature.unset_extremely_poor = function(self) - self.value = self.value & (~self.EXTREMELY_POOR & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.FAIR | - Feature.MODERATE | - Feature.VERY_POOR | - Feature.EXTREMELY_POOR - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_fair_set = Feature.is_fair_set, - set_fair = Feature.set_fair, - unset_fair = Feature.unset_fair, - is_moderate_set = Feature.is_moderate_set, - set_moderate = Feature.set_moderate, - unset_moderate = Feature.unset_moderate, - is_very_poor_set = Feature.is_very_poor_set, - set_very_poor = Feature.set_very_poor, - unset_very_poor = Feature.unset_very_poor, - is_extremely_poor_set = Feature.is_extremely_poor_set, - set_extremely_poor = Feature.set_extremely_poor, - unset_extremely_poor = Feature.unset_extremely_poor, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/types/init.lua b/drivers/SmartThings/matter-sensor/src/AirQuality/types/init.lua deleted file mode 100644 index 88a2b861b7..0000000000 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/types/init.lua +++ /dev/null @@ -1,15 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("AirQuality.types." .. key) - end - return types_mt.__types_cache[key] -end - -local AirQualityTypes = {} - -setmetatable(AirQualityTypes, types_mt) - -return AirQualityTypes - diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/init.lua b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/init.lua deleted file mode 100644 index f5258619fa..0000000000 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/init.lua +++ /dev/null @@ -1,81 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local BooleanStateConfigurationServerAttributes = require "BooleanStateConfiguration.server.attributes" -local BooleanStateConfigurationTypes = require "BooleanStateConfiguration.types" - -local BooleanStateConfiguration = {} - -BooleanStateConfiguration.ID = 0x0080 -BooleanStateConfiguration.NAME = "BooleanStateConfiguration" -BooleanStateConfiguration.server = {} -BooleanStateConfiguration.client = {} -BooleanStateConfiguration.server.attributes = BooleanStateConfigurationServerAttributes:set_parent_cluster(BooleanStateConfiguration) -BooleanStateConfiguration.types = BooleanStateConfigurationTypes - -function BooleanStateConfiguration:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "CurrentSensitivityLevel", - [0x0001] = "SupportedSensitivityLevels", - [0x0002] = "DefaultSensitivityLevel", - [0x0003] = "AlarmsActive", - [0x0004] = "AlarmsSuppressed", - [0x0005] = "AlarmsEnabled", - [0x0006] = "AlarmsSupported", - [0x0007] = "SensorFault", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - -BooleanStateConfiguration.attribute_direction_map = { - ["CurrentSensitivityLevel"] = "server", - ["SupportedSensitivityLevels"] = "server", - ["DefaultSensitivityLevel"] = "server", - ["AlarmsActive"] = "server", - ["AlarmsSuppressed"] = "server", - ["AlarmsEnabled"] = "server", - ["AlarmsSupported"] = "server", - ["SensorFault"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -do - local has_aliases, aliases = pcall(require, "BooleanStateConfiguration.server.attributes") - if has_aliases then - for alias, _ in pairs(aliases) do - BooleanStateConfiguration.attribute_direction_map[alias] = "server" - end - end -end - -BooleanStateConfiguration.FeatureMap = BooleanStateConfiguration.types.Feature - -function BooleanStateConfiguration.are_features_supported(feature, feature_map) - if (BooleanStateConfiguration.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = BooleanStateConfiguration.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, BooleanStateConfiguration.NAME)) - end - return BooleanStateConfiguration[direction].attributes[key] -end -BooleanStateConfiguration.attributes = {} -setmetatable(BooleanStateConfiguration.attributes, attribute_helper_mt) - -setmetatable(BooleanStateConfiguration, {__index = cluster_base}) - -return BooleanStateConfiguration - diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/init.lua deleted file mode 100644 index 47ef55963b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/init.lua +++ /dev/null @@ -1,25 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("BooleanStateConfiguration.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local BooleanStateConfigurationServerAttributes = {} - -function BooleanStateConfigurationServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(BooleanStateConfigurationServerAttributes, attr_mt) - -return BooleanStateConfigurationServerAttributes - - diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/Feature.lua deleted file mode 100644 index 3a4cb77058..0000000000 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/Feature.lua +++ /dev/null @@ -1,119 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.VISUAL = 0x0001 -Feature.AUDIBLE = 0x0002 -Feature.ALARM_SUPPRESS = 0x0004 -Feature.SENSITIVITY_LEVEL = 0x0008 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - VISUAL = 0x0001, - AUDIBLE = 0x0002, - ALARM_SUPPRESS = 0x0004, - SENSITIVITY_LEVEL = 0x0008, -} - -Feature.is_visual_set = function(self) - return (self.value & self.VISUAL) ~= 0 -end - -Feature.set_visual = function(self) - if self.value ~= nil then - self.value = self.value | self.VISUAL - else - self.value = self.VISUAL - end -end - -Feature.unset_visual = function(self) - self.value = self.value & (~self.VISUAL & self.BASE_MASK) -end -Feature.is_audible_set = function(self) - return (self.value & self.AUDIBLE) ~= 0 -end - -Feature.set_audible = function(self) - if self.value ~= nil then - self.value = self.value | self.AUDIBLE - else - self.value = self.AUDIBLE - end -end - -Feature.unset_audible = function(self) - self.value = self.value & (~self.AUDIBLE & self.BASE_MASK) -end - -Feature.is_alarm_suppress_set = function(self) - return (self.value & self.ALARM_SUPPRESS) ~= 0 -end - -Feature.set_alarm_suppress = function(self) - if self.value ~= nil then - self.value = self.value | self.ALARM_SUPPRESS - else - self.value = self.ALARM_SUPPRESS - end -end - -Feature.unset_alarm_suppress = function(self) - self.value = self.value & (~self.ALARM_SUPPRESS & self.BASE_MASK) -end - -Feature.is_sensitivity_level_set = function(self) - return (self.value & self.SENSITIVITY_LEVEL) ~= 0 -end - -Feature.set_sensitivity_level = function(self) - if self.value ~= nil then - self.value = self.value | self.SENSITIVITY_LEVEL - else - self.value = self.SENSITIVITY_LEVEL - end -end - -Feature.unset_sensitivity_level = function(self) - self.value = self.value & (~self.SENSITIVITY_LEVEL & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.VISUAL | - Feature.AUDIBLE | - Feature.ALARM_SUPPRESS | - Feature.SENSITIVITY_LEVEL - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_visual_set = Feature.is_visual_set, - set_visual = Feature.set_visual, - unset_visual = Feature.unset_visual, - is_audible_set = Feature.is_audible_set, - set_audible = Feature.set_audible, - unset_audible = Feature.unset_audible, - is_alarm_suppress_set = Feature.is_alarm_suppress_set, - set_alarm_suppress = Feature.set_alarm_suppress, - unset_alarm_suppress = Feature.unset_alarm_suppress, - is_sensitivity_level_set = Feature.is_sensitivity_level_set, - set_sensitivity_level = Feature.set_sensitivity_level, - unset_sensitivity_level = Feature.unset_sensitivity_level, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/init.lua b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/init.lua deleted file mode 100644 index 79aae9c6e9..0000000000 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/init.lua +++ /dev/null @@ -1,14 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("BooleanStateConfiguration.types." .. key) - end - return types_mt.__types_cache[key] -end - -local BooleanStateConfigurationTypes = {} - -setmetatable(BooleanStateConfigurationTypes, types_mt) - -return BooleanStateConfigurationTypes diff --git a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/init.lua deleted file mode 100644 index f5109f8943..0000000000 --- a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local CarbonDioxideConcentrationMeasurementServerAttributes = require "CarbonDioxideConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local CarbonDioxideConcentrationMeasurement = {} - -CarbonDioxideConcentrationMeasurement.ID = 0x040D -CarbonDioxideConcentrationMeasurement.NAME = "CarbonDioxideConcentrationMeasurement" -CarbonDioxideConcentrationMeasurement.server = {} -CarbonDioxideConcentrationMeasurement.client = {} -CarbonDioxideConcentrationMeasurement.server.attributes = CarbonDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(CarbonDioxideConcentrationMeasurement) -CarbonDioxideConcentrationMeasurement.types = ConcentrationMeasurement.types - -function CarbonDioxideConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function CarbonDioxideConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -CarbonDioxideConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -CarbonDioxideConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function CarbonDioxideConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = CarbonDioxideConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, CarbonDioxideConcentrationMeasurement.NAME)) - end - return CarbonDioxideConcentrationMeasurement[direction].attributes[key] -end -CarbonDioxideConcentrationMeasurement.attributes = {} -setmetatable(CarbonDioxideConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(CarbonDioxideConcentrationMeasurement, {__index = cluster_base}) - -return CarbonDioxideConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index 93583c2080..0000000000 --- a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("CarbonDioxideConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local CarbonDioxideConcentrationMeasurementServerAttributes = {} - -function CarbonDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(CarbonDioxideConcentrationMeasurementServerAttributes, attr_mt) - -return CarbonDioxideConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/init.lua deleted file mode 100644 index e8cdb487f5..0000000000 --- a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local CarbonMonoxideConcentrationMeasurementServerAttributes = require "CarbonMonoxideConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local CarbonMonoxideConcentrationMeasurement = {} - -CarbonMonoxideConcentrationMeasurement.ID = 0x040C -CarbonMonoxideConcentrationMeasurement.NAME = "CarbonMonoxideConcentrationMeasurement" -CarbonMonoxideConcentrationMeasurement.server = {} -CarbonMonoxideConcentrationMeasurement.client = {} -CarbonMonoxideConcentrationMeasurement.server.attributes = CarbonMonoxideConcentrationMeasurementServerAttributes:set_parent_cluster(CarbonMonoxideConcentrationMeasurement) -CarbonMonoxideConcentrationMeasurement.types = ConcentrationMeasurement.types - -function CarbonMonoxideConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function CarbonMonoxideConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -CarbonMonoxideConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -CarbonMonoxideConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function CarbonMonoxideConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = CarbonMonoxideConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, CarbonMonoxideConcentrationMeasurement.NAME)) - end - return CarbonMonoxideConcentrationMeasurement[direction].attributes[key] -end -CarbonMonoxideConcentrationMeasurement.attributes = {} -setmetatable(CarbonMonoxideConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(CarbonMonoxideConcentrationMeasurement, {__index = cluster_base}) - -return CarbonMonoxideConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index 2307e8977d..0000000000 --- a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("CarbonMonoxideConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local CarbonMonoxideConcentrationMeasurementServerAttributes = {} - -function CarbonMonoxideConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(CarbonMonoxideConcentrationMeasurementServerAttributes, attr_mt) - -return CarbonMonoxideConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/init.lua deleted file mode 100644 index 596d1bfe80..0000000000 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/init.lua +++ /dev/null @@ -1,108 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local ConcentrationMeasurementServerAttributes = require "ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurementTypes = require "ConcentrationMeasurement.types" - -local ConcentrationMeasurement = {} - -ConcentrationMeasurement.ID = 0x040C -ConcentrationMeasurement.NAME = "CarbonMonoxideConcentrationMeasurement" -ConcentrationMeasurement.server = {} -ConcentrationMeasurement.client = {} -ConcentrationMeasurement.server.attributes = ConcentrationMeasurementServerAttributes:set_parent_cluster(ConcentrationMeasurement) -ConcentrationMeasurement.types = ConcentrationMeasurementTypes - -function ConcentrationMeasurement:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "MeasuredValue", - [0x0001] = "MinMeasuredValue", - [0x0002] = "MaxMeasuredValue", - [0x0003] = "PeakMeasuredValue", - [0x0004] = "PeakMeasuredValueWindow", - [0x0005] = "AverageMeasuredValue", - [0x0006] = "AverageMeasuredValueWindow", - [0x0007] = "Uncertainty", - [0x0008] = "MeasurementUnit", - [0x0009] = "MeasurementMedium", - [0x000A] = "LevelValue", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - -function ConcentrationMeasurement:get_server_command_by_id(command_id) - local server_id_map = { - } - if server_id_map[command_id] ~= nil then - return self.server.commands[server_id_map[command_id]] - end - return nil -end - -ConcentrationMeasurement.attribute_direction_map = { - ["MeasuredValue"] = "server", - ["MinMeasuredValue"] = "server", - ["MaxMeasuredValue"] = "server", - ["PeakMeasuredValue"] = "server", - ["PeakMeasuredValueWindow"] = "server", - ["AverageMeasuredValue"] = "server", - ["AverageMeasuredValueWindow"] = "server", - ["Uncertainty"] = "server", - ["MeasurementUnit"] = "server", - ["MeasurementMedium"] = "server", - ["LevelValue"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -ConcentrationMeasurement.command_direction_map = { -} - -ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function ConcentrationMeasurement.are_features_supported(feature, feature_map) - if (ConcentrationMeasurement.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = ConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, ConcentrationMeasurement.NAME)) - end - return ConcentrationMeasurement[direction].attributes[key] -end -ConcentrationMeasurement.attributes = {} -setmetatable(ConcentrationMeasurement.attributes, attribute_helper_mt) - -local command_helper_mt = {} -command_helper_mt.__index = function(self, key) - local direction = ConcentrationMeasurement.command_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown command %s on cluster %s", key, ConcentrationMeasurement.NAME)) - end - return ConcentrationMeasurement[direction].commands[key] -end -ConcentrationMeasurement.commands = {} -setmetatable(ConcentrationMeasurement.commands, command_helper_mt) - -local event_helper_mt = {} -event_helper_mt.__index = function(self, key) - return ConcentrationMeasurement.server.events[key] -end -ConcentrationMeasurement.events = {} -setmetatable(ConcentrationMeasurement.events, event_helper_mt) - -setmetatable(ConcentrationMeasurement, {__index = cluster_base}) - -return ConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index e7023a336c..0000000000 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,70 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function LevelValue:read(device, endpoint_id, cluster_id) - return cluster_base.read( - device, - endpoint_id, - cluster_id, - self.ID, - nil --event_id - ) -end - - -function LevelValue:subscribe(device, endpoint_id, cluster_id) - return cluster_base.subscribe( - device, - endpoint_id, - cluster_id, - self.ID, - nil --event_id - ) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status, - cluster_id -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - cluster_id, - self.ID, - data, - status - ) -end - -function LevelValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index c658d2d3aa..0000000000 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,70 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function MeasuredValue:read(device, endpoint_id, cluster_id) - return cluster_base.read( - device, - endpoint_id, - cluster_id, - self.ID, - nil --event_id - ) -end - - -function MeasuredValue:subscribe(device, endpoint_id, cluster_id) - return cluster_base.subscribe( - device, - endpoint_id, - cluster_id, - self.ID, - nil --event_id - ) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status, - cluster_id -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - cluster_id, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index 3d50e8b97b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,69 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function MeasurementUnit:read(device, endpoint_id, cluster_id) - return cluster_base.read( - device, - endpoint_id, - cluster_id, - self.ID, - nil --event_id - ) -end - -function MeasurementUnit:subscribe(device, endpoint_id, cluster_id) - return cluster_base.subscribe( - device, - endpoint_id, - cluster_id, - self.ID, - nil --event_id - ) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status, - cluster_id -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - cluster_id, - self.ID, - data, - status - ) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index a1a0092151..0000000000 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("ConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local ConcentrationMeasurementServerAttributes = {} - -function ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(ConcentrationMeasurementServerAttributes, attr_mt) - -return ConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/Feature.lua deleted file mode 100644 index 9aa2413903..0000000000 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/Feature.lua +++ /dev/null @@ -1,164 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.NUMERIC_MEASUREMENT = 0x0001 -Feature.LEVEL_INDICATION = 0x0002 -Feature.MEDIUM_LEVEL = 0x0004 -Feature.CRITICAL_LEVEL = 0x0008 -Feature.PEAK_MEASUREMENT = 0x0010 -Feature.AVERAGE_MEASUREMENT = 0x0020 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - NUMERIC_MEASUREMENT = 0x0001, - LEVEL_INDICATION = 0x0002, - MEDIUM_LEVEL = 0x0004, - CRITICAL_LEVEL = 0x0008, - PEAK_MEASUREMENT = 0x0010, - AVERAGE_MEASUREMENT = 0x0020, -} - -Feature.is_numeric_measurement_set = function(self) - return (self.value & self.NUMERIC_MEASUREMENT) ~= 0 -end - -Feature.set_numeric_measurement = function(self) - if self.value ~= nil then - self.value = self.value | self.NUMERIC_MEASUREMENT - else - self.value = self.NUMERIC_MEASUREMENT - end -end - -Feature.unset_numeric_measurement = function(self) - self.value = self.value & (~self.NUMERIC_MEASUREMENT & self.BASE_MASK) -end - -Feature.is_level_indication_set = function(self) - return (self.value & self.LEVEL_INDICATION) ~= 0 -end - -Feature.set_level_indication = function(self) - if self.value ~= nil then - self.value = self.value | self.LEVEL_INDICATION - else - self.value = self.LEVEL_INDICATION - end -end - -Feature.unset_level_indication = function(self) - self.value = self.value & (~self.LEVEL_INDICATION & self.BASE_MASK) -end - -Feature.is_medium_level_set = function(self) - return (self.value & self.MEDIUM_LEVEL) ~= 0 -end - -Feature.set_medium_level = function(self) - if self.value ~= nil then - self.value = self.value | self.MEDIUM_LEVEL - else - self.value = self.MEDIUM_LEVEL - end -end - -Feature.unset_medium_level = function(self) - self.value = self.value & (~self.MEDIUM_LEVEL & self.BASE_MASK) -end - -Feature.is_critical_level_set = function(self) - return (self.value & self.CRITICAL_LEVEL) ~= 0 -end - -Feature.set_critical_level = function(self) - if self.value ~= nil then - self.value = self.value | self.CRITICAL_LEVEL - else - self.value = self.CRITICAL_LEVEL - end -end - -Feature.unset_critical_level = function(self) - self.value = self.value & (~self.CRITICAL_LEVEL & self.BASE_MASK) -end - -Feature.is_peak_measurement_set = function(self) - return (self.value & self.PEAK_MEASUREMENT) ~= 0 -end - -Feature.set_peak_measurement = function(self) - if self.value ~= nil then - self.value = self.value | self.PEAK_MEASUREMENT - else - self.value = self.PEAK_MEASUREMENT - end -end - -Feature.unset_peak_measurement = function(self) - self.value = self.value & (~self.PEAK_MEASUREMENT & self.BASE_MASK) -end - -Feature.is_average_measurement_set = function(self) - return (self.value & self.AVERAGE_MEASUREMENT) ~= 0 -end - -Feature.set_average_measurement = function(self) - if self.value ~= nil then - self.value = self.value | self.AVERAGE_MEASUREMENT - else - self.value = self.AVERAGE_MEASUREMENT - end -end - -Feature.unset_average_measurement = function(self) - self.value = self.value & (~self.AVERAGE_MEASUREMENT & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.NUMERIC_MEASUREMENT | - Feature.LEVEL_INDICATION | - Feature.MEDIUM_LEVEL | - Feature.CRITICAL_LEVEL | - Feature.PEAK_MEASUREMENT | - Feature.AVERAGE_MEASUREMENT - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_numeric_measurement_set = Feature.is_numeric_measurement_set, - set_numeric_measurement = Feature.set_numeric_measurement, - unset_numeric_measurement = Feature.unset_numeric_measurement, - is_level_indication_set = Feature.is_level_indication_set, - set_level_indication = Feature.set_level_indication, - unset_level_indication = Feature.unset_level_indication, - is_medium_level_set = Feature.is_medium_level_set, - set_medium_level = Feature.set_medium_level, - unset_medium_level = Feature.unset_medium_level, - is_critical_level_set = Feature.is_critical_level_set, - set_critical_level = Feature.set_critical_level, - unset_critical_level = Feature.unset_critical_level, - is_peak_measurement_set = Feature.is_peak_measurement_set, - set_peak_measurement = Feature.set_peak_measurement, - unset_peak_measurement = Feature.unset_peak_measurement, - is_average_measurement_set = Feature.is_average_measurement_set, - set_average_measurement = Feature.set_average_measurement, - unset_average_measurement = Feature.unset_average_measurement, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/LevelValueEnum.lua b/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/LevelValueEnum.lua deleted file mode 100644 index b1264d72b8..0000000000 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/LevelValueEnum.lua +++ /dev/null @@ -1,39 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local LevelValueEnum = {} --- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility --- with how types were handled in api < 10. -local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) -new_mt.__index.pretty_print = function(self) - local name_lookup = { - [self.UNKNOWN] = "UNKNOWN", - [self.LOW] = "LOW", - [self.MEDIUM] = "MEDIUM", - [self.HIGH] = "HIGH", - [self.CRITICAL] = "CRITICAL", - } - return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) -end -new_mt.__tostring = new_mt.__index.pretty_print - -new_mt.__index.UNKNOWN = 0x00 -new_mt.__index.LOW = 0x01 -new_mt.__index.MEDIUM = 0x02 -new_mt.__index.HIGH = 0x03 -new_mt.__index.CRITICAL = 0x04 - -LevelValueEnum.UNKNOWN = 0x00 -LevelValueEnum.LOW = 0x01 -LevelValueEnum.MEDIUM = 0x02 -LevelValueEnum.HIGH = 0x03 -LevelValueEnum.CRITICAL = 0x04 - -LevelValueEnum.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(LevelValueEnum, new_mt) - -return LevelValueEnum - diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/MeasurementUnitEnum.lua b/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/MeasurementUnitEnum.lua deleted file mode 100644 index c8302c5cc4..0000000000 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/MeasurementUnitEnum.lua +++ /dev/null @@ -1,48 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local MeasurementUnitEnum = {} --- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility --- with how types were handled in api < 10. -local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) -new_mt.__index.pretty_print = function(self) - local name_lookup = { - [self.PPM] = "PPM", - [self.PPB] = "PPB", - [self.PPT] = "PPT", - [self.MGM3] = "MGM3", - [self.UGM3] = "UGM3", - [self.NGM3] = "NGM3", - [self.PM3] = "PM3", - [self.BQM3] = "BQM3", - } - return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) -end -new_mt.__tostring = new_mt.__index.pretty_print - -new_mt.__index.PPM = 0x00 -new_mt.__index.PPB = 0x01 -new_mt.__index.PPT = 0x02 -new_mt.__index.MGM3 = 0x03 -new_mt.__index.UGM3 = 0x04 -new_mt.__index.NGM3 = 0x05 -new_mt.__index.PM3 = 0x06 -new_mt.__index.BQM3 = 0x07 - -MeasurementUnitEnum.PPM = 0x00 -MeasurementUnitEnum.PPB = 0x01 -MeasurementUnitEnum.PPT = 0x02 -MeasurementUnitEnum.MGM3 = 0x03 -MeasurementUnitEnum.UGM3 = 0x04 -MeasurementUnitEnum.NGM3 = 0x05 -MeasurementUnitEnum.PM3 = 0x06 -MeasurementUnitEnum.BQM3 = 0x07 - -MeasurementUnitEnum.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(MeasurementUnitEnum, new_mt) - -return MeasurementUnitEnum - diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/init.lua b/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/init.lua deleted file mode 100644 index f6da1e6b62..0000000000 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/init.lua +++ /dev/null @@ -1,15 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("ConcentrationMeasurement.types." .. key) - end - return types_mt.__types_cache[key] -end - -local ConcentrationMeasurementTypes = {} - -setmetatable(ConcentrationMeasurementTypes, types_mt) - -return ConcentrationMeasurementTypes - diff --git a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/init.lua deleted file mode 100644 index 5920a9dc66..0000000000 --- a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local FormaldehydeConcentrationMeasurementServerAttributes = require "FormaldehydeConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local FormaldehydeConcentrationMeasurement = {} - -FormaldehydeConcentrationMeasurement.ID = 0x042B -FormaldehydeConcentrationMeasurement.NAME = "FormaldehydeConcentrationMeasurement" -FormaldehydeConcentrationMeasurement.server = {} -FormaldehydeConcentrationMeasurement.client = {} -FormaldehydeConcentrationMeasurement.server.attributes = FormaldehydeConcentrationMeasurementServerAttributes:set_parent_cluster(FormaldehydeConcentrationMeasurement) -FormaldehydeConcentrationMeasurement.types = ConcentrationMeasurement.types - -function FormaldehydeConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function FormaldehydeConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -FormaldehydeConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -FormaldehydeConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function FormaldehydeConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = FormaldehydeConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, FormaldehydeConcentrationMeasurement.NAME)) - end - return FormaldehydeConcentrationMeasurement[direction].attributes[key] -end -FormaldehydeConcentrationMeasurement.attributes = {} -setmetatable(FormaldehydeConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(FormaldehydeConcentrationMeasurement, {__index = cluster_base}) - -return FormaldehydeConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index 37900b0fb1..0000000000 --- a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("FormaldehydeConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local FormaldehydeConcentrationMeasurementServerAttributes = {} - -function FormaldehydeConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(FormaldehydeConcentrationMeasurementServerAttributes, attr_mt) - -return FormaldehydeConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/init.lua deleted file mode 100644 index b60e71050a..0000000000 --- a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local NitrogenDioxideConcentrationMeasurementServerAttributes = require "NitrogenDioxideConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local NitrogenDioxideConcentrationMeasurement = {} - -NitrogenDioxideConcentrationMeasurement.ID = 0x0413 -NitrogenDioxideConcentrationMeasurement.NAME = "NitrogenDioxideConcentrationMeasurement" -NitrogenDioxideConcentrationMeasurement.server = {} -NitrogenDioxideConcentrationMeasurement.client = {} -NitrogenDioxideConcentrationMeasurement.server.attributes = NitrogenDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(NitrogenDioxideConcentrationMeasurement) -NitrogenDioxideConcentrationMeasurement.types = ConcentrationMeasurement.types - -function NitrogenDioxideConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function NitrogenDioxideConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -NitrogenDioxideConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -NitrogenDioxideConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function NitrogenDioxideConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = NitrogenDioxideConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, NitrogenDioxideConcentrationMeasurement.NAME)) - end - return NitrogenDioxideConcentrationMeasurement[direction].attributes[key] -end -NitrogenDioxideConcentrationMeasurement.attributes = {} -setmetatable(NitrogenDioxideConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(NitrogenDioxideConcentrationMeasurement, {__index = cluster_base}) - -return NitrogenDioxideConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index 06c3d6dd55..0000000000 --- a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("NitrogenDioxideConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local NitrogenDioxideConcentrationMeasurementServerAttributes = {} - -function NitrogenDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(NitrogenDioxideConcentrationMeasurementServerAttributes, attr_mt) - -return NitrogenDioxideConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/init.lua deleted file mode 100644 index 83fd04857e..0000000000 --- a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local OzoneConcentrationMeasurementServerAttributes = require "OzoneConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local OzoneConcentrationMeasurement = {} - -OzoneConcentrationMeasurement.ID = 0x0415 -OzoneConcentrationMeasurement.NAME = "OzoneConcentrationMeasurement" -OzoneConcentrationMeasurement.server = {} -OzoneConcentrationMeasurement.client = {} -OzoneConcentrationMeasurement.server.attributes = OzoneConcentrationMeasurementServerAttributes:set_parent_cluster(OzoneConcentrationMeasurement) -OzoneConcentrationMeasurement.types = ConcentrationMeasurement.types - -function OzoneConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function OzoneConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -OzoneConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -OzoneConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function OzoneConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = OzoneConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, OzoneConcentrationMeasurement.NAME)) - end - return OzoneConcentrationMeasurement[direction].attributes[key] -end -OzoneConcentrationMeasurement.attributes = {} -setmetatable(OzoneConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(OzoneConcentrationMeasurement, {__index = cluster_base}) - -return OzoneConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index fe0048cd99..0000000000 --- a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("OzoneConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local OzoneConcentrationMeasurementServerAttributes = {} - -function OzoneConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(OzoneConcentrationMeasurementServerAttributes, attr_mt) - -return OzoneConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/init.lua deleted file mode 100644 index 98eebd407e..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local Pm10ConcentrationMeasurementServerAttributes = require "Pm10ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local Pm10ConcentrationMeasurement = {} - -Pm10ConcentrationMeasurement.ID = 0x042D -Pm10ConcentrationMeasurement.NAME = "Pm10ConcentrationMeasurement" -Pm10ConcentrationMeasurement.server = {} -Pm10ConcentrationMeasurement.client = {} -Pm10ConcentrationMeasurement.server.attributes = Pm10ConcentrationMeasurementServerAttributes:set_parent_cluster(Pm10ConcentrationMeasurement) -Pm10ConcentrationMeasurement.types = ConcentrationMeasurement.types - -function Pm10ConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function Pm10ConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -Pm10ConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -Pm10ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function Pm10ConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = Pm10ConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, Pm10ConcentrationMeasurement.NAME)) - end - return Pm10ConcentrationMeasurement[direction].attributes[key] -end -Pm10ConcentrationMeasurement.attributes = {} -setmetatable(Pm10ConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(Pm10ConcentrationMeasurement, {__index = cluster_base}) - -return Pm10ConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index 55f08c7e43..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("Pm10ConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local Pm10ConcentrationMeasurementServerAttributes = {} - -function Pm10ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(Pm10ConcentrationMeasurementServerAttributes, attr_mt) - -return Pm10ConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/init.lua deleted file mode 100644 index 0b3caa3bd2..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local Pm1ConcentrationMeasurementServerAttributes = require "Pm1ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local Pm1ConcentrationMeasurement = {} - -Pm1ConcentrationMeasurement.ID = 0x042C -Pm1ConcentrationMeasurement.NAME = "Pm1ConcentrationMeasurement" -Pm1ConcentrationMeasurement.server = {} -Pm1ConcentrationMeasurement.client = {} -Pm1ConcentrationMeasurement.server.attributes = Pm1ConcentrationMeasurementServerAttributes:set_parent_cluster(Pm1ConcentrationMeasurement) -Pm1ConcentrationMeasurement.types = ConcentrationMeasurement.types - -function Pm1ConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function Pm1ConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -Pm1ConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -Pm1ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function Pm1ConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = Pm1ConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, Pm1ConcentrationMeasurement.NAME)) - end - return Pm1ConcentrationMeasurement[direction].attributes[key] -end -Pm1ConcentrationMeasurement.attributes = {} -setmetatable(Pm1ConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(Pm1ConcentrationMeasurement, {__index = cluster_base}) - -return Pm1ConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index f668e41a07..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("Pm1ConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local Pm1ConcentrationMeasurementServerAttributes = {} - -function Pm1ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(Pm1ConcentrationMeasurementServerAttributes, attr_mt) - -return Pm1ConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/init.lua deleted file mode 100644 index 5234346d60..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local Pm25ConcentrationMeasurementServerAttributes = require "Pm25ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local Pm25ConcentrationMeasurement = {} - -Pm25ConcentrationMeasurement.ID = 0x042A -Pm25ConcentrationMeasurement.NAME = "Pm25ConcentrationMeasurement" -Pm25ConcentrationMeasurement.server = {} -Pm25ConcentrationMeasurement.client = {} -Pm25ConcentrationMeasurement.server.attributes = Pm25ConcentrationMeasurementServerAttributes:set_parent_cluster(Pm25ConcentrationMeasurement) -Pm25ConcentrationMeasurement.types = ConcentrationMeasurement.types - -function Pm25ConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function Pm25ConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -Pm25ConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -Pm25ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function Pm25ConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = Pm25ConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, Pm25ConcentrationMeasurement.NAME)) - end - return Pm25ConcentrationMeasurement[direction].attributes[key] -end -Pm25ConcentrationMeasurement.attributes = {} -setmetatable(Pm25ConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(Pm25ConcentrationMeasurement, {__index = cluster_base}) - -return Pm25ConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index 2c7d5fce7b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("Pm25ConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local Pm25ConcentrationMeasurementServerAttributes = {} - -function Pm25ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(Pm25ConcentrationMeasurementServerAttributes, attr_mt) - -return Pm25ConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/PressureMeasurement/init.lua deleted file mode 100644 index d47c239242..0000000000 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/init.lua +++ /dev/null @@ -1,144 +0,0 @@ --- Copyright 2022 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. - --- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. - -local cluster_base = require "st.matter.cluster_base" -local PressureMeasurementServerAttributes = require "PressureMeasurement.server.attributes" -local PressureMeasurementServerCommands = require "PressureMeasurement.server.commands" -local PressureMeasurementTypes = require "PressureMeasurement.types" - ---- @class PressureMeasurement ---- @alias PressureMeasurement ---- ---- @field public ID number 0x0403 the ID of this cluster ---- @field public NAME string "PressureMeasurement" the name of this cluster ---- @field public attributes PressureMeasurementServerAttributes | PressureMeasurementClientAttributes ---- @field public commands PressureMeasurementServerCommands | PressureMeasurementClientCommands ---- @field public types PressureMeasurementTypes - -local PressureMeasurement = {} - -PressureMeasurement.ID = 0x0403 -PressureMeasurement.NAME = "PressureMeasurement" -PressureMeasurement.server = {} -PressureMeasurement.client = {} -PressureMeasurement.server.attributes = PressureMeasurementServerAttributes:set_parent_cluster(PressureMeasurement) -PressureMeasurement.server.commands = PressureMeasurementServerCommands:set_parent_cluster(PressureMeasurement) -PressureMeasurement.types = PressureMeasurementTypes - - ---- Find an attribute by id ---- ---- @param attr_id number -function PressureMeasurement:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "MeasuredValue", - [0x0001] = "MinMeasuredValue", - [0x0002] = "MaxMeasuredValue", - [0x0003] = "Tolerance", - [0x0010] = "ScaledValue", - [0x0011] = "MinScaledValue", - [0x0012] = "MaxScaledValue", - [0x0013] = "ScaledTolerance", - [0x0014] = "Scale", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - ---- Find a server command by id ---- ---- @param command_id number -function PressureMeasurement:get_server_command_by_id(command_id) - local server_id_map = { - } - if server_id_map[command_id] ~= nil then - return self.server.commands[server_id_map[command_id]] - end - return nil -end - - --- Attribute Mapping -PressureMeasurement.attribute_direction_map = { - ["MeasuredValue"] = "server", - ["MinMeasuredValue"] = "server", - ["MaxMeasuredValue"] = "server", - ["Tolerance"] = "server", - ["ScaledValue"] = "server", - ["MinScaledValue"] = "server", - ["MaxScaledValue"] = "server", - ["ScaledTolerance"] = "server", - ["Scale"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - - --- Command Mapping -PressureMeasurement.command_direction_map = { -} - - -PressureMeasurement.FeatureMap = PressureMeasurement.types.Feature - -function PressureMeasurement.are_features_supported(feature, feature_map) - if (PressureMeasurement.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - --- Cluster Completion -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = PressureMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, PressureMeasurement.NAME)) - end - return PressureMeasurement[direction].attributes[key] -end -PressureMeasurement.attributes = {} -setmetatable(PressureMeasurement.attributes, attribute_helper_mt) - -local command_helper_mt = {} -command_helper_mt.__index = function(self, key) - local direction = PressureMeasurement.command_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown command %s on cluster %s", key, PressureMeasurement.NAME)) - end - return PressureMeasurement[direction].commands[key] -end -PressureMeasurement.commands = {} -setmetatable(PressureMeasurement.commands, command_helper_mt) - -local event_helper_mt = {} -event_helper_mt.__index = function(self, key) - return PressureMeasurement.server.events[key] -end -PressureMeasurement.events = {} -setmetatable(PressureMeasurement.events, event_helper_mt) - -setmetatable(PressureMeasurement, {__index = cluster_base}) - -return PressureMeasurement - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/AcceptedCommandList.lua deleted file mode 100644 index ee9fad2526..0000000000 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/AcceptedCommandList.lua +++ /dev/null @@ -1,126 +0,0 @@ --- Copyright 2022 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. - --- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. - -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - ---- @class st.matter.clusters.PressureMeasurement.AcceptedCommandList ---- @alias AcceptedCommandList ---- ---- @field public ID number 0xFFF9 the ID of this attribute ---- @field public NAME string "AcceptedCommandList" the name of this attribute ---- @field public data_type st.matter.data_types.Array the data type of this attribute - -local AcceptedCommandList = { - ID = 0xFFF9, - NAME = "AcceptedCommandList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - ---- Add additional functionality to the base type object ---- ---- @param base_type_obj st.matter.data_types.Array the base data type object to add functionality to -function AcceptedCommandList:augment_type(data_type_obj) - for i, v in ipairs(data_type_obj.elements) do - data_type_obj.elements[i] = data_types.validate_or_build_type(v, AcceptedCommandList.element_type) - end -end - ---- Create a Array object of this attribute with any additional features provided for the attribute ---- This is also usable with the AcceptedCommandList(...) syntax ---- ---- @vararg vararg the values needed to construct a Array ---- @return st.matter.data_types.Array -function AcceptedCommandList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - ---- Constructs an st.matter.interaction_model.InteractionRequest to read ---- this attribute from a device ---- @param device st.matter.Device ---- @param endpoint_id number|nil ---- @return st.matter.interaction_model.InteractionRequest containing an Interaction Request -function AcceptedCommandList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - - ---- Reporting policy: AcceptedCommandList => true => mandatory - ---- Sets up a Subscribe Interaction ---- ---- @param device any ---- @param endpoint_id number|nil ---- @return any -function AcceptedCommandList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function AcceptedCommandList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - ---- Builds an AcceptedCommandList test attribute reponse for the driver integration testing framework ---- ---- @param device st.matter.Device the device to build this message for ---- @param endpoint_id number|nil ---- @param value any ---- @param status string Interaction status associated with the path ---- @return st.matter.interaction_model.InteractionResponse of type REPORT_DATA -function AcceptedCommandList:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function AcceptedCommandList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) -return AcceptedCommandList - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/AttributeList.lua deleted file mode 100644 index d4d85b00a0..0000000000 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/AttributeList.lua +++ /dev/null @@ -1,126 +0,0 @@ --- Copyright 2022 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. - --- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. - -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - ---- @class st.matter.clusters.PressureMeasurement.AttributeList ---- @alias AttributeList ---- ---- @field public ID number 0xFFFB the ID of this attribute ---- @field public NAME string "AttributeList" the name of this attribute ---- @field public data_type st.matter.data_types.Array the data type of this attribute - -local AttributeList = { - ID = 0xFFFB, - NAME = "AttributeList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - ---- Add additional functionality to the base type object ---- ---- @param base_type_obj st.matter.data_types.Array the base data type object to add functionality to -function AttributeList:augment_type(data_type_obj) - for i, v in ipairs(data_type_obj.elements) do - data_type_obj.elements[i] = data_types.validate_or_build_type(v, AttributeList.element_type) - end -end - ---- Create a Array object of this attribute with any additional features provided for the attribute ---- This is also usable with the AttributeList(...) syntax ---- ---- @vararg vararg the values needed to construct a Array ---- @return st.matter.data_types.Array -function AttributeList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - ---- Constructs an st.matter.interaction_model.InteractionRequest to read ---- this attribute from a device ---- @param device st.matter.Device ---- @param endpoint_id number|nil ---- @return st.matter.interaction_model.InteractionRequest containing an Interaction Request -function AttributeList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - - ---- Reporting policy: AttributeList => true => mandatory - ---- Sets up a Subscribe Interaction ---- ---- @param device any ---- @param endpoint_id number|nil ---- @return any -function AttributeList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function AttributeList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - ---- Builds an AttributeList test attribute reponse for the driver integration testing framework ---- ---- @param device st.matter.Device the device to build this message for ---- @param endpoint_id number|nil ---- @param value any ---- @param status string Interaction status associated with the path ---- @return st.matter.interaction_model.InteractionResponse of type REPORT_DATA -function AttributeList:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function AttributeList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) -return AttributeList - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/EventList.lua b/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/EventList.lua deleted file mode 100644 index b3dc7bf94c..0000000000 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/EventList.lua +++ /dev/null @@ -1,126 +0,0 @@ --- Copyright 2022 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. - --- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. - -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - ---- @class st.matter.clusters.PressureMeasurement.EventList ---- @alias EventList ---- ---- @field public ID number 0xFFFA the ID of this attribute ---- @field public NAME string "EventList" the name of this attribute ---- @field public data_type st.matter.data_types.Array the data type of this attribute - -local EventList = { - ID = 0xFFFA, - NAME = "EventList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - ---- Add additional functionality to the base type object ---- ---- @param base_type_obj st.matter.data_types.Array the base data type object to add functionality to -function EventList:augment_type(data_type_obj) - for i, v in ipairs(data_type_obj.elements) do - data_type_obj.elements[i] = data_types.validate_or_build_type(v, EventList.element_type) - end -end - ---- Create a Array object of this attribute with any additional features provided for the attribute ---- This is also usable with the EventList(...) syntax ---- ---- @vararg vararg the values needed to construct a Array ---- @return st.matter.data_types.Array -function EventList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - ---- Constructs an st.matter.interaction_model.InteractionRequest to read ---- this attribute from a device ---- @param device st.matter.Device ---- @param endpoint_id number|nil ---- @return st.matter.interaction_model.InteractionRequest containing an Interaction Request -function EventList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - - ---- Reporting policy: EventList => true => mandatory - ---- Sets up a Subscribe Interaction ---- ---- @param device any ---- @param endpoint_id number|nil ---- @return any -function EventList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function EventList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - ---- Builds an EventList test attribute reponse for the driver integration testing framework ---- ---- @param device st.matter.Device the device to build this message for ---- @param endpoint_id number|nil ---- @param value any ---- @param status string Interaction status associated with the path ---- @return st.matter.interaction_model.InteractionResponse of type REPORT_DATA -function EventList:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function EventList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(EventList, {__call = EventList.new_value, __index = EventList.base_type}) -return EventList - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 7d850f1a7c..0000000000 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,116 +0,0 @@ --- Copyright 2022 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. - --- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. - -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - ---- @class st.matter.clusters.PressureMeasurement.MeasuredValue ---- @alias MeasuredValue ---- ---- @field public ID number 0x0000 the ID of this attribute ---- @field public NAME string "MeasuredValue" the name of this attribute ---- @field public data_type st.matter.data_types.Int16 the data type of this attribute - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.Int16", -} - ---- Create a Int16 object of this attribute with any additional features provided for the attribute ---- This is also usable with the MeasuredValue(...) syntax ---- ---- @vararg vararg the values needed to construct a Int16 ---- @return st.matter.data_types.Int16 -function MeasuredValue:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - ---- Constructs an st.matter.interaction_model.InteractionRequest to read ---- this attribute from a device ---- @param device st.matter.Device ---- @param endpoint_id number|nil ---- @return st.matter.interaction_model.InteractionRequest containing an Interaction Request -function MeasuredValue:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - - ---- Reporting policy: MeasuredValue => true => suggested - ---- Sets up a Subscribe Interaction ---- ---- @param device any ---- @param endpoint_id number|nil ---- @return any -function MeasuredValue:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - ---- Builds an MeasuredValue test attribute reponse for the driver integration testing framework ---- ---- @param device st.matter.Device the device to build this message for ---- @param endpoint_id number|nil ---- @param value any ---- @param status string Interaction status associated with the path ---- @return st.matter.interaction_model.InteractionResponse of type REPORT_DATA -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/init.lua deleted file mode 100644 index 34c1959039..0000000000 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,54 +0,0 @@ --- Copyright 2022 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. - --- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. - -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("PressureMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - ---- @class PressureMeasurementServerAttributes ---- ---- @field public MeasuredValue PressureMeasurement.server.attributes.MeasuredValue ---- @field public MinMeasuredValue PressureMeasurement.server.attributes.MinMeasuredValue ---- @field public MaxMeasuredValue PressureMeasurement.server.attributes.MaxMeasuredValue ---- @field public Tolerance PressureMeasurement.server.attributes.Tolerance ---- @field public ScaledValue PressureMeasurement.server.attributes.ScaledValue ---- @field public MinScaledValue PressureMeasurement.server.attributes.MinScaledValue ---- @field public MaxScaledValue PressureMeasurement.server.attributes.MaxScaledValue ---- @field public ScaledTolerance PressureMeasurement.server.attributes.ScaledTolerance ---- @field public Scale PressureMeasurement.server.attributes.Scale ---- @field public AcceptedCommandList PressureMeasurement.server.attributes.AcceptedCommandList ---- @field public EventList PressureMeasurement.server.attributes.EventList ---- @field public AttributeList PressureMeasurement.server.attributes.AttributeList -local PressureMeasurementServerAttributes = {} - -function PressureMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(PressureMeasurementServerAttributes, attr_mt) - -return PressureMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/commands/init.lua b/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/commands/init.lua deleted file mode 100644 index 3bb136bcb5..0000000000 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/commands/init.lua +++ /dev/null @@ -1,41 +0,0 @@ --- Copyright 2022 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. - --- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. - -local command_mt = {} -command_mt.__command_cache = {} -command_mt.__index = function(self, key) - if command_mt.__command_cache[key] == nil then - local req_loc = string.format("PressureMeasurement.server.commands.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) - end - return command_mt.__command_cache[key] -end - ---- @class PressureMeasurementServerCommands ---- -local PressureMeasurementServerCommands = {} - -function PressureMeasurementServerCommands:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(PressureMeasurementServerCommands, command_mt) - -return PressureMeasurementServerCommands - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/events/init.lua b/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/events/init.lua deleted file mode 100644 index c6fec3b6b5..0000000000 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/events/init.lua +++ /dev/null @@ -1,42 +0,0 @@ --- Copyright 2022 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. - --- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. - -local event_mt = {} -event_mt.__event_cache = {} -event_mt.__index = function(self, key) - if event_mt.__event_cache[key] == nil then - local req_loc = string.format("PressureMeasurement.server.events.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - event_mt.__event_cache[key] = raw_def - end - return event_mt.__event_cache[key] -end - ---- @class PressureMeasurementEvents ---- -local PressureMeasurementEvents = {} - -function PressureMeasurementEvents:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(PressureMeasurementEvents, event_mt) - -return PressureMeasurementEvents - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/PressureMeasurement/types/Feature.lua deleted file mode 100644 index 5f5f070b7d..0000000000 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/types/Feature.lua +++ /dev/null @@ -1,81 +0,0 @@ --- Copyright 2022 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. - --- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. - -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - ---- @class st.matter.clusters.PressureMeasurement.types.Feature ---- @alias Feature ---- ---- @field public EXTENDED number 1 - -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.EXTENDED = 0x0001 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - EXTENDED = 0x0001, -} - ---- @function Feature:is_extended_set ---- @return boolean True if the value of EXTENDED is non-zero -Feature.is_extended_set = function(self) - return (self.value & self.EXTENDED) ~= 0 -end - ---- @function Feature:set_extended ---- Set the value of the bit in the EXTENDED field to 1 -Feature.set_extended = function(self) - if self.value ~= nil then - self.value = self.value | self.EXTENDED - else - self.value = self.EXTENDED - end -end - ---- @function Feature:unset_extended ---- Set the value of the bits in the EXTENDED field to 0 -Feature.unset_extended = function(self) - self.value = self.value & (~self.EXTENDED & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.EXTENDED - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_extended_set = Feature.is_extended_set, - set_extended = Feature.set_extended, - unset_extended = Feature.unset_extended, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/types/init.lua b/drivers/SmartThings/matter-sensor/src/PressureMeasurement/types/init.lua deleted file mode 100644 index 682c6db041..0000000000 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/types/init.lua +++ /dev/null @@ -1,35 +0,0 @@ --- Copyright 2022 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. - --- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. - -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("PressureMeasurement.types." .. key) - end - return types_mt.__types_cache[key] -end - ---- @class PressureMeasurementTypes ---- - ---- @field public Feature PressureMeasurement.types.Feature -local PressureMeasurementTypes = {} - -setmetatable(PressureMeasurementTypes, types_mt) - -return PressureMeasurementTypes - diff --git a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/init.lua deleted file mode 100644 index 2a4cc04a0d..0000000000 --- a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local RadonConcentrationMeasurementServerAttributes = require "RadonConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local RadonConcentrationMeasurement = {} - -RadonConcentrationMeasurement.ID = 0x042F -RadonConcentrationMeasurement.NAME = "RadonConcentrationMeasurement" -RadonConcentrationMeasurement.server = {} -RadonConcentrationMeasurement.client = {} -RadonConcentrationMeasurement.server.attributes = RadonConcentrationMeasurementServerAttributes:set_parent_cluster(RadonConcentrationMeasurement) -RadonConcentrationMeasurement.types = ConcentrationMeasurement.types - -function RadonConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function RadonConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -RadonConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -RadonConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function RadonConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = RadonConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, RadonConcentrationMeasurement.NAME)) - end - return RadonConcentrationMeasurement[direction].attributes[key] -end -RadonConcentrationMeasurement.attributes = {} -setmetatable(RadonConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(RadonConcentrationMeasurement, {__index = cluster_base}) - -return RadonConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index b83ef67bfc..0000000000 --- a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("RadonConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local RadonConcentrationMeasurementServerAttributes = {} - -function RadonConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(RadonConcentrationMeasurementServerAttributes, attr_mt) - -return RadonConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/init.lua b/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/init.lua deleted file mode 100644 index 6246ac2ceb..0000000000 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/init.lua +++ /dev/null @@ -1,109 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local SmokeCoAlarmServerAttributes = require "SmokeCoAlarm.server.attributes" -local SmokeCoAlarmServerCommands = require "SmokeCoAlarm.server.commands" -local SmokeCoAlarmTypes = require "SmokeCoAlarm.types" - -local SmokeCoAlarm = {} - -SmokeCoAlarm.ID = 0x005C -SmokeCoAlarm.NAME = "SmokeCoAlarm" -SmokeCoAlarm.server = {} -SmokeCoAlarm.client = {} -SmokeCoAlarm.server.attributes = SmokeCoAlarmServerAttributes:set_parent_cluster(SmokeCoAlarm) -SmokeCoAlarm.server.commands = SmokeCoAlarmServerCommands:set_parent_cluster(SmokeCoAlarm) -SmokeCoAlarm.types = SmokeCoAlarmTypes - -function SmokeCoAlarm:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "ExpressedState", - [0x0001] = "SmokeState", - [0x0002] = "COState", - [0x0003] = "BatteryAlert", - [0x0004] = "DeviceMuted", - [0x0005] = "TestInProgress", - [0x0006] = "HardwareFaultAlert", - [0x0007] = "EndOfServiceAlert", - [0x0008] = "InterconnectSmokeAlarm", - [0x0009] = "InterconnectCOAlarm", - [0x000A] = "ContaminationState", - [0x000B] = "SmokeSensitivityLevel", - [0x000C] = "ExpiryDate", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - -function SmokeCoAlarm:get_server_command_by_id(command_id) - local server_id_map = { - [0x0000] = "SelfTestRequest", - } - if server_id_map[command_id] ~= nil then - return self.server.commands[server_id_map[command_id]] - end - return nil -end - -SmokeCoAlarm.attribute_direction_map = { - ["ExpressedState"] = "server", - ["SmokeState"] = "server", - ["COState"] = "server", - ["BatteryAlert"] = "server", - ["DeviceMuted"] = "server", - ["TestInProgress"] = "server", - ["HardwareFaultAlert"] = "server", - ["EndOfServiceAlert"] = "server", - ["InterconnectSmokeAlarm"] = "server", - ["InterconnectCOAlarm"] = "server", - ["ContaminationState"] = "server", - ["SmokeSensitivityLevel"] = "server", - ["ExpiryDate"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -SmokeCoAlarm.command_direction_map = { - ["SelfTestRequest"] = "server", -} - -SmokeCoAlarm.FeatureMap = SmokeCoAlarm.types.Feature - -function SmokeCoAlarm.are_features_supported(feature, feature_map) - if (SmokeCoAlarm.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = SmokeCoAlarm.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, SmokeCoAlarm.NAME)) - end - return SmokeCoAlarm[direction].attributes[key] -end -SmokeCoAlarm.attributes = {} -setmetatable(SmokeCoAlarm.attributes, attribute_helper_mt) - -local command_helper_mt = {} -command_helper_mt.__index = function(self, key) - local direction = SmokeCoAlarm.command_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown command %s on cluster %s", key, SmokeCoAlarm.NAME)) - end - return SmokeCoAlarm[direction].commands[key] -end -SmokeCoAlarm.commands = {} -setmetatable(SmokeCoAlarm.commands, command_helper_mt) - -setmetatable(SmokeCoAlarm, {__index = cluster_base}) - -return SmokeCoAlarm - diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/init.lua deleted file mode 100644 index 6cc54ec5e5..0000000000 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("SmokeCoAlarm.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local SmokeCoAlarmServerAttributes = {} - -function SmokeCoAlarmServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(SmokeCoAlarmServerAttributes, attr_mt) - -return SmokeCoAlarmServerAttributes - diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/commands/init.lua b/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/commands/init.lua deleted file mode 100644 index 6d768c069c..0000000000 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/commands/init.lua +++ /dev/null @@ -1,23 +0,0 @@ -local command_mt = {} -command_mt.__command_cache = {} -command_mt.__index = function(self, key) - if command_mt.__command_cache[key] == nil then - local req_loc = string.format("SmokeCoAlarm.server.commands.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) - end - return command_mt.__command_cache[key] -end - -local SmokeCoAlarmServerCommands = {} - -function SmokeCoAlarmServerCommands:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(SmokeCoAlarmServerCommands, command_mt) - -return SmokeCoAlarmServerCommands - diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/Feature.lua deleted file mode 100644 index 22c7465a71..0000000000 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/Feature.lua +++ /dev/null @@ -1,76 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.SMOKE_ALARM = 0x0001 -Feature.CO_ALARM = 0x0002 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - SMOKE_ALARM = 0x0001, - CO_ALARM = 0x0002, -} - -Feature.is_smoke_alarm_set = function(self) - return (self.value & self.SMOKE_ALARM) ~= 0 -end - -Feature.set_smoke_alarm = function(self) - if self.value ~= nil then - self.value = self.value | self.SMOKE_ALARM - else - self.value = self.SMOKE_ALARM - end -end - -Feature.unset_smoke_alarm = function(self) - self.value = self.value & (~self.SMOKE_ALARM & self.BASE_MASK) -end - -Feature.is_co_alarm_set = function(self) - return (self.value & self.CO_ALARM) ~= 0 -end - -Feature.set_co_alarm = function(self) - if self.value ~= nil then - self.value = self.value | self.CO_ALARM - else - self.value = self.CO_ALARM - end -end - -Feature.unset_co_alarm = function(self) - self.value = self.value & (~self.CO_ALARM & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.SMOKE_ALARM | - Feature.CO_ALARM - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_smoke_alarm_set = Feature.is_smoke_alarm_set, - set_smoke_alarm = Feature.set_smoke_alarm, - unset_smoke_alarm = Feature.unset_smoke_alarm, - is_co_alarm_set = Feature.is_co_alarm_set, - set_co_alarm = Feature.set_co_alarm, - unset_co_alarm = Feature.unset_co_alarm, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/init.lua b/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/init.lua deleted file mode 100644 index 8467396dc8..0000000000 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/init.lua +++ /dev/null @@ -1,15 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("SmokeCoAlarm.types." .. key) - end - return types_mt.__types_cache[key] -end - -local SmokeCoAlarmTypes = {} - -setmetatable(SmokeCoAlarmTypes, types_mt) - -return SmokeCoAlarmTypes - diff --git a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua deleted file mode 100644 index a99c1dea50..0000000000 --- a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes = require "TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local TotalVolatileOrganicCompoundsConcentrationMeasurement = {} - -TotalVolatileOrganicCompoundsConcentrationMeasurement.ID = 0x042E -TotalVolatileOrganicCompoundsConcentrationMeasurement.NAME = "TotalVolatileOrganicCompoundsConcentrationMeasurement" -TotalVolatileOrganicCompoundsConcentrationMeasurement.server = {} -TotalVolatileOrganicCompoundsConcentrationMeasurement.client = {} -TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes = TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes:set_parent_cluster(TotalVolatileOrganicCompoundsConcentrationMeasurement) -TotalVolatileOrganicCompoundsConcentrationMeasurement.types = ConcentrationMeasurement.types - -function TotalVolatileOrganicCompoundsConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function TotalVolatileOrganicCompoundsConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -TotalVolatileOrganicCompoundsConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -TotalVolatileOrganicCompoundsConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function TotalVolatileOrganicCompoundsConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = TotalVolatileOrganicCompoundsConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, TotalVolatileOrganicCompoundsConcentrationMeasurement.NAME)) - end - return TotalVolatileOrganicCompoundsConcentrationMeasurement[direction].attributes[key] -end -TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes = {} -setmetatable(TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(TotalVolatileOrganicCompoundsConcentrationMeasurement, {__index = cluster_base}) - -return TotalVolatileOrganicCompoundsConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index c0c1b65c37..0000000000 --- a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes = {} - -function TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes, attr_mt) - -return TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua b/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua deleted file mode 100644 index b4af0445c3..0000000000 --- a/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua +++ /dev/null @@ -1,673 +0,0 @@ --- Copyright 2024 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 capabilities = require "st.capabilities" -local clusters = require "st.matter.clusters" -local utils = require "st.utils" -local embedded_cluster_utils = require "embedded-cluster-utils" - -local log = require "log" -local AIR_QUALITY_SENSOR_DEVICE_TYPE_ID = 0x002C - -local SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities" - - --- Include driver-side definitions when lua libs api version is < 10 -local version = require "version" -if version.api < 10 then - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" -end - -local function is_matter_air_quality_sensor(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == AIR_QUALITY_SENSOR_DEVICE_TYPE_ID then - return true - end - end - end - - return false - end - -local subscribed_attributes = { - [capabilities.airQualityHealthConcern.ID] = { - clusters.AirQuality.attributes.AirQuality - }, - [capabilities.temperatureMeasurement.ID] = { - clusters.TemperatureMeasurement.attributes.MeasuredValue - }, - [capabilities.relativeHumidityMeasurement.ID] = { - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue - }, - [capabilities.carbonMonoxideMeasurement.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.carbonMonoxideHealthConcern.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.carbonDioxideMeasurement.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.carbonDioxideHealthConcern.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.nitrogenDioxideMeasurement.ID] = { - clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit - }, - [capabilities.nitrogenDioxideHealthConcern.ID] = { - clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.ozoneMeasurement.ID] = { - clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue, - clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit - }, - [capabilities.ozoneHealthConcern.ID] = { - clusters.OzoneConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.formaldehydeMeasurement.ID] = { - clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue, - clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.formaldehydeHealthConcern.ID] = { - clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.veryFineDustSensor.ID] = { - clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.veryFineDustHealthConcern.ID] = { - clusters.Pm1ConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.fineDustHealthConcern.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.dustSensor.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, - clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.dustHealthConcern.ID] = { - clusters.Pm10ConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.radonMeasurement.ID] = { - clusters.RadonConcentrationMeasurement.attributes.MeasuredValue, - clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.radonHealthConcern.ID] = { - clusters.RadonConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.tvocMeasurement.ID] = { - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue, - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.tvocHealthConcern.ID] = { - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue - } -} - -local units_required = { - clusters.CarbonMonoxideConcentrationMeasurement, - clusters.CarbonDioxideConcentrationMeasurement, - clusters.NitrogenDioxideConcentrationMeasurement, - clusters.OzoneConcentrationMeasurement, - clusters.FormaldehydeConcentrationMeasurement, - clusters.Pm1ConcentrationMeasurement, - clusters.Pm25ConcentrationMeasurement, - clusters.Pm10ConcentrationMeasurement, - clusters.RadonConcentrationMeasurement, - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement -} - -local tbl_contains = function(t, val) - for _, v in pairs(t) do - if v == val then - return true - end - end - return false -end - -local supported_profiles = -{ - "aqs", - "aqs-temp-humidity-all-level-all-meas", - "aqs-temp-humidity-all-level", - "aqs-temp-humidity-all-meas", - "aqs-temp-humidity-co2-pm25-tvoc-meas", - "aqs-temp-humidity-co2-pm1-pm25-pm10-meas", - "aqs-temp-humidity-tvoc-level-pm25-meas", - "aqs-temp-humidity-tvoc-meas", -} - -local CONCENTRATION_MEASUREMENT_MAP = { - [capabilities.carbonMonoxideMeasurement] = {"-co", clusters.CarbonMonoxideConcentrationMeasurement, "N/A"}, - [capabilities.carbonMonoxideHealthConcern] = {"-co", clusters.CarbonMonoxideConcentrationMeasurement, capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues}, - [capabilities.carbonDioxideMeasurement] = {"-co2", clusters.CarbonDioxideConcentrationMeasurement, "N/A"}, - [capabilities.carbonDioxideHealthConcern] = {"-co2", clusters.CarbonDioxideConcentrationMeasurement, capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues}, - [capabilities.nitrogenDioxideMeasurement] = {"-no2", clusters.NitrogenDioxideConcentrationMeasurement, "N/A"}, - [capabilities.nitrogenDioxideHealthConcern] = {"-no2", clusters.NitrogenDioxideConcentrationMeasurement, capabilities.nitrogenDioxideHealthConcern.supportedNitrogenDioxideValues}, - [capabilities.ozoneMeasurement] = {"-ozone", clusters.OzoneConcentrationMeasurement, "N/A"}, - [capabilities.ozoneHealthConcern] = {"-ozone", clusters.OzoneConcentrationMeasurement, capabilities.ozoneHealthConcern.supportedOzoneValues}, - [capabilities.formaldehydeMeasurement] = {"-ch2o", clusters.FormaldehydeConcentrationMeasurement, "N/A"}, - [capabilities.formaldehydeHealthConcern] = {"-ch2o", clusters.FormaldehydeConcentrationMeasurement, capabilities.formaldehydeHealthConcern.supportedFormaldehydeValues}, - [capabilities.veryFineDustSensor] = {"-pm1", clusters.Pm1ConcentrationMeasurement, "N/A"}, - [capabilities.veryFineDustHealthConcern] = {"-pm1", clusters.Pm1ConcentrationMeasurement, capabilities.veryFineDustHealthConcern.supportedVeryFineDustValues}, - [capabilities.fineDustSensor] = {"-pm25", clusters.Pm25ConcentrationMeasurement, "N/A"}, - [capabilities.fineDustHealthConcern] = {"-pm25", clusters.Pm25ConcentrationMeasurement, capabilities.fineDustHealthConcern.supportedFineDustValues}, - [capabilities.dustSensor] = {"-pm10", clusters.Pm10ConcentrationMeasurement, "N/A"}, - [capabilities.dustHealthConcern] = {"-pm10", clusters.Pm10ConcentrationMeasurement, capabilities.dustHealthConcern.supportedDustValues}, - [capabilities.radonMeasurement] = {"-radon", clusters.RadonConcentrationMeasurement, "N/A"}, - [capabilities.radonHealthConcern] = {"-radon", clusters.RadonConcentrationMeasurement, capabilities.radonHealthConcern.supportedRadonValues}, - [capabilities.tvocMeasurement] = {"-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement, "N/A"}, - [capabilities.tvocHealthConcern] = {"-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement, capabilities.tvocHealthConcern.supportedTvocValues}, -} - - -local CONCENTRATION_MEASUREMENT_PROFILE_ORDERING = { - capabilities.carbonMonoxideMeasurement, - capabilities.carbonMonoxideHealthConcern, - capabilities.carbonDioxideMeasurement, - capabilities.carbonDioxideHealthConcern, - capabilities.nitrogenDioxideMeasurement, - capabilities.nitrogenDioxideHealthConcern, - capabilities.ozoneMeasurement, - capabilities.ozoneHealthConcern, - capabilities.formaldehydeMeasurement, - capabilities.formaldehydeHealthConcern, - capabilities.veryFineDustSensor, - capabilities.veryFineDustHealthConcern, - capabilities.fineDustSensor, - capabilities.fineDustHealthConcern, - capabilities.dustSensor, - capabilities.dustHealthConcern, - capabilities.radonMeasurement, - capabilities.radonHealthConcern, - capabilities.tvocMeasurement, - capabilities.tvocHealthConcern, -} - -local function set_supported_health_concern_values(device, setter_function, cluster, cluster_ep) - -- read_datatype_value works since all the healthConcern capabilities' datatypes are equivalent to the one in airQualityHealthConcern - local read_datatype_value = capabilities.airQualityHealthConcern.airQualityHealthConcern - local supported_values = {read_datatype_value.unknown.NAME, read_datatype_value.good.NAME, read_datatype_value.unhealthy.NAME} - if cluster == clusters.AirQuality then - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.FAIR }) > 0 then - table.insert(supported_values, 3, read_datatype_value.moderate.NAME) - end - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.MODERATE }) > 0 then - table.insert(supported_values, 4, read_datatype_value.slightlyUnhealthy.NAME) - end - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.VERY_POOR }) > 0 then - table.insert(supported_values, read_datatype_value.veryUnhealthy.NAME) - end - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.EXTREMELY_POOR }) > 0 then - table.insert(supported_values, read_datatype_value.hazardous.NAME) - end - else -- ConcentrationMeasurement clusters - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.MEDIUM_LEVEL }) > 0 then - table.insert(supported_values, 3, read_datatype_value.moderate.NAME) - end - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.CRITICAL_LEVEL }) > 0 then - table.insert(supported_values, read_datatype_value.hazardous.NAME) - end - end - device:emit_event_for_endpoint(cluster_ep, setter_function(supported_values, { visibility = { displayed = false }})) -end - -local function create_level_measurement_profile(device) - local meas_name, level_name = "", "" - for _, cap in ipairs(CONCENTRATION_MEASUREMENT_PROFILE_ORDERING) do - local cap_id = cap.ID - local cluster = CONCENTRATION_MEASUREMENT_MAP[cap][2] - -- capability describes either a HealthConcern or Measurement/Sensor - if (cap_id:match("HealthConcern$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) - if #attr_eps > 0 then - level_name = level_name .. CONCENTRATION_MEASUREMENT_MAP[cap][1] - set_supported_health_concern_values(device, CONCENTRATION_MEASUREMENT_MAP[cap][3], cluster, attr_eps[1]) - end - elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) - if #attr_eps > 0 then - meas_name = meas_name .. CONCENTRATION_MEASUREMENT_MAP[cap][1] - end - end - end - return meas_name, level_name -end - -local function supported_level_measurements(device) - local measurement_caps, level_caps = {}, {} - for _, cap in ipairs(CONCENTRATION_MEASUREMENT_PROFILE_ORDERING) do - local cap_id = cap.ID - local cluster = CONCENTRATION_MEASUREMENT_MAP[cap][2] - -- capability describes either a HealthConcern or Measurement/Sensor - if (cap_id:match("HealthConcern$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) - if #attr_eps > 0 then - table.insert(level_caps, cap_id) - end - elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) - if #attr_eps > 0 then - table.insert(measurement_caps, cap_id) - end - end - end - return measurement_caps, level_caps -end - -local function match_profile_switch(driver, device) - local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) - local humidity_eps = embedded_cluster_utils.get_endpoints(device, clusters.RelativeHumidityMeasurement.ID) - - local profile_name = "aqs" - local aq_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) - set_supported_health_concern_values(device, capabilities.airQualityHealthConcern.supportedAirQualityValues, clusters.AirQuality, aq_eps[1]) - - if #temp_eps > 0 then - profile_name = profile_name .. "-temp" - end - if #humidity_eps > 0 then - profile_name = profile_name .. "-humidity" - end - - local meas_name, level_name = create_level_measurement_profile(device) - - -- If all endpoints are supported, use '-all' in the profile name so that it - -- remains under the profile name character limit - if level_name == "-co-co2-no2-ozone-ch2o-pm1-pm25-pm10-radon-tvoc" then - level_name = "-all" - end - if level_name ~= "" then - profile_name = profile_name .. level_name .. "-level" - end - - -- If all endpoints are supported, use '-all' in the profile name so that it - -- remains under the profile name character limit - if meas_name == "-co-co2-no2-ozone-ch2o-pm1-pm25-pm10-radon-tvoc" then - meas_name = "-all" - end - if meas_name ~= "" then - profile_name = profile_name .. meas_name .. "-meas" - end - - if not tbl_contains(supported_profiles, profile_name) then - device.log.warn_with({hub_logs=true}, string.format("No matching profile for device. Tried to use profile %s", profile_name)) - - local function meas_find(sub_name) - return string.match(meas_name, sub_name) ~= nil - end - - -- try to best match to existing profiles - -- these checks, meas_find("co%-") and meas_find("co$"), match the string to co and NOT co2. - if meas_find("co%-") or meas_find("co$") or meas_find("no2") or meas_find("ozone") or meas_find("ch2o") or - meas_find("pm1") or meas_find("pm10") or meas_find("radon") then - profile_name = "aqs-temp-humidity-all-meas" - elseif #humidity_eps > 0 or #temp_eps > 0 or meas_find("co2") or meas_find("pm25") or meas_find("tvoc") then - profile_name = "aqs-temp-humidity-co2-pm25-tvoc-meas" - else - -- device only supports air quality at this point - profile_name = "aqs" - end - end - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s", profile_name)) - device:try_update_metadata({profile = profile_name}) -end - -local function supports_capability_by_id_modular(device, capability, component) - if not device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - device.log.warn_with({hub_logs = true}, "Device has overriden supports_capability_by_id, but does not have supported capabilities set.") - return false - end - for _, component_capabilities in ipairs(device:get_field(SUPPORTED_COMPONENT_CAPABILITIES)) do - local comp_id = component_capabilities[1] - local capability_ids = component_capabilities[2] - if (component == nil) or (component == comp_id) then - for _, cap in ipairs(capability_ids) do - if cap == capability then - return true - end - end - end - end - return false -end - -local function match_modular_profile(driver, device) - local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) - local humidity_eps = embedded_cluster_utils.get_endpoints(device, clusters.RelativeHumidityMeasurement.ID) - - local optional_supported_component_capabilities = {} - local main_component_capabilities = {} - local profile_name - local MAIN_COMPONENT_IDX = 1 - local CAPABILITIES_LIST_IDX = 2 - - if #temp_eps > 0 then - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - end - if #humidity_eps > 0 then - table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) - end - - local measurement_caps, level_caps = supported_level_measurements(device) - - for _, cap_id in ipairs(measurement_caps) do - table.insert(main_component_capabilities, cap_id) - end - - for _, cap_id in ipairs(level_caps) do - table.insert(main_component_capabilities, cap_id) - end - - table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) - - if #temp_eps > 0 and #humidity_eps > 0 then - profile_name = "aqs-modular-temp-humidity" - elseif #temp_eps > 0 then - profile_name = "aqs-modular-temp" - elseif #humidity_eps > 0 then - profile_name = "aqs-modular-humidity" - else - profile_name = "aqs-modular" - end - - device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) - - -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities - table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airQualityHealthConcern.ID) - table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.refresh.ID) - table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.firmwareUpdate.ID) - - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) -end - -local function do_configure(driver, device) - -- we have to read the unit before reports of values will do anything - for _, cluster in ipairs(units_required) do - device:send(cluster.attributes.MeasurementUnit:read(device)) - end - if version.api >= 14 and version.rpc >= 8 then - match_modular_profile(driver, device) - else - match_profile_switch(driver, device) - end -end - -local function driver_switched(driver, device) - -- we have to read the unit before reports of values will do anything - for _, cluster in ipairs(units_required) do - device:send(cluster.attributes.MeasurementUnit:read(device)) - end - if version.api >= 14 and version.rpc >= 8 then - match_modular_profile(driver, device) - else - match_profile_switch(driver, device) - end -end - -local function device_init(driver, device) - if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - -- assume that device is using a modular profile, override supports_capability_by_id - -- library function to utilize optional capabilities - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) - end - device:subscribe() -end - -local function store_unit_factory(capability_name) - return function(driver, device, ib, response) - device:set_field(capability_name.."_unit", ib.data.value, {persist = true}) - end -end - -local units = { - PPM = 0, - PPB = 1, - PPT = 2, - MGM3 = 3, - UGM3 = 4, - NGM3 = 5, - PM3 = 6, - BQM3 = 7, - PCIL = 0xFF -- not in matter spec -} - -local unit_strings = { - [units.PPM] = "ppm", - [units.PPB] = "ppb", - [units.PPT] = "ppt", - [units.MGM3] = "mg/m^3", - [units.NGM3] = "ng/m^3", - [units.UGM3] = "μg/m^3", - [units.BQM3] = "Bq/m^3", - [units.PCIL] = "pCi/L" -} - -local unit_default = { - [capabilities.carbonMonoxideMeasurement.NAME] = units.PPM, - [capabilities.carbonDioxideMeasurement.NAME] = units.PPM, - [capabilities.nitrogenDioxideMeasurement.NAME] = units.PPM, - [capabilities.ozoneMeasurement.NAME] = units.PPM, - [capabilities.formaldehydeMeasurement.NAME] = units.PPM, - [capabilities.veryFineDustSensor.NAME] = units.UGM3, - [capabilities.fineDustSensor.NAME] = units.UGM3, - [capabilities.dustSensor.NAME] = units.UGM3, - [capabilities.radonMeasurement.NAME] = units.BQM3, - [capabilities.tvocMeasurement.NAME] = units.PPB -- TVOC is typically within the range of 0-5500 ppb, with good to moderate values being < 660 ppb -} - --- All ConcentrationMeasurement clusters inherit from the same base cluster definitions, --- so CarbonMonoxideConcentrationMeasurement is used below but the same enum types exist --- in all ConcentrationMeasurement clusters -local level_strings = { - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.UNKNOWN] = "unknown", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.LOW] = "good", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.MEDIUM] = "moderate", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.HIGH] = "unhealthy", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.CRITICAL] = "hazardous", -} - -local conversion_tables = { - [units.PPM] = { - [units.PPM] = function(value) return utils.round(value) end, - [units.PPB] = function(value) return utils.round(value * (10^3)) end - }, - [units.PPB] = { - [units.PPM] = function(value) return utils.round(value/(10^3)) end, - [units.PPB] = function(value) return utils.round(value) end - }, - [units.PPT] = { - [units.PPM] = function(value) return utils.round(value/(10^6)) end - }, - [units.MGM3] = { - [units.UGM3] = function(value) return utils.round(value * (10^3)) end - }, - [units.UGM3] = { - [units.UGM3] = function(value) return utils.round(value) end - }, - [units.NGM3] = { - [units.UGM3] = function(value) return utils.round(value/(10^3)) end - }, - [units.BQM3] = { - [units.PCIL] = function(value) return utils.round(value/37) end - } -} - -local function unit_conversion(value, from_unit, to_unit) - local conversion_function = conversion_tables[from_unit][to_unit] - if conversion_function == nil then - log.info_with( {hub_logs = true} , string.format("Unsupported unit conversion from %s to %s", unit_strings[from_unit], unit_strings[to_unit])) - return 1 - end - - if value == nil then - log.info_with( {hub_logs = true} , "unit conversion value is nil") - return 1 - end - return conversion_function(value) -end - -local function measurementHandlerFactory(capability_name, attribute, target_unit) - return function(driver, device, ib, response) - local reporting_unit = device:get_field(capability_name.."_unit") - - if reporting_unit == nil then - reporting_unit = unit_default[capability_name] - device:set_field(capability_name.."_unit", reporting_unit, {persist = true}) - end - - if reporting_unit then - local value = unit_conversion(ib.data.value, reporting_unit, target_unit) - device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = value, unit = unit_strings[target_unit]})) - - -- handle case where device profile supports both fineDustLevel and dustLevel - if capability_name == capabilities.fineDustSensor.NAME and device:supports_capability(capabilities.dustSensor) then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.dustSensor.fineDustLevel({value = value, unit = unit_strings[target_unit]})) - end - end - end -end - -local function levelHandlerFactory(attribute) - return function(driver, device, ib, response) - device:emit_event_for_endpoint(ib.endpoint_id, attribute(level_strings[ib.data.value])) - end -end - --- Matter Handlers -- -local function air_quality_attr_handler(driver, device, ib, response) - local state = ib.data.value - if state == 0 then -- Unknown - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unknown()) - elseif state == 1 then -- Good - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.good()) - elseif state == 2 then -- Fair - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.moderate()) - elseif state == 3 then -- Moderate - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.slightlyUnhealthy()) - elseif state == 4 then -- Poor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unhealthy()) - elseif state == 5 then -- VeryPoor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.veryUnhealthy()) - elseif state == 6 then -- ExtremelyPoor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.hazardous()) - end -end - -local function pressure_attr_handler(driver, device, ib, response) - local pressure = utils.round(ib.data.value / 10.0) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.atmosphericPressureMeasurement.atmosphericPressure(pressure)) -end - -local function info_changed(driver, device, event, args) - if device.profile.id ~= args.old_st_store.profile.id then - if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - --re-up subscription with new capabilities using the modular supports_capability override - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) - end - device:subscribe() - end -end - -local matter_air_quality_sensor_handler = { - NAME = "matter-air-quality-sensor", - lifecycle_handlers = { - init = device_init, - doConfigure = do_configure, - infoChanged = info_changed, - driverSwitched = driver_switched - }, - matter_handlers = { - attr = { - [clusters.AirQuality.ID] = { - [clusters.AirQuality.attributes.AirQuality.ID] = air_quality_attr_handler, - }, - [clusters.PressureMeasurement.ID] = { - [clusters.PressureMeasurement.attributes.MeasuredValue.ID] = pressure_attr_handler - }, - [clusters.CarbonMonoxideConcentrationMeasurement.ID] = { - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.carbonMonoxideMeasurement.NAME, capabilities.carbonMonoxideMeasurement.carbonMonoxideLevel, units.PPM), - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.carbonMonoxideMeasurement.NAME), - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.carbonMonoxideHealthConcern.carbonMonoxideHealthConcern), - }, - [clusters.CarbonDioxideConcentrationMeasurement.ID] = { - [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.carbonDioxideMeasurement.NAME, capabilities.carbonDioxideMeasurement.carbonDioxide, units.PPM), - [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.carbonDioxideMeasurement.NAME), - [clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern), - }, - [clusters.NitrogenDioxideConcentrationMeasurement.ID] = { - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.nitrogenDioxideMeasurement.NAME, capabilities.nitrogenDioxideMeasurement.nitrogenDioxide, units.PPM), - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.nitrogenDioxideMeasurement.NAME), - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.nitrogenDioxideHealthConcern.nitrogenDioxideHealthConcern) - }, - [clusters.OzoneConcentrationMeasurement.ID] = { - [clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.ozoneMeasurement.NAME, capabilities.ozoneMeasurement.ozone, units.PPM), - [clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.ozoneMeasurement.NAME), - [clusters.OzoneConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.ozoneHealthConcern.ozoneHealthConcern) - }, - [clusters.FormaldehydeConcentrationMeasurement.ID] = { - [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.formaldehydeMeasurement.NAME, capabilities.formaldehydeMeasurement.formaldehydeLevel, units.PPM), - [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.formaldehydeMeasurement.NAME), - [clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.formaldehydeHealthConcern.formaldehydeHealthConcern), - }, - [clusters.Pm1ConcentrationMeasurement.ID] = { - [clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.veryFineDustSensor.NAME, capabilities.veryFineDustSensor.veryFineDustLevel, units.UGM3), - [clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.veryFineDustSensor.NAME), - [clusters.Pm1ConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern), - }, - [clusters.Pm25ConcentrationMeasurement.ID] = { - [clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.fineDustSensor.NAME, capabilities.fineDustSensor.fineDustLevel, units.UGM3), - [clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.fineDustSensor.NAME), - [clusters.Pm25ConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.fineDustHealthConcern.fineDustHealthConcern), - }, - [clusters.Pm10ConcentrationMeasurement.ID] = { - [clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.dustSensor.NAME, capabilities.dustSensor.dustLevel, units.UGM3), - [clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.dustSensor.NAME), - [clusters.Pm10ConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.dustHealthConcern.dustHealthConcern), - }, - [clusters.RadonConcentrationMeasurement.ID] = { - [clusters.RadonConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.radonMeasurement.NAME, capabilities.radonMeasurement.radonLevel, units.PCIL), - [clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.radonMeasurement.NAME), - [clusters.RadonConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.radonHealthConcern.radonHealthConcern) - }, - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.ID] = { - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.tvocMeasurement.NAME, capabilities.tvocMeasurement.tvocLevel, units.PPB), - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.tvocMeasurement.NAME), - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.tvocHealthConcern.tvocHealthConcern) - } - } - }, - subscribed_attributes = subscribed_attributes, - can_handle = is_matter_air_quality_sensor -} - -return matter_air_quality_sensor_handler diff --git a/drivers/SmartThings/matter-sensor/src/bosch-button-contact/init.lua b/drivers/SmartThings/matter-sensor/src/bosch-button-contact/init.lua deleted file mode 100644 index 1dfd3960ce..0000000000 --- a/drivers/SmartThings/matter-sensor/src/bosch-button-contact/init.lua +++ /dev/null @@ -1,162 +0,0 @@ --- 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 capabilities = require "st.capabilities" -local clusters = require "st.matter.clusters" -local device_lib = require "st.device" -local lua_socket = require "socket" -local log = require "log" - -local START_BUTTON_PRESS = "__start_button_press" - -local BOSCH_VENDOR_ID = 0x1209 -local BOSCH_PRODUCT_ID = 0x3015 - -local function is_bosch_button_contact(opts, driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER and - device.manufacturer_info.vendor_id == BOSCH_VENDOR_ID and - device.manufacturer_info.product_id == BOSCH_PRODUCT_ID then - return true - end - return false -end - -local function get_field_for_endpoint(device, field, endpoint) - return device:get_field(string.format("%s_%d", field, endpoint)) -end - -local function set_field_for_endpoint(device, field, endpoint, value, additional_params) - device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) -end - -local function init_press(device, endpoint) - set_field_for_endpoint(device, START_BUTTON_PRESS, endpoint, lua_socket.gettime(), {persist = false}) -end - --- Some switches will send a MultiPressComplete event as part of a long press sequence. Normally the driver will create a --- button capability event on receipt of MultiPressComplete, but in this case that would result in an extra event because --- the "held" capability event is generated when the LongPress event is received. The IGNORE_NEXT_MPC flag is used --- to tell the driver to ignore MultiPressComplete if it is received after a long press to avoid this extra event. -local IGNORE_NEXT_MPC = "__ignore_next_mpc" --- These are essentially storing the supported features of a given endpoint --- TODO: add an is_feature_supported_for_endpoint function to matter.device that takes an endpoint -local EMULATE_HELD = "__emulate_held" -- for non-MSR (MomentarySwitchRelease) devices we can emulate this on the software side -local SUPPORTS_MULTI_PRESS = "__multi_button" -- for MSM devices (MomentarySwitchMultiPress), create an event on receipt of MultiPressComplete -local INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS (MomentarySwitch), but not MSR (MomentarySwitchRelease) - -local function initial_press_event_handler(driver, device, ib, response) - if get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then - -- Receipt of an InitialPress event means we do not want to ignore the next MultiPressComplete event - -- or else we would potentially not create the expected button capability event - set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, nil) - elseif get_field_for_endpoint(device, INITIAL_PRESS_ONLY, ib.endpoint_id) then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true})) - elseif get_field_for_endpoint(device, EMULATE_HELD, ib.endpoint_id) then - -- if our button doesn't differentiate between short and long holds, do it in code by keeping track of the press down time - init_press(device, ib.endpoint_id) - end -end - -local function long_press_event_handler(driver, device, ib, response) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.held({state_change = true})) - if get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then - -- Ignore the next MultiPressComplete event if it is sent as part of this "long press" event sequence - set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, true) - end -end - ---helper function to create list of multi press values -local function create_multi_press_values_list(size, supportsHeld) - local list = {"pushed", "double"} - if supportsHeld then table.insert(list, "held") end - -- add multi press values of 3 or greater to the list - for i=3, size do - table.insert(list, string.format("pushed_%dx", i)) - end - return list -end - -local function tbl_contains(array, value) - for _, element in ipairs(array) do - if element == value then - return true - end - end - return false -end - -local function device_init (driver, device) - device:subscribe() - device:send(clusters.Switch.attributes.MultiPressMax:read(device)) -end - -local function max_press_handler(driver, device, ib, response) - local max = ib.data.value or 1 --get max number of presses - device.log.debug("Device supports "..max.." presses") - -- capability only supports up to 6 presses - if max > 6 then - log.info("Device supports more than 6 presses") - max = 6 - end - local MSL = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS}) - local supportsHeld = tbl_contains(MSL, ib.endpoint_id) - local values = create_multi_press_values_list(max, supportsHeld) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.supportedButtonValues(values, {visibility = {displayed = false}})) -end - -local function multi_press_complete_event_handler(driver, device, ib, response) - -- in the case of multiple button presses - -- emit number of times, multiple presses have been completed - if ib.data and not get_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id) then - local press_value = ib.data.elements.total_number_of_presses_counted.value - --capability only supports up to 6 presses - if press_value < 7 then - local button_event = capabilities.button.button.pushed({state_change = true}) - if press_value == 2 then - button_event = capabilities.button.button.double({state_change = true}) - elseif press_value > 2 then - button_event = capabilities.button.button(string.format("pushed_%dx", press_value), {state_change = true}) - end - - device:emit_event_for_endpoint(ib.endpoint_id, button_event) - else - log.info(string.format("Number of presses (%d) not supported by capability", press_value)) - end - end - set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, nil) -end - -local Bosch_Button_Contact_Sensor = { - NAME = "Bosch_Button_Contact_Sensor", - lifecycle_handlers = { - init = device_init - }, - matter_handlers = { - attr = { - [clusters.Switch.ID] = { - [clusters.Switch.attributes.MultiPressMax.ID] = max_press_handler - } - }, - event = { - [clusters.Switch.ID] = { - [clusters.Switch.events.InitialPress.ID] = initial_press_event_handler, - [clusters.Switch.events.LongPress.ID] = long_press_event_handler, - [clusters.Switch.events.MultiPressComplete.ID] = multi_press_complete_event_handler - } - }, - }, - can_handle = is_bosch_button_contact, -} - -return Bosch_Button_Contact_Sensor diff --git a/drivers/SmartThings/matter-sensor/src/embedded-cluster-utils.lua b/drivers/SmartThings/matter-sensor/src/embedded-cluster-utils.lua deleted file mode 100644 index 244ff77fc2..0000000000 --- a/drivers/SmartThings/matter-sensor/src/embedded-cluster-utils.lua +++ /dev/null @@ -1,80 +0,0 @@ -local clusters = require "st.matter.clusters" -local utils = require "st.utils" - --- Include driver-side definitions when lua libs api version is < 10 -local version = require "version" -if version.api < 10 then - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" - clusters.SmokeCoAlarm = require "SmokeCoAlarm" -end - -if version.api < 11 then - clusters.BooleanStateConfiguration = require "BooleanStateConfiguration" -end - -local embedded_cluster_utils = {} - -local embedded_clusters_api_10 = { - [clusters.AirQuality.ID] = clusters.AirQuality, - [clusters.CarbonMonoxideConcentrationMeasurement.ID] = clusters.CarbonMonoxideConcentrationMeasurement, - [clusters.CarbonDioxideConcentrationMeasurement.ID] = clusters.CarbonDioxideConcentrationMeasurement, - [clusters.FormaldehydeConcentrationMeasurement.ID] = clusters.FormaldehydeConcentrationMeasurement, - [clusters.NitrogenDioxideConcentrationMeasurement.ID] = clusters.NitrogenDioxideConcentrationMeasurement, - [clusters.OzoneConcentrationMeasurement.ID] = clusters.OzoneConcentrationMeasurement, - [clusters.Pm1ConcentrationMeasurement.ID] = clusters.Pm1ConcentrationMeasurement, - [clusters.Pm10ConcentrationMeasurement.ID] = clusters.Pm10ConcentrationMeasurement, - [clusters.Pm25ConcentrationMeasurement.ID] = clusters.Pm25ConcentrationMeasurement, - [clusters.RadonConcentrationMeasurement.ID] = clusters.RadonConcentrationMeasurement, - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.ID] = clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement, - [clusters.SmokeCoAlarm.ID] = clusters.SmokeCoAlarm, -} - -local embedded_clusters_api_11 = { - [clusters.BooleanStateConfiguration.ID] = clusters.BooleanStateConfiguration -} - -function embedded_cluster_utils.get_endpoints(device, cluster_id, opts) - -- If using older lua libs and need to check for an embedded cluster feature, - -- we must use the embedded cluster definitions here - if version.api < 10 and embedded_clusters_api_10[cluster_id] ~= nil or - version.api < 11 and embedded_clusters_api_11[cluster_id] ~= nil then - local embedded_cluster = embedded_clusters_api_10[cluster_id] or embedded_clusters_api_11[cluster_id] - local opts = opts or {} - if utils.table_size(opts) > 1 then - device.log.warn_with({hub_logs = true}, "Invalid options for get_endpoints") - return - end - local clus_has_features = function(clus, feature_bitmap) - if not feature_bitmap or not clus then return false end - return embedded_cluster.are_features_supported(feature_bitmap, clus.feature_map) - end - local eps = {} - for _, ep in ipairs(device.endpoints) do - for _, clus in ipairs(ep.clusters) do - if ((clus.cluster_id == cluster_id) - and (opts.feature_bitmap == nil or clus_has_features(clus, opts.feature_bitmap)) - and ((opts.cluster_type == nil and clus.cluster_type == "SERVER" or clus.cluster_type == "BOTH") - or (opts.cluster_type == clus.cluster_type)) - or (cluster_id == nil)) then - table.insert(eps, ep.endpoint_id) - if cluster_id == nil then break end - end - end - end - return eps - else - return device:get_endpoints(cluster_id, opts) - end - end - - return embedded_cluster_utils \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/init.lua new file mode 100644 index 0000000000..4f6d19cc9f --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/init.lua @@ -0,0 +1,62 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local AirQualityServerAttributes = require "embedded_clusters.AirQuality.server.attributes" +local AirQualityTypes = require "embedded_clusters.AirQuality.types" + +local AirQuality = {} + +AirQuality.ID = 0x005B +AirQuality.NAME = "AirQuality" +AirQuality.server = {} +AirQuality.client = {} +AirQuality.server.attributes = AirQualityServerAttributes:set_parent_cluster(AirQuality) +AirQuality.types = AirQualityTypes + +function AirQuality:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "AirQuality", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +-- Attribute Mapping +AirQuality.attribute_direction_map = { + ["AirQuality"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +AirQuality.FeatureMap = AirQuality.types.Feature + +function AirQuality.are_features_supported(feature, feature_map) + if (AirQuality.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = AirQuality.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, AirQuality.NAME)) + end + return AirQuality[direction].attributes[key] +end +AirQuality.attributes = {} +setmetatable(AirQuality.attributes, attribute_helper_mt) + +setmetatable(AirQuality, {__index = cluster_base}) + +return AirQuality + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AcceptedCommandList.lua new file mode 100644 index 0000000000..a1e56c6597 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AcceptedCommandList.lua @@ -0,0 +1,78 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AcceptedCommandList = { + ID = 0xFFF9, + NAME = "AcceptedCommandList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +function AcceptedCommandList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, AcceptedCommandList.element_type) + end +end + +function AcceptedCommandList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function AcceptedCommandList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AcceptedCommandList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AcceptedCommandList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AcceptedCommandList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AcceptedCommandList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) +return AcceptedCommandList + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AirQuality.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AirQuality.lua new file mode 100644 index 0000000000..1beb6218f9 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AirQuality.lua @@ -0,0 +1,71 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AirQuality = { + ID = 0x0000, + NAME = "AirQuality", + base_type = require "embedded_clusters.AirQuality.types.AirQualityEnum", +} + +function AirQuality:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function AirQuality:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AirQuality:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AirQuality:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AirQuality:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AirQuality:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(AirQuality, {__call = AirQuality.new_value, __index = AirQuality.base_type}) +return AirQuality + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AttributeList.lua new file mode 100644 index 0000000000..238b50ade3 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AttributeList.lua @@ -0,0 +1,78 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AttributeList = { + ID = 0xFFFB, + NAME = "AttributeList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +function AttributeList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, AttributeList.element_type) + end +end + +function AttributeList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function AttributeList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AttributeList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AttributeList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AttributeList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AttributeList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) +return AttributeList + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/EventList.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/EventList.lua new file mode 100644 index 0000000000..719f17a231 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/EventList.lua @@ -0,0 +1,78 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local EventList = { + ID = 0xFFFA, + NAME = "EventList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +function EventList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, EventList.element_type) + end +end + +function EventList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function EventList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function EventList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function EventList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function EventList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function EventList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(EventList, {__call = EventList.new_value, __index = EventList.base_type}) +return EventList + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/init.lua new file mode 100644 index 0000000000..50295b081a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.AirQuality.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local AirQualityServerAttributes = {} + +function AirQualityServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(AirQualityServerAttributes, attr_mt) + +return AirQualityServerAttributes + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/AirQualityEnum.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/AirQualityEnum.lua new file mode 100644 index 0000000000..c2c255614a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/AirQualityEnum.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local AirQualityEnum = {} +-- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility +-- with how types were handled in api < 10. +local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.UNKNOWN] = "UNKNOWN", + [self.GOOD] = "GOOD", + [self.FAIR] = "FAIR", + [self.MODERATE] = "MODERATE", + [self.POOR] = "POOR", + [self.VERY_POOR] = "VERY_POOR", + [self.EXTREMELY_POOR] = "EXTREMELY_POOR", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.UNKNOWN = 0x00 +new_mt.__index.GOOD = 0x01 +new_mt.__index.FAIR = 0x02 +new_mt.__index.MODERATE = 0x03 +new_mt.__index.POOR = 0x04 +new_mt.__index.VERY_POOR = 0x05 +new_mt.__index.EXTREMELY_POOR = 0x06 + +AirQualityEnum.UNKNOWN = 0x00 +AirQualityEnum.GOOD = 0x01 +AirQualityEnum.FAIR = 0x02 +AirQualityEnum.MODERATE = 0x03 +AirQualityEnum.POOR = 0x04 +AirQualityEnum.VERY_POOR = 0x05 +AirQualityEnum.EXTREMELY_POOR = 0x06 + +AirQualityEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(AirQualityEnum, new_mt) + +return AirQualityEnum + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/Feature.lua new file mode 100644 index 0000000000..906a09a2bb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/Feature.lua @@ -0,0 +1,123 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.FAIR = 0x0001 +Feature.MODERATE = 0x0002 +Feature.VERY_POOR = 0x0004 +Feature.EXTREMELY_POOR = 0x0008 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + FAIR = 0x0001, + MODERATE = 0x0002, + VERY_POOR = 0x0004, + EXTREMELY_POOR = 0x0008, +} + +Feature.is_fair_set = function(self) + return (self.value & self.FAIR) ~= 0 +end + +Feature.set_fair = function(self) + if self.value ~= nil then + self.value = self.value | self.FAIR + else + self.value = self.FAIR + end +end + +Feature.unset_fair = function(self) + self.value = self.value & (~self.FAIR & self.BASE_MASK) +end + +Feature.is_moderate_set = function(self) + return (self.value & self.MODERATE) ~= 0 +end + +Feature.set_moderate = function(self) + if self.value ~= nil then + self.value = self.value | self.MODERATE + else + self.value = self.MODERATE + end +end + +Feature.unset_moderate = function(self) + self.value = self.value & (~self.MODERATE & self.BASE_MASK) +end + +Feature.is_very_poor_set = function(self) + return (self.value & self.VERY_POOR) ~= 0 +end + +Feature.set_very_poor = function(self) + if self.value ~= nil then + self.value = self.value | self.VERY_POOR + else + self.value = self.VERY_POOR + end +end + +Feature.unset_very_poor = function(self) + self.value = self.value & (~self.VERY_POOR & self.BASE_MASK) +end + +Feature.is_extremely_poor_set = function(self) + return (self.value & self.EXTREMELY_POOR) ~= 0 +end + +Feature.set_extremely_poor = function(self) + if self.value ~= nil then + self.value = self.value | self.EXTREMELY_POOR + else + self.value = self.EXTREMELY_POOR + end +end + +Feature.unset_extremely_poor = function(self) + self.value = self.value & (~self.EXTREMELY_POOR & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.FAIR | + Feature.MODERATE | + Feature.VERY_POOR | + Feature.EXTREMELY_POOR + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_fair_set = Feature.is_fair_set, + set_fair = Feature.set_fair, + unset_fair = Feature.unset_fair, + is_moderate_set = Feature.is_moderate_set, + set_moderate = Feature.set_moderate, + unset_moderate = Feature.unset_moderate, + is_very_poor_set = Feature.is_very_poor_set, + set_very_poor = Feature.set_very_poor, + unset_very_poor = Feature.unset_very_poor, + is_extremely_poor_set = Feature.is_extremely_poor_set, + set_extremely_poor = Feature.set_extremely_poor, + unset_extremely_poor = Feature.unset_extremely_poor, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/init.lua new file mode 100644 index 0000000000..b77d67de82 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/init.lua @@ -0,0 +1,18 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.AirQuality.types." .. key) + end + return types_mt.__types_cache[key] +end + +local AirQualityTypes = {} + +setmetatable(AirQualityTypes, types_mt) + +return AirQualityTypes + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/init.lua new file mode 100644 index 0000000000..c8e754419b --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/init.lua @@ -0,0 +1,81 @@ +local cluster_base = require "st.matter.cluster_base" +local BooleanStateConfigurationServerAttributes = require "embedded_clusters.BooleanStateConfiguration.server.attributes" +local BooleanStateConfigurationTypes = require "embedded_clusters.BooleanStateConfiguration.types" + +local BooleanStateConfiguration = {} + +BooleanStateConfiguration.ID = 0x0080 +BooleanStateConfiguration.NAME = "BooleanStateConfiguration" +BooleanStateConfiguration.server = {} +BooleanStateConfiguration.client = {} +BooleanStateConfiguration.server.attributes = BooleanStateConfigurationServerAttributes:set_parent_cluster(BooleanStateConfiguration) +BooleanStateConfiguration.types = BooleanStateConfigurationTypes + +function BooleanStateConfiguration:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "CurrentSensitivityLevel", + [0x0001] = "SupportedSensitivityLevels", + [0x0002] = "DefaultSensitivityLevel", + [0x0003] = "AlarmsActive", + [0x0004] = "AlarmsSuppressed", + [0x0005] = "AlarmsEnabled", + [0x0006] = "AlarmsSupported", + [0x0007] = "SensorFault", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +BooleanStateConfiguration.attribute_direction_map = { + ["CurrentSensitivityLevel"] = "server", + ["SupportedSensitivityLevels"] = "server", + ["DefaultSensitivityLevel"] = "server", + ["AlarmsActive"] = "server", + ["AlarmsSuppressed"] = "server", + ["AlarmsEnabled"] = "server", + ["AlarmsSupported"] = "server", + ["SensorFault"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +do + local has_aliases, aliases = pcall(require, "BooleanStateConfiguration.server.attributes") + if has_aliases then + for alias, _ in pairs(aliases) do + BooleanStateConfiguration.attribute_direction_map[alias] = "server" + end + end +end + +BooleanStateConfiguration.FeatureMap = BooleanStateConfiguration.types.Feature + +function BooleanStateConfiguration.are_features_supported(feature, feature_map) + if (BooleanStateConfiguration.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = BooleanStateConfiguration.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, BooleanStateConfiguration.NAME)) + end + return BooleanStateConfiguration[direction].attributes[key] +end +BooleanStateConfiguration.attributes = {} +setmetatable(BooleanStateConfiguration.attributes, attribute_helper_mt) + +setmetatable(BooleanStateConfiguration, {__index = cluster_base}) + +return BooleanStateConfiguration + diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua similarity index 95% rename from drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua index ea28550422..b2c9b67fbb 100644 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua similarity index 94% rename from drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua index dc343735dc..760d7f397c 100644 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SensorFault.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/SensorFault.lua similarity index 87% rename from drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SensorFault.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/SensorFault.lua index 83db4bc404..3786904abf 100644 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SensorFault.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/SensorFault.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local SensorFault = { ID = 0x0007, NAME = "SensorFault", - base_type = require "BooleanStateConfiguration.types.SensorFaultBitmap", + base_type = require "embedded_clusters.BooleanStateConfiguration.types.SensorFaultBitmap", } function SensorFault:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua similarity index 94% rename from drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua index c80177d043..12bff3e4a9 100644 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/init.lua new file mode 100644 index 0000000000..daca1b7707 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/init.lua @@ -0,0 +1,28 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.BooleanStateConfiguration.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local BooleanStateConfigurationServerAttributes = {} + +function BooleanStateConfigurationServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(BooleanStateConfigurationServerAttributes, attr_mt) + +return BooleanStateConfigurationServerAttributes + + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/Feature.lua new file mode 100644 index 0000000000..dbc08cc0ed --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/Feature.lua @@ -0,0 +1,122 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.VISUAL = 0x0001 +Feature.AUDIBLE = 0x0002 +Feature.ALARM_SUPPRESS = 0x0004 +Feature.SENSITIVITY_LEVEL = 0x0008 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + VISUAL = 0x0001, + AUDIBLE = 0x0002, + ALARM_SUPPRESS = 0x0004, + SENSITIVITY_LEVEL = 0x0008, +} + +Feature.is_visual_set = function(self) + return (self.value & self.VISUAL) ~= 0 +end + +Feature.set_visual = function(self) + if self.value ~= nil then + self.value = self.value | self.VISUAL + else + self.value = self.VISUAL + end +end + +Feature.unset_visual = function(self) + self.value = self.value & (~self.VISUAL & self.BASE_MASK) +end +Feature.is_audible_set = function(self) + return (self.value & self.AUDIBLE) ~= 0 +end + +Feature.set_audible = function(self) + if self.value ~= nil then + self.value = self.value | self.AUDIBLE + else + self.value = self.AUDIBLE + end +end + +Feature.unset_audible = function(self) + self.value = self.value & (~self.AUDIBLE & self.BASE_MASK) +end + +Feature.is_alarm_suppress_set = function(self) + return (self.value & self.ALARM_SUPPRESS) ~= 0 +end + +Feature.set_alarm_suppress = function(self) + if self.value ~= nil then + self.value = self.value | self.ALARM_SUPPRESS + else + self.value = self.ALARM_SUPPRESS + end +end + +Feature.unset_alarm_suppress = function(self) + self.value = self.value & (~self.ALARM_SUPPRESS & self.BASE_MASK) +end + +Feature.is_sensitivity_level_set = function(self) + return (self.value & self.SENSITIVITY_LEVEL) ~= 0 +end + +Feature.set_sensitivity_level = function(self) + if self.value ~= nil then + self.value = self.value | self.SENSITIVITY_LEVEL + else + self.value = self.SENSITIVITY_LEVEL + end +end + +Feature.unset_sensitivity_level = function(self) + self.value = self.value & (~self.SENSITIVITY_LEVEL & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.VISUAL | + Feature.AUDIBLE | + Feature.ALARM_SUPPRESS | + Feature.SENSITIVITY_LEVEL + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_visual_set = Feature.is_visual_set, + set_visual = Feature.set_visual, + unset_visual = Feature.unset_visual, + is_audible_set = Feature.is_audible_set, + set_audible = Feature.set_audible, + unset_audible = Feature.unset_audible, + is_alarm_suppress_set = Feature.is_alarm_suppress_set, + set_alarm_suppress = Feature.set_alarm_suppress, + unset_alarm_suppress = Feature.unset_alarm_suppress, + is_sensitivity_level_set = Feature.is_sensitivity_level_set, + set_sensitivity_level = Feature.set_sensitivity_level, + unset_sensitivity_level = Feature.unset_sensitivity_level, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/SensorFaultBitmap.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/SensorFaultBitmap.lua similarity index 93% rename from drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/SensorFaultBitmap.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/SensorFaultBitmap.lua index c9399e50f1..9661734152 100644 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/SensorFaultBitmap.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/SensorFaultBitmap.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/init.lua new file mode 100644 index 0000000000..bc6835b4ac --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/init.lua @@ -0,0 +1,17 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.BooleanStateConfiguration.types." .. key) + end + return types_mt.__types_cache[key] +end + +local BooleanStateConfigurationTypes = {} + +setmetatable(BooleanStateConfigurationTypes, types_mt) + +return BooleanStateConfigurationTypes diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..4de97147e4 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local CarbonDioxideConcentrationMeasurementServerAttributes = require "embedded_clusters.CarbonDioxideConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local CarbonDioxideConcentrationMeasurement = {} + +CarbonDioxideConcentrationMeasurement.ID = 0x040D +CarbonDioxideConcentrationMeasurement.NAME = "CarbonDioxideConcentrationMeasurement" +CarbonDioxideConcentrationMeasurement.server = {} +CarbonDioxideConcentrationMeasurement.client = {} +CarbonDioxideConcentrationMeasurement.server.attributes = CarbonDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(CarbonDioxideConcentrationMeasurement) +CarbonDioxideConcentrationMeasurement.types = ConcentrationMeasurement.types + +function CarbonDioxideConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function CarbonDioxideConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +CarbonDioxideConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +CarbonDioxideConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function CarbonDioxideConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = CarbonDioxideConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, CarbonDioxideConcentrationMeasurement.NAME)) + end + return CarbonDioxideConcentrationMeasurement[direction].attributes[key] +end +CarbonDioxideConcentrationMeasurement.attributes = {} +setmetatable(CarbonDioxideConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(CarbonDioxideConcentrationMeasurement, {__index = cluster_base}) + +return CarbonDioxideConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..0206213e6f --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.CarbonDioxideConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local CarbonDioxideConcentrationMeasurementServerAttributes = {} + +function CarbonDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(CarbonDioxideConcentrationMeasurementServerAttributes, attr_mt) + +return CarbonDioxideConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..a6e1f24d1d --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local CarbonMonoxideConcentrationMeasurementServerAttributes = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local CarbonMonoxideConcentrationMeasurement = {} + +CarbonMonoxideConcentrationMeasurement.ID = 0x040C +CarbonMonoxideConcentrationMeasurement.NAME = "CarbonMonoxideConcentrationMeasurement" +CarbonMonoxideConcentrationMeasurement.server = {} +CarbonMonoxideConcentrationMeasurement.client = {} +CarbonMonoxideConcentrationMeasurement.server.attributes = CarbonMonoxideConcentrationMeasurementServerAttributes:set_parent_cluster(CarbonMonoxideConcentrationMeasurement) +CarbonMonoxideConcentrationMeasurement.types = ConcentrationMeasurement.types + +function CarbonMonoxideConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function CarbonMonoxideConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +CarbonMonoxideConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +CarbonMonoxideConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function CarbonMonoxideConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = CarbonMonoxideConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, CarbonMonoxideConcentrationMeasurement.NAME)) + end + return CarbonMonoxideConcentrationMeasurement[direction].attributes[key] +end +CarbonMonoxideConcentrationMeasurement.attributes = {} +setmetatable(CarbonMonoxideConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(CarbonMonoxideConcentrationMeasurement, {__index = cluster_base}) + +return CarbonMonoxideConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..1a7e7b508c --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.CarbonMonoxideConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local CarbonMonoxideConcentrationMeasurementServerAttributes = {} + +function CarbonMonoxideConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(CarbonMonoxideConcentrationMeasurementServerAttributes, attr_mt) + +return CarbonMonoxideConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..cb4cffa2d2 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/init.lua @@ -0,0 +1,111 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local ConcentrationMeasurementServerAttributes = require "embedded_clusters.ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurementTypes = require "embedded_clusters.ConcentrationMeasurement.types" + +local ConcentrationMeasurement = {} + +ConcentrationMeasurement.ID = 0x040C +ConcentrationMeasurement.NAME = "CarbonMonoxideConcentrationMeasurement" +ConcentrationMeasurement.server = {} +ConcentrationMeasurement.client = {} +ConcentrationMeasurement.server.attributes = ConcentrationMeasurementServerAttributes:set_parent_cluster(ConcentrationMeasurement) +ConcentrationMeasurement.types = ConcentrationMeasurementTypes + +function ConcentrationMeasurement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "MeasuredValue", + [0x0001] = "MinMeasuredValue", + [0x0002] = "MaxMeasuredValue", + [0x0003] = "PeakMeasuredValue", + [0x0004] = "PeakMeasuredValueWindow", + [0x0005] = "AverageMeasuredValue", + [0x0006] = "AverageMeasuredValueWindow", + [0x0007] = "Uncertainty", + [0x0008] = "MeasurementUnit", + [0x0009] = "MeasurementMedium", + [0x000A] = "LevelValue", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function ConcentrationMeasurement:get_server_command_by_id(command_id) + local server_id_map = { + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +ConcentrationMeasurement.attribute_direction_map = { + ["MeasuredValue"] = "server", + ["MinMeasuredValue"] = "server", + ["MaxMeasuredValue"] = "server", + ["PeakMeasuredValue"] = "server", + ["PeakMeasuredValueWindow"] = "server", + ["AverageMeasuredValue"] = "server", + ["AverageMeasuredValueWindow"] = "server", + ["Uncertainty"] = "server", + ["MeasurementUnit"] = "server", + ["MeasurementMedium"] = "server", + ["LevelValue"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +ConcentrationMeasurement.command_direction_map = { +} + +ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function ConcentrationMeasurement.are_features_supported(feature, feature_map) + if (ConcentrationMeasurement.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ConcentrationMeasurement.NAME)) + end + return ConcentrationMeasurement[direction].attributes[key] +end +ConcentrationMeasurement.attributes = {} +setmetatable(ConcentrationMeasurement.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = ConcentrationMeasurement.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, ConcentrationMeasurement.NAME)) + end + return ConcentrationMeasurement[direction].commands[key] +end +ConcentrationMeasurement.commands = {} +setmetatable(ConcentrationMeasurement.commands, command_helper_mt) + +local event_helper_mt = {} +event_helper_mt.__index = function(self, key) + return ConcentrationMeasurement.server.events[key] +end +ConcentrationMeasurement.events = {} +setmetatable(ConcentrationMeasurement.events, event_helper_mt) + +setmetatable(ConcentrationMeasurement, {__index = cluster_base}) + +return ConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..e4f88c8491 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,73 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function LevelValue:read(device, endpoint_id, cluster_id) + return cluster_base.read( + device, + endpoint_id, + cluster_id, + self.ID, + nil --event_id + ) +end + + +function LevelValue:subscribe(device, endpoint_id, cluster_id) + return cluster_base.subscribe( + device, + endpoint_id, + cluster_id, + self.ID, + nil --event_id + ) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status, + cluster_id +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + cluster_id, + self.ID, + data, + status + ) +end + +function LevelValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..2ab739841f --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,73 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function MeasuredValue:read(device, endpoint_id, cluster_id) + return cluster_base.read( + device, + endpoint_id, + cluster_id, + self.ID, + nil --event_id + ) +end + + +function MeasuredValue:subscribe(device, endpoint_id, cluster_id) + return cluster_base.subscribe( + device, + endpoint_id, + cluster_id, + self.ID, + nil --event_id + ) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status, + cluster_id +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + cluster_id, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..0fa14745c0 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,72 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function MeasurementUnit:read(device, endpoint_id, cluster_id) + return cluster_base.read( + device, + endpoint_id, + cluster_id, + self.ID, + nil --event_id + ) +end + +function MeasurementUnit:subscribe(device, endpoint_id, cluster_id) + return cluster_base.subscribe( + device, + endpoint_id, + cluster_id, + self.ID, + nil --event_id + ) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status, + cluster_id +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + cluster_id, + self.ID, + data, + status + ) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..19cde9aa55 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.ConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local ConcentrationMeasurementServerAttributes = {} + +function ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ConcentrationMeasurementServerAttributes, attr_mt) + +return ConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/Feature.lua new file mode 100644 index 0000000000..0bb19bec62 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/Feature.lua @@ -0,0 +1,167 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.NUMERIC_MEASUREMENT = 0x0001 +Feature.LEVEL_INDICATION = 0x0002 +Feature.MEDIUM_LEVEL = 0x0004 +Feature.CRITICAL_LEVEL = 0x0008 +Feature.PEAK_MEASUREMENT = 0x0010 +Feature.AVERAGE_MEASUREMENT = 0x0020 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + NUMERIC_MEASUREMENT = 0x0001, + LEVEL_INDICATION = 0x0002, + MEDIUM_LEVEL = 0x0004, + CRITICAL_LEVEL = 0x0008, + PEAK_MEASUREMENT = 0x0010, + AVERAGE_MEASUREMENT = 0x0020, +} + +Feature.is_numeric_measurement_set = function(self) + return (self.value & self.NUMERIC_MEASUREMENT) ~= 0 +end + +Feature.set_numeric_measurement = function(self) + if self.value ~= nil then + self.value = self.value | self.NUMERIC_MEASUREMENT + else + self.value = self.NUMERIC_MEASUREMENT + end +end + +Feature.unset_numeric_measurement = function(self) + self.value = self.value & (~self.NUMERIC_MEASUREMENT & self.BASE_MASK) +end + +Feature.is_level_indication_set = function(self) + return (self.value & self.LEVEL_INDICATION) ~= 0 +end + +Feature.set_level_indication = function(self) + if self.value ~= nil then + self.value = self.value | self.LEVEL_INDICATION + else + self.value = self.LEVEL_INDICATION + end +end + +Feature.unset_level_indication = function(self) + self.value = self.value & (~self.LEVEL_INDICATION & self.BASE_MASK) +end + +Feature.is_medium_level_set = function(self) + return (self.value & self.MEDIUM_LEVEL) ~= 0 +end + +Feature.set_medium_level = function(self) + if self.value ~= nil then + self.value = self.value | self.MEDIUM_LEVEL + else + self.value = self.MEDIUM_LEVEL + end +end + +Feature.unset_medium_level = function(self) + self.value = self.value & (~self.MEDIUM_LEVEL & self.BASE_MASK) +end + +Feature.is_critical_level_set = function(self) + return (self.value & self.CRITICAL_LEVEL) ~= 0 +end + +Feature.set_critical_level = function(self) + if self.value ~= nil then + self.value = self.value | self.CRITICAL_LEVEL + else + self.value = self.CRITICAL_LEVEL + end +end + +Feature.unset_critical_level = function(self) + self.value = self.value & (~self.CRITICAL_LEVEL & self.BASE_MASK) +end + +Feature.is_peak_measurement_set = function(self) + return (self.value & self.PEAK_MEASUREMENT) ~= 0 +end + +Feature.set_peak_measurement = function(self) + if self.value ~= nil then + self.value = self.value | self.PEAK_MEASUREMENT + else + self.value = self.PEAK_MEASUREMENT + end +end + +Feature.unset_peak_measurement = function(self) + self.value = self.value & (~self.PEAK_MEASUREMENT & self.BASE_MASK) +end + +Feature.is_average_measurement_set = function(self) + return (self.value & self.AVERAGE_MEASUREMENT) ~= 0 +end + +Feature.set_average_measurement = function(self) + if self.value ~= nil then + self.value = self.value | self.AVERAGE_MEASUREMENT + else + self.value = self.AVERAGE_MEASUREMENT + end +end + +Feature.unset_average_measurement = function(self) + self.value = self.value & (~self.AVERAGE_MEASUREMENT & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.NUMERIC_MEASUREMENT | + Feature.LEVEL_INDICATION | + Feature.MEDIUM_LEVEL | + Feature.CRITICAL_LEVEL | + Feature.PEAK_MEASUREMENT | + Feature.AVERAGE_MEASUREMENT + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_numeric_measurement_set = Feature.is_numeric_measurement_set, + set_numeric_measurement = Feature.set_numeric_measurement, + unset_numeric_measurement = Feature.unset_numeric_measurement, + is_level_indication_set = Feature.is_level_indication_set, + set_level_indication = Feature.set_level_indication, + unset_level_indication = Feature.unset_level_indication, + is_medium_level_set = Feature.is_medium_level_set, + set_medium_level = Feature.set_medium_level, + unset_medium_level = Feature.unset_medium_level, + is_critical_level_set = Feature.is_critical_level_set, + set_critical_level = Feature.set_critical_level, + unset_critical_level = Feature.unset_critical_level, + is_peak_measurement_set = Feature.is_peak_measurement_set, + set_peak_measurement = Feature.set_peak_measurement, + unset_peak_measurement = Feature.unset_peak_measurement, + is_average_measurement_set = Feature.is_average_measurement_set, + set_average_measurement = Feature.set_average_measurement, + unset_average_measurement = Feature.unset_average_measurement, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/LevelValueEnum.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/LevelValueEnum.lua new file mode 100644 index 0000000000..02b4f727df --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/LevelValueEnum.lua @@ -0,0 +1,42 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local LevelValueEnum = {} +-- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility +-- with how types were handled in api < 10. +local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.UNKNOWN] = "UNKNOWN", + [self.LOW] = "LOW", + [self.MEDIUM] = "MEDIUM", + [self.HIGH] = "HIGH", + [self.CRITICAL] = "CRITICAL", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.UNKNOWN = 0x00 +new_mt.__index.LOW = 0x01 +new_mt.__index.MEDIUM = 0x02 +new_mt.__index.HIGH = 0x03 +new_mt.__index.CRITICAL = 0x04 + +LevelValueEnum.UNKNOWN = 0x00 +LevelValueEnum.LOW = 0x01 +LevelValueEnum.MEDIUM = 0x02 +LevelValueEnum.HIGH = 0x03 +LevelValueEnum.CRITICAL = 0x04 + +LevelValueEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(LevelValueEnum, new_mt) + +return LevelValueEnum + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/MeasurementUnitEnum.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/MeasurementUnitEnum.lua new file mode 100644 index 0000000000..6efd90901a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/MeasurementUnitEnum.lua @@ -0,0 +1,51 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local MeasurementUnitEnum = {} +-- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility +-- with how types were handled in api < 10. +local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.PPM] = "PPM", + [self.PPB] = "PPB", + [self.PPT] = "PPT", + [self.MGM3] = "MGM3", + [self.UGM3] = "UGM3", + [self.NGM3] = "NGM3", + [self.PM3] = "PM3", + [self.BQM3] = "BQM3", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.PPM = 0x00 +new_mt.__index.PPB = 0x01 +new_mt.__index.PPT = 0x02 +new_mt.__index.MGM3 = 0x03 +new_mt.__index.UGM3 = 0x04 +new_mt.__index.NGM3 = 0x05 +new_mt.__index.PM3 = 0x06 +new_mt.__index.BQM3 = 0x07 + +MeasurementUnitEnum.PPM = 0x00 +MeasurementUnitEnum.PPB = 0x01 +MeasurementUnitEnum.PPT = 0x02 +MeasurementUnitEnum.MGM3 = 0x03 +MeasurementUnitEnum.UGM3 = 0x04 +MeasurementUnitEnum.NGM3 = 0x05 +MeasurementUnitEnum.PM3 = 0x06 +MeasurementUnitEnum.BQM3 = 0x07 + +MeasurementUnitEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(MeasurementUnitEnum, new_mt) + +return MeasurementUnitEnum + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/init.lua new file mode 100644 index 0000000000..c339f17414 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/init.lua @@ -0,0 +1,18 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.ConcentrationMeasurement.types." .. key) + end + return types_mt.__types_cache[key] +end + +local ConcentrationMeasurementTypes = {} + +setmetatable(ConcentrationMeasurementTypes, types_mt) + +return ConcentrationMeasurementTypes + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..cdfd1d597e --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local FormaldehydeConcentrationMeasurementServerAttributes = require "embedded_clusters.FormaldehydeConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local FormaldehydeConcentrationMeasurement = {} + +FormaldehydeConcentrationMeasurement.ID = 0x042B +FormaldehydeConcentrationMeasurement.NAME = "FormaldehydeConcentrationMeasurement" +FormaldehydeConcentrationMeasurement.server = {} +FormaldehydeConcentrationMeasurement.client = {} +FormaldehydeConcentrationMeasurement.server.attributes = FormaldehydeConcentrationMeasurementServerAttributes:set_parent_cluster(FormaldehydeConcentrationMeasurement) +FormaldehydeConcentrationMeasurement.types = ConcentrationMeasurement.types + +function FormaldehydeConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function FormaldehydeConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +FormaldehydeConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +FormaldehydeConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function FormaldehydeConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = FormaldehydeConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, FormaldehydeConcentrationMeasurement.NAME)) + end + return FormaldehydeConcentrationMeasurement[direction].attributes[key] +end +FormaldehydeConcentrationMeasurement.attributes = {} +setmetatable(FormaldehydeConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(FormaldehydeConcentrationMeasurement, {__index = cluster_base}) + +return FormaldehydeConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..3dee13abe3 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.FormaldehydeConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local FormaldehydeConcentrationMeasurementServerAttributes = {} + +function FormaldehydeConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(FormaldehydeConcentrationMeasurementServerAttributes, attr_mt) + +return FormaldehydeConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..eae9be65f0 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local NitrogenDioxideConcentrationMeasurementServerAttributes = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local NitrogenDioxideConcentrationMeasurement = {} + +NitrogenDioxideConcentrationMeasurement.ID = 0x0413 +NitrogenDioxideConcentrationMeasurement.NAME = "NitrogenDioxideConcentrationMeasurement" +NitrogenDioxideConcentrationMeasurement.server = {} +NitrogenDioxideConcentrationMeasurement.client = {} +NitrogenDioxideConcentrationMeasurement.server.attributes = NitrogenDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(NitrogenDioxideConcentrationMeasurement) +NitrogenDioxideConcentrationMeasurement.types = ConcentrationMeasurement.types + +function NitrogenDioxideConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function NitrogenDioxideConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +NitrogenDioxideConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +NitrogenDioxideConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function NitrogenDioxideConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = NitrogenDioxideConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, NitrogenDioxideConcentrationMeasurement.NAME)) + end + return NitrogenDioxideConcentrationMeasurement[direction].attributes[key] +end +NitrogenDioxideConcentrationMeasurement.attributes = {} +setmetatable(NitrogenDioxideConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(NitrogenDioxideConcentrationMeasurement, {__index = cluster_base}) + +return NitrogenDioxideConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..c82517d362 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.NitrogenDioxideConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local NitrogenDioxideConcentrationMeasurementServerAttributes = {} + +function NitrogenDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(NitrogenDioxideConcentrationMeasurementServerAttributes, attr_mt) + +return NitrogenDioxideConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..c49ea94b39 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local OzoneConcentrationMeasurementServerAttributes = require "embedded_clusters.OzoneConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local OzoneConcentrationMeasurement = {} + +OzoneConcentrationMeasurement.ID = 0x0415 +OzoneConcentrationMeasurement.NAME = "OzoneConcentrationMeasurement" +OzoneConcentrationMeasurement.server = {} +OzoneConcentrationMeasurement.client = {} +OzoneConcentrationMeasurement.server.attributes = OzoneConcentrationMeasurementServerAttributes:set_parent_cluster(OzoneConcentrationMeasurement) +OzoneConcentrationMeasurement.types = ConcentrationMeasurement.types + +function OzoneConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function OzoneConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +OzoneConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +OzoneConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function OzoneConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = OzoneConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, OzoneConcentrationMeasurement.NAME)) + end + return OzoneConcentrationMeasurement[direction].attributes[key] +end +OzoneConcentrationMeasurement.attributes = {} +setmetatable(OzoneConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(OzoneConcentrationMeasurement, {__index = cluster_base}) + +return OzoneConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..918b680495 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.OzoneConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local OzoneConcentrationMeasurementServerAttributes = {} + +function OzoneConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(OzoneConcentrationMeasurementServerAttributes, attr_mt) + +return OzoneConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..3b333b5417 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local Pm10ConcentrationMeasurementServerAttributes = require "embedded_clusters.Pm10ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local Pm10ConcentrationMeasurement = {} + +Pm10ConcentrationMeasurement.ID = 0x042D +Pm10ConcentrationMeasurement.NAME = "Pm10ConcentrationMeasurement" +Pm10ConcentrationMeasurement.server = {} +Pm10ConcentrationMeasurement.client = {} +Pm10ConcentrationMeasurement.server.attributes = Pm10ConcentrationMeasurementServerAttributes:set_parent_cluster(Pm10ConcentrationMeasurement) +Pm10ConcentrationMeasurement.types = ConcentrationMeasurement.types + +function Pm10ConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function Pm10ConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +Pm10ConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +Pm10ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function Pm10ConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = Pm10ConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, Pm10ConcentrationMeasurement.NAME)) + end + return Pm10ConcentrationMeasurement[direction].attributes[key] +end +Pm10ConcentrationMeasurement.attributes = {} +setmetatable(Pm10ConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(Pm10ConcentrationMeasurement, {__index = cluster_base}) + +return Pm10ConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..3b1e6617a4 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.Pm10ConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local Pm10ConcentrationMeasurementServerAttributes = {} + +function Pm10ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(Pm10ConcentrationMeasurementServerAttributes, attr_mt) + +return Pm10ConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..b2e6656a9a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local Pm1ConcentrationMeasurementServerAttributes = require "embedded_clusters.Pm1ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local Pm1ConcentrationMeasurement = {} + +Pm1ConcentrationMeasurement.ID = 0x042C +Pm1ConcentrationMeasurement.NAME = "Pm1ConcentrationMeasurement" +Pm1ConcentrationMeasurement.server = {} +Pm1ConcentrationMeasurement.client = {} +Pm1ConcentrationMeasurement.server.attributes = Pm1ConcentrationMeasurementServerAttributes:set_parent_cluster(Pm1ConcentrationMeasurement) +Pm1ConcentrationMeasurement.types = ConcentrationMeasurement.types + +function Pm1ConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function Pm1ConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +Pm1ConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +Pm1ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function Pm1ConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = Pm1ConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, Pm1ConcentrationMeasurement.NAME)) + end + return Pm1ConcentrationMeasurement[direction].attributes[key] +end +Pm1ConcentrationMeasurement.attributes = {} +setmetatable(Pm1ConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(Pm1ConcentrationMeasurement, {__index = cluster_base}) + +return Pm1ConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..2635da32a6 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.Pm1ConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local Pm1ConcentrationMeasurementServerAttributes = {} + +function Pm1ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(Pm1ConcentrationMeasurementServerAttributes, attr_mt) + +return Pm1ConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..e6e6144f94 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local Pm25ConcentrationMeasurementServerAttributes = require "embedded_clusters.Pm25ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local Pm25ConcentrationMeasurement = {} + +Pm25ConcentrationMeasurement.ID = 0x042A +Pm25ConcentrationMeasurement.NAME = "Pm25ConcentrationMeasurement" +Pm25ConcentrationMeasurement.server = {} +Pm25ConcentrationMeasurement.client = {} +Pm25ConcentrationMeasurement.server.attributes = Pm25ConcentrationMeasurementServerAttributes:set_parent_cluster(Pm25ConcentrationMeasurement) +Pm25ConcentrationMeasurement.types = ConcentrationMeasurement.types + +function Pm25ConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function Pm25ConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +Pm25ConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +Pm25ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function Pm25ConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = Pm25ConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, Pm25ConcentrationMeasurement.NAME)) + end + return Pm25ConcentrationMeasurement[direction].attributes[key] +end +Pm25ConcentrationMeasurement.attributes = {} +setmetatable(Pm25ConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(Pm25ConcentrationMeasurement, {__index = cluster_base}) + +return Pm25ConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..5c432da0ec --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.Pm25ConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local Pm25ConcentrationMeasurementServerAttributes = {} + +function Pm25ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(Pm25ConcentrationMeasurementServerAttributes, attr_mt) + +return Pm25ConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/init.lua new file mode 100644 index 0000000000..42b058eb02 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/init.lua @@ -0,0 +1,133 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local cluster_base = require "st.matter.cluster_base" +local PressureMeasurementServerAttributes = require "embedded_clusters.PressureMeasurement.server.attributes" +local PressureMeasurementServerCommands = require "embedded_clusters.PressureMeasurement.server.commands" +local PressureMeasurementTypes = require "embedded_clusters.PressureMeasurement.types" + +--- @class PressureMeasurement +--- @alias PressureMeasurement +--- +--- @field public ID number 0x0403 the ID of this cluster +--- @field public NAME string "PressureMeasurement" the name of this cluster +--- @field public attributes PressureMeasurementServerAttributes | PressureMeasurementClientAttributes +--- @field public commands PressureMeasurementServerCommands | PressureMeasurementClientCommands +--- @field public types PressureMeasurementTypes + +local PressureMeasurement = {} + +PressureMeasurement.ID = 0x0403 +PressureMeasurement.NAME = "PressureMeasurement" +PressureMeasurement.server = {} +PressureMeasurement.client = {} +PressureMeasurement.server.attributes = PressureMeasurementServerAttributes:set_parent_cluster(PressureMeasurement) +PressureMeasurement.server.commands = PressureMeasurementServerCommands:set_parent_cluster(PressureMeasurement) +PressureMeasurement.types = PressureMeasurementTypes + + +--- Find an attribute by id +--- +--- @param attr_id number +function PressureMeasurement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "MeasuredValue", + [0x0001] = "MinMeasuredValue", + [0x0002] = "MaxMeasuredValue", + [0x0003] = "Tolerance", + [0x0010] = "ScaledValue", + [0x0011] = "MinScaledValue", + [0x0012] = "MaxScaledValue", + [0x0013] = "ScaledTolerance", + [0x0014] = "Scale", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +--- Find a server command by id +--- +--- @param command_id number +function PressureMeasurement:get_server_command_by_id(command_id) + local server_id_map = { + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + + +-- Attribute Mapping +PressureMeasurement.attribute_direction_map = { + ["MeasuredValue"] = "server", + ["MinMeasuredValue"] = "server", + ["MaxMeasuredValue"] = "server", + ["Tolerance"] = "server", + ["ScaledValue"] = "server", + ["MinScaledValue"] = "server", + ["MaxScaledValue"] = "server", + ["ScaledTolerance"] = "server", + ["Scale"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + + +-- Command Mapping +PressureMeasurement.command_direction_map = { +} + + +PressureMeasurement.FeatureMap = PressureMeasurement.types.Feature + +function PressureMeasurement.are_features_supported(feature, feature_map) + if (PressureMeasurement.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +-- Cluster Completion +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = PressureMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, PressureMeasurement.NAME)) + end + return PressureMeasurement[direction].attributes[key] +end +PressureMeasurement.attributes = {} +setmetatable(PressureMeasurement.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = PressureMeasurement.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, PressureMeasurement.NAME)) + end + return PressureMeasurement[direction].commands[key] +end +PressureMeasurement.commands = {} +setmetatable(PressureMeasurement.commands, command_helper_mt) + +local event_helper_mt = {} +event_helper_mt.__index = function(self, key) + return PressureMeasurement.server.events[key] +end +PressureMeasurement.events = {} +setmetatable(PressureMeasurement.events, event_helper_mt) + +setmetatable(PressureMeasurement, {__index = cluster_base}) + +return PressureMeasurement diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AcceptedCommandList.lua new file mode 100644 index 0000000000..dc301eba35 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AcceptedCommandList.lua @@ -0,0 +1,115 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +--- @class st.matter.clusters.PressureMeasurement.AcceptedCommandList +--- @alias AcceptedCommandList +--- +--- @field public ID number 0xFFF9 the ID of this attribute +--- @field public NAME string "AcceptedCommandList" the name of this attribute +--- @field public data_type st.matter.data_types.Array the data type of this attribute + +local AcceptedCommandList = { + ID = 0xFFF9, + NAME = "AcceptedCommandList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +--- Add additional functionality to the base type object +--- +--- @param base_type_obj st.matter.data_types.Array the base data type object to add functionality to +function AcceptedCommandList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, AcceptedCommandList.element_type) + end +end + +--- Create a Array object of this attribute with any additional features provided for the attribute +--- This is also usable with the AcceptedCommandList(...) syntax +--- +--- @vararg vararg the values needed to construct a Array +--- @return st.matter.data_types.Array +function AcceptedCommandList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +--- Constructs an st.matter.interaction_model.InteractionRequest to read +--- this attribute from a device +--- @param device st.matter.Device +--- @param endpoint_id number|nil +--- @return st.matter.interaction_model.InteractionRequest containing an Interaction Request +function AcceptedCommandList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + + +--- Reporting policy: AcceptedCommandList => true => mandatory + +--- Sets up a Subscribe Interaction +--- +--- @param device any +--- @param endpoint_id number|nil +--- @return any +function AcceptedCommandList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +--- Builds an AcceptedCommandList test attribute reponse for the driver integration testing framework +--- +--- @param device st.matter.Device the device to build this message for +--- @param endpoint_id number|nil +--- @param value any +--- @param status string Interaction status associated with the path +--- @return st.matter.interaction_model.InteractionResponse of type REPORT_DATA +function AcceptedCommandList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AcceptedCommandList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) +return AcceptedCommandList diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AttributeList.lua new file mode 100644 index 0000000000..deba9d030f --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AttributeList.lua @@ -0,0 +1,115 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +--- @class st.matter.clusters.PressureMeasurement.AttributeList +--- @alias AttributeList +--- +--- @field public ID number 0xFFFB the ID of this attribute +--- @field public NAME string "AttributeList" the name of this attribute +--- @field public data_type st.matter.data_types.Array the data type of this attribute + +local AttributeList = { + ID = 0xFFFB, + NAME = "AttributeList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +--- Add additional functionality to the base type object +--- +--- @param base_type_obj st.matter.data_types.Array the base data type object to add functionality to +function AttributeList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, AttributeList.element_type) + end +end + +--- Create a Array object of this attribute with any additional features provided for the attribute +--- This is also usable with the AttributeList(...) syntax +--- +--- @vararg vararg the values needed to construct a Array +--- @return st.matter.data_types.Array +function AttributeList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +--- Constructs an st.matter.interaction_model.InteractionRequest to read +--- this attribute from a device +--- @param device st.matter.Device +--- @param endpoint_id number|nil +--- @return st.matter.interaction_model.InteractionRequest containing an Interaction Request +function AttributeList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + + +--- Reporting policy: AttributeList => true => mandatory + +--- Sets up a Subscribe Interaction +--- +--- @param device any +--- @param endpoint_id number|nil +--- @return any +function AttributeList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AttributeList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +--- Builds an AttributeList test attribute reponse for the driver integration testing framework +--- +--- @param device st.matter.Device the device to build this message for +--- @param endpoint_id number|nil +--- @param value any +--- @param status string Interaction status associated with the path +--- @return st.matter.interaction_model.InteractionResponse of type REPORT_DATA +function AttributeList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AttributeList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) +return AttributeList diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/EventList.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/EventList.lua new file mode 100644 index 0000000000..f6040a17a0 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/EventList.lua @@ -0,0 +1,115 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +--- @class st.matter.clusters.PressureMeasurement.EventList +--- @alias EventList +--- +--- @field public ID number 0xFFFA the ID of this attribute +--- @field public NAME string "EventList" the name of this attribute +--- @field public data_type st.matter.data_types.Array the data type of this attribute + +local EventList = { + ID = 0xFFFA, + NAME = "EventList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +--- Add additional functionality to the base type object +--- +--- @param base_type_obj st.matter.data_types.Array the base data type object to add functionality to +function EventList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, EventList.element_type) + end +end + +--- Create a Array object of this attribute with any additional features provided for the attribute +--- This is also usable with the EventList(...) syntax +--- +--- @vararg vararg the values needed to construct a Array +--- @return st.matter.data_types.Array +function EventList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +--- Constructs an st.matter.interaction_model.InteractionRequest to read +--- this attribute from a device +--- @param device st.matter.Device +--- @param endpoint_id number|nil +--- @return st.matter.interaction_model.InteractionRequest containing an Interaction Request +function EventList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + + +--- Reporting policy: EventList => true => mandatory + +--- Sets up a Subscribe Interaction +--- +--- @param device any +--- @param endpoint_id number|nil +--- @return any +function EventList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function EventList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +--- Builds an EventList test attribute reponse for the driver integration testing framework +--- +--- @param device st.matter.Device the device to build this message for +--- @param endpoint_id number|nil +--- @param value any +--- @param status string Interaction status associated with the path +--- @return st.matter.interaction_model.InteractionResponse of type REPORT_DATA +function EventList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function EventList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(EventList, {__call = EventList.new_value, __index = EventList.base_type}) +return EventList diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MaxMeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxMeasuredValue.lua similarity index 83% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MaxMeasuredValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxMeasuredValue.lua index 2484d99ef3..7bbf34d9e5 100644 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MaxMeasuredValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxMeasuredValue.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(MaxMeasuredValue, {__call = MaxMeasuredValue.new_value, __index = MaxMeasuredValue.base_type}) return MaxMeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MaxScaledValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxScaledValue.lua similarity index 83% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MaxScaledValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxScaledValue.lua index 56fe3132f9..91e5f4edc4 100644 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MaxScaledValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxScaledValue.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(MaxScaledValue, {__call = MaxScaledValue.new_value, __index = MaxScaledValue.base_type}) return MaxScaledValue - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..b7c0484722 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,105 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +--- @class st.matter.clusters.PressureMeasurement.MeasuredValue +--- @alias MeasuredValue +--- +--- @field public ID number 0x0000 the ID of this attribute +--- @field public NAME string "MeasuredValue" the name of this attribute +--- @field public data_type st.matter.data_types.Int16 the data type of this attribute + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.Int16", +} + +--- Create a Int16 object of this attribute with any additional features provided for the attribute +--- This is also usable with the MeasuredValue(...) syntax +--- +--- @vararg vararg the values needed to construct a Int16 +--- @return st.matter.data_types.Int16 +function MeasuredValue:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +--- Constructs an st.matter.interaction_model.InteractionRequest to read +--- this attribute from a device +--- @param device st.matter.Device +--- @param endpoint_id number|nil +--- @return st.matter.interaction_model.InteractionRequest containing an Interaction Request +function MeasuredValue:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + + +--- Reporting policy: MeasuredValue => true => suggested + +--- Sets up a Subscribe Interaction +--- +--- @param device any +--- @param endpoint_id number|nil +--- @return any +function MeasuredValue:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +--- Builds an MeasuredValue test attribute reponse for the driver integration testing framework +--- +--- @param device st.matter.Device the device to build this message for +--- @param endpoint_id number|nil +--- @param value any +--- @param status string Interaction status associated with the path +--- @return st.matter.interaction_model.InteractionResponse of type REPORT_DATA +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MinMeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinMeasuredValue.lua similarity index 83% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MinMeasuredValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinMeasuredValue.lua index 35ec62854f..ed4e421c77 100644 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MinMeasuredValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinMeasuredValue.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(MinMeasuredValue, {__call = MinMeasuredValue.new_value, __index = MinMeasuredValue.base_type}) return MinMeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MinScaledValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinScaledValue.lua similarity index 83% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MinScaledValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinScaledValue.lua index 97bb0d44df..552ae6160a 100644 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MinScaledValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinScaledValue.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(MinScaledValue, {__call = MinScaledValue.new_value, __index = MinScaledValue.base_type}) return MinScaledValue - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/Scale.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Scale.lua similarity index 82% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/Scale.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Scale.lua index 5533e4e486..085153bd7a 100644 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/Scale.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Scale.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(Scale, {__call = Scale.new_value, __index = Scale.base_type}) return Scale - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/ScaledTolerance.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledTolerance.lua similarity index 83% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/ScaledTolerance.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledTolerance.lua index d00d2a4195..4c14d72ea3 100644 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/ScaledTolerance.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledTolerance.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(ScaledTolerance, {__call = ScaledTolerance.new_value, __index = ScaledTolerance.base_type}) return ScaledTolerance - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/ScaledValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledValue.lua similarity index 82% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/ScaledValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledValue.lua index f2e513477b..28cfe27a7a 100644 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/ScaledValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledValue.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(ScaledValue, {__call = ScaledValue.new_value, __index = ScaledValue.base_type}) return ScaledValue - diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/Tolerance.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Tolerance.lua similarity index 82% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/Tolerance.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Tolerance.lua index 55a39514a3..4d1b45ee48 100644 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/Tolerance.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Tolerance.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(Tolerance, {__call = Tolerance.new_value, __index = Tolerance.base_type}) return Tolerance - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..ebd4480609 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/init.lua @@ -0,0 +1,43 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.PressureMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +--- @class PressureMeasurementServerAttributes +--- +--- @field public MeasuredValue PressureMeasurement.server.attributes.MeasuredValue +--- @field public MinMeasuredValue PressureMeasurement.server.attributes.MinMeasuredValue +--- @field public MaxMeasuredValue PressureMeasurement.server.attributes.MaxMeasuredValue +--- @field public Tolerance PressureMeasurement.server.attributes.Tolerance +--- @field public ScaledValue PressureMeasurement.server.attributes.ScaledValue +--- @field public MinScaledValue PressureMeasurement.server.attributes.MinScaledValue +--- @field public MaxScaledValue PressureMeasurement.server.attributes.MaxScaledValue +--- @field public ScaledTolerance PressureMeasurement.server.attributes.ScaledTolerance +--- @field public Scale PressureMeasurement.server.attributes.Scale +--- @field public AcceptedCommandList PressureMeasurement.server.attributes.AcceptedCommandList +--- @field public EventList PressureMeasurement.server.attributes.EventList +--- @field public AttributeList PressureMeasurement.server.attributes.AttributeList +local PressureMeasurementServerAttributes = {} + +function PressureMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(PressureMeasurementServerAttributes, attr_mt) + +return PressureMeasurementServerAttributes diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/commands/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/commands/init.lua new file mode 100644 index 0000000000..db5c3d9d45 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/commands/init.lua @@ -0,0 +1,30 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("embedded_clusters.PressureMeasurement.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +--- @class PressureMeasurementServerCommands +--- +local PressureMeasurementServerCommands = {} + +function PressureMeasurementServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(PressureMeasurementServerCommands, command_mt) + +return PressureMeasurementServerCommands diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/events/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/events/init.lua new file mode 100644 index 0000000000..d96e0d7438 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/events/init.lua @@ -0,0 +1,31 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local event_mt = {} +event_mt.__event_cache = {} +event_mt.__index = function(self, key) + if event_mt.__event_cache[key] == nil then + local req_loc = string.format("embedded_clusters.PressureMeasurement.server.events.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + event_mt.__event_cache[key] = raw_def + end + return event_mt.__event_cache[key] +end + +--- @class PressureMeasurementEvents +--- +local PressureMeasurementEvents = {} + +function PressureMeasurementEvents:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(PressureMeasurementEvents, event_mt) + +return PressureMeasurementEvents diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/Feature.lua new file mode 100644 index 0000000000..814c5ef226 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/Feature.lua @@ -0,0 +1,70 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +--- @class st.matter.clusters.PressureMeasurement.types.Feature +--- @alias Feature +--- +--- @field public EXTENDED number 1 + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.EXTENDED = 0x0001 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + EXTENDED = 0x0001, +} + +--- @function Feature:is_extended_set +--- @return boolean True if the value of EXTENDED is non-zero +Feature.is_extended_set = function(self) + return (self.value & self.EXTENDED) ~= 0 +end + +--- @function Feature:set_extended +--- Set the value of the bit in the EXTENDED field to 1 +Feature.set_extended = function(self) + if self.value ~= nil then + self.value = self.value | self.EXTENDED + else + self.value = self.EXTENDED + end +end + +--- @function Feature:unset_extended +--- Set the value of the bits in the EXTENDED field to 0 +Feature.unset_extended = function(self) + self.value = self.value & (~self.EXTENDED & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.EXTENDED + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_extended_set = Feature.is_extended_set, + set_extended = Feature.set_extended, + unset_extended = Feature.unset_extended, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/init.lua new file mode 100644 index 0000000000..4f0c8b6bdc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/init.lua @@ -0,0 +1,24 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.PressureMeasurement.types." .. key) + end + return types_mt.__types_cache[key] +end + +--- @class PressureMeasurementTypes +--- + +--- @field public Feature PressureMeasurement.types.Feature +local PressureMeasurementTypes = {} + +setmetatable(PressureMeasurementTypes, types_mt) + +return PressureMeasurementTypes diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..3d4b21d602 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local RadonConcentrationMeasurementServerAttributes = require "embedded_clusters.RadonConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local RadonConcentrationMeasurement = {} + +RadonConcentrationMeasurement.ID = 0x042F +RadonConcentrationMeasurement.NAME = "RadonConcentrationMeasurement" +RadonConcentrationMeasurement.server = {} +RadonConcentrationMeasurement.client = {} +RadonConcentrationMeasurement.server.attributes = RadonConcentrationMeasurementServerAttributes:set_parent_cluster(RadonConcentrationMeasurement) +RadonConcentrationMeasurement.types = ConcentrationMeasurement.types + +function RadonConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function RadonConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +RadonConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +RadonConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function RadonConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = RadonConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, RadonConcentrationMeasurement.NAME)) + end + return RadonConcentrationMeasurement[direction].attributes[key] +end +RadonConcentrationMeasurement.attributes = {} +setmetatable(RadonConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(RadonConcentrationMeasurement, {__index = cluster_base}) + +return RadonConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..8aa225e510 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.RadonConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local RadonConcentrationMeasurementServerAttributes = {} + +function RadonConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(RadonConcentrationMeasurementServerAttributes, attr_mt) + +return RadonConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/init.lua new file mode 100644 index 0000000000..b492c28f18 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/init.lua @@ -0,0 +1,112 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local SmokeCoAlarmServerAttributes = require "embedded_clusters.SmokeCoAlarm.server.attributes" +local SmokeCoAlarmServerCommands = require "embedded_clusters.SmokeCoAlarm.server.commands" +local SmokeCoAlarmTypes = require "embedded_clusters.SmokeCoAlarm.types" + +local SmokeCoAlarm = {} + +SmokeCoAlarm.ID = 0x005C +SmokeCoAlarm.NAME = "SmokeCoAlarm" +SmokeCoAlarm.server = {} +SmokeCoAlarm.client = {} +SmokeCoAlarm.server.attributes = SmokeCoAlarmServerAttributes:set_parent_cluster(SmokeCoAlarm) +SmokeCoAlarm.server.commands = SmokeCoAlarmServerCommands:set_parent_cluster(SmokeCoAlarm) +SmokeCoAlarm.types = SmokeCoAlarmTypes + +function SmokeCoAlarm:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "ExpressedState", + [0x0001] = "SmokeState", + [0x0002] = "COState", + [0x0003] = "BatteryAlert", + [0x0004] = "DeviceMuted", + [0x0005] = "TestInProgress", + [0x0006] = "HardwareFaultAlert", + [0x0007] = "EndOfServiceAlert", + [0x0008] = "InterconnectSmokeAlarm", + [0x0009] = "InterconnectCOAlarm", + [0x000A] = "ContaminationState", + [0x000B] = "SmokeSensitivityLevel", + [0x000C] = "ExpiryDate", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function SmokeCoAlarm:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "SelfTestRequest", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +SmokeCoAlarm.attribute_direction_map = { + ["ExpressedState"] = "server", + ["SmokeState"] = "server", + ["COState"] = "server", + ["BatteryAlert"] = "server", + ["DeviceMuted"] = "server", + ["TestInProgress"] = "server", + ["HardwareFaultAlert"] = "server", + ["EndOfServiceAlert"] = "server", + ["InterconnectSmokeAlarm"] = "server", + ["InterconnectCOAlarm"] = "server", + ["ContaminationState"] = "server", + ["SmokeSensitivityLevel"] = "server", + ["ExpiryDate"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +SmokeCoAlarm.command_direction_map = { + ["SelfTestRequest"] = "server", +} + +SmokeCoAlarm.FeatureMap = SmokeCoAlarm.types.Feature + +function SmokeCoAlarm.are_features_supported(feature, feature_map) + if (SmokeCoAlarm.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = SmokeCoAlarm.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, SmokeCoAlarm.NAME)) + end + return SmokeCoAlarm[direction].attributes[key] +end +SmokeCoAlarm.attributes = {} +setmetatable(SmokeCoAlarm.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = SmokeCoAlarm.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, SmokeCoAlarm.NAME)) + end + return SmokeCoAlarm[direction].commands[key] +end +SmokeCoAlarm.commands = {} +setmetatable(SmokeCoAlarm.commands, command_helper_mt) + +setmetatable(SmokeCoAlarm, {__index = cluster_base}) + +return SmokeCoAlarm + diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/BatteryAlert.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/BatteryAlert.lua similarity index 89% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/BatteryAlert.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/BatteryAlert.lua index 9129921b7b..4ba713dcc4 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/BatteryAlert.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/BatteryAlert.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local BatteryAlert = { ID = 0x0003, NAME = "BatteryAlert", - base_type = require "SmokeCoAlarm.types.AlarmStateEnum", + base_type = require "embedded_clusters.SmokeCoAlarm.types.AlarmStateEnum", } function BatteryAlert:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/COState.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/COState.lua similarity index 88% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/COState.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/COState.lua index 0f99496b4f..db21d19922 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/COState.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/COState.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local COState = { ID = 0x0002, NAME = "COState", - base_type = require "SmokeCoAlarm.types.AlarmStateEnum", + base_type = require "embedded_clusters.SmokeCoAlarm.types.AlarmStateEnum", } function COState:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/HardwareFaultAlert.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/HardwareFaultAlert.lua similarity index 94% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/HardwareFaultAlert.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/HardwareFaultAlert.lua index 2a4a8ed949..2c7cfdde88 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/HardwareFaultAlert.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/HardwareFaultAlert.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/SmokeSensitivityLevel.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/SmokeSensitivityLevel.lua similarity index 91% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/SmokeSensitivityLevel.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/SmokeSensitivityLevel.lua index b193c3673e..e97da91b05 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/SmokeSensitivityLevel.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/SmokeSensitivityLevel.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local SmokeSensitivityLevel = { ID = 0x000B, NAME = "SmokeSensitivityLevel", - base_type = require "SmokeCoAlarm.types.SensitivityEnum", + base_type = require "embedded_clusters.SmokeCoAlarm.types.SensitivityEnum", } function SmokeSensitivityLevel:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/SmokeState.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/SmokeState.lua similarity index 88% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/SmokeState.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/SmokeState.lua index 08b04a9534..4879810c61 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/SmokeState.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/SmokeState.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local SmokeState = { ID = 0x0001, NAME = "SmokeState", - base_type = require "SmokeCoAlarm.types.AlarmStateEnum", + base_type = require "embedded_clusters.SmokeCoAlarm.types.AlarmStateEnum", } function SmokeState:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/TestInProgress.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/TestInProgress.lua similarity index 93% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/TestInProgress.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/TestInProgress.lua index d9ab8d2c25..42af52c87b 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/TestInProgress.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/TestInProgress.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/init.lua new file mode 100644 index 0000000000..f08cf54fd0 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.SmokeCoAlarm.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local SmokeCoAlarmServerAttributes = {} + +function SmokeCoAlarmServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(SmokeCoAlarmServerAttributes, attr_mt) + +return SmokeCoAlarmServerAttributes + diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/commands/SelfTestRequest.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/commands/SelfTestRequest.lua similarity index 96% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/commands/SelfTestRequest.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/commands/SelfTestRequest.lua index 49454a078c..e8d892d1c3 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/commands/SelfTestRequest.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/commands/SelfTestRequest.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/commands/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/commands/init.lua new file mode 100644 index 0000000000..f1ff1ab72a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/commands/init.lua @@ -0,0 +1,26 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("embedded_clusters.SmokeCoAlarm.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local SmokeCoAlarmServerCommands = {} + +function SmokeCoAlarmServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(SmokeCoAlarmServerCommands, command_mt) + +return SmokeCoAlarmServerCommands + diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/AlarmStateEnum.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/AlarmStateEnum.lua similarity index 92% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/AlarmStateEnum.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/AlarmStateEnum.lua index 7352dbcd6e..ac20cac525 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/AlarmStateEnum.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/AlarmStateEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/Feature.lua new file mode 100644 index 0000000000..1918a3be00 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/Feature.lua @@ -0,0 +1,79 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.SMOKE_ALARM = 0x0001 +Feature.CO_ALARM = 0x0002 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + SMOKE_ALARM = 0x0001, + CO_ALARM = 0x0002, +} + +Feature.is_smoke_alarm_set = function(self) + return (self.value & self.SMOKE_ALARM) ~= 0 +end + +Feature.set_smoke_alarm = function(self) + if self.value ~= nil then + self.value = self.value | self.SMOKE_ALARM + else + self.value = self.SMOKE_ALARM + end +end + +Feature.unset_smoke_alarm = function(self) + self.value = self.value & (~self.SMOKE_ALARM & self.BASE_MASK) +end + +Feature.is_co_alarm_set = function(self) + return (self.value & self.CO_ALARM) ~= 0 +end + +Feature.set_co_alarm = function(self) + if self.value ~= nil then + self.value = self.value | self.CO_ALARM + else + self.value = self.CO_ALARM + end +end + +Feature.unset_co_alarm = function(self) + self.value = self.value & (~self.CO_ALARM & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.SMOKE_ALARM | + Feature.CO_ALARM + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_smoke_alarm_set = Feature.is_smoke_alarm_set, + set_smoke_alarm = Feature.set_smoke_alarm, + unset_smoke_alarm = Feature.unset_smoke_alarm, + is_co_alarm_set = Feature.is_co_alarm_set, + set_co_alarm = Feature.set_co_alarm, + unset_co_alarm = Feature.unset_co_alarm, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/SensitivityEnum.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/SensitivityEnum.lua similarity index 91% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/SensitivityEnum.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/SensitivityEnum.lua index 4ba098ccfa..5728278657 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/SensitivityEnum.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/SensitivityEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/init.lua new file mode 100644 index 0000000000..027aaaf344 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/init.lua @@ -0,0 +1,18 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.SmokeCoAlarm.types." .. key) + end + return types_mt.__types_cache[key] +end + +local SmokeCoAlarmTypes = {} + +setmetatable(SmokeCoAlarmTypes, types_mt) + +return SmokeCoAlarmTypes + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..cad49a14e1 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local TotalVolatileOrganicCompoundsConcentrationMeasurement = {} + +TotalVolatileOrganicCompoundsConcentrationMeasurement.ID = 0x042E +TotalVolatileOrganicCompoundsConcentrationMeasurement.NAME = "TotalVolatileOrganicCompoundsConcentrationMeasurement" +TotalVolatileOrganicCompoundsConcentrationMeasurement.server = {} +TotalVolatileOrganicCompoundsConcentrationMeasurement.client = {} +TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes = TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes:set_parent_cluster(TotalVolatileOrganicCompoundsConcentrationMeasurement) +TotalVolatileOrganicCompoundsConcentrationMeasurement.types = ConcentrationMeasurement.types + +function TotalVolatileOrganicCompoundsConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function TotalVolatileOrganicCompoundsConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +TotalVolatileOrganicCompoundsConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +TotalVolatileOrganicCompoundsConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function TotalVolatileOrganicCompoundsConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = TotalVolatileOrganicCompoundsConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, TotalVolatileOrganicCompoundsConcentrationMeasurement.NAME)) + end + return TotalVolatileOrganicCompoundsConcentrationMeasurement[direction].attributes[key] +end +TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes = {} +setmetatable(TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(TotalVolatileOrganicCompoundsConcentrationMeasurement, {__index = cluster_base}) + +return TotalVolatileOrganicCompoundsConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..b67cbcd7b6 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes = {} + +function TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes, attr_mt) + +return TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-sensor/src/init.lua b/drivers/SmartThings/matter-sensor/src/init.lua index 5b9d88149e..73e34fcd56 100644 --- a/drivers/SmartThings/matter-sensor/src/init.lua +++ b/drivers/SmartThings/matter-sensor/src/init.lua @@ -1,222 +1,68 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" -local log = require "log" local clusters = require "st.matter.clusters" -local im = require "st.matter.interaction_model" local MatterDriver = require "st.matter.driver" -local utils = require "st.utils" -local embedded_cluster_utils = require "embedded-cluster-utils" +local version = require "version" + +local fields = require "sensor_utils.fields" +local device_cfg = require "sensor_utils.device_configuration" +local attribute_handlers = require "sensor_handlers.attribute_handlers" -- This can be removed once LuaLibs supports the PressureMeasurement cluster if not pcall(function(cluster) return clusters[cluster] end, "PressureMeasurement") then - clusters.PressureMeasurement = require "PressureMeasurement" + clusters.PressureMeasurement = require "embedded_clusters.PressureMeasurement" end -- Include driver-side definitions when lua libs api version is < 10 -local version = require "version" if version.api < 10 then - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.SmokeCoAlarm = require "SmokeCoAlarm" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.SmokeCoAlarm = require "embedded_clusters.SmokeCoAlarm" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" end -- Include driver-side definitions when lua libs api version is < 11 if version.api < 11 then - clusters.BooleanStateConfiguration = require "BooleanStateConfiguration" -end - -local TEMP_BOUND_RECEIVED = "__temp_bound_received" -local TEMP_MIN = "__temp_min" -local TEMP_MAX = "__temp_max" -local FLOW_BOUND_RECEIVED = "__flow_bound_received" -local FLOW_MIN = "__flow_min" -local FLOW_MAX = "__flow_max" - -local battery_support = { - NO_BATTERY = "NO_BATTERY", - BATTERY_LEVEL = "BATTERY_LEVEL", - BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" -} - -local function get_field_for_endpoint(device, field, endpoint) - return device:get_field(string.format("%s_%d", field, endpoint)) -end - -local function set_field_for_endpoint(device, field, endpoint, value, additional_params) - device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) -end - -local BOOLEAN_DEVICE_TYPE_INFO = { - ["RAIN_SENSOR"] = { id = 0x0044, sensitivity_preference = "rainSensitivity", sensitivity_max = "rainMax" }, - ["WATER_FREEZE_DETECTOR"] = { id = 0x0041, sensitivity_preference = "freezeSensitivity", sensitivity_max = "freezeMax" }, - ["WATER_LEAK_DETECTOR"] = { id = 0x0043, sensitivity_preference = "leakSensitivity", sensitivity_max = "leakMax" }, - ["CONTACT_SENSOR"] = { id = 0x0015, sensitivity_preference = "N/A", sensitivity_max = "N/A" }, -} - -local ORDERED_DEVICE_TYPE_INFO = { - "RAIN_SENSOR", - "WATER_FREEZE_DETECTOR", - "WATER_LEAK_DETECTOR", - "CONTACT_SENSOR" -} - -local function set_boolean_device_type_per_endpoint(driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - for dt_name, info in pairs(BOOLEAN_DEVICE_TYPE_INFO) do - if dt.device_type_id == info.id then - device:set_field(dt_name, ep.endpoint_id, { persist = true }) - device:send(clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(device, ep.endpoint_id)) - end - end - end - end + clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" end -local function supports_sensitivity_preferences(device) - local preference_names = "" - local sensitivity_eps = embedded_cluster_utils.get_endpoints(device, clusters.BooleanStateConfiguration.ID, - {feature_bitmap = clusters.BooleanStateConfiguration.types.Feature.SENSITIVITY_LEVEL}) - if sensitivity_eps and #sensitivity_eps > 0 then - for _, dt_name in ipairs(ORDERED_DEVICE_TYPE_INFO) do - for _, sensitivity_ep in pairs(sensitivity_eps) do - if device:get_field(dt_name) == sensitivity_ep and BOOLEAN_DEVICE_TYPE_INFO[dt_name].sensitivity_preference ~= "N/A" then - preference_names = preference_names .. "-" .. BOOLEAN_DEVICE_TYPE_INFO[dt_name].sensitivity_preference - end - end - end - end - return preference_names -end - -local function match_profile(driver, device, battery_supported) - local profile_name = "" - - if device:supports_capability(capabilities.contactSensor) then - profile_name = profile_name .. "-contact" - end - - if device:supports_capability(capabilities.illuminanceMeasurement) then - profile_name = profile_name .. "-illuminance" - end - - if device:supports_capability(capabilities.temperatureMeasurement) then - profile_name = profile_name .. "-temperature" - end - - if device:supports_capability(capabilities.relativeHumidityMeasurement) then - profile_name = profile_name .. "-humidity" - end - - if device:supports_capability(capabilities.atmosphericPressureMeasurement) then - profile_name = profile_name .. "-pressure" - end - - if device:supports_capability(capabilities.rainSensor) then - profile_name = profile_name .. "-rain" - end - - if device:supports_capability(capabilities.temperatureAlarm) then - profile_name = profile_name .. "-freeze" - end - - if device:supports_capability(capabilities.waterSensor) then - profile_name = profile_name .. "-leak" - end - - if device:supports_capability(capabilities.flowMeasurement) then - profile_name = profile_name .. "-flow" - end - - if device:supports_capability(capabilities.button) then - profile_name = profile_name .. "-button" - end - - if battery_supported == battery_support.BATTERY_PERCENTAGE then - profile_name = profile_name .. "-battery" - elseif battery_supported == battery_support.BATTERY_LEVEL then - profile_name = profile_name .. "-batteryLevel" - end - - if device:supports_capability(capabilities.hardwareFault) then - profile_name = profile_name .. "-fault" - end - - local concatenated_preferences = supports_sensitivity_preferences(device) - profile_name = profile_name .. concatenated_preferences - - if device:supports_capability(capabilities.motionSensor) then - local occupancy_support = "-motion" - -- If the Occupancy Sensing Cluster’s revision is >= 5 (corresponds to Lua Libs version 13+), and any of the AIR / RAD / RFS / VIS - -- features are supported by the device, use the presenceSensor capability. Otherwise, use the motionSensor capability. Currently, - -- presenceSensor only used for devices fingerprinting to the motion-illuminance-temperature-humidity-battery profile. - if profile_name == "-illuminance-temperature-humidity-battery" and version.api >= 13 then - if #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.ACTIVE_INFRARED}) > 0 or - #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.RADAR}) > 0 or - #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.RF_SENSING}) > 0 or - #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.VISION}) then - occupancy_support = "-presence" - end - end - profile_name = occupancy_support .. profile_name - end - - -- remove leading "-" - profile_name = string.sub(profile_name, 2) - - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) -end +local SensorLifecycleHandlers = {} -local function do_configure(driver, device) +function SensorLifecycleHandlers.do_configure(driver, device) local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) if #battery_feature_eps > 0 then - local attribute_list_read = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - attribute_list_read:merge(clusters.PowerSource.attributes.AttributeList:read()) - device:send(attribute_list_read) + device:send(clusters.PowerSource.attributes.AttributeList:read()) else - match_profile(driver, device, battery_support.NO_BATTERY) + device_cfg.match_profile(driver, device, fields.battery_support.NO_BATTERY) end end -local function device_init(driver, device) - log.info("device init") - set_boolean_device_type_per_endpoint(driver, device) +function SensorLifecycleHandlers.device_init(driver, device) + device.log.info("device init") + device_cfg.set_boolean_device_type_per_endpoint(driver, device) device:subscribe() end -local function info_changed(driver, device, event, args) +function SensorLifecycleHandlers.info_changed(driver, device, event, args) if device.profile.id ~= args.old_st_store.profile.id then - set_boolean_device_type_per_endpoint(driver, device) + device_cfg.set_boolean_device_type_per_endpoint(driver, device) device:subscribe() end if not device.preferences then return end - for dt_name, info in pairs(BOOLEAN_DEVICE_TYPE_INFO) do + for dt_name, info in pairs(fields.BOOLEAN_DEVICE_TYPE_INFO) do local dt_ep = device:get_field(dt_name) if dt_ep and info.sensitivity_preference and (device.preferences[info.sensitivity_preference] ~= args.old_st_store.preferences[info.sensitivity_preference]) then local sensitivity_preference = device.preferences[info.sensitivity_preference] @@ -234,244 +80,76 @@ local function info_changed(driver, device, event, args) end end -local function illuminance_attr_handler(driver, device, ib, response) - local lux = math.floor(10 ^ ((ib.data.value - 1) / 10000)) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.illuminanceMeasurement.illuminance(lux)) -end - -local function temperature_attr_handler(driver, device, ib, response) - local measured_value = ib.data.value - if measured_value ~= nil then - local temp = measured_value / 100.0 - local unit = "C" - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperature({value = temp, unit = unit})) - end -end - -local temp_attr_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local temp = ib.data.value / 100.0 - local unit = "C" - set_field_for_endpoint(device, TEMP_BOUND_RECEIVED..minOrMax, ib.endpoint_id, temp) - local min = get_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MIN, ib.endpoint_id) - local max = get_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MAX, ib.endpoint_id) - if min ~= nil and max ~= nil then - if min < max then - -- Only emit the capability for RPC version >= 5 (unit conversion for - -- temperature range capability is only supported for RPC >= 5) - if version.rpc >= 5 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) - end - set_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MIN, ib.endpoint_id, nil) - set_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MAX, ib.endpoint_id, nil) - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) - end - end - end -end - -local function humidity_attr_handler(driver, device, ib, response) - local measured_value = ib.data.value - if measured_value ~= nil then - local humidity = utils.round(measured_value / 100.0) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) - end -end - -local BOOLEAN_CAP_EVENT_MAP = { - [true] = { - ["WATER_FREEZE_DETECTOR"] = capabilities.temperatureAlarm.temperatureAlarm.freeze(), - ["WATER_LEAK_DETECTOR"] = capabilities.waterSensor.water.wet(), - ["RAIN_SENSOR"] = capabilities.rainSensor.rain.detected(), - ["CONTACT_SENSOR"] = capabilities.contactSensor.contact.closed(), - }, - [false] = { - ["WATER_FREEZE_DETECTOR"] = capabilities.temperatureAlarm.temperatureAlarm.cleared(), - ["WATER_LEAK_DETECTOR"] = capabilities.waterSensor.water.dry(), - ["RAIN_SENSOR"] = capabilities.rainSensor.rain.undetected(), - ["CONTACT_SENSOR"] = capabilities.contactSensor.contact.open(), - } -} - -local function boolean_attr_handler(driver, device, ib, response) - local name - for dt_name, _ in pairs(BOOLEAN_DEVICE_TYPE_INFO) do - local dt_ep_id = device:get_field(dt_name) - if ib.endpoint_id == dt_ep_id then - name = dt_name - break - end - end - if name then - device:emit_event_for_endpoint(ib.endpoint_id, BOOLEAN_CAP_EVENT_MAP[ib.data.value][name]) - elseif device:supports_capability(capabilities.contactSensor) then - -- The generic case where no device type has been specified but the profile uses this capability. - device:emit_event_for_endpoint(ib.endpoint_id, BOOLEAN_CAP_EVENT_MAP[ib.data.value]["CONTACT_SENSOR"]) - else - log.error("No Boolean device type found on an endpoint, BooleanState handler aborted") - end -end - -local function supported_sensitivities_handler(driver, device, ib, response) - if not ib.data.value then - return - end - - for dt_name, info in pairs(BOOLEAN_DEVICE_TYPE_INFO) do - if device:get_field(dt_name) == ib.endpoint_id then - device:set_field(info.sensitivity_max, ib.data.value, {persist = true}) - end - end -end - -local function sensor_fault_handler(driver, device, ib, response) - if ib.data.value > 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.hardwareFault.hardwareFault.detected()) - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.hardwareFault.hardwareFault.clear()) - end -end - -local function battery_percent_remaining_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) - end -end - -local function battery_charge_level_attr_handler(driver, device, ib, response) - if ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.OK then - device:emit_event(capabilities.batteryLevel.battery.normal()) - elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.WARNING then - device:emit_event(capabilities.batteryLevel.battery.warning()) - elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.CRITICAL then - device:emit_event(capabilities.batteryLevel.battery.critical()) - end -end - -local function power_source_attribute_list_handler(driver, device, ib, response) - for _, attr in ipairs(ib.data.elements) do - -- Re-profile the device if BatPercentRemaining (Attribute ID 0x0C) or - -- BatChargeLevel (Attribute ID 0x0E) is present. - if attr.value == 0x0C then - match_profile(driver, device, battery_support.BATTERY_PERCENTAGE) - return - elseif attr.value == 0x0E then - match_profile(driver, device, battery_support.BATTERY_LEVEL) - return - end - end -end - -local function occupancy_attr_handler(driver, device, ib, response) - if device:supports_capability(capabilities.motionSensor) then - device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) - else - device:emit_event(ib.data.value == 0x01 and capabilities.presenceSensor.presence("present") or capabilities.presenceSensor.presence("not present")) - end -end - -local function pressure_attr_handler(driver, device, ib, response) - local measured_value = ib.data.value - if measured_value ~= nil then - local kPa = utils.round(measured_value / 10.0) - local unit = "kPa" - device:emit_event(capabilities.atmosphericPressureMeasurement.atmosphericPressure({value = kPa, unit = unit})) - end -end - -local function flow_attr_handler(driver, device, ib, response) - local measured_value = ib.data.value - if measured_value ~= nil then - local flow = measured_value / 10.0 - local unit = "m^3/h" - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flow({value = flow, unit = unit})) - end -end - -local flow_attr_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local flow_bound = ib.data.value / 10.0 - local unit = "m^3/h" - set_field_for_endpoint(device, FLOW_BOUND_RECEIVED..minOrMax, ib.endpoint_id, flow_bound) - local min = get_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MIN, ib.endpoint_id) - local max = get_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MAX, ib.endpoint_id) - if min ~= nil and max ~= nil then - if min < max then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flowRange({ value = { minimum = min, maximum = max }, unit = unit })) - set_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MIN, ib.endpoint_id, nil) - set_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MAX, ib.endpoint_id, nil) - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min flow measurement %d that is not lower than the reported max flow measurement %d", min, max)) - end - end - end -end - local matter_driver_template = { lifecycle_handlers = { - init = device_init, - infoChanged = info_changed, - doConfigure = do_configure, + doConfigure = SensorLifecycleHandlers.do_configure, + init = SensorLifecycleHandlers.device_init, + infoChanged = SensorLifecycleHandlers.info_changed, }, matter_handlers = { attr = { - [clusters.RelativeHumidityMeasurement.ID] = { - [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = humidity_attr_handler + [clusters.BooleanState.ID] = { + [clusters.BooleanState.attributes.StateValue.ID] = attribute_handlers.boolean_state_value_handler }, - [clusters.TemperatureMeasurement.ID] = { - [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = temperature_attr_handler, - [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = temp_attr_handler_factory(TEMP_MIN), - [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = temp_attr_handler_factory(TEMP_MAX), + [clusters.BooleanStateConfiguration.ID] = { + [clusters.BooleanStateConfiguration.attributes.SensorFault.ID] = attribute_handlers.sensor_fault_handler, + [clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels.ID] = attribute_handlers.supported_sensitivity_levels_handler, + }, + [clusters.FlowMeasurement.ID] = { + [clusters.FlowMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.flow_measured_value_handler, + [clusters.FlowMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.flow_measured_value_bounds_factory(fields.FLOW_MIN), + [clusters.FlowMeasurement.attributes.MaxMeasuredValue.ID] = attribute_handlers.flow_measured_value_bounds_factory(fields.FLOW_MAX) }, [clusters.IlluminanceMeasurement.ID] = { - [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = illuminance_attr_handler + [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.illuminance_measured_value_handler }, - [clusters.BooleanState.ID] = { - [clusters.BooleanState.attributes.StateValue.ID] = boolean_attr_handler + [clusters.OccupancySensing.ID] = { + [clusters.OccupancySensing.attributes.Occupancy.ID] = attribute_handlers.occupancy_measured_value_handler, }, [clusters.PowerSource.ID] = { - [clusters.PowerSource.attributes.AttributeList.ID] = power_source_attribute_list_handler, - [clusters.PowerSource.attributes.BatChargeLevel.ID] = battery_charge_level_attr_handler, - [clusters.PowerSource.attributes.BatPercentRemaining.ID] = battery_percent_remaining_attr_handler, - }, - [clusters.OccupancySensing.ID] = { - [clusters.OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler, + [clusters.PowerSource.attributes.AttributeList.ID] = attribute_handlers.power_source_attribute_list_handler, + [clusters.PowerSource.attributes.BatChargeLevel.ID] = attribute_handlers.bat_charge_level_handler, + [clusters.PowerSource.attributes.BatPercentRemaining.ID] = attribute_handlers.bat_percent_remaining_handler, }, [clusters.PressureMeasurement.ID] = { - [clusters.PressureMeasurement.attributes.MeasuredValue.ID] = pressure_attr_handler, + [clusters.PressureMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.pressure_measured_value_handler, }, - [clusters.BooleanStateConfiguration.ID] = { - [clusters.BooleanStateConfiguration.attributes.SensorFault.ID] = sensor_fault_handler, - [clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels.ID] = supported_sensitivities_handler, + [clusters.RelativeHumidityMeasurement.ID] = { + [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.humidity_measured_value_handler + }, + [clusters.TemperatureMeasurement.ID] = { + [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.temperature_measured_value_handler, + [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.TEMP_MIN), + [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.TEMP_MAX), }, [clusters.Thermostat.ID] = { - [clusters.Thermostat.attributes.LocalTemperature.ID] = temperature_attr_handler + [clusters.Thermostat.attributes.LocalTemperature.ID] = attribute_handlers.temperature_measured_value_handler -- TemperatureMeasurement.MeasuredValue handler can support this attibute }, - [clusters.FlowMeasurement.ID] = { - [clusters.FlowMeasurement.attributes.MeasuredValue.ID] = flow_attr_handler, - [clusters.FlowMeasurement.attributes.MinMeasuredValue.ID] = flow_attr_handler_factory(FLOW_MIN), - [clusters.FlowMeasurement.attributes.MaxMeasuredValue.ID] = flow_attr_handler_factory(FLOW_MAX) - } } }, - -- TODO Once capabilities all have default handlers move this info there, and - -- use `supported_capabilities` subscribed_attributes = { - [capabilities.relativeHumidityMeasurement.ID] = { - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue + [capabilities.battery.ID] = { + clusters.PowerSource.attributes.BatPercentRemaining }, - [capabilities.temperatureMeasurement.ID] = { - clusters.TemperatureMeasurement.attributes.MeasuredValue, - clusters.TemperatureMeasurement.attributes.MinMeasuredValue, - clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, - clusters.Thermostat.attributes.LocalTemperature + [capabilities.batteryLevel.ID] = { + clusters.PowerSource.attributes.BatChargeLevel, + clusters.SmokeCoAlarm.attributes.BatteryAlert, + }, + [capabilities.contactSensor.ID] = { + clusters.BooleanState.attributes.StateValue + }, + [capabilities.flowMeasurement.ID] = { + clusters.FlowMeasurement.attributes.MeasuredValue, + clusters.FlowMeasurement.attributes.MinMeasuredValue, + clusters.FlowMeasurement.attributes.MaxMeasuredValue + }, + [capabilities.hardwareFault.ID] = { + clusters.BooleanStateConfiguration.attributes.SensorFault, + -- THESE ARE USED IN THE CASE OF THE SMOKE CO ALARM. + -- TODO: move this unique subscription logic into the subdriver + clusters.SmokeCoAlarm.attributes.HardwareFaultAlert, + clusters.SmokeCoAlarm.attributes.BatteryAlert, + clusters.PowerSource.attributes.BatChargeLevel, }, [capabilities.illuminanceMeasurement.ID] = { clusters.IlluminanceMeasurement.attributes.MeasuredValue @@ -482,119 +160,114 @@ local matter_driver_template = { [capabilities.presenceSensor.ID] = { clusters.OccupancySensing.attributes.Occupancy }, - [capabilities.contactSensor.ID] = { - clusters.BooleanState.attributes.StateValue + [capabilities.rainSensor.ID] = { + clusters.BooleanState.attributes.StateValue, }, - [capabilities.battery.ID] = { - clusters.PowerSource.attributes.BatPercentRemaining + [capabilities.relativeHumidityMeasurement.ID] = { + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue }, - [capabilities.batteryLevel.ID] = { - clusters.PowerSource.attributes.BatChargeLevel, - clusters.SmokeCoAlarm.attributes.BatteryAlert, + [capabilities.temperatureAlarm.ID] = { + clusters.BooleanState.attributes.StateValue, }, - [capabilities.atmosphericPressureMeasurement.ID] = { - clusters.PressureMeasurement.attributes.MeasuredValue + [capabilities.temperatureMeasurement.ID] = { + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, + clusters.Thermostat.attributes.LocalTemperature }, + [capabilities.waterSensor.ID] = { + clusters.BooleanState.attributes.StateValue, + }, + -- AIR QUALITY SENSOR SPECIFIC CAPABILITIES -- [capabilities.airQualityHealthConcern.ID] = { clusters.AirQuality.attributes.AirQuality }, - [capabilities.carbonMonoxideMeasurement.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, + [capabilities.atmosphericPressureMeasurement.ID] = { + clusters.PressureMeasurement.attributes.MeasuredValue }, - [capabilities.carbonMonoxideHealthConcern.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue, + [capabilities.carbonDioxideHealthConcern.ID] = { + clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue, }, [capabilities.carbonDioxideMeasurement.ID] = { clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue, clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.carbonDioxideHealthConcern.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue, + [capabilities.carbonMonoxideHealthConcern.ID] = { + clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue, }, - [capabilities.nitrogenDioxideMeasurement.ID] = { - clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit + [capabilities.carbonMonoxideMeasurement.ID] = { + clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, + clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.nitrogenDioxideHealthConcern.ID] = { - clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue, + [capabilities.dustHealthConcern.ID] = { + clusters.Pm10ConcentrationMeasurement.attributes.LevelValue, }, - [capabilities.ozoneMeasurement.ID] = { - clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue, - clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit + [capabilities.dustSensor.ID] = { + clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, + clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.ozoneHealthConcern.ID] = { - clusters.OzoneConcentrationMeasurement.attributes.LevelValue, + [capabilities.fineDustHealthConcern.ID] = { + clusters.Pm25ConcentrationMeasurement.attributes.LevelValue, }, - [capabilities.formaldehydeMeasurement.ID] = { - clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue, - clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit, + [capabilities.fineDustSensor.ID] = { + clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, }, [capabilities.formaldehydeHealthConcern.ID] = { clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue, }, - [capabilities.veryFineDustSensor.ID] = { - clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit, + [capabilities.formaldehydeMeasurement.ID] = { + clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue, + clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.veryFineDustHealthConcern.ID] = { - clusters.Pm1ConcentrationMeasurement.attributes.LevelValue, + [capabilities.nitrogenDioxideHealthConcern.ID] = { + clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue, }, - [capabilities.fineDustHealthConcern.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.LevelValue, + [capabilities.nitrogenDioxideMeasurement.ID] = { + clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue, + clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit }, - [capabilities.fineDustSensor.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, + [capabilities.ozoneHealthConcern.ID] = { + clusters.OzoneConcentrationMeasurement.attributes.LevelValue, }, - [capabilities.dustSensor.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, - clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit, + [capabilities.ozoneMeasurement.ID] = { + clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue, + clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit }, - [capabilities.dustHealthConcern.ID] = { - clusters.Pm10ConcentrationMeasurement.attributes.LevelValue, + [capabilities.radonHealthConcern.ID] = { + clusters.RadonConcentrationMeasurement.attributes.LevelValue, }, [capabilities.radonMeasurement.ID] = { clusters.RadonConcentrationMeasurement.attributes.MeasuredValue, clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.radonHealthConcern.ID] = { - clusters.RadonConcentrationMeasurement.attributes.LevelValue, + [capabilities.relativeHumidityMeasurement.ID] = { + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue + }, + [capabilities.tvocHealthConcern.ID] = { + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue }, [capabilities.tvocMeasurement.ID] = { clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue, clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.tvocHealthConcern.ID] = { - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue, + [capabilities.veryFineDustHealthConcern.ID] = { + clusters.Pm1ConcentrationMeasurement.attributes.LevelValue, }, - [capabilities.smokeDetector.ID] = { - clusters.SmokeCoAlarm.attributes.SmokeState, - clusters.SmokeCoAlarm.attributes.TestInProgress, + [capabilities.veryFineDustSensor.ID] = { + clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit, }, + -- SMOKE CO ALARM SPECIFIC CAPABILITIES -- [capabilities.carbonMonoxideDetector.ID] = { clusters.SmokeCoAlarm.attributes.COState, clusters.SmokeCoAlarm.attributes.TestInProgress, }, - [capabilities.hardwareFault.ID] = { - clusters.SmokeCoAlarm.attributes.HardwareFaultAlert, - clusters.BooleanStateConfiguration.attributes.SensorFault, - }, - [capabilities.waterSensor.ID] = { - clusters.BooleanState.attributes.StateValue, - }, - [capabilities.temperatureAlarm.ID] = { - clusters.BooleanState.attributes.StateValue, - }, - [capabilities.rainSensor.ID] = { - clusters.BooleanState.attributes.StateValue, - }, - [capabilities.flowMeasurement.ID] = { - clusters.FlowMeasurement.attributes.MeasuredValue, - clusters.FlowMeasurement.attributes.MinMeasuredValue, - clusters.FlowMeasurement.attributes.MaxMeasuredValue + [capabilities.smokeDetector.ID] = { + clusters.SmokeCoAlarm.attributes.SmokeState, + clusters.SmokeCoAlarm.attributes.TestInProgress, }, }, subscribed_events = { @@ -604,8 +277,7 @@ local matter_driver_template = { clusters.Switch.events.MultiPressComplete, } }, - capability_handlers = { - }, + capability_handlers = {}, supported_capabilities = { capabilities.temperatureMeasurement, capabilities.contactSensor, @@ -623,11 +295,7 @@ local matter_driver_template = { capabilities.hardwareFault, capabilities.flowMeasurement, }, - sub_drivers = { - require("air-quality-sensor"), - require("smoke-co-alarm"), - require("bosch-button-contact") - } + sub_drivers = require("sub_drivers"), } local matter_driver = MatterDriver("matter-sensor", matter_driver_template) diff --git a/drivers/SmartThings/matter-sensor/src/lazy_load_subdriver.lua b/drivers/SmartThings/matter-sensor/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..a04740d267 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/lazy_load_subdriver.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + local MatterDriver = require "st.matter.driver" + local version = require "version" + if version.api >= 16 then + return MatterDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return MatterDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua new file mode 100644 index 0000000000..9bba480855 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua @@ -0,0 +1,204 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local st_utils = require "st.utils" +local sensor_utils = require "sensor_utils.utils" +local fields = require "sensor_utils.fields" +local device_cfg = require "sensor_utils.device_configuration" +local version = require "version" + +local AttributeHandlers = {} + + +-- [[ ILLUMINANCE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.illuminance_measured_value_handler(driver, device, ib, response) + local lux = math.floor(10 ^ ((ib.data.value - 1) / 10000)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.illuminanceMeasurement.illuminance(lux)) +end + + +-- [[ TEMPERATURE MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.temperature_measured_value_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local temp = measured_value / 100.0 + local unit = "C" + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperature({value = temp, unit = unit})) + end +end + +function AttributeHandlers.temperature_measured_value_bounds_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local temp = ib.data.value / 100.0 + local unit = "C" + sensor_utils.set_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..minOrMax, ib.endpoint_id, temp) + local min = sensor_utils.get_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MIN, ib.endpoint_id) + local max = sensor_utils.get_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MAX, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + -- Only emit the capability for RPC version >= 5 (unit conversion for + -- temperature range capability is only supported for RPC >= 5) + if version.rpc >= 5 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) + end + sensor_utils.set_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MIN, ib.endpoint_id, nil) + sensor_utils.set_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MAX, ib.endpoint_id, nil) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) + end + end + end +end + + +-- [[ RELATIVE HUMIDITY MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.humidity_measured_value_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local humidity = st_utils.round(measured_value / 100.0) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) + end +end + + +-- [[ BOOLEAN STATE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.boolean_state_value_handler(driver, device, ib, response) + local name + for dt_name, _ in pairs(fields.BOOLEAN_DEVICE_TYPE_INFO) do + local dt_ep_id = device:get_field(dt_name) + if ib.endpoint_id == dt_ep_id then + name = dt_name + break + end + end + if name then + device:emit_event_for_endpoint(ib.endpoint_id, fields.BOOLEAN_CAP_EVENT_MAP[ib.data.value][name]) + elseif device:supports_capability(capabilities.contactSensor) then + -- The generic case where no device type has been specified but the profile uses this capability. + device:emit_event_for_endpoint(ib.endpoint_id, fields.BOOLEAN_CAP_EVENT_MAP[ib.data.value]["CONTACT_SENSOR"]) + else + device.log.error("No Boolean device type found on an endpoint, BooleanState handler aborted") + end +end + + +-- [[ BOOLEAN STATE CONFIGURATION CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.sensor_fault_handler(driver, device, ib, response) + if ib.data.value > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.hardwareFault.hardwareFault.detected()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.hardwareFault.hardwareFault.clear()) + end +end + +function AttributeHandlers.supported_sensitivity_levels_handler(driver, device, ib, response) + if ib.data.value then + for dt_name, info in pairs(fields.BOOLEAN_DEVICE_TYPE_INFO) do + if device:get_field(dt_name) == ib.endpoint_id then + device:set_field(info.sensitivity_max, ib.data.value, {persist = true}) + end + end + end +end + + +-- [[ POWER SOURCE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.bat_percent_remaining_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) + end +end + +function AttributeHandlers.bat_charge_level_handler(driver, device, ib, response) + if ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.OK then + device:emit_event(capabilities.batteryLevel.battery.normal()) + elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.WARNING then + device:emit_event(capabilities.batteryLevel.battery.warning()) + elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.CRITICAL then + device:emit_event(capabilities.batteryLevel.battery.critical()) + end +end + +function AttributeHandlers.power_source_attribute_list_handler(driver, device, ib, response) + for _, attr in ipairs(ib.data.elements) do + -- Re-profile the device if BatPercentRemaining (Attribute ID 0x0C) or + -- BatChargeLevel (Attribute ID 0x0E) is present. + if attr.value == 0x0C then + device_cfg.match_profile(driver, device, fields.battery_support.BATTERY_PERCENTAGE) + return + elseif attr.value == 0x0E then + device_cfg.match_profile(driver, device, fields.battery_support.BATTERY_LEVEL) + return + end + end +end + + +-- [[ OCCUPANCY SENSING CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.occupancy_measured_value_handler(driver, device, ib, response) + if device:supports_capability(capabilities.motionSensor) then + device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) + else + device:emit_event(ib.data.value == 0x01 and capabilities.presenceSensor.presence("present") or capabilities.presenceSensor.presence("not present")) + end +end + + +-- [[ PRESSURE MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.pressure_measured_value_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local kPa = st_utils.round(measured_value / 10.0) + local unit = "kPa" + device:emit_event(capabilities.atmosphericPressureMeasurement.atmosphericPressure({value = kPa, unit = unit})) + end +end + + +-- [[ FLOW MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.flow_measured_value_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local flow = measured_value / 10.0 + local unit = "m^3/h" + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flow({value = flow, unit = unit})) + end +end + +function AttributeHandlers.flow_measured_value_bounds_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local flow_bound = ib.data.value / 10.0 + local unit = "m^3/h" + sensor_utils.set_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..minOrMax, ib.endpoint_id, flow_bound) + local min = sensor_utils.get_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MIN, ib.endpoint_id) + local max = sensor_utils.get_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MAX, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flowRange({ value = { minimum = min, maximum = max }, unit = unit })) + sensor_utils.set_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MIN, ib.endpoint_id, nil) + sensor_utils.set_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MAX, ib.endpoint_id, nil) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min flow measurement %d that is not lower than the reported max flow measurement %d", min, max)) + end + end + end +end + +return AttributeHandlers \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua new file mode 100644 index 0000000000..2c5f38524a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua @@ -0,0 +1,124 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils" +local fields = require "sensor_utils.fields" +local version = require "version" + +if version.api < 11 then + clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" +end + +local DeviceConfiguration = {} + +function DeviceConfiguration.set_boolean_device_type_per_endpoint(driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + for dt_name, info in pairs(fields.BOOLEAN_DEVICE_TYPE_INFO) do + if dt.device_type_id == info.id then + device:set_field(dt_name, ep.endpoint_id, { persist = true }) + device:send(clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(device, ep.endpoint_id)) + end + end + end + end +end + +local function supports_sensitivity_preferences(device) + local preference_names = "" + local sensitivity_eps = embedded_cluster_utils.get_endpoints(device, clusters.BooleanStateConfiguration.ID, + {feature_bitmap = clusters.BooleanStateConfiguration.types.Feature.SENSITIVITY_LEVEL}) + if sensitivity_eps and #sensitivity_eps > 0 then + for _, dt_name in ipairs(fields.ORDERED_DEVICE_TYPE_INFO) do + for _, sensitivity_ep in pairs(sensitivity_eps) do + if device:get_field(dt_name) == sensitivity_ep and fields.BOOLEAN_DEVICE_TYPE_INFO[dt_name].sensitivity_preference ~= "N/A" then + preference_names = preference_names .. "-" .. fields.BOOLEAN_DEVICE_TYPE_INFO[dt_name].sensitivity_preference + end + end + end + end + return preference_names +end + +function DeviceConfiguration.match_profile(driver, device, battery_supported) + local profile_name = "" + + if device:supports_capability(capabilities.contactSensor) then + profile_name = profile_name .. "-contact" + end + + if device:supports_capability(capabilities.illuminanceMeasurement) then + profile_name = profile_name .. "-illuminance" + end + + if device:supports_capability(capabilities.temperatureMeasurement) then + profile_name = profile_name .. "-temperature" + end + + if device:supports_capability(capabilities.relativeHumidityMeasurement) then + profile_name = profile_name .. "-humidity" + end + + if device:supports_capability(capabilities.atmosphericPressureMeasurement) then + profile_name = profile_name .. "-pressure" + end + + if device:supports_capability(capabilities.rainSensor) then + profile_name = profile_name .. "-rain" + end + + if device:supports_capability(capabilities.temperatureAlarm) then + profile_name = profile_name .. "-freeze" + end + + if device:supports_capability(capabilities.waterSensor) then + profile_name = profile_name .. "-leak" + end + + if device:supports_capability(capabilities.flowMeasurement) then + profile_name = profile_name .. "-flow" + end + + if device:supports_capability(capabilities.button) then + profile_name = profile_name .. "-button" + end + + if battery_supported == fields.battery_support.BATTERY_PERCENTAGE then + profile_name = profile_name .. "-battery" + elseif battery_supported == fields.battery_support.BATTERY_LEVEL then + profile_name = profile_name .. "-batteryLevel" + end + + if device:supports_capability(capabilities.hardwareFault) then + profile_name = profile_name .. "-fault" + end + + local concatenated_preferences = supports_sensitivity_preferences(device) + profile_name = profile_name .. concatenated_preferences + + if device:supports_capability(capabilities.motionSensor) then + local occupancy_support = "-motion" + -- If the Occupancy Sensing Cluster’s revision is >= 5 (corresponds to Lua Libs version 13+), and any of the AIR / RAD / RFS / VIS + -- features are supported by the device, use the presenceSensor capability. Otherwise, use the motionSensor capability. Currently, + -- presenceSensor only used for devices fingerprinting to the motion-illuminance-temperature-humidity-battery profile. + if profile_name == "-illuminance-temperature-humidity-battery" and version.api >= 13 then + if #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.ACTIVE_INFRARED}) > 0 or + #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.RADAR}) > 0 or + #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.RF_SENSING}) > 0 or + #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.VISION}) then + occupancy_support = "-presence" + end + end + profile_name = occupancy_support .. profile_name + end + + -- remove leading "-" + profile_name = string.sub(profile_name, 2) + + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) +end + +return DeviceConfiguration \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/embedded_cluster_utils.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/embedded_cluster_utils.lua new file mode 100644 index 0000000000..e2384098d9 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/embedded_cluster_utils.lua @@ -0,0 +1,83 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" +local utils = require "st.utils" + +-- Include driver-side definitions when lua libs api version is < 10 +local version = require "version" +if version.api < 10 then + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" + clusters.SmokeCoAlarm = require "embedded_clusters.SmokeCoAlarm" +end + +if version.api < 11 then + clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" +end + +local embedded_cluster_utils = {} + +local embedded_clusters_api_10 = { + [clusters.AirQuality.ID] = clusters.AirQuality, + [clusters.CarbonMonoxideConcentrationMeasurement.ID] = clusters.CarbonMonoxideConcentrationMeasurement, + [clusters.CarbonDioxideConcentrationMeasurement.ID] = clusters.CarbonDioxideConcentrationMeasurement, + [clusters.FormaldehydeConcentrationMeasurement.ID] = clusters.FormaldehydeConcentrationMeasurement, + [clusters.NitrogenDioxideConcentrationMeasurement.ID] = clusters.NitrogenDioxideConcentrationMeasurement, + [clusters.OzoneConcentrationMeasurement.ID] = clusters.OzoneConcentrationMeasurement, + [clusters.Pm1ConcentrationMeasurement.ID] = clusters.Pm1ConcentrationMeasurement, + [clusters.Pm10ConcentrationMeasurement.ID] = clusters.Pm10ConcentrationMeasurement, + [clusters.Pm25ConcentrationMeasurement.ID] = clusters.Pm25ConcentrationMeasurement, + [clusters.RadonConcentrationMeasurement.ID] = clusters.RadonConcentrationMeasurement, + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.ID] = clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement, + [clusters.SmokeCoAlarm.ID] = clusters.SmokeCoAlarm, +} + +local embedded_clusters_api_11 = { + [clusters.BooleanStateConfiguration.ID] = clusters.BooleanStateConfiguration +} + +function embedded_cluster_utils.get_endpoints(device, cluster_id, opts) + -- If using older lua libs and need to check for an embedded cluster feature, + -- we must use the embedded cluster definitions here + if version.api < 10 and embedded_clusters_api_10[cluster_id] ~= nil or + version.api < 11 and embedded_clusters_api_11[cluster_id] ~= nil then + local embedded_cluster = embedded_clusters_api_10[cluster_id] or embedded_clusters_api_11[cluster_id] + local opts = opts or {} + if utils.table_size(opts) > 1 then + device.log.warn_with({hub_logs = true}, "Invalid options for get_endpoints") + return + end + local clus_has_features = function(clus, feature_bitmap) + if not feature_bitmap or not clus then return false end + return embedded_cluster.are_features_supported(feature_bitmap, clus.feature_map) + end + local eps = {} + for _, ep in ipairs(device.endpoints) do + for _, clus in ipairs(ep.clusters) do + if ((clus.cluster_id == cluster_id) + and (opts.feature_bitmap == nil or clus_has_features(clus, opts.feature_bitmap)) + and ((opts.cluster_type == nil and clus.cluster_type == "SERVER" or clus.cluster_type == "BOTH") + or (opts.cluster_type == clus.cluster_type)) + or (cluster_id == nil)) then + table.insert(eps, ep.endpoint_id) + if cluster_id == nil then break end + end + end + end + return eps + else + return device:get_endpoints(cluster_id, opts) + end + end + + return embedded_cluster_utils \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/fields.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/fields.lua new file mode 100644 index 0000000000..f0b2a5c7a6 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/fields.lua @@ -0,0 +1,50 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" + +local SensorFields = {} + +SensorFields.TEMP_BOUND_RECEIVED = "__temp_bound_received" +SensorFields.TEMP_MIN = "__temp_min" +SensorFields.TEMP_MAX = "__temp_max" +SensorFields.FLOW_BOUND_RECEIVED = "__flow_bound_received" +SensorFields.FLOW_MIN = "__flow_min" +SensorFields.FLOW_MAX = "__flow_max" + +SensorFields.battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_LEVEL = "BATTERY_LEVEL", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" +} + +SensorFields.BOOLEAN_DEVICE_TYPE_INFO = { + ["RAIN_SENSOR"] = { id = 0x0044, sensitivity_preference = "rainSensitivity", sensitivity_max = "rainMax" }, + ["WATER_FREEZE_DETECTOR"] = { id = 0x0041, sensitivity_preference = "freezeSensitivity", sensitivity_max = "freezeMax" }, + ["WATER_LEAK_DETECTOR"] = { id = 0x0043, sensitivity_preference = "leakSensitivity", sensitivity_max = "leakMax" }, + ["CONTACT_SENSOR"] = { id = 0x0015, sensitivity_preference = "N/A", sensitivity_max = "N/A" }, +} + +SensorFields.ORDERED_DEVICE_TYPE_INFO = { + "RAIN_SENSOR", + "WATER_FREEZE_DETECTOR", + "WATER_LEAK_DETECTOR", + "CONTACT_SENSOR" +} + +SensorFields.BOOLEAN_CAP_EVENT_MAP = { + [true] = { + ["WATER_FREEZE_DETECTOR"] = capabilities.temperatureAlarm.temperatureAlarm.freeze(), + ["WATER_LEAK_DETECTOR"] = capabilities.waterSensor.water.wet(), + ["RAIN_SENSOR"] = capabilities.rainSensor.rain.detected(), + ["CONTACT_SENSOR"] = capabilities.contactSensor.contact.closed(), + }, + [false] = { + ["WATER_FREEZE_DETECTOR"] = capabilities.temperatureAlarm.temperatureAlarm.cleared(), + ["WATER_LEAK_DETECTOR"] = capabilities.waterSensor.water.dry(), + ["RAIN_SENSOR"] = capabilities.rainSensor.rain.undetected(), + ["CONTACT_SENSOR"] = capabilities.contactSensor.contact.open(), + } +} + +return SensorFields diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua new file mode 100644 index 0000000000..5a0421fb0c --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua @@ -0,0 +1,24 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local utils = {} + +function utils.get_field_for_endpoint(device, field, endpoint) + return device:get_field(string.format("%s_%d", field, endpoint)) +end + +function utils.set_field_for_endpoint(device, field, endpoint, value, additional_params) + device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) +end + +function utils.tbl_contains(array, value) + if value == nil then return false end + for _, element in pairs(array or {}) do + if element == value then + return true + end + end + return false +end + +return utils diff --git a/drivers/SmartThings/matter-sensor/src/smoke-co-alarm/init.lua b/drivers/SmartThings/matter-sensor/src/smoke-co-alarm/init.lua deleted file mode 100644 index c5fe00a4be..0000000000 --- a/drivers/SmartThings/matter-sensor/src/smoke-co-alarm/init.lua +++ /dev/null @@ -1,320 +0,0 @@ --- Copyright 2024 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 capabilities = require "st.capabilities" -local clusters = require "st.matter.clusters" -local embedded_cluster_utils = require "embedded-cluster-utils" - -local CARBON_MONOXIDE_MEASUREMENT_UNIT = "CarbonMonoxideConcentrationMeasurement_unit" -local SMOKE_CO_ALARM_DEVICE_TYPE_ID = 0x0076 - -local HardwareFaultAlert = "__HardwareFaultAlert" -local BatteryAlert = "__BatteryAlert" -local BatteryLevel = "__BatteryLevel" - -local battery_support = { - NO_BATTERY = "NO_BATTERY", - BATTERY_LEVEL = "BATTERY_LEVEL", - BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" -} - -local version = require "version" -if version.api < 10 then - clusters.SmokeCoAlarm = require "SmokeCoAlarm" -end - -local function is_matter_smoke_co_alarm(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == SMOKE_CO_ALARM_DEVICE_TYPE_ID then - return true - end - end - end - - return false -end - -local tbl_contains = function(t, val) - for _, v in pairs(t) do - if v == val then - return true - end - end - return false -end - -local supported_profiles = -{ - "co", - "co-battery", - "co-comeas", - "co-comeas-battery", - "co-comeas-colevel-battery", - "smoke", - "smoke-battery", - "smoke-co-comeas", - "smoke-co-comeas-battery", - "smoke-co-temp-humidity-comeas", - "smoke-co-temp-humidity-comeas-battery" -} - -local function match_profile(device, battery_supported) - local smoke_eps = embedded_cluster_utils.get_endpoints(device, clusters.SmokeCoAlarm.ID, {feature_bitmap = clusters.SmokeCoAlarm.types.Feature.SMOKE_ALARM}) - local co_eps = embedded_cluster_utils.get_endpoints(device, clusters.SmokeCoAlarm.ID, {feature_bitmap = clusters.SmokeCoAlarm.types.Feature.CO_ALARM}) - local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) - local humidity_eps = embedded_cluster_utils.get_endpoints(device, clusters.RelativeHumidityMeasurement.ID) - local co_meas_eps = embedded_cluster_utils.get_endpoints(device, clusters.CarbonMonoxideConcentrationMeasurement.ID, {feature_bitmap = clusters.CarbonMonoxideConcentrationMeasurement.types.Feature.NUMERIC_MEASUREMENT}) - local co_level_eps = embedded_cluster_utils.get_endpoints(device, clusters.CarbonMonoxideConcentrationMeasurement.ID, {feature_bitmap = clusters.CarbonMonoxideConcentrationMeasurement.types.Feature.LEVEL_INDICATION}) - - local profile_name = "" - - -- battery and hardware fault are mandatory - if #smoke_eps > 0 then - profile_name = profile_name .. "-smoke" - end - if #co_eps > 0 then - profile_name = profile_name .. "-co" - end - if #temp_eps > 0 then - profile_name = profile_name .. "-temp" - end - if #humidity_eps > 0 then - profile_name = profile_name .. "-humidity" - end - if #co_meas_eps > 0 then - profile_name = profile_name .. "-comeas" - end - if #co_level_eps > 0 then - profile_name = profile_name .. "-colevel" - end - if battery_supported == battery_support.BATTERY_PERCENTAGE then - profile_name = profile_name .. "-battery" - end - - -- remove leading "-" - profile_name = string.sub(profile_name, 2) - - if tbl_contains(supported_profiles, profile_name) then - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) - else - device.log.warn_with({hub_logs=true}, string.format("No matching profile for device. Tried to use profile %s.", profile_name)) - profile_name = "" - if #smoke_eps > 0 and #co_eps > 0 then - profile_name = "smoke-co" - elseif #smoke_eps > 0 and #co_eps == 0 then - profile_name = "smoke" - elseif #co_eps > 0 and #smoke_eps == 0 then - profile_name = "co" - end - device.log.info_with({hub_logs=true}, string.format("Using generic device profile %s.", profile_name)) - end - device:try_update_metadata({profile = profile_name}) -end - -local function device_init(driver, device) - device:subscribe() -end - -local function info_changed(self, device, event, args) - if device.preferences then - if device.preferences["certifiedpreferences.smokeSensorSensitivity"] ~= args.old_st_store.preferences["certifiedpreferences.smokeSensorSensitivity"] then - local eps = embedded_cluster_utils.get_endpoints(device, clusters.SmokeCoAlarm.ID) - if #eps > 0 then - local smokeSensorSensitivity = device.preferences["certifiedpreferences.smokeSensorSensitivity"] - if smokeSensorSensitivity == "0" then -- High - device:send(clusters.SmokeCoAlarm.attributes.SmokeSensitivityLevel:write(device, eps[1], clusters.SmokeCoAlarm.types.SensitivityEnum.HIGH)) - elseif smokeSensorSensitivity == "1" then -- Medium - device:send(clusters.SmokeCoAlarm.attributes.SmokeSensitivityLevel:write(device, eps[1], clusters.SmokeCoAlarm.types.SensitivityEnum.STANDARD)) - elseif smokeSensorSensitivity == "2" then -- Low - device:send(clusters.SmokeCoAlarm.attributes.SmokeSensitivityLevel:write(device, eps[1], clusters.SmokeCoAlarm.types.SensitivityEnum.LOW)) - end - end - end - end - - -- resubscribe to new attributes as needed if a profile switch occured - if device.profile.id ~= args.old_st_store.profile.id then - device:subscribe() - end -end - --- Matter Handlers -- -local function binary_state_handler_factory(zeroEvent, nonZeroEvent) - return function(driver, device, ib, response) - if ib.data.value == 0 and zeroEvent ~= nil then - device:emit_event_for_endpoint(ib.endpoint_id, zeroEvent) - elseif nonZeroEvent ~= nil then - device:emit_event_for_endpoint(ib.endpoint_id, nonZeroEvent) - end - end -end - -local function test_in_progress_event_handler(driver, device, ib, response) - if device:supports_capability(capabilities.smokeDetector) then - if ib.data.value then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.smokeDetector.smoke.tested()) - else - device:send(clusters.SmokeCoAlarm.attributes.SmokeState:read(device)) - end - end - if device:supports_capability(capabilities.carbonMonoxideDetector) then - if ib.data.value then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.carbonMonoxideDetector.carbonMonoxide.tested()) - else - device:send(clusters.SmokeCoAlarm.attributes.COState:read(device)) - end - end -end - -local function carbon_monoxide_attr_handler(driver, device, ib, response) - local value = ib.data.value - local unit = device:get_field(CARBON_MONOXIDE_MEASUREMENT_UNIT) - if unit == clusters.CarbonMonoxideConcentrationMeasurement.types.MeasurementUnitEnum.PPB then - value = value / 1000 - elseif unit == clusters.CarbonMonoxideConcentrationMeasurement.types.MeasurementUnitEnum.PPT then - value = value / 1000000 - end - value = math.floor(value) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.carbonMonoxideMeasurement.carbonMonoxideLevel({value = value, unit = "ppm"})) -end - -local function carbon_monoxide_unit_attr_handler(driver, device, ib, response) - local unit = ib.data.value - device:set_field(CARBON_MONOXIDE_MEASUREMENT_UNIT, unit, { persist = true }) -end - -local function hardware_fault_capability_handler(device) - local batLevel, batAlert = device:get_field(BatteryLevel), device:get_field(BatteryAlert) - if device:get_field(HardwareFaultAlert) == true or (batLevel and batAlert and (batAlert > batLevel)) then - device:emit_event(capabilities.hardwareFault.hardwareFault.detected()) - else - device:emit_event(capabilities.hardwareFault.hardwareFault.clear()) - end -end - -local function hardware_fault_alert_handler(driver, device, ib, response) - device:set_field(HardwareFaultAlert, ib.data.value, {persist = true}) - hardware_fault_capability_handler(device) -end - -local function battery_alert_attr_handler(driver, device, ib, response) - device:set_field(BatteryAlert, ib.data.value, {persist = true}) - hardware_fault_capability_handler(device) -end - -local function power_source_attribute_list_handler(driver, device, ib, response) - for _, attr in ipairs(ib.data.elements) do - -- Re-profile the device if BatPercentRemaining (Attribute ID 0x0C) or - -- BatChargeLevel (Attribute ID 0x0E) is present. - if attr.value == 0x0C then - match_profile(device, battery_support.BATTERY_PERCENTAGE) - return - elseif attr.value == 0x0E then - match_profile(device, battery_support.BATTERY_LEVEL) - return - end - end -end - -local function handle_battery_charge_level(driver, device, ib, response) - device:set_field(BatteryLevel, ib.data.value, {persist = true}) -- value used in hardware_fault_capability_handler - if device:supports_capability(capabilities.batteryLevel) then -- check required since attribute is subscribed to even without batteryLevel support, to set the field above - if ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.OK then - device:emit_event(capabilities.batteryLevel.battery.normal()) - elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.WARNING then - device:emit_event(capabilities.batteryLevel.battery.warning()) - elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.CRITICAL then - device:emit_event(capabilities.batteryLevel.battery.critical()) - end - end -end - -local function handle_battery_percent_remaining(driver, device, ib, response) - if ib.data.value ~= nil then - device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) - end -end - -local function do_configure(driver, device) - local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) - if #battery_feature_eps > 0 then - device:send(clusters.PowerSource.attributes.AttributeList:read()) - else - match_profile(device, battery_support.NO_BATTERY) - end -end - -local matter_smoke_co_alarm_handler = { - NAME = "matter-smoke-co-alarm", - lifecycle_handlers = { - init = device_init, - infoChanged = info_changed, - doConfigure = do_configure - }, - matter_handlers = { - attr = { - [clusters.SmokeCoAlarm.ID] = { - [clusters.SmokeCoAlarm.attributes.SmokeState.ID] = binary_state_handler_factory(capabilities.smokeDetector.smoke.clear(), capabilities.smokeDetector.smoke.detected()), - [clusters.SmokeCoAlarm.attributes.COState.ID] = binary_state_handler_factory(capabilities.carbonMonoxideDetector.carbonMonoxide.clear(), capabilities.carbonMonoxideDetector.carbonMonoxide.detected()), - [clusters.SmokeCoAlarm.attributes.BatteryAlert.ID] = battery_alert_attr_handler, - [clusters.SmokeCoAlarm.attributes.TestInProgress.ID] = test_in_progress_event_handler, - [clusters.SmokeCoAlarm.attributes.HardwareFaultAlert.ID] = hardware_fault_alert_handler, - }, - [clusters.CarbonMonoxideConcentrationMeasurement.ID] = { - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = carbon_monoxide_attr_handler, - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = carbon_monoxide_unit_attr_handler, - }, - [clusters.PowerSource.ID] = { - [clusters.PowerSource.attributes.AttributeList.ID] = power_source_attribute_list_handler, - [clusters.PowerSource.attributes.BatPercentRemaining.ID] = handle_battery_percent_remaining, - [clusters.PowerSource.attributes.BatChargeLevel.ID] = handle_battery_charge_level, - }, - }, - }, - subscribed_attributes = { - [capabilities.smokeDetector.ID] = { - clusters.SmokeCoAlarm.attributes.SmokeState, - clusters.SmokeCoAlarm.attributes.TestInProgress, - }, - [capabilities.carbonMonoxideDetector.ID] = { - clusters.SmokeCoAlarm.attributes.COState, - clusters.SmokeCoAlarm.attributes.TestInProgress, - }, - [capabilities.hardwareFault.ID] = { - clusters.SmokeCoAlarm.attributes.HardwareFaultAlert, - clusters.SmokeCoAlarm.attributes.BatteryAlert, - clusters.PowerSource.attributes.BatChargeLevel, - }, - [capabilities.temperatureMeasurement.ID] = { - clusters.TemperatureMeasurement.attributes.MeasuredValue - }, - [capabilities.relativeHumidityMeasurement.ID] = { - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue - }, - [capabilities.carbonMonoxideMeasurement.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.batteryLevel.ID] = { - clusters.PowerSource.attributes.BatChargeLevel, - }, - [capabilities.battery.ID] = { - clusters.PowerSource.attributes.BatPercentRemaining, - } - }, - can_handle = is_matter_smoke_co_alarm -} - -return matter_smoke_co_alarm_handler \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers.lua new file mode 100644 index 0000000000..3bf4c46f73 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("sub_drivers.air_quality_sensor"), + lazy_load_if_possible("sub_drivers.smoke_co_alarm"), + lazy_load_if_possible("sub_drivers.bosch_button_contact"), +} +return sub_drivers diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_handlers/attribute_handlers.lua new file mode 100644 index 0000000000..345f7c5782 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_handlers/attribute_handlers.lua @@ -0,0 +1,74 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local st_utils = require "st.utils" +local capabilities = require "st.capabilities" +local aqs_fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields" + +local AirQualitySensorAttributeHandlers = {} + + +-- [[ GENERIC CONCENTRATION MEASUREMENT CLUSTER ATTRIBUTES ]] + +function AirQualitySensorAttributeHandlers.measurement_unit_factory(capability_name) + return function(driver, device, ib, response) + device:set_field(capability_name.."_unit", ib.data.value, {persist = true}) + end +end + +function AirQualitySensorAttributeHandlers.level_value_factory(attribute) + return function(driver, device, ib, response) + device:emit_event_for_endpoint(ib.endpoint_id, attribute(aqs_fields.level_strings[ib.data.value])) + end +end + +function AirQualitySensorAttributeHandlers.measured_value_factory(capability_name, attribute, target_unit) + return function(driver, device, ib, response) + if ib.data.value then + local reporting_unit = device:get_field(capability_name.."_unit") or aqs_fields.unit_default[capability_name] + local conversion_function = aqs_fields.conversion_tables[reporting_unit][target_unit] + if conversion_function then + local converted_value = conversion_function(ib.data.value) + device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = converted_value, unit = aqs_fields.unit_strings[target_unit]})) + -- handle case where device profile supports both fineDustLevel and dustLevel + if capability_name == capabilities.fineDustSensor.NAME and device:supports_capability(capabilities.dustSensor) then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.dustSensor.fineDustLevel({value = converted_value, unit = aqs_fields.unit_strings[target_unit]})) + end + else + device.log.info_with({hub_logs=true}, string.format("Unsupported unit conversion from %s to %s", aqs_fields.unit_strings[reporting_unit], aqs_fields.unit_strings[target_unit])) + end + end + end +end + + +-- [[ AIR QUALITY CLUSTER ATTRIBUTES ]] -- + +function AirQualitySensorAttributeHandlers.air_quality_handler(driver, device, ib, response) + local state = ib.data.value + if state == 0 then -- Unknown + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unknown()) + elseif state == 1 then -- Good + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.good()) + elseif state == 2 then -- Fair + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.moderate()) + elseif state == 3 then -- Moderate + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.slightlyUnhealthy()) + elseif state == 4 then -- Poor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unhealthy()) + elseif state == 5 then -- VeryPoor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.veryUnhealthy()) + elseif state == 6 then -- ExtremelyPoor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.hazardous()) + end +end + + +-- [[ PRESSURE MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AirQualitySensorAttributeHandlers.pressure_measured_value_handler(driver, device, ib, response) + local pressure = st_utils.round(ib.data.value / 10.0) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.atmosphericPressureMeasurement.atmosphericPressure(pressure)) +end + +return AirQualitySensorAttributeHandlers diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/device_configuration.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/device_configuration.lua new file mode 100644 index 0000000000..a30eaed0f8 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/device_configuration.lua @@ -0,0 +1,88 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local version = require "version" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils" +local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields" + +local DeviceConfiguration = {} + +function DeviceConfiguration.supported_level_measurements(device) + local measurement_caps, level_caps = {}, {} + for _, cap in ipairs(fields.CONCENTRATION_MEASUREMENT_PROFILE_ORDERING) do + local cap_id = cap.ID + local cluster = fields.CONCENTRATION_MEASUREMENT_MAP[cap][2] + -- capability describes either a HealthConcern or Measurement/Sensor + if (cap_id:match("HealthConcern$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) + if #attr_eps > 0 then + table.insert(level_caps, cap_id) + end + elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) + if #attr_eps > 0 then + table.insert(measurement_caps, cap_id) + end + end + end + return measurement_caps, level_caps +end + +-- Match Modular Profile +function DeviceConfiguration.match_profile(device) + local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) + local humidity_eps = embedded_cluster_utils.get_endpoints(device, clusters.RelativeHumidityMeasurement.ID) + + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + local profile_name + local MAIN_COMPONENT_IDX = 1 + local CAPABILITIES_LIST_IDX = 2 + + if #temp_eps > 0 then + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + end + if #humidity_eps > 0 then + table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) + end + + local measurement_caps, level_caps = DeviceConfiguration.supported_level_measurements(device) + + for _, cap_id in ipairs(measurement_caps) do + table.insert(main_component_capabilities, cap_id) + end + + for _, cap_id in ipairs(level_caps) do + table.insert(main_component_capabilities, cap_id) + end + + table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + + if #temp_eps > 0 and #humidity_eps > 0 then + profile_name = "aqs-modular-temp-humidity" + elseif #temp_eps > 0 then + profile_name = "aqs-modular-temp" + elseif #humidity_eps > 0 then + profile_name = "aqs-modular-humidity" + else + profile_name = "aqs-modular" + end + + device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) + + -- earlier modular profile gating (min api v14, rpc 8) ensures we are running >= 0.57 FW. + -- This gating specifies a workaround required only for 0.57 FW, which is not needed for 0.58 and higher. + if version.api < 15 or version.rpc < 9 then + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airQualityHealthConcern.ID) + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.refresh.ID) + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.firmwareUpdate.ID) + + device:set_field(fields.SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end +end + +return DeviceConfiguration \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/fields.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/fields.lua new file mode 100644 index 0000000000..1e4658c99e --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/fields.lua @@ -0,0 +1,178 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local version = require "version" +local utils = require "st.utils" +local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" + +-- Include driver-side definitions when lua libs api version is < 10 +if version.api < 10 then + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" +end + + +local AirQualitySensorFields = {} + +AirQualitySensorFields.AIR_QUALITY_SENSOR_DEVICE_TYPE_ID = 0x002C + +AirQualitySensorFields.SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities" + +AirQualitySensorFields.units_required = { + clusters.CarbonMonoxideConcentrationMeasurement, + clusters.CarbonDioxideConcentrationMeasurement, + clusters.NitrogenDioxideConcentrationMeasurement, + clusters.OzoneConcentrationMeasurement, + clusters.FormaldehydeConcentrationMeasurement, + clusters.Pm1ConcentrationMeasurement, + clusters.Pm25ConcentrationMeasurement, + clusters.Pm10ConcentrationMeasurement, + clusters.RadonConcentrationMeasurement, + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement +} + +AirQualitySensorFields.supported_profiles = +{ + "aqs", + "aqs-temp-humidity-all-level-all-meas", + "aqs-temp-humidity-all-level", + "aqs-temp-humidity-all-meas", + "aqs-temp-humidity-co2-pm25-tvoc-meas", + "aqs-temp-humidity-co2-pm1-pm25-pm10-meas", + "aqs-temp-humidity-tvoc-level-pm25-meas", + "aqs-temp-humidity-tvoc-meas", +} + +AirQualitySensorFields.CONCENTRATION_MEASUREMENT_MAP = { + [capabilities.carbonMonoxideMeasurement] = {"-co", clusters.CarbonMonoxideConcentrationMeasurement, "N/A"}, + [capabilities.carbonMonoxideHealthConcern] = {"-co", clusters.CarbonMonoxideConcentrationMeasurement, capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues}, + [capabilities.carbonDioxideMeasurement] = {"-co2", clusters.CarbonDioxideConcentrationMeasurement, "N/A"}, + [capabilities.carbonDioxideHealthConcern] = {"-co2", clusters.CarbonDioxideConcentrationMeasurement, capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues}, + [capabilities.nitrogenDioxideMeasurement] = {"-no2", clusters.NitrogenDioxideConcentrationMeasurement, "N/A"}, + [capabilities.nitrogenDioxideHealthConcern] = {"-no2", clusters.NitrogenDioxideConcentrationMeasurement, capabilities.nitrogenDioxideHealthConcern.supportedNitrogenDioxideValues}, + [capabilities.ozoneMeasurement] = {"-ozone", clusters.OzoneConcentrationMeasurement, "N/A"}, + [capabilities.ozoneHealthConcern] = {"-ozone", clusters.OzoneConcentrationMeasurement, capabilities.ozoneHealthConcern.supportedOzoneValues}, + [capabilities.formaldehydeMeasurement] = {"-ch2o", clusters.FormaldehydeConcentrationMeasurement, "N/A"}, + [capabilities.formaldehydeHealthConcern] = {"-ch2o", clusters.FormaldehydeConcentrationMeasurement, capabilities.formaldehydeHealthConcern.supportedFormaldehydeValues}, + [capabilities.veryFineDustSensor] = {"-pm1", clusters.Pm1ConcentrationMeasurement, "N/A"}, + [capabilities.veryFineDustHealthConcern] = {"-pm1", clusters.Pm1ConcentrationMeasurement, capabilities.veryFineDustHealthConcern.supportedVeryFineDustValues}, + [capabilities.fineDustSensor] = {"-pm25", clusters.Pm25ConcentrationMeasurement, "N/A"}, + [capabilities.fineDustHealthConcern] = {"-pm25", clusters.Pm25ConcentrationMeasurement, capabilities.fineDustHealthConcern.supportedFineDustValues}, + [capabilities.dustSensor] = {"-pm10", clusters.Pm10ConcentrationMeasurement, "N/A"}, + [capabilities.dustHealthConcern] = {"-pm10", clusters.Pm10ConcentrationMeasurement, capabilities.dustHealthConcern.supportedDustValues}, + [capabilities.radonMeasurement] = {"-radon", clusters.RadonConcentrationMeasurement, "N/A"}, + [capabilities.radonHealthConcern] = {"-radon", clusters.RadonConcentrationMeasurement, capabilities.radonHealthConcern.supportedRadonValues}, + [capabilities.tvocMeasurement] = {"-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement, "N/A"}, + [capabilities.tvocHealthConcern] = {"-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement, capabilities.tvocHealthConcern.supportedTvocValues}, +} + + +AirQualitySensorFields.CONCENTRATION_MEASUREMENT_PROFILE_ORDERING = { + capabilities.carbonMonoxideMeasurement, + capabilities.carbonMonoxideHealthConcern, + capabilities.carbonDioxideMeasurement, + capabilities.carbonDioxideHealthConcern, + capabilities.nitrogenDioxideMeasurement, + capabilities.nitrogenDioxideHealthConcern, + capabilities.ozoneMeasurement, + capabilities.ozoneHealthConcern, + capabilities.formaldehydeMeasurement, + capabilities.formaldehydeHealthConcern, + capabilities.veryFineDustSensor, + capabilities.veryFineDustHealthConcern, + capabilities.fineDustSensor, + capabilities.fineDustHealthConcern, + capabilities.dustSensor, + capabilities.dustHealthConcern, + capabilities.radonMeasurement, + capabilities.radonHealthConcern, + capabilities.tvocMeasurement, + capabilities.tvocHealthConcern, +} + +AirQualitySensorFields.units = { + PPM = 0, + PPB = 1, + PPT = 2, + MGM3 = 3, + UGM3 = 4, + NGM3 = 5, + PM3 = 6, + BQM3 = 7, + PCIL = 0xFF -- not in matter spec +} + +local units = AirQualitySensorFields.units -- copy to remove the prefix in uses below + +AirQualitySensorFields.unit_strings = { + [units.PPM] = "ppm", + [units.PPB] = "ppb", + [units.PPT] = "ppt", + [units.MGM3] = "mg/m^3", + [units.NGM3] = "ng/m^3", + [units.UGM3] = "μg/m^3", + [units.BQM3] = "Bq/m^3", + [units.PCIL] = "pCi/L" +} + +AirQualitySensorFields.unit_default = { + [capabilities.carbonMonoxideMeasurement.NAME] = units.PPM, + [capabilities.carbonDioxideMeasurement.NAME] = units.PPM, + [capabilities.nitrogenDioxideMeasurement.NAME] = units.PPM, + [capabilities.ozoneMeasurement.NAME] = units.PPM, + [capabilities.formaldehydeMeasurement.NAME] = units.PPM, + [capabilities.veryFineDustSensor.NAME] = units.UGM3, + [capabilities.fineDustSensor.NAME] = units.UGM3, + [capabilities.dustSensor.NAME] = units.UGM3, + [capabilities.radonMeasurement.NAME] = units.BQM3, + [capabilities.tvocMeasurement.NAME] = units.PPB -- TVOC is typically within the range of 0-5500 ppb, with good to moderate values being < 660 ppb +} + +-- All ConcentrationMeasurement clusters inherit from the same base cluster definitions, +-- so CarbonMonoxideConcentrationMeasurement is used below but the same enum types exist +-- in all ConcentrationMeasurement clusters +AirQualitySensorFields.level_strings = { + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.UNKNOWN] = "unknown", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.LOW] = "good", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.MEDIUM] = "moderate", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.HIGH] = "unhealthy", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.CRITICAL] = "hazardous", +} + +AirQualitySensorFields.conversion_tables = { + [units.PPM] = { + [units.PPM] = function(value) return utils.round(value) end, + [units.PPB] = function(value) return utils.round(value * (10^3)) end + }, + [units.PPB] = { + [units.PPM] = function(value) return utils.round(value/(10^3)) end, + [units.PPB] = function(value) return utils.round(value) end + }, + [units.PPT] = { + [units.PPM] = function(value) return utils.round(value/(10^6)) end + }, + [units.MGM3] = { + [units.UGM3] = function(value) return utils.round(value * (10^3)) end + }, + [units.UGM3] = { + [units.UGM3] = function(value) return utils.round(value) end + }, + [units.NGM3] = { + [units.UGM3] = function(value) return utils.round(value/(10^3)) end + }, + [units.BQM3] = { + [units.PCIL] = function(value) return utils.round(value/37) end + } +} + +return AirQualitySensorFields diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/legacy_device_configuration.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/legacy_device_configuration.lua new file mode 100644 index 0000000000..bc45021341 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/legacy_device_configuration.lua @@ -0,0 +1,89 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" +local sensor_utils = require "sensor_utils.utils" +local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils" +local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields" + +local LegacyDeviceConfiguration = {} + +function LegacyDeviceConfiguration.create_level_measurement_profile(device) + local meas_name, level_name = "", "" + for _, cap in ipairs(fields.CONCENTRATION_MEASUREMENT_PROFILE_ORDERING) do + local cap_id = cap.ID + local cluster = fields.CONCENTRATION_MEASUREMENT_MAP[cap][2] + -- capability describes either a HealthConcern or Measurement/Sensor + if (cap_id:match("HealthConcern$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) + if #attr_eps > 0 then + level_name = level_name .. fields.CONCENTRATION_MEASUREMENT_MAP[cap][1] + end + elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) + if #attr_eps > 0 then + meas_name = meas_name .. fields.CONCENTRATION_MEASUREMENT_MAP[cap][1] + end + end + end + return meas_name, level_name +end + +-- MATCH STATIC PROFILE +function LegacyDeviceConfiguration.match_profile(device) + local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) + local humidity_eps = embedded_cluster_utils.get_endpoints(device, clusters.RelativeHumidityMeasurement.ID) + + local profile_name = "aqs" + + if #temp_eps > 0 then + profile_name = profile_name .. "-temp" + end + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + + local meas_name, level_name = LegacyDeviceConfiguration.create_level_measurement_profile(device) + + -- If all endpoints are supported, use '-all' in the profile name so that it + -- remains under the profile name character limit + if level_name == "-co-co2-no2-ozone-ch2o-pm1-pm25-pm10-radon-tvoc" then + level_name = "-all" + end + if level_name ~= "" then + profile_name = profile_name .. level_name .. "-level" + end + + -- If all endpoints are supported, use '-all' in the profile name so that it + -- remains under the profile name character limit + if meas_name == "-co-co2-no2-ozone-ch2o-pm1-pm25-pm10-radon-tvoc" then + meas_name = "-all" + end + if meas_name ~= "" then + profile_name = profile_name .. meas_name .. "-meas" + end + + if not sensor_utils.tbl_contains(fields.supported_profiles, profile_name) then + device.log.warn_with({hub_logs=true}, string.format("No matching profile for device. Tried to use profile %s", profile_name)) + + local function meas_find(sub_name) + return string.match(meas_name, sub_name) ~= nil + end + + -- try to best match to existing profiles + -- these checks, meas_find("co%-") and meas_find("co$"), match the string to co and NOT co2. + if meas_find("co%-") or meas_find("co$") or meas_find("no2") or meas_find("ozone") or meas_find("ch2o") or + meas_find("pm1") or meas_find("pm10") or meas_find("radon") then + profile_name = "aqs-temp-humidity-all-meas" + elseif #humidity_eps > 0 or #temp_eps > 0 or meas_find("co2") or meas_find("pm25") or meas_find("tvoc") then + profile_name = "aqs-temp-humidity-co2-pm25-tvoc-meas" + else + -- device only supports air quality at this point + profile_name = "aqs" + end + end + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s", profile_name)) + device:try_update_metadata({profile = profile_name}) +end + +return LegacyDeviceConfiguration \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/utils.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/utils.lua new file mode 100644 index 0000000000..95ca80964c --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/utils.lua @@ -0,0 +1,98 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils" +local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields" + + +local AirQualitySensorUtils = {} + +function AirQualitySensorUtils.supports_capability_by_id_modular(device, capability, component) + if not device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then + device.log.warn_with({hub_logs = true}, "Device has overriden supports_capability_by_id, but does not have supported capabilities set.") + return false + end + for _, component_capabilities in ipairs(device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES)) do + local comp_id = component_capabilities[1] + local capability_ids = component_capabilities[2] + if (component == nil) or (component == comp_id) then + for _, cap in ipairs(capability_ids) do + if cap == capability then + return true + end + end + end + end + return false +end + +local function get_supported_health_concern_values_for_air_quality(device) + local health_concern_datatype = capabilities.airQualityHealthConcern.airQualityHealthConcern + local supported_values = {health_concern_datatype.unknown.NAME, health_concern_datatype.good.NAME, health_concern_datatype.unhealthy.NAME} + if #embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID, { feature_bitmap = clusters.AirQuality.types.Feature.FAIR }) > 0 then + table.insert(supported_values, health_concern_datatype.moderate.NAME) + end + if #embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID, { feature_bitmap = clusters.AirQuality.types.Feature.MODERATE }) > 0 then + table.insert(supported_values, health_concern_datatype.slightlyUnhealthy.NAME) + end + if #embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID, { feature_bitmap = clusters.AirQuality.types.Feature.VERY_POOR }) > 0 then + table.insert(supported_values, health_concern_datatype.veryUnhealthy.NAME) + end + if #embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID, { feature_bitmap = clusters.AirQuality.types.Feature.EXTREMELY_POOR }) > 0 then + table.insert(supported_values, health_concern_datatype.hazardous.NAME) + end + return supported_values +end + +local function get_supported_health_concern_values_for_concentration_cluster(device, cluster) + -- note: health_concern_datatype is generic since all the healthConcern capabilities' datatypes are equivalent to those in airQualityHealthConcern + local health_concern_datatype = capabilities.airQualityHealthConcern.airQualityHealthConcern + local supported_values = {health_concern_datatype.unknown.NAME, health_concern_datatype.good.NAME, health_concern_datatype.unhealthy.NAME} + if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.MEDIUM_LEVEL }) > 0 then + table.insert(supported_values, health_concern_datatype.moderate.NAME) + end + if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.CRITICAL_LEVEL }) > 0 then + table.insert(supported_values, health_concern_datatype.hazardous.NAME) + end + return supported_values +end + +function AirQualitySensorUtils.set_supported_health_concern_values(device) + -- handle AQ Health Concern, since this is a mandatory capability + local supported_aqs_values = get_supported_health_concern_values_for_air_quality(device) + local aqs_ep_ids = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) or {} + device:emit_event_for_endpoint(aqs_ep_ids[1], capabilities.airQualityHealthConcern.supportedAirQualityValues(supported_aqs_values, { visibility = { displayed = false }})) + + for _, capability in ipairs(fields.CONCENTRATION_MEASUREMENT_PROFILE_ORDERING) do + -- all of these capabilities are optional, and capabilities stored in this field are for either a HealthConcern or a Measurement/Sensor + if device:supports_capability_by_id(capability.ID) and capability.ID:match("HealthConcern$") then + local cluster_info = fields.CONCENTRATION_MEASUREMENT_MAP[capability][2] + local supported_values_setter = fields.CONCENTRATION_MEASUREMENT_MAP[capability][3] + local supported_values = get_supported_health_concern_values_for_concentration_cluster(device, cluster_info) + local cluster_ep_ids = embedded_cluster_utils.get_endpoints(device, cluster_info.ID, { feature_bitmap = cluster_info.types.Feature.LEVEL_INDICATION }) or {} -- cluster associated with the supported capability + device:emit_event_for_endpoint(cluster_ep_ids[1], supported_values_setter(supported_values, { visibility = { displayed = false }})) + end + end +end + +function AirQualitySensorUtils.profile_changed(synced_components, prev_components) + if #synced_components ~= #prev_components then + return true + end + for _, component in pairs(synced_components or {}) do + if (prev_components[component.id] == nil) or + (#component.capabilities ~= #prev_components[component.id].capabilities) then + return true + end + for _, capability in pairs(component.capabilities or {}) do + if prev_components[component.id][capability.id] == nil then + return true + end + end + end + return false +end + +return AirQualitySensorUtils diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/can_handle.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/can_handle.lua new file mode 100644 index 0000000000..9c1a1ab762 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_matter_air_quality_sensor(opts, driver, device) + local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields" + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == fields.AIR_QUALITY_SENSOR_DEVICE_TYPE_ID then + return true, require("sub_drivers.air_quality_sensor") + end + end + end + + return false +end + +return is_matter_air_quality_sensor diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua new file mode 100644 index 0000000000..98b8430c98 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua @@ -0,0 +1,154 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local version = require "version" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local aqs_utils = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.utils" +local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields" +local attribute_handlers = require "sub_drivers.air_quality_sensor.air_quality_sensor_handlers.attribute_handlers" + +-- Include driver-side definitions when lua libs api version is < 10 +if version.api < 10 then + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" +end + +-- AIR QUALITY SENSOR LIFECYCLE HANDLERS -- + +local AirQualitySensorLifecycleHandlers = {} + +function AirQualitySensorLifecycleHandlers.do_configure(driver, device) + -- we have to read the unit before reports of values will do anything + for _, cluster in ipairs(fields.units_required) do + device:send(cluster.attributes.MeasurementUnit:read(device)) + end + if version.api >= 14 and version.rpc >= 8 then + local modular_device_cfg = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.device_configuration" + modular_device_cfg.match_profile(device) + else + local legacy_device_cfg = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.legacy_device_configuration" + legacy_device_cfg.match_profile(device) + end +end + +function AirQualitySensorLifecycleHandlers.driver_switched(driver, device) + -- we have to read the unit before reports of values will do anything + for _, cluster in ipairs(fields.units_required) do + device:send(cluster.attributes.MeasurementUnit:read(device)) + end + if version.api >= 14 and version.rpc >= 8 then + local modular_device_cfg = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.device_configuration" + modular_device_cfg.match_profile(device) + else + local legacy_device_cfg = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.legacy_device_configuration" + legacy_device_cfg.match_profile(device) + end +end + +function AirQualitySensorLifecycleHandlers.device_init(driver, device) + if device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) and (version.api < 15 or version.rpc < 9) then + -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id + -- library function to utilize optional capabilities + device:extend_device("supports_capability_by_id", aqs_utils.supports_capability_by_id_modular) + end + aqs_utils.set_supported_health_concern_values(device) + device:subscribe() +end + +function AirQualitySensorLifecycleHandlers.info_changed(driver, device, event, args) + if device.profile.id ~= args.old_st_store.profile.id or + aqs_utils.profile_changed(device.profile.components, args.old_st_store.profile.components) then + if device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then + --re-up subscription with new capabilities using the modular supports_capability override + device:extend_device("supports_capability_by_id", aqs_utils.supports_capability_by_id_modular) + end + aqs_utils.set_supported_health_concern_values(device) + device:subscribe() + end +end + + +-- SUBDRIVER TEMPLATE -- + +local matter_air_quality_sensor_handler = { + NAME = "matter-air-quality-sensor", + lifecycle_handlers = { + doConfigure = AirQualitySensorLifecycleHandlers.do_configure, + driverSwitched = AirQualitySensorLifecycleHandlers.driver_switched, + infoChanged = AirQualitySensorLifecycleHandlers.info_changed, + init = AirQualitySensorLifecycleHandlers.device_init, + }, + matter_handlers = { + attr = { + [clusters.AirQuality.ID] = { + [clusters.AirQuality.attributes.AirQuality.ID] = attribute_handlers.air_quality_handler, + }, + [clusters.CarbonDioxideConcentrationMeasurement.ID] = { + [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.carbonDioxideMeasurement.NAME, capabilities.carbonDioxideMeasurement.carbonDioxide, fields.units.PPM), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.carbonDioxideMeasurement.NAME), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern), + }, + [clusters.CarbonMonoxideConcentrationMeasurement.ID] = { + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.carbonMonoxideMeasurement.NAME, capabilities.carbonMonoxideMeasurement.carbonMonoxideLevel, fields.units.PPM), + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.carbonMonoxideMeasurement.NAME), + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.carbonMonoxideHealthConcern.carbonMonoxideHealthConcern), + }, + [clusters.FormaldehydeConcentrationMeasurement.ID] = { + [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.formaldehydeMeasurement.NAME, capabilities.formaldehydeMeasurement.formaldehydeLevel, fields.units.PPM), + [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.formaldehydeMeasurement.NAME), + [clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.formaldehydeHealthConcern.formaldehydeHealthConcern), + }, + [clusters.NitrogenDioxideConcentrationMeasurement.ID] = { + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.nitrogenDioxideMeasurement.NAME, capabilities.nitrogenDioxideMeasurement.nitrogenDioxide, fields.units.PPM), + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.nitrogenDioxideMeasurement.NAME), + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.nitrogenDioxideHealthConcern.nitrogenDioxideHealthConcern) + }, + [clusters.OzoneConcentrationMeasurement.ID] = { + [clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.ozoneMeasurement.NAME, capabilities.ozoneMeasurement.ozone, fields.units.PPM), + [clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.ozoneMeasurement.NAME), + [clusters.OzoneConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.ozoneHealthConcern.ozoneHealthConcern) + }, + [clusters.Pm1ConcentrationMeasurement.ID] = { + [clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.veryFineDustSensor.NAME, capabilities.veryFineDustSensor.veryFineDustLevel, fields.units.UGM3), + [clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.veryFineDustSensor.NAME), + [clusters.Pm1ConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern), + }, + [clusters.Pm10ConcentrationMeasurement.ID] = { + [clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.dustSensor.NAME, capabilities.dustSensor.dustLevel, fields.units.UGM3), + [clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.dustSensor.NAME), + [clusters.Pm10ConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.dustHealthConcern.dustHealthConcern), + }, + [clusters.Pm25ConcentrationMeasurement.ID] = { + [clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.fineDustSensor.NAME, capabilities.fineDustSensor.fineDustLevel, fields.units.UGM3), + [clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.fineDustSensor.NAME), + [clusters.Pm25ConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.fineDustHealthConcern.fineDustHealthConcern), + }, + [clusters.PressureMeasurement.ID] = { + [clusters.PressureMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.pressure_measured_value_handler + }, + [clusters.RadonConcentrationMeasurement.ID] = { + [clusters.RadonConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.radonMeasurement.NAME, capabilities.radonMeasurement.radonLevel, fields.units.PCIL), + [clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.radonMeasurement.NAME), + [clusters.RadonConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.radonHealthConcern.radonHealthConcern) + }, + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.ID] = { + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.tvocMeasurement.NAME, capabilities.tvocMeasurement.tvocLevel, fields.units.PPB), + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.tvocMeasurement.NAME), + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.tvocHealthConcern.tvocHealthConcern) + } + } + }, + can_handle = require("sub_drivers.air_quality_sensor.can_handle") +} + +return matter_air_quality_sensor_handler diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/can_handle.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/can_handle.lua new file mode 100644 index 0000000000..9c679f3607 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_bosch_button_contact(opts, driver, device) + local device_lib = require "st.device" + local BOSCH_VENDOR_ID = 0x1209 + local BOSCH_PRODUCT_ID = 0x3015 + if device.network_type == device_lib.NETWORK_TYPE_MATTER and + device.manufacturer_info.vendor_id == BOSCH_VENDOR_ID and + device.manufacturer_info.product_id == BOSCH_PRODUCT_ID then + return true, require("sub_drivers.bosch_button_contact") + end + return false +end + +return is_bosch_button_contact diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/init.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/init.lua new file mode 100644 index 0000000000..cbbd71cf53 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/init.lua @@ -0,0 +1,138 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local lua_socket = require "socket" +local log = require "log" + +local START_BUTTON_PRESS = "__start_button_press" + +local function get_field_for_endpoint(device, field, endpoint) + return device:get_field(string.format("%s_%d", field, endpoint)) +end + +local function set_field_for_endpoint(device, field, endpoint, value, additional_params) + device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) +end + +local function init_press(device, endpoint) + set_field_for_endpoint(device, START_BUTTON_PRESS, endpoint, lua_socket.gettime(), {persist = false}) +end + +-- Some switches will send a MultiPressComplete event as part of a long press sequence. Normally the driver will create a +-- button capability event on receipt of MultiPressComplete, but in this case that would result in an extra event because +-- the "held" capability event is generated when the LongPress event is received. The IGNORE_NEXT_MPC flag is used +-- to tell the driver to ignore MultiPressComplete if it is received after a long press to avoid this extra event. +local IGNORE_NEXT_MPC = "__ignore_next_mpc" +-- These are essentially storing the supported features of a given endpoint +-- TODO: add an is_feature_supported_for_endpoint function to matter.device that takes an endpoint +local EMULATE_HELD = "__emulate_held" -- for non-MSR (MomentarySwitchRelease) devices we can emulate this on the software side +local SUPPORTS_MULTI_PRESS = "__multi_button" -- for MSM devices (MomentarySwitchMultiPress), create an event on receipt of MultiPressComplete +local INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS (MomentarySwitch), but not MSR (MomentarySwitchRelease) + +local function initial_press_event_handler(driver, device, ib, response) + if get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then + -- Receipt of an InitialPress event means we do not want to ignore the next MultiPressComplete event + -- or else we would potentially not create the expected button capability event + set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, nil) + elseif get_field_for_endpoint(device, INITIAL_PRESS_ONLY, ib.endpoint_id) then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true})) + elseif get_field_for_endpoint(device, EMULATE_HELD, ib.endpoint_id) then + -- if our button doesn't differentiate between short and long holds, do it in code by keeping track of the press down time + init_press(device, ib.endpoint_id) + end +end + +local function long_press_event_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.held({state_change = true})) + if get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then + -- Ignore the next MultiPressComplete event if it is sent as part of this "long press" event sequence + set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, true) + end +end + +--helper function to create list of multi press values +local function create_multi_press_values_list(size, supportsHeld) + local list = {"pushed", "double"} + if supportsHeld then table.insert(list, "held") end + -- add multi press values of 3 or greater to the list + for i=3, size do + table.insert(list, string.format("pushed_%dx", i)) + end + return list +end + +local function tbl_contains(array, value) + for _, element in ipairs(array) do + if element == value then + return true + end + end + return false +end + +local function device_init (driver, device) + device:subscribe() + device:send(clusters.Switch.attributes.MultiPressMax:read(device)) +end + +local function max_press_handler(driver, device, ib, response) + local max = ib.data.value or 1 --get max number of presses + device.log.debug("Device supports "..max.." presses") + -- capability only supports up to 6 presses + if max > 6 then + log.info("Device supports more than 6 presses") + max = 6 + end + local MSL = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS}) + local supportsHeld = tbl_contains(MSL, ib.endpoint_id) + local values = create_multi_press_values_list(max, supportsHeld) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.supportedButtonValues(values, {visibility = {displayed = false}})) +end + +local function multi_press_complete_event_handler(driver, device, ib, response) + -- in the case of multiple button presses + -- emit number of times, multiple presses have been completed + if ib.data and not get_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id) then + local press_value = ib.data.elements.total_number_of_presses_counted.value + --capability only supports up to 6 presses + if press_value < 7 then + local button_event = capabilities.button.button.pushed({state_change = true}) + if press_value == 2 then + button_event = capabilities.button.button.double({state_change = true}) + elseif press_value > 2 then + button_event = capabilities.button.button(string.format("pushed_%dx", press_value), {state_change = true}) + end + + device:emit_event_for_endpoint(ib.endpoint_id, button_event) + else + log.info(string.format("Number of presses (%d) not supported by capability", press_value)) + end + end + set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, nil) +end + +local Bosch_Button_Contact_Sensor = { + NAME = "Bosch_Button_Contact_Sensor", + lifecycle_handlers = { + init = device_init + }, + matter_handlers = { + attr = { + [clusters.Switch.ID] = { + [clusters.Switch.attributes.MultiPressMax.ID] = max_press_handler + } + }, + event = { + [clusters.Switch.ID] = { + [clusters.Switch.events.InitialPress.ID] = initial_press_event_handler, + [clusters.Switch.events.LongPress.ID] = long_press_event_handler, + [clusters.Switch.events.MultiPressComplete.ID] = multi_press_complete_event_handler + } + }, + }, + can_handle = require("sub_drivers.bosch_button_contact.can_handle"), +} + +return Bosch_Button_Contact_Sensor diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/can_handle.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/can_handle.lua new file mode 100644 index 0000000000..31bd7e025b --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_matter_smoke_co_alarm(opts, driver, device) + local SMOKE_CO_ALARM_DEVICE_TYPE_ID = 0x0076 + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == SMOKE_CO_ALARM_DEVICE_TYPE_ID then + return true, require("sub_drivers.smoke_co_alarm") + end + end + end + + return false +end + +return is_matter_smoke_co_alarm diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/init.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/init.lua new file mode 100644 index 0000000000..8f8653b936 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/init.lua @@ -0,0 +1,265 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local version = require "version" +local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils" +local sensor_utils = require "sensor_utils.utils" +local fields = require "sensor_utils.fields" + +if version.api < 10 then + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.SmokeCoAlarm = require "embedded_clusters.SmokeCoAlarm" +end + + +-- SUBDRIVER UTILS -- + +local smoke_co_alarm_utils = {} + +local CARBON_MONOXIDE_MEASUREMENT_UNIT = "CarbonMonoxideConcentrationMeasurement_unit" + +local HardwareFaultAlert = "__HardwareFaultAlert" +local BatteryAlert = "__BatteryAlert" +local BatteryLevel = "__BatteryLevel" + + +local supported_profiles = +{ + "co", + "co-battery", + "co-comeas", + "co-comeas-battery", + "co-comeas-colevel-battery", + "smoke", + "smoke-battery", + "smoke-temp-humidity-battery", + "smoke-co-comeas", + "smoke-co-comeas-battery", + "smoke-co-temp-humidity-comeas", + "smoke-co-temp-humidity-comeas-battery" +} + +function smoke_co_alarm_utils.match_profile(device, battery_supported) + local smoke_eps = embedded_cluster_utils.get_endpoints(device, clusters.SmokeCoAlarm.ID, {feature_bitmap = clusters.SmokeCoAlarm.types.Feature.SMOKE_ALARM}) + local co_eps = embedded_cluster_utils.get_endpoints(device, clusters.SmokeCoAlarm.ID, {feature_bitmap = clusters.SmokeCoAlarm.types.Feature.CO_ALARM}) + local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) + local humidity_eps = embedded_cluster_utils.get_endpoints(device, clusters.RelativeHumidityMeasurement.ID) + local co_meas_eps = embedded_cluster_utils.get_endpoints(device, clusters.CarbonMonoxideConcentrationMeasurement.ID, {feature_bitmap = clusters.CarbonMonoxideConcentrationMeasurement.types.Feature.NUMERIC_MEASUREMENT}) + local co_level_eps = embedded_cluster_utils.get_endpoints(device, clusters.CarbonMonoxideConcentrationMeasurement.ID, {feature_bitmap = clusters.CarbonMonoxideConcentrationMeasurement.types.Feature.LEVEL_INDICATION}) + + local profile_name = "" + + -- battery and hardware fault are mandatory + if #smoke_eps > 0 then + profile_name = profile_name .. "-smoke" + end + if #co_eps > 0 then + profile_name = profile_name .. "-co" + end + if #temp_eps > 0 then + profile_name = profile_name .. "-temp" + end + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + if #co_meas_eps > 0 then + profile_name = profile_name .. "-comeas" + end + if #co_level_eps > 0 then + profile_name = profile_name .. "-colevel" + end + if battery_supported == fields.battery_support.BATTERY_PERCENTAGE then + profile_name = profile_name .. "-battery" + end + + -- remove leading "-" + profile_name = string.sub(profile_name, 2) + + if sensor_utils.tbl_contains(supported_profiles, profile_name) then + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + else + device.log.warn_with({hub_logs=true}, string.format("No matching profile for device. Tried to use profile %s.", profile_name)) + profile_name = "" + if #smoke_eps > 0 and #co_eps > 0 then + profile_name = "smoke-co" + elseif #smoke_eps > 0 and #co_eps == 0 then + profile_name = "smoke" + elseif #co_eps > 0 and #smoke_eps == 0 then + profile_name = "co" + end + device.log.info_with({hub_logs=true}, string.format("Using generic device profile %s.", profile_name)) + end + device:try_update_metadata({profile = profile_name}) +end + + +-- SMOKE CO ALARM LIFECYCLE HANDLERS -- + +local SmokeLifeycleHandlers = {} + +function SmokeLifeycleHandlers.device_init(driver, device) + device:subscribe() +end + +function SmokeLifeycleHandlers.do_configure(driver, device) + local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) + if #battery_feature_eps > 0 then + device:send(clusters.PowerSource.attributes.AttributeList:read()) + else + smoke_co_alarm_utils.match_profile(device, fields.battery_support.NO_BATTERY) + end +end + +function SmokeLifeycleHandlers.info_changed(self, device, event, args) + if device.preferences then + if device.preferences["certifiedpreferences.smokeSensorSensitivity"] ~= args.old_st_store.preferences["certifiedpreferences.smokeSensorSensitivity"] then + local eps = embedded_cluster_utils.get_endpoints(device, clusters.SmokeCoAlarm.ID) + if #eps > 0 then + local smokeSensorSensitivity = device.preferences["certifiedpreferences.smokeSensorSensitivity"] + if smokeSensorSensitivity == "0" then -- High + device:send(clusters.SmokeCoAlarm.attributes.SmokeSensitivityLevel:write(device, eps[1], clusters.SmokeCoAlarm.types.SensitivityEnum.HIGH)) + elseif smokeSensorSensitivity == "1" then -- Medium + device:send(clusters.SmokeCoAlarm.attributes.SmokeSensitivityLevel:write(device, eps[1], clusters.SmokeCoAlarm.types.SensitivityEnum.STANDARD)) + elseif smokeSensorSensitivity == "2" then -- Low + device:send(clusters.SmokeCoAlarm.attributes.SmokeSensitivityLevel:write(device, eps[1], clusters.SmokeCoAlarm.types.SensitivityEnum.LOW)) + end + end + end + end + + -- resubscribe to new attributes as needed if a profile switch occured + if device.profile.id ~= args.old_st_store.profile.id then + device:subscribe() + end +end + + +-- CLUSTER ATTRIBUTE HANDLERS -- + +local sub_driver_handlers = {} + +function sub_driver_handlers.smoke_co_alarm_state_factory(zeroEvent, nonZeroEvent) + return function(driver, device, ib, response) + if ib.data.value == 0 and zeroEvent ~= nil then + device:emit_event_for_endpoint(ib.endpoint_id, zeroEvent) + elseif nonZeroEvent ~= nil then + device:emit_event_for_endpoint(ib.endpoint_id, nonZeroEvent) + end + end +end + +function sub_driver_handlers.test_in_progress_handler(driver, device, ib, response) + if device:supports_capability(capabilities.smokeDetector) then + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.smokeDetector.smoke.tested()) + else + device:send(clusters.SmokeCoAlarm.attributes.SmokeState:read(device)) + end + end + if device:supports_capability(capabilities.carbonMonoxideDetector) then + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.carbonMonoxideDetector.carbonMonoxide.tested()) + else + device:send(clusters.SmokeCoAlarm.attributes.COState:read(device)) + end + end +end + +function sub_driver_handlers.carbon_monoxide_measured_value_handler(driver, device, ib, response) + local value = ib.data.value + local unit = device:get_field(CARBON_MONOXIDE_MEASUREMENT_UNIT) + if unit == clusters.CarbonMonoxideConcentrationMeasurement.types.MeasurementUnitEnum.PPB then + value = value / 1000 + elseif unit == clusters.CarbonMonoxideConcentrationMeasurement.types.MeasurementUnitEnum.PPT then + value = value / 1000000 + end + value = math.floor(value) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.carbonMonoxideMeasurement.carbonMonoxideLevel({value = value, unit = "ppm"})) +end + +function sub_driver_handlers.carbon_monoxide_measurement_unit_handler(driver, device, ib, response) + local unit = ib.data.value + device:set_field(CARBON_MONOXIDE_MEASUREMENT_UNIT, unit, { persist = true }) +end + +function sub_driver_handlers.hardware_fault_capability_handler(device) + local batLevel, batAlert = device:get_field(BatteryLevel), device:get_field(BatteryAlert) + if device:get_field(HardwareFaultAlert) == true or (batLevel and batAlert and (batAlert > batLevel)) then + device:emit_event(capabilities.hardwareFault.hardwareFault.detected()) + else + device:emit_event(capabilities.hardwareFault.hardwareFault.clear()) + end +end + +function sub_driver_handlers.hardware_fault_alert_handler(driver, device, ib, response) + device:set_field(HardwareFaultAlert, ib.data.value, {persist = true}) + sub_driver_handlers.hardware_fault_capability_handler(device) +end + +function sub_driver_handlers.battery_alert_handler(driver, device, ib, response) + device:set_field(BatteryAlert, ib.data.value, {persist = true}) + sub_driver_handlers.hardware_fault_capability_handler(device) +end + +function sub_driver_handlers.power_source_attribute_list_handler(driver, device, ib, response) + for _, attr in ipairs(ib.data.elements) do + -- Re-profile the device if BatPercentRemaining (Attribute ID 0x0C) or + -- BatChargeLevel (Attribute ID 0x0E) is present. + if attr.value == 0x0C then + smoke_co_alarm_utils.match_profile(device, fields.battery_support.BATTERY_PERCENTAGE) + return + elseif attr.value == 0x0E then + smoke_co_alarm_utils.match_profile(device, fields.battery_support.BATTERY_LEVEL) + return + end + end +end + +function sub_driver_handlers.bat_charge_level_handler(driver, device, ib, response) + device:set_field(BatteryLevel, ib.data.value, {persist = true}) -- value used in hardware_fault_capability_handler + if device:supports_capability(capabilities.batteryLevel) then -- check required since attribute is subscribed to even without batteryLevel support, to set the field above + if ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.OK then + device:emit_event(capabilities.batteryLevel.battery.normal()) + elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.WARNING then + device:emit_event(capabilities.batteryLevel.battery.warning()) + elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.CRITICAL then + device:emit_event(capabilities.batteryLevel.battery.critical()) + end + end +end + + +-- SUBDRIVER TEMPLATE -- + +local matter_smoke_co_alarm_handler = { + NAME = "matter-smoke-co-alarm", + lifecycle_handlers = { + init = SmokeLifeycleHandlers.device_init, + infoChanged = SmokeLifeycleHandlers.info_changed, + doConfigure = SmokeLifeycleHandlers.do_configure + }, + matter_handlers = { + attr = { + [clusters.CarbonMonoxideConcentrationMeasurement.ID] = { + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.carbon_monoxide_measured_value_handler, + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.carbon_monoxide_measurement_unit_handler, + }, + [clusters.PowerSource.ID] = { + [clusters.PowerSource.attributes.AttributeList.ID] = sub_driver_handlers.power_source_attribute_list_handler, + [clusters.PowerSource.attributes.BatChargeLevel.ID] = sub_driver_handlers.bat_charge_level_handler, + }, + [clusters.SmokeCoAlarm.ID] = { + [clusters.SmokeCoAlarm.attributes.BatteryAlert.ID] = sub_driver_handlers.battery_alert_handler, + [clusters.SmokeCoAlarm.attributes.COState.ID] = sub_driver_handlers.smoke_co_alarm_state_factory(capabilities.carbonMonoxideDetector.carbonMonoxide.clear(), capabilities.carbonMonoxideDetector.carbonMonoxide.detected()), + [clusters.SmokeCoAlarm.attributes.HardwareFaultAlert.ID] = sub_driver_handlers.hardware_fault_alert_handler, + [clusters.SmokeCoAlarm.attributes.SmokeState.ID] = sub_driver_handlers.smoke_co_alarm_state_factory(capabilities.smokeDetector.smoke.clear(), capabilities.smokeDetector.smoke.detected()), + [clusters.SmokeCoAlarm.attributes.TestInProgress.ID] = sub_driver_handlers.test_in_progress_handler, + }, + }, + }, + can_handle = require("sub_drivers.smoke_co_alarm.can_handle") +} + +return matter_smoke_co_alarm_handler diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor.lua index b2b8ae4ebf..1988563bc8 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -19,17 +8,23 @@ local SinglePrecisionFloat = require "st.matter.data_types.SinglePrecisionFloat" local clusters = require "st.matter.clusters" -clusters.AirQuality = require "AirQuality" -clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" -clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" -clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" -clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" -clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" -clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" -clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" -clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" -clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" -clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" +local version = require "version" + +-- Include driver-side definitions when lua libs api version is < 10 +if version.api < 10 then + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.SmokeCoAlarm = require "embedded_clusters.SmokeCoAlarm" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" +end local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("aqs-temp-humidity-all-level-all-meas.yml"), @@ -145,7 +140,7 @@ local mock_device_level = test.mock_device.build_test_matter_device({ }) local mock_device_co = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("aqs-temp-humidity-all-level-all-meas.yml"), + profile = t_utils.get_profile_definition("aqs.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -174,7 +169,7 @@ local mock_device_co = test.mock_device.build_test_matter_device({ }) local mock_device_co2 = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("aqs-temp-humidity-all-level-all-meas.yml"), + profile = t_utils.get_profile_definition("aqs.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -234,7 +229,12 @@ local mock_device_tvoc = test.mock_device.build_test_matter_device({ }) -- create test_init functions -local function initialize_mock_device(generic_mock_device, generic_subscribed_attributes) +local function initialize_mock_device(generic_mock_device, generic_subscribed_attributes, expected_supported_values_setters) + test.mock_device.add_test_device(generic_mock_device) + test.socket.capability:__expect_send(generic_mock_device:generate_test_message("main", capabilities.airQualityHealthConcern.supportedAirQualityValues({"unknown", "good", "unhealthy", "moderate", "slightlyUnhealthy",}, {visibility={displayed=false}}))) + if expected_supported_values_setters ~= nil then + expected_supported_values_setters() + end local subscribe_request = nil for _, attributes in pairs(generic_subscribed_attributes) do for _, attribute in ipairs(attributes) do @@ -246,9 +246,11 @@ local function initialize_mock_device(generic_mock_device, generic_subscribed_at end end test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) - test.mock_device.add_test_device(generic_mock_device) end +-- TODO add tests for configuration using modular profiles +test.set_rpc_version(7) + local function test_init() local subscribed_attributes = { [capabilities.relativeHumidityMeasurement.ID] = { @@ -331,7 +333,19 @@ local function test_init() clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue, }, } - initialize_mock_device(mock_device, subscribed_attributes) + local expected_supported_values_setters = function() + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.nitrogenDioxideHealthConcern.supportedNitrogenDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.ozoneHealthConcern.supportedOzoneValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.formaldehydeHealthConcern.supportedFormaldehydeValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.veryFineDustHealthConcern.supportedVeryFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.fineDustHealthConcern.supportedFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.dustHealthConcern.supportedDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.radonHealthConcern.supportedRadonValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tvocHealthConcern.supportedTvocValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + end + initialize_mock_device(mock_device, subscribed_attributes, expected_supported_values_setters) end test.set_test_init_function(test_init) @@ -408,7 +422,19 @@ local function test_init_level() clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue, } } - initialize_mock_device(mock_device_level, subscribed_attributes) + local expected_supported_values_setters = function() + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.nitrogenDioxideHealthConcern.supportedNitrogenDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.ozoneHealthConcern.supportedOzoneValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.formaldehydeHealthConcern.supportedFormaldehydeValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.veryFineDustHealthConcern.supportedVeryFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.fineDustHealthConcern.supportedFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.dustHealthConcern.supportedDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.radonHealthConcern.supportedRadonValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.tvocHealthConcern.supportedTvocValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + end + initialize_mock_device(mock_device_level, subscribed_attributes, expected_supported_values_setters) end local function test_init_tvoc() @@ -437,26 +463,12 @@ local function test_init_co_co2() [capabilities.airQualityHealthConcern.ID] = { clusters.AirQuality.attributes.AirQuality }, - [capabilities.carbonMonoxideMeasurement.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.carbonMonoxideHealthConcern.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue, - }, } local attr_co2 = { [capabilities.airQualityHealthConcern.ID] = { clusters.AirQuality.attributes.AirQuality - }, - [capabilities.carbonDioxideMeasurement.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.carbonDioxideHealthConcern.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue, - }, + } } initialize_mock_device(mock_device_co, attr_co) @@ -465,7 +477,7 @@ end -- run the profile configuration tests -local function test_aqs_device_type_do_configure(generic_mock_device, expected_profile, expected_supported_values_setters) +local function test_aqs_device_type_do_configure(generic_mock_device, expected_profile) test.socket.device_lifecycle:__queue_receive({generic_mock_device.id, "doConfigure"}) test.socket.matter:__expect_send({generic_mock_device.id, clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit:read()}) test.socket.matter:__expect_send({generic_mock_device.id, clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit:read()}) @@ -477,10 +489,6 @@ local function test_aqs_device_type_do_configure(generic_mock_device, expected_p test.socket.matter:__expect_send({generic_mock_device.id, clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit:read()}) test.socket.matter:__expect_send({generic_mock_device.id, clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit:read()}) test.socket.matter:__expect_send({generic_mock_device.id, clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit:read()}) - test.socket.capability:__expect_send(generic_mock_device:generate_test_message("main", capabilities.airQualityHealthConcern.supportedAirQualityValues({"unknown", "good", "moderate", "slightlyUnhealthy", "unhealthy"}, {visibility={displayed=false}}))) - if expected_supported_values_setters ~= nil then - expected_supported_values_setters() - end generic_mock_device:expect_metadata_update({ profile = expected_profile }) generic_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -488,19 +496,7 @@ end test.register_coroutine_test( "Configure should read units from device and profile change as needed", function() - local expected_supported_values_setters = function() - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.nitrogenDioxideHealthConcern.supportedNitrogenDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.ozoneHealthConcern.supportedOzoneValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.formaldehydeHealthConcern.supportedFormaldehydeValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.veryFineDustHealthConcern.supportedVeryFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.fineDustHealthConcern.supportedFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.dustHealthConcern.supportedDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.radonHealthConcern.supportedRadonValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tvocHealthConcern.supportedTvocValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - end - test_aqs_device_type_do_configure(mock_device, "aqs-temp-humidity-all-level-all-meas", expected_supported_values_setters) + test_aqs_device_type_do_configure(mock_device, "aqs-temp-humidity-all-level-all-meas") end ) @@ -515,19 +511,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Configure should read units from device and profile change as needed", function() - local expected_supported_values_setters = function() - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.nitrogenDioxideHealthConcern.supportedNitrogenDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.ozoneHealthConcern.supportedOzoneValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.formaldehydeHealthConcern.supportedFormaldehydeValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.veryFineDustHealthConcern.supportedVeryFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.fineDustHealthConcern.supportedFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.dustHealthConcern.supportedDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.radonHealthConcern.supportedRadonValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.tvocHealthConcern.supportedTvocValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - end - test_aqs_device_type_do_configure(mock_device_level, "aqs-temp-humidity-all-level", expected_supported_values_setters) + test_aqs_device_type_do_configure(mock_device_level, "aqs-temp-humidity-all-level") end, { test_init = test_init_level } ) @@ -535,14 +519,8 @@ test.register_coroutine_test( test.register_coroutine_test( "Configure should not catch co2, only co in the first check", function() - local expected_supported_co_values = function() - test.socket.capability:__expect_send(mock_device_co:generate_test_message("main", capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - end - local expected_supported_co2_values = function() - test.socket.capability:__expect_send(mock_device_co2:generate_test_message("main", capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - end - test_aqs_device_type_do_configure(mock_device_co, "aqs-temp-humidity-all-meas", expected_supported_co_values) - test_aqs_device_type_do_configure(mock_device_co2, "aqs-temp-humidity-co2-pm25-tvoc-meas", expected_supported_co2_values) + test_aqs_device_type_do_configure(mock_device_co, "aqs-temp-humidity-all-meas") + test_aqs_device_type_do_configure(mock_device_co2, "aqs-temp-humidity-co2-pm25-tvoc-meas") end, { test_init = test_init_co_co2 } ) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua index d799c31b64..5b6b43f2f5 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua @@ -1,29 +1,16 @@ --- Copyright 2023 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" -local dkjson = require "dkjson" local clusters = require "st.matter.clusters" -test.set_rpc_version(8) +test.disable_startup_messages() -local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("aqs-temp-humidity-all-level-all-meas.yml"), +local mock_device_all = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("aqs.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -41,7 +28,7 @@ local mock_device = test.mock_device.build_test_matter_device({ { endpoint_id = 1, clusters = { - {cluster_id = clusters.AirQuality.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.AirQuality.ID, cluster_type = "SERVER", feature_map = 3}, {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.CarbonMonoxideConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 3}, @@ -63,7 +50,7 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local mock_device_common = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("aqs-temp-humidity-co2-pm25-tvoc-meas.yml"), + profile = t_utils.get_profile_definition("aqs.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -81,7 +68,7 @@ local mock_device_common = test.mock_device.build_test_matter_device({ { endpoint_id = 1, clusters = { - {cluster_id = clusters.AirQuality.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.AirQuality.ID, cluster_type = "SERVER", feature_map = 3}, {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.CarbonDioxideConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 1}, @@ -95,25 +82,34 @@ local mock_device_common = test.mock_device.build_test_matter_device({ } }) --- create test_init functions -local function initialize_mock_device(generic_mock_device, generic_subscribed_attributes) - local subscribe_request = nil - for _, attributes in pairs(generic_subscribed_attributes) do - for _, attribute in ipairs(attributes) do - if subscribe_request == nil then - subscribe_request = attribute:subscribe(generic_mock_device) - else - subscribe_request:merge(attribute:subscribe(generic_mock_device)) - end - end - end - test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) - test.mock_device.add_test_device(generic_mock_device) - return subscribe_request +local function test_init_all() + test.mock_device.add_test_device(mock_device_all) + test.socket.device_lifecycle:__queue_receive({ mock_device_all.id, "init" }) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", + capabilities.airQualityHealthConcern.supportedAirQualityValues({"unknown", "good", "unhealthy", "moderate", "slightlyUnhealthy"}, + {visibility={displayed=false}})) + ) + -- on device create, a generic AQS device will be profiled as aqs, thus only subscribing to one attribute + local subscribe_request = clusters.AirQuality.attributes.AirQuality:subscribe(mock_device_all) + test.socket.matter:__expect_send({mock_device_all.id, subscribe_request}) +end + +local function test_init_common() + test.mock_device.add_test_device(mock_device_common) + test.socket.device_lifecycle:__queue_receive({ mock_device_common.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_common.id, "init" }) + test.socket.capability:__expect_send(mock_device_common:generate_test_message("main", + capabilities.airQualityHealthConcern.supportedAirQualityValues({"unknown", "good", "unhealthy", "moderate", "slightlyUnhealthy"}, + {visibility={displayed=false}})) + ) + -- on device create, a generic AQS device will be profiled as aqs, thus only subscribing to one attribute + local subscribe_request = clusters.AirQuality.attributes.AirQuality:subscribe(mock_device_common) + test.socket.matter:__expect_send({mock_device_common.id, subscribe_request}) end -local subscribe_request_all -local function test_init() +test.set_test_init_function(test_init_all) + +local function get_subscribe_request_all() local subscribed_attributes = { [capabilities.relativeHumidityMeasurement.ID] = { clusters.RelativeHumidityMeasurement.attributes.MeasuredValue @@ -195,11 +191,20 @@ local function test_init() clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue, }, } - subscribe_request_all = initialize_mock_device(mock_device, subscribed_attributes) + local subscribe_request = nil + for _, attributes in pairs(subscribed_attributes) do + for _, attribute in pairs(attributes) do + if subscribe_request == nil then + subscribe_request = attribute:subscribe(mock_device_all) + else + subscribe_request:merge(attribute:subscribe(mock_device_all)) + end + end + end + return subscribe_request end -local subscribe_request_common -local function test_init_common() +local function get_subscribe_request_common() local subscribed_attributes = { [capabilities.relativeHumidityMeasurement.ID] = { clusters.RelativeHumidityMeasurement.attributes.MeasuredValue @@ -225,12 +230,21 @@ local function test_init_common() clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit, }, } - subscribe_request_common = initialize_mock_device(mock_device_common, subscribed_attributes) + local subscribe_request = nil + for _, attributes in pairs(subscribed_attributes) do + for _, attribute in pairs(attributes) do + if subscribe_request == nil then + subscribe_request = attribute:subscribe(mock_device_common) + else + subscribe_request:merge(attribute:subscribe(mock_device_common)) + end + end + end + return subscribe_request end -test.set_test_init_function(test_init) -- run the profile configuration tests -local function test_aqs_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request) +local function test_aqs_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request, expected_supported_values_setters) test.socket.device_lifecycle:__queue_receive({generic_mock_device.id, "doConfigure"}) test.socket.matter:__expect_send({generic_mock_device.id, clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit:read()}) test.socket.matter:__expect_send({generic_mock_device.id, clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit:read()}) @@ -244,73 +258,93 @@ local function test_aqs_device_type_update_modular_profile(generic_mock_device, test.socket.matter:__expect_send({generic_mock_device.id, clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit:read()}) generic_mock_device:expect_metadata_update(expected_metadata) generic_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - local device_info_copy = utils.deep_copy(generic_mock_device.raw_st_data) - device_info_copy.profile.id = "aqs-modular" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ generic_mock_device.id, "infoChanged", device_info_json }) + local updated_device_profile = t_utils.get_profile_definition("aqs-modular.yml", + {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} + ) + test.socket.device_lifecycle:__queue_receive(generic_mock_device:generate_info_changed({ profile = updated_device_profile })) + if expected_supported_values_setters ~= nil then + expected_supported_values_setters() + end test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) end -local expected_metadata_all = { - optional_component_capabilities={ - { - "main", - { - "temperatureMeasurement", - "relativeHumidityMeasurement", - "carbonMonoxideMeasurement", - "carbonDioxideMeasurement", - "nitrogenDioxideMeasurement", - "ozoneMeasurement", - "formaldehydeMeasurement", - "veryFineDustSensor", - "fineDustSensor", - "dustSensor", - "radonMeasurement", - "tvocMeasurement", - "carbonMonoxideHealthConcern", - "carbonDioxideHealthConcern", - "nitrogenDioxideHealthConcern", - "ozoneHealthConcern", - "formaldehydeHealthConcern", - "veryFineDustHealthConcern", - "fineDustHealthConcern", - "dustHealthConcern", - "radonHealthConcern", - "tvocHealthConcern", - }, - }, - }, - profile="aqs-modular-temp-humidity", -} - test.register_coroutine_test( "Device with modular profile should enable correct optional capabilities - all clusters", function() - test_aqs_device_type_update_modular_profile(mock_device, expected_metadata_all, subscribe_request_all) - end -) - -local expected_metadata_common = { - optional_component_capabilities={ - { - "main", - { - "temperatureMeasurement", - "relativeHumidityMeasurement", - "carbonDioxideMeasurement", - "fineDustSensor", - "tvocMeasurement", + local expected_metadata_all = { + optional_component_capabilities={ + { + "main", + { + "temperatureMeasurement", + "relativeHumidityMeasurement", + "carbonMonoxideMeasurement", + "carbonDioxideMeasurement", + "nitrogenDioxideMeasurement", + "ozoneMeasurement", + "formaldehydeMeasurement", + "veryFineDustSensor", + "fineDustSensor", + "dustSensor", + "radonMeasurement", + "tvocMeasurement", + "carbonMonoxideHealthConcern", + "carbonDioxideHealthConcern", + "nitrogenDioxideHealthConcern", + "ozoneHealthConcern", + "formaldehydeHealthConcern", + "veryFineDustHealthConcern", + "fineDustHealthConcern", + "dustHealthConcern", + "radonHealthConcern", + "tvocHealthConcern", + }, + }, }, - }, - }, - profile="aqs-modular-temp-humidity", -} + profile="aqs-modular-temp-humidity", + } + local expected_supported_values_setters = function() + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.airQualityHealthConcern.supportedAirQualityValues({"unknown", "good", "unhealthy", "moderate", "slightlyUnhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.nitrogenDioxideHealthConcern.supportedNitrogenDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.ozoneHealthConcern.supportedOzoneValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.formaldehydeHealthConcern.supportedFormaldehydeValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.veryFineDustHealthConcern.supportedVeryFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.fineDustHealthConcern.supportedFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.dustHealthConcern.supportedDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.radonHealthConcern.supportedRadonValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.tvocHealthConcern.supportedTvocValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + end + local subscribe_request_all = get_subscribe_request_all() + test_aqs_device_type_update_modular_profile(mock_device_all, expected_metadata_all, subscribe_request_all, expected_supported_values_setters) + end, + { test_init = test_init_all } +) test.register_coroutine_test( "Device with modular profile should enable correct optional capabilities - common clusters", function() - test_aqs_device_type_update_modular_profile(mock_device_common, expected_metadata_common, subscribe_request_common) + local expected_metadata_common = { + optional_component_capabilities={ + { + "main", + { + "temperatureMeasurement", + "relativeHumidityMeasurement", + "carbonDioxideMeasurement", + "fineDustSensor", + "tvocMeasurement", + }, + }, + }, + profile="aqs-modular-temp-humidity", + } + local expected_supported_values_setters = function() + test.socket.capability:__expect_send(mock_device_common:generate_test_message("main", capabilities.airQualityHealthConcern.supportedAirQualityValues({"unknown", "good", "unhealthy", "moderate", "slightlyUnhealthy"}, {visibility={displayed=false}}))) + end + local subscribe_request_common = get_subscribe_request_common() + test_aqs_device_type_update_modular_profile(mock_device_common, expected_metadata_common, subscribe_request_common, expected_supported_values_setters) end, { test_init = test_init_common } ) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_bosch_button_contact.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_bosch_button_contact.lua index b63abcea14..1ca2add2ff 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_bosch_button_contact.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_bosch_button_contact.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- package.path = package.path .. ";./?lua" -- package.loaded["path"] = dofile("mock_path.lua") local test = require "integration_test" diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_flow_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_flow_sensor.lua index 880ed55c89..6b1301cb96 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_flow_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_flow_sensor.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -53,6 +42,7 @@ local subscribed_attributes = { } local function test_init() + test.mock_device.add_test_device(mock_device) local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) for i, cluster in ipairs(subscribed_attributes) do if i > 1 then @@ -60,8 +50,6 @@ local function test_init() end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_freeze_leak_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_freeze_leak_sensor.lua index 46bc88a771..44a984b2a7 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_freeze_leak_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_freeze_leak_sensor.lua @@ -1,91 +1,73 @@ --- Copyright 2024 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" -clusters.BooleanStateConfiguration = require "BooleanStateConfiguration" +local version = require "version" + +if version.api < 11 then + clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" +end local mock_device_freeze_leak = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("freeze-leak-fault-freezeSensitivity-leakSensitivity.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, + profile = t_utils.get_profile_definition("freeze-leak-fault-freezeSensitivity-leakSensitivity.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.BooleanState.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.BooleanStateConfiguration.ID, cluster_type = "SERVER", feature_map = 31}, }, - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.BooleanState.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.BooleanStateConfiguration.ID, cluster_type = "SERVER", feature_map = 31}, - }, - device_types = { - {device_type_id = 0x0043, device_type_revision = 1} -- Water Leak Detector - } + device_types = { + {device_type_id = 0x0043, device_type_revision = 1} -- Water Leak Detector + } + }, + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.BooleanState.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.BooleanStateConfiguration.ID, cluster_type = "SERVER", feature_map = 31}, }, - { - endpoint_id = 2, - clusters = { - {cluster_id = clusters.BooleanState.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.BooleanStateConfiguration.ID, cluster_type = "SERVER", feature_map = 31}, - }, - device_types = { - {device_type_id = 0x0041, device_type_revision = 1} -- Water Freeze Detector - } + device_types = { + {device_type_id = 0x0041, device_type_revision = 1} -- Water Freeze Detector } } + } }) -local subscribed_attributes = { - clusters.BooleanState.attributes.StateValue, - clusters.BooleanStateConfiguration.attributes.SensorFault, -} - local function test_init_freeze_leak() - local subscribe_request = subscribed_attributes[1]:subscribe(mock_device_freeze_leak) - for i, cluster in ipairs(subscribed_attributes) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_freeze_leak)) - end - end + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_freeze_leak) + test.socket.device_lifecycle:__queue_receive({ mock_device_freeze_leak.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_freeze_leak.id, "init" }) test.socket.matter:__expect_send({mock_device_freeze_leak.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_freeze_leak, 1)}) test.socket.matter:__expect_send({mock_device_freeze_leak.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_freeze_leak, 2)}) + local subscribe_request = clusters.BooleanState.attributes.StateValue:subscribe(mock_device_freeze_leak) + subscribe_request:merge(clusters.BooleanStateConfiguration.attributes.SensorFault:subscribe(mock_device_freeze_leak)) test.socket.matter:__expect_send({mock_device_freeze_leak.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_freeze_leak) + + test.socket.device_lifecycle:__queue_receive({ mock_device_freeze_leak.id, "doConfigure" }) + mock_device_freeze_leak:expect_metadata_update({ profile = "freeze-leak-fault-freezeSensitivity-leakSensitivity" }) + mock_device_freeze_leak:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init_freeze_leak) -test.register_coroutine_test( - "Test profile change on init for Freeze and Leak combined device type", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device_freeze_leak.id, "doConfigure" }) - mock_device_freeze_leak:expect_metadata_update({ profile = "freeze-leak-fault-freezeSensitivity-leakSensitivity" }) - mock_device_freeze_leak:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end, - { test_init = test_init_freeze_leak } -) - test.register_message_test( "Boolean state freeze detection reports should generate correct messages", { diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_pressure_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_pressure_sensor.lua index 27d3b90842..f7b3d7afe0 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_pressure_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_pressure_sensor.lua @@ -1,22 +1,15 @@ --- Copyright 2022 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" -local PressureMeasurementCluster = require "PressureMeasurement" + +if not pcall(function(cluster) return clusters[cluster] end, + "PressureMeasurement") then + clusters.PressureMeasurement = require "embedded_clusters.PressureMeasurement" +end --Note all endpoints are being mapped to the main component -- in the matter-sensor driver. If any devices require invoke/write @@ -35,7 +28,7 @@ local matter_endpoints = { { endpoint_id = 1, clusters = { - {cluster_id = PressureMeasurementCluster.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PressureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, }, device_types = { @@ -49,17 +42,11 @@ local mock_device = test.mock_device.build_test_matter_device({ endpoints = matter_endpoints }) -local function subscribe_on_init(dev) - local subscribe_request = PressureMeasurementCluster.attributes.MeasuredValue:subscribe(mock_device) - subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) - return subscribe_request -end - local function test_init() - test.socket.matter:__expect_send({mock_device.id, subscribe_on_init(mock_device)}) test.mock_device.add_test_device(mock_device) - -- don't check the battery for this device since we are just testing the "pressure-battery" profile specifically - mock_device:set_field("__battery_checked", 1, {persist = true}) + local subscribe_request = clusters.PressureMeasurement.attributes.MeasuredValue:subscribe(mock_device) + subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) end test.set_test_init_function(test_init) @@ -71,7 +58,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - PressureMeasurementCluster.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 1054) + clusters.PressureMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 1054) } }, { @@ -84,7 +71,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - PressureMeasurementCluster.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 1055) + clusters.PressureMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 1055) } }, { @@ -97,7 +84,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - PressureMeasurementCluster.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 0) + clusters.PressureMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 0) } }, { @@ -130,7 +117,7 @@ test.register_message_test( local function refresh_commands(dev) local req = clusters.PowerSource.attributes.BatPercentRemaining:read(dev) - req:merge(PressureMeasurementCluster.attributes.MeasuredValue:read(dev)) + req:merge(clusters.PressureMeasurement.attributes.MeasuredValue:read(dev)) return req end diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_rain_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_rain_sensor.lua index b23f72de53..21f8a0fa51 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_rain_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_rain_sensor.lua @@ -1,24 +1,16 @@ --- Copyright 2024 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" +local version = require "version" -clusters.BooleanStateConfiguration = require "BooleanStateConfiguration" +if version.api < 11 then + clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" +end local mock_device_rain = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("rain-fault.yml"), @@ -55,28 +47,24 @@ local subscribed_attributes = { } local function test_init_rain() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_rain) local subscribe_request = subscribed_attributes[1]:subscribe(mock_device_rain) for i, cluster in ipairs(subscribed_attributes) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device_rain)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device_rain.id, "init" }) test.socket.matter:__expect_send({mock_device_rain.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_rain, 1)}) test.socket.matter:__expect_send({mock_device_rain.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_rain) + + test.socket.device_lifecycle:__queue_receive({ mock_device_rain.id, "doConfigure" }) + mock_device_rain:expect_metadata_update({ profile = "rain-fault-rainSensitivity" }) + mock_device_rain:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init_rain) -test.register_coroutine_test( - "Test profile change on init for Freeze and Leak combined device type", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device_rain.id, "doConfigure" }) - mock_device_rain:expect_metadata_update({ profile = "rain-fault-rainSensitivity" }) - mock_device_rain:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end, - { test_init = test_init_rain } -) - test.register_message_test( "Boolean state rain detection reports should generate correct messages", { diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor.lua index 5bed934846..a66e575310 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -118,7 +107,6 @@ end local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_on_init(mock_device)}) test.mock_device.add_test_device(mock_device) - test.set_rpc_version(5) end test.set_test_init_function(test_init) @@ -134,8 +122,10 @@ local function subscribe_on_init_presence_sensor(dev) end local function test_init_presence_sensor() - test.socket.matter:__expect_send({mock_device_presence_sensor.id, subscribe_on_init_presence_sensor(mock_device_presence_sensor)}) + test.disable_startup_messages() test.mock_device.add_test_device(mock_device_presence_sensor) + test.socket.device_lifecycle:__queue_receive({ mock_device_presence_sensor.id, "init" }) + test.socket.matter:__expect_send({mock_device_presence_sensor.id, subscribe_on_init_presence_sensor(mock_device_presence_sensor)}) test.socket.device_lifecycle:__queue_receive({ mock_device_presence_sensor.id, "doConfigure" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device_presence_sensor.id, read_attribute_list}) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_battery.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_battery.lua index 48b72e232c..8d3962de66 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_battery.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_battery.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -56,20 +45,21 @@ local cluster_subscribe_list_humidity_battery = { } local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_humidity_battery) local subscribe_request_humidity_battery = cluster_subscribe_list_humidity_battery[1]:subscribe(mock_device_humidity_battery) for i, cluster in ipairs(cluster_subscribe_list_humidity_battery) do if i > 1 then subscribe_request_humidity_battery:merge(cluster:subscribe(mock_device_humidity_battery)) end end - test.socket.matter:__expect_send({mock_device_humidity_battery.id, subscribe_request_humidity_battery}) - test.mock_device.add_test_device(mock_device_humidity_battery) - test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "init" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "doConfigure" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device_humidity_battery.id, read_attribute_list}) - test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "doConfigure" }) mock_device_humidity_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_featuremap.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_featuremap.lua index 91d795d885..b1e5d39248 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_featuremap.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_featuremap.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -125,52 +114,52 @@ local cluster_subscribe_list_temp_humidity = { } local function test_init_humidity_battery() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_humidity_battery) local subscribe_request_humidity_battery = cluster_subscribe_list_humidity_battery[1]:subscribe(mock_device_humidity_battery) for i, cluster in ipairs(cluster_subscribe_list_humidity_battery) do if i > 1 then subscribe_request_humidity_battery:merge(cluster:subscribe(mock_device_humidity_battery)) end end - + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "init" }) test.socket.matter:__expect_send({mock_device_humidity_battery.id, subscribe_request_humidity_battery}) - test.mock_device.add_test_device(mock_device_humidity_battery) - test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "added" }) test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "doConfigure" }) - mock_device_humidity_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device_humidity_battery.id, read_attribute_list}) + mock_device_humidity_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_humidity_no_battery() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_humidity_no_battery) local subscribe_request_humidity_no_battery = cluster_subscribe_list_humidity_no_battery[1]:subscribe(mock_device_humidity_no_battery) for i, cluster in ipairs(cluster_subscribe_list_humidity_no_battery) do if i > 1 then subscribe_request_humidity_no_battery:merge(cluster:subscribe(mock_device_humidity_no_battery)) end end - + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_no_battery.id, "init" }) test.socket.matter:__expect_send({mock_device_humidity_no_battery.id, subscribe_request_humidity_no_battery}) - test.mock_device.add_test_device(mock_device_humidity_no_battery) - test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_no_battery.id, "added" }) test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_no_battery.id, "doConfigure" }) mock_device_humidity_no_battery:expect_metadata_update({ profile = "humidity" }) mock_device_humidity_no_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_temp_humidity() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_temp_humidity) local subscribe_request_temp_humidity = cluster_subscribe_list_temp_humidity[1]:subscribe(mock_device_temp_humidity) for i, cluster in ipairs(cluster_subscribe_list_temp_humidity) do if i > 1 then subscribe_request_temp_humidity:merge(cluster:subscribe(mock_device_temp_humidity)) end end - + test.socket.device_lifecycle:__queue_receive({ mock_device_temp_humidity.id, "init" }) test.socket.matter:__expect_send({mock_device_temp_humidity.id, subscribe_request_temp_humidity}) - test.mock_device.add_test_device(mock_device_temp_humidity) - test.socket.device_lifecycle:__queue_receive({ mock_device_temp_humidity.id, "added" }) test.socket.device_lifecycle:__queue_receive({ mock_device_temp_humidity.id, "doConfigure" }) mock_device_temp_humidity:expect_metadata_update({ profile = "temperature-humidity" }) mock_device_temp_humidity:expect_metadata_update({ provisioning_state = "PROVISIONED" }) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_rpc.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_rpc.lua index d8b38e392a..88af036e19 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_rpc.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_rpc.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" @@ -57,8 +46,6 @@ end local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_on_init(mock_device)}) test.mock_device.add_test_device(mock_device) - -- don't check the battery for this device because we are using the catch-all "sensor.yml" profile just for testing - mock_device:set_field("__battery_checked", 1, {persist = true}) test.set_rpc_version(3) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm.lua index f5029147d7..c7cfe26c2a 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -18,15 +7,15 @@ local t_utils = require "integration_test.utils" local SinglePrecisionFloat = require "st.matter.data_types.SinglePrecisionFloat" local clusters = require "st.matter.clusters" -clusters.SmokeCoAlarm = require "SmokeCoAlarm" local version = require "version" if version.api < 10 then - clusters.SmokeCoAlarm = require "SmokeCoAlarm" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" + clusters.SmokeCoAlarm = require "embedded_clusters.SmokeCoAlarm" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" end local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("smoke-co-temp-humidity-comeas.yml"), + _provisioning_state = "TYPED", -- we want this to have doConfigure on startup manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -73,6 +62,10 @@ local cluster_subscribe_list = { } local function test_init() + -- The startup messages are enabled, so this device will get an init, + -- and doConfigure (because provisioning_state is TYPED on the device). + test.mock_device.add_test_device(mock_device) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -80,11 +73,9 @@ local function test_init() end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm_battery.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm_battery.lua index 9bcdc54e44..816552935e 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm_battery.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm_battery.lua @@ -1,28 +1,16 @@ --- Copyright 2024 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" - local clusters = require "st.matter.clusters" -clusters.SmokeCoAlarm = require "SmokeCoAlarm" + local version = require "version" if version.api < 10 then - clusters.SmokeCoAlarm = require "SmokeCoAlarm" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" + clusters.SmokeCoAlarm = require "embedded_clusters.SmokeCoAlarm" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" end local mock_device = test.mock_device.build_test_matter_device({ @@ -69,21 +57,25 @@ local cluster_subscribe_list = { clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, clusters.PowerSource.attributes.BatPercentRemaining, + clusters.PowerSource.attributes.BatChargeLevel, + clusters.SmokeCoAlarm.attributes.BatteryAlert, } local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index b6b2540755..1c8adddff4 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -25,6 +25,31 @@ matterManufacturer: vendorId: 0x115F productId: 0x2004 deviceProfileName: 3-button-battery-temperature-humidity + - id: "4447/4105" + deviceLabel: Aqara Light Switch H2 EU (4 Buttons 2 Channel) + vendorId: 0x115F + productId: 0x1009 + deviceProfileName: 4-button + - id: "4447/4104" + deviceLabel: Aqara Light Switch H2 EU (2 Buttons 1 Channel) + vendorId: 0x115F + productId: 0x1008 + deviceProfileName: 2-button + - id: "4447/4099" + deviceLabel: Aqara Light Switch H2 US (2 Buttons 1 Channel) + vendorId: 0x115F + productId: 0x1003 + deviceProfileName: 2-button + - id: "4447/4100" + deviceLabel: Aqara Light Switch H2 US (2 Buttons 2 Channel) + vendorId: 0x115F + productId: 0x1004 + deviceProfileName: 2-button + - id: "4447/4101" + deviceLabel: Aqara Light Switch H2 US (4 Buttons 3 Channel) + vendorId: 0x115F + productId: 0x1005 + deviceProfileName: 4-button #AiDot - id: "Linkind/Smart/Light/Bulb/A19/RGBTW" deviceLabel: Linkind Smart Light Bulb A19 RGBTW @@ -131,6 +156,32 @@ matterManufacturer: vendorId: 0x1396 productId: 0x1001 deviceProfileName: light-color-level-fan + - id: "5014/4214" + deviceLabel: Linkind Smart Light Bulb + vendorId: 0x1396 + productId: 0x1076 + deviceProfileName: light-color-level + - id: "5014/4246" + deviceLabel: OREiN Matter smart Bathroom Fan + vendorId: 0x1396 + productId: 0x1096 + deviceProfileName: fan-modular + - id: "5014/4247" + deviceLabel: OREiN Matter smart Bathroom Fan + vendorId: 0x1396 + productId: 0x1097 + deviceProfileName: fan-modular + - id: "5014/4215" + deviceLabel: Linkind Smart Light Bulb + vendorId: 0x1396 + productId: 0x1077 + deviceProfileName: light-color-level +#Bosch Smart Home + - id: "4617/12310" + deviceLabel: Plug Compact [M] + vendorId: 0x1209 + productId: 0x3016 + deviceProfileName: plug-power-energy-powerConsumption #Chengdu - id: "5218/8197" deviceLabel: Magic Cube DS001 @@ -431,13 +482,299 @@ matterManufacturer: vendorId: 0x1339 productId: 0x0016 deviceProfileName: light-color-level-2000K-7000K - -# Govee - - id: "4999/24584" - deviceLabel: Govee Smart Bulb +#Govee + - id: "4999/24740" + deviceLabel: Govee Square Ceiling Light (12 inch) vendorId: 0x1387 - productId: 0x6008 - deviceProfileName: light-color-level-2700K-6500K + productId: 0x60A4 + deviceProfileName: light-color-level + - id: "4999/28871" + deviceLabel: Govee Christmas String Lights 2 (Clear Wire) 164ft/50m + vendorId: 0x1387 + productId: 0x70C7 + deviceProfileName: light-color-level + - id: "4999/25043" + deviceLabel: Govee Neon Rope Light 2 9.8ft/3m + vendorId: 0x1387 + productId: 0x61D3 + deviceProfileName: light-color-level + - id: "4999/24595" + deviceLabel: Govee RGBWW Smart LED Bulb BR30 850lm + vendorId: 0x1387 + productId: 0x6013 + deviceProfileName: light-color-level + - id: "4999/25011" + deviceLabel: Govee Strip Light with Cover 9.8f/3m + vendorId: 0x1387 + productId: 0x61B3 + deviceProfileName: light-color-level + - id: "4999/24586" + deviceLabel: Govee RGBWW Smart LED Bulb A19 1200lm + vendorId: 0x1387 + productId: 0x600A + deviceProfileName: light-color-level + - id: "4999/26116" + deviceLabel: Govee HDMI 2.1 Sync Box 2 75-85 + vendorId: 0x1387 + productId: 0x6604 + deviceProfileName: light-color-level + - id: "4999/24729" + deviceLabel: Govee TV Backlight 3 Lite 75-85 + vendorId: 0x1387 + productId: 0x6099 + deviceProfileName: light-color-level + # - id: "4999/28871" + # deviceLabel: Govee Christmas String Lights 2 (Green Wire) 164ft/50m + # vendorId: 0x1387 + # productId: 0x70C7 + # deviceProfileName: light-color-level + - id: "4999/28873" + deviceLabel: Govee Christmas String Lights 2 (Green Wire) 328ft/100m + vendorId: 0x1387 + productId: 0x70C9 + deviceProfileName: light-color-level + - id: "4999/24754" + deviceLabel: Govee Tree Floor Lamp + vendorId: 0x1387 + productId: 0x60B2 + deviceProfileName: light-color-level + - id: "4999/25078" + deviceLabel: Govee Strip Light 2 Pro 32.8ft/10m + vendorId: 0x1387 + productId: 0x61F6 + deviceProfileName: light-color-level + - id: "4999/25077" + deviceLabel: Govee Strip Light 2 Pro 16.4ft/5m + vendorId: 0x1387 + productId: 0x61F5 + deviceProfileName: light-color-level + - id: "4999/24588" + deviceLabel: Govee RGBWW Smart LED Bulb E14 450lm + vendorId: 0x1387 + productId: 0x600C + deviceProfileName: light-color-level + - id: "4999/28868" + deviceLabel: Govee Christmas String Lights 2 (Clear Wire) 66ft/20m + vendorId: 0x1387 + productId: 0x70C4 + deviceProfileName: light-color-level + - id: "4999/25074" + deviceLabel: Govee Strip Light 2 Pro 6.56ft/2m + vendorId: 0x1387 + productId: 0x61F2 + deviceProfileName: light-color-level + - id: "4999/25013" + deviceLabel: Govee Strip Light with Cover 16.4ft/5m + vendorId: 0x1387 + productId: 0x61B5 + deviceProfileName: light-color-level + - id: "4999/24753" + deviceLabel: Govee Torchiere Floor Lamp + vendorId: 0x1387 + productId: 0x60B1 + deviceProfileName: light-color-level + - id: "4999/28869" + deviceLabel: Govee Christmas String Lights 2 (Green Wire) 99ft/30m + vendorId: 0x1387 + productId: 0x70C5 + deviceProfileName: light-color-level + - id: "4999/24592" + deviceLabel: Govee RGBWW Smart LED Bulb BR30 1200lm + vendorId: 0x1387 + productId: 0x6010 + deviceProfileName: light-color-level + # - id: "4999/28869" + # deviceLabel: Govee Christmas String Lights 2 (Clear Wire) 99ft/30m + # vendorId: 0x1387 + # productId: 0x70C5 + # deviceProfileName: light-color-level + - id: "4999/24607" + deviceLabel: Govee Recessed Downlight 2 (6 inch) + vendorId: 0x1387 + productId: 0x601F + deviceProfileName: light-color-level + - id: "4999/25045" + deviceLabel: Govee Neon Rope Light 2 16.4ft/5m + vendorId: 0x1387 + productId: 0x61D5 + deviceProfileName: light-color-level + - id: "4999/24603" + deviceLabel: Govee Recessed Downlight (4 inch) + vendorId: 0x1387 + productId: 0x601B + deviceProfileName: light-color-level + - id: "4999/24602" + deviceLabel: Govee Recessed Downlight (6 inch) + vendorId: 0x1387 + productId: 0x601A + deviceProfileName: light-color-level + - id: "4999/25014" + deviceLabel: Govee Strip Light with Cover 32.8ft/10m + vendorId: 0x1387 + productId: 0x61B6 + deviceProfileName: light-color-level + - id: "4999/24610" + deviceLabel: Govee Table Lamp 2 + vendorId: 0x1387 + productId: 0x6022 + deviceProfileName: light-color-level + - id: "4999/24606" + deviceLabel: Govee Recessed Downlight 2 (4 inch) + vendorId: 0x1387 + productId: 0x601E + deviceProfileName: light-color-level + - id: "4999/24589" + deviceLabel: Govee RGBWW Smart LED Bulb MR16 400lm + vendorId: 0x1387 + productId: 0x600D + deviceProfileName: light-color-level + - id: "4999/24700" + deviceLabel: Govee Floor Lamp 2 + vendorId: 0x1387 + productId: 0x607C + deviceProfileName: light-color-level + # - id: "4999/26116" + # deviceLabel: Govee HDMI 2.1 Sync Box 2 55-65 + # vendorId: 0x1387 + # productId: 0x6604 + # deviceProfileName: light-color-level + - id: "4999/24694" + deviceLabel: Govee Floor Lamp Basic + vendorId: 0x1387 + productId: 0x6076 + deviceProfileName: light-color-level + - id: "4999/24587" + deviceLabel: Govee RGBWW Smart LED Bulb E12 450lm + vendorId: 0x1387 + productId: 0x600B + deviceProfileName: light-color-level + - id: "4999/25017" + deviceLabel: Govee Strip Light with Skyline Kit 19.7ft/6m + vendorId: 0x1387 + productId: 0x61B9 + deviceProfileName: light-color-level + - id: "4999/24742" + deviceLabel: Govee Ceiling Light Pro (15 inch) + vendorId: 0x1387 + productId: 0x60A6 + deviceProfileName: light-color-level + - id: "4999/24666" + deviceLabel: Govee TV Backlight 3 Lite Kit 55-65 + vendorId: 0x1387 + productId: 0x605A + deviceProfileName: light-color-level + - id: "4999/24608" + deviceLabel: Govee Table Lamp 2 Pro Sound by JBL + vendorId: 0x1387 + productId: 0x6020 + deviceProfileName: light-color-level + # - id: "4999/28868" + # deviceLabel: Govee Christmas String Lights 2 (Green Wire) 66ft/20m + # vendorId: 0x1387 + # productId: 0x70C4 + # deviceProfileName: light-color-level + - id: "4999/25046" + deviceLabel: Govee Neon Rope Light 2 32.8ft/10m + vendorId: 0x1387 + productId: 0x61D6 + deviceProfileName: light-color-level + - id: "4999/25061" + deviceLabel: Govee COB Strip Light Pro 9.8ft/3m + vendorId: 0x1387 + productId: 0x61E5 + deviceProfileName: light-color-level + - id: "4999/24723" + deviceLabel: Govee Star Light Projector (Aurora) + vendorId: 0x1387 + productId: 0x6093 + deviceProfileName: light-color-level + - id: "4999/25062" + deviceLabel: Govee COB Strip Light Pro 16.4ft/5m + vendorId: 0x1387 + productId: 0x61E6 + deviceProfileName: light-color-level + - id: "4999/24752" + deviceLabel: Govee Uplighter Floor Lamp + vendorId: 0x1387 + productId: 0x60B0 + deviceProfileName: light-color-level + - id: "4999/24727" + deviceLabel: Govee TV Backlight 3 Lite 40-50 + vendorId: 0x1387 + productId: 0x6097 + deviceProfileName: light-color-level + - id: "4999/24582" + deviceLabel: Govee RGBWW Smart LED Bulb A19 1000lm + vendorId: 0x1387 + productId: 0x6006 + deviceProfileName: light-color-level + - id: "4999/25016" + deviceLabel: Govee Strip Light with Skyline Kit 13.1ft/4m + vendorId: 0x1387 + productId: 0x61B8 + deviceProfileName: light-color-level + - id: "4999/26737" + deviceLabel: Govee Christmas Sparkle String Lights 99ft + vendorId: 0x1387 + productId: 0x6871 + deviceProfileName: light-color-level + - id: "4999/26736" + deviceLabel: Govee Christmas Sparkle String Lights 66ft + vendorId: 0x1387 + productId: 0x6870 + deviceProfileName: light-color-level + - id: "4999/26272" + deviceLabel: Govee TV Backlight 3 Pro 55-65 + vendorId: 0x1387 + productId: 0x66A0 + deviceProfileName: light-color-level + # - id: "4999/26272" + # deviceLabel: Govee TV Backlight 3 Pro 75-85 + # vendorId: 0x1387 + # productId: 0x66A0 + # deviceProfileName: light-color-level + - id: "4999/28854" + deviceLabel: Govee Curtain Lights Pro + vendorId: 0x1387 + productId: 0x70B6 + deviceProfileName: light-color-level + - id: "4999/24733" + deviceLabel: Govee Galaxy Light Star Projector 2 Pro + vendorId: 0x1387 + productId: 0x609D + deviceProfileName: light-color-level + - id: "4999/24725" + deviceLabel: Govee Star Projector Light + vendorId: 0x1387 + productId: 0x6095 + deviceProfileName: light-color-level + - id: "4999/24724" + deviceLabel: Govee Star Projector Light + vendorId: 0x1387 + productId: 0x6094 + deviceProfileName: light-color-level +# Hue + - id: "4107/2049" + deviceLabel: Hue W 1600 A21 E26 1P NAM + vendorId: 0x100B + productId: 0x0801 + deviceProfileName: light-level + - id: "4107/297" + deviceLabel: Hue WCA 650 BR30 E26 1P NAM + vendorId: 0x100B + productId: 0x0129 + deviceProfileName: light-color-level + - id: "4107/2048" + deviceLabel: Hue WA 810 A19 E26 1P NAM + vendorId: 0x100B + productId: 0x0800 + deviceProfileName: light-level-colorTemperature +# Intecular + - id: "5226/32769" + deviceLabel: InvisOutlet + vendorId: 0x146A + productId: 0x8001 + deviceProfileName: plug-binary #Ledvance - id: "4489/843" @@ -520,6 +857,17 @@ matterManufacturer: vendorId: 0x1189 productId: 0x0633 deviceProfileName: plug-binary +#Ikea + - id: "4476/32768" + deviceLabel: BILRESA scroll wheel + vendorId: 0x117C + productId: 0x8000 + deviceProfileName: ikea-scroll + - id: "4476/32769" + deviceLabel: BILRESA dual button + vendorId: 0x117C + productId: 0x8001 + deviceProfileName: ikea-2-button-battery #Innovation Matters - id: "4978/1" deviceLabel: M2D Bridge @@ -530,7 +878,7 @@ matterManufacturer: deviceLabel: IM Pushbutton Module vendorId: 0x1372 productId: 0x0002 - deviceProfileName: switch-4 + deviceProfileName: 4-button #JUNG - id: "5161/1" deviceLabel: Matter Push button 2-gang @@ -563,6 +911,77 @@ matterManufacturer: vendorId: 0x1021 productId: 0x0006 deviceProfileName: switch-level +#Leviton + - id: "4251/4097" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) Switch + vendorId: 0x109B + productId: 0x1001 + deviceProfileName: switch-binary + - id: "4251/4096" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) 600W Dimmer + vendorId: 0x109B + productId: 0x1000 + deviceProfileName: switch-level + - id: "4251/4107" + deviceLabel: Decora Smart Wi-Fi ELV Dimmer + vendorId: 0x109B + productId: 0x100B + deviceProfileName: switch-level + - id: "4251/4110" + deviceLabel: Decora Smart Wi-Fi 0-10V Dimmer + vendorId: 0x109B + productId: 0x100E + deviceProfileName: switch-level + - id: "4251/4108" + deviceLabel: Decora Evolve Smart Dimmer Module + vendorId: 0x109B + productId: 0x100C + deviceProfileName: switch-level + - id: "4251/4109" + deviceLabel: Decora Evolve Smart Switch Module + vendorId: 0x109B + productId: 0x100D + deviceProfileName: switch-binary + - id: "4251/4105" + deviceLabel: Decora Smart Wi-Fi Outdoor Plug-In Switch + vendorId: 0x109B + productId: 0x1009 + deviceProfileName: switch-binary + - id: "4251/4099" + deviceLabel: Leviton Decora Smart Wi-Fi Mini Plug-In Outlet + vendorId: 0x109B + productId: 0x1003 + deviceProfileName: switch-binary + - id: "4251/4100" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) Tamper Resistant Outlet + vendorId: 0x109B + productId: 0x1004 + deviceProfileName: switch-binary + - id: "4251/4098" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) Mini Plug-In Dimmer + vendorId: 0x109B + productId: 0x1002 + deviceProfileName: switch-level + - id: "4251/4102" + deviceLabel: Decora Smart Motion Sensing Dimmer Switch + vendorId: 0x109B + productId: 0x1006 + deviceProfileName: light-level-motion + - id: "4251/4103" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) Scene Controller Switch + vendorId: 0x109B + productId: 0x1007 + deviceProfileName: 3-button + - id: "4251/4113" + deviceLabel: "Decora Smart Wi-Fi (3rd Gen) 15A Switch" + vendorId: 0x109B + productId: 0x1011 + deviceProfileName: switch-binary + - id: "4251/4112" + deviceLabel: "Decora Smart Wi-Fi (3rd Gen) 600W Dimmer" + vendorId: 0x109B + productId: 0x1010 + deviceProfileName: switch-level #LeTianPai - id: "5163/4097" deviceLabel: LeTianPai Smart Light Bulb @@ -711,12 +1130,183 @@ matterManufacturer: vendorId: 0x1423 productId: 0x0077 deviceProfileName: light-level-colorTemperature-1500k-9000k + - id: "5155/191" + deviceLabel: LIFX Everyday Smart Light 2-Pack + vendorId: 0x1423 + productId: 0x00BF + deviceProfileName: light-color-level + - id: "5155/163" + deviceLabel: LIFX Supercolor (A19) + vendorId: 0x1423 + productId: 0x00A3 + deviceProfileName: light-level-colorTemperature + - id: "5155/118" + deviceLabel: LIFX Lightstrip + vendorId: 0x1423 + productId: 0x0076 + deviceProfileName: light-level-colorTemperature + - id: "5155/221" + deviceLabel: LIFX Spot + vendorId: 0x1423 + productId: 0x00DD + deviceProfileName: light-level-colorTemperature + - id: "5155/144" + deviceLabel: LIFX String + vendorId: 0x1423 + productId: 0x0090 + deviceProfileName: light-level-colorTemperature + - id: "5155/216" + deviceLabel: LIFX Candle Color (B10) + vendorId: 0x1423 + productId: 0x00D8 + deviceProfileName: light-level-colorTemperature + - id: "5155/225" + deviceLabel: LIFX PAR38 + vendorId: 0x1423 + productId: 0x00E1 + deviceProfileName: light-level-colorTemperature + - id: "5155/186" + deviceLabel: LIFX Candle Color + vendorId: 0x1423 + productId: 0x00BA + deviceProfileName: light-level-colorTemperature + - id: "5155/202" + deviceLabel: LIFX Ceiling 13x26 + vendorId: 0x1423 + productId: 0x00CA + deviceProfileName: light-level-colorTemperature + - id: "5155/143" + deviceLabel: LIFX String + vendorId: 0x1423 + productId: 0x008F + deviceProfileName: light-level-colorTemperature + - id: "5155/166" + deviceLabel: LIFX Supercolour (BR30) + vendorId: 0x1423 + productId: 0x00A6 + deviceProfileName: light-level-colorTemperature + - id: "5155/167" + deviceLabel: LIFX Downlight + vendorId: 0x1423 + productId: 0x00A7 + deviceProfileName: light-level-colorTemperature + - id: "5155/207" + deviceLabel: LIFX Everyday Lightstrip + vendorId: 0x1423 + productId: 0x00CF + deviceProfileName: light-level-colorTemperature + - id: "5155/222" + deviceLabel: LIFX Path (Round) + vendorId: 0x1423 + productId: 0x00DE + deviceProfileName: light-level-colorTemperature + - id: "5155/203" + deviceLabel: LIFX String + vendorId: 0x1423 + productId: 0x00CB + deviceProfileName: light-level-colorTemperature + - id: "5155/218" + deviceLabel: LIFX Tube + vendorId: 0x1423 + productId: 0x00DA + deviceProfileName: light-level-colorTemperature + - id: "5155/214" + deviceLabel: LIFX Permanent Outdoor + vendorId: 0x1423 + productId: 0x00D6 + deviceProfileName: light-level-colorTemperature + - id: "5155/117" + deviceLabel: LIFX Lightstrip + vendorId: 0x1423 + productId: 0x0075 + deviceProfileName: light-level-colorTemperature + - id: "5155/223" + deviceLabel: LIFX Downlight (6 Retro Downlight) + vendorId: 0x1423 + productId: 0x00DF + deviceProfileName: light-level-colorTemperature + - id: "5155/224" + deviceLabel: LIFX Downlight (90mm Downlight) + vendorId: 0x1423 + productId: 0x00E0 + deviceProfileName: light-level-colorTemperature + - id: "5155/204" + deviceLabel: LIFX String + vendorId: 0x1423 + productId: 0x00CC + deviceProfileName: light-level-colorTemperature + - id: "5155/206" + deviceLabel: LIFX Neon + vendorId: 0x1423 + productId: 0x00CE + deviceProfileName: light-level-colorTemperature + - id: "5155/164" + deviceLabel: LIFX Supercolor (BR30) + vendorId: 0x1423 + productId: 0x00A4 + deviceProfileName: light-level-colorTemperature + - id: "5155/120" + deviceLabel: LIFX Beam + vendorId: 0x1423 + productId: 0x0078 + deviceProfileName: light-level-colorTemperature + - id: "5155/208" + deviceLabel: LIFX Everyday Lightstrip + vendorId: 0x1423 + productId: 0x00D0 + deviceProfileName: light-level-colorTemperature + - id: "5155/165" + deviceLabel: LIFX Supercolour (A19) + vendorId: 0x1423 + productId: 0x00A5 + deviceProfileName: light-level-colorTemperature + - id: "5155/142" + deviceLabel: LIFX Neon + vendorId: 0x1423 + productId: 0x008E + deviceProfileName: light-level-colorTemperature + - id: "5155/141" + deviceLabel: LIFX Neon + vendorId: 0x1423 + productId: 0x008D + deviceProfileName: light-level-colorTemperature + - id: "5155/177" + deviceLabel: LIFX Ceiling + vendorId: 0x1423 + productId: 0x00B1 + deviceProfileName: light-level-colorTemperature + - id: "5155/170" + deviceLabel: LIFX Supercolour (A21) + vendorId: 0x1423 + productId: 0x00AA + deviceProfileName: light-level-colorTemperature + - id: "5155/205" + deviceLabel: LIFX Neon + vendorId: 0x1423 + productId: 0x00CD + deviceProfileName: light-level-colorTemperature #LG - id: "4142/8784" deviceLabel: LG Smart Button (1 Button) vendorId: 0x102E productId: 0x2250 deviceProfileName: button-battery +#Meross + - id: "4933/40987" + deviceLabel: Smart Wi-Fi Switch + vendorId: 0x1345 + productId: 0xA01B + deviceProfileName: switch-binary + - id: "4933/40978" + deviceLabel: Smart Wi-Fi Switch + vendorId: 0x1345 + productId: 0xA012 + deviceProfileName: switch-binary + - id: "4933/45057" + deviceLabel: Smart Wi-Fi Power Strip + vendorId: 0x1345 + productId: 0xB001 + deviceProfileName: switch-binary #Nanoleaf - id: "Nanoleaf NL53" deviceLabel: Essentials BR30 @@ -774,6 +1364,494 @@ matterManufacturer: vendorId: 0x137F productId: 0x027B deviceProfileName: plug-binary +#Netatmo + - id: "4129/8" + deviceLabel: Netatmo Thermo Hub + vendorId: 0x1021 + productId: 0x0008 + deviceProfileName: matter-bridge +#Onvis + - id: "5181/4097" + deviceLabel: Onvis Smart Plug S4EU + vendorId: 0x143D + productId: 0x1001 + deviceProfileName: plug-binary +#Osram + - id: "4489/2564" + deviceLabel: OSRAM MATTER PLUG UK + vendorId: 0x1189 + productId: 0x0A04 + deviceProfileName: plug-binary + - id: "4489/2757" + deviceLabel: SMART MAT P40 RGBW 827 FR E27 + vendorId: 0x1189 + productId: 0x0AC5 + deviceProfileName: light-color-level + - id: "4489/61442" + deviceLabel: OSRAM MATTER CLASSIC A 100W + vendorId: 0x1189 + productId: 0xF002 + deviceProfileName: light-color-level + - id: "4489/2755" + deviceLabel: SMART MAT A60 RGBW 827 FR E27 + vendorId: 0x1189 + productId: 0x0AC3 + deviceProfileName: light-color-level + - id: "4489/61444" + deviceLabel: OSRAM MATTER PLUG UK + vendorId: 0x1189 + productId: 0xF004 + deviceProfileName: plug-binary + - id: "4489/61443" + deviceLabel: OSRAM MATTER PLUG EU WH + vendorId: 0x1189 + productId: 0xF003 + deviceProfileName: plug-binary + - id: "4489/61441" + deviceLabel: OSRAM MATTER CLASSIC A 60W + vendorId: 0x1189 + productId: 0xF001 + deviceProfileName: light-color-level + - id: "4489/2353" + deviceLabel: SMART MATTER FLOORCORN200 MGC WT + vendorId: 0x1189 + productId: 0x0931 + deviceProfileName: light-color-level + - id: "4489/2350" + deviceLabel: SMART MATTER FLOORCORN140 MGC BK + vendorId: 0x1189 + productId: 0x092E + deviceProfileName: light-color-level + - id: "4489/2352" + deviceLabel: SMART MATTER FLOORCORN140 MGC WT + vendorId: 0x1189 + productId: 0x0930 + deviceProfileName: light-color-level + - id: "4489/2351" + deviceLabel: SMART MATTER FLOORCORN200 MGC BK + vendorId: 0x1189 + productId: 0x092F + deviceProfileName: light-color-level + - id: "4489/2715" + deviceLabel: SMART MAT B40 TW 827 FR E14 + vendorId: 0x1189 + productId: 0x0A9B + deviceProfileName: light-level-colorTemperature + - id: "4489/2686" + deviceLabel: SMART MAT A53 DIM FILGD 824 E27 + vendorId: 0x1189 + productId: 0x0A7E + deviceProfileName: light-level + - id: "4489/2725" + deviceLabel: SMART MAT PAR16 RGBW GU10 + vendorId: 0x1189 + productId: 0x0AA5 + deviceProfileName: light-color-level + - id: "4489/2766" + deviceLabel: SMART MAT B40 RGBW 827 FR E14 + vendorId: 0x1189 + productId: 0x0ACE + deviceProfileName: light-color-level + - id: "4489/2765" + deviceLabel: SMART MAT P40 RGBW 827 FR E14 + vendorId: 0x1189 + productId: 0x0ACD + deviceProfileName: light-color-level + - id: "4489/2764" + deviceLabel: SMART MAT G95 RGBW 827 FR E27 + vendorId: 0x1189 + productId: 0x0ACC + deviceProfileName: light-color-level + - id: "4489/2774" + deviceLabel: SMART MAT A40 FIL RGBW 827 E27 + vendorId: 0x1189 + productId: 0x0AD6 + deviceProfileName: light-color-level + - id: "4489/2775" + deviceLabel: SMART MAT E40 FIL RGBW 827 E27 + vendorId: 0x1189 + productId: 0x0AD7 + deviceProfileName: light-color-level + - id: "4489/2776" + deviceLabel: SMART MAT G40 FIL RGBW 827 E27 + vendorId: 0x1189 + productId: 0x0AD8 + deviceProfileName: light-color-level + - id: "4489/2837" + deviceLabel: SMART MAT A75 RGBW 827 FR E27 + vendorId: 0x1189 + productId: 0x0B15 + deviceProfileName: light-color-level + - id: "4489/2713" + deviceLabel: SMART MAT A60 TW 827 FR E27 + vendorId: 0x1189 + productId: 0x0A99 + deviceProfileName: light-level-colorTemperature + - id: "4489/2711" + deviceLabel: SMART MAT A100 TW 827 FR E27 + vendorId: 0x1189 + productId: 0x0A97 + deviceProfileName: light-level-colorTemperature + - id: "4489/2716" + deviceLabel: SMART MAT P40 TW 827 FR E14 + vendorId: 0x1189 + productId: 0x0A9C + deviceProfileName: light-level-colorTemperature + - id: "4489/2726" + deviceLabel: SMART MAT PAR16 TW 827 GU10 + vendorId: 0x1189 + productId: 0x0AA6 + deviceProfileName: light-level-colorTemperature + - id: "4489/2678" + deviceLabel: SMART MAT E60 DIM FIL 827 E27 + vendorId: 0x1189 + productId: 0x0A76 + deviceProfileName: light-level + - id: "4489/2676" + deviceLabel: SMART MAT G60 DIM FIL 827 E27 + vendorId: 0x1189 + productId: 0x0A74 + deviceProfileName: light-level + - id: "4489/2687" + deviceLabel: SMART MAT B40 DIM FIL 827 E14 + vendorId: 0x1189 + productId: 0x0A7F + deviceProfileName: light-level + - id: "4489/2685" + deviceLabel: SMART MAT E53 DIM FILGD 824 E27 + vendorId: 0x1189 + productId: 0x0A7D + deviceProfileName: light-level + - id: "4489/2684" + deviceLabel: SMART MAT G53 DIM FILGD 824 E27 + vendorId: 0x1189 + productId: 0x0A7C + deviceProfileName: light-level + - id: "4489/2681" + deviceLabel: SMART MAT A60 DIM FIL 827 E27 + vendorId: 0x1189 + productId: 0x0A79 + deviceProfileName: light-level + - id: "4489/2682" + deviceLabel: SMART MAT P40 DIM FIL 827 E14 + vendorId: 0x1189 + productId: 0x0A7A + deviceProfileName: light-level + - id: "4489/2727" + deviceLabel: SMART MAT PAR16 DIM GU10 + vendorId: 0x1189 + productId: 0x0AA7 + deviceProfileName: light-level + - id: "4489/2712" + deviceLabel: SMART MAT G95 100 TW 827 E27 + vendorId: 0x1189 + productId: 0x0A98 + deviceProfileName: light-level-colorTemperature + - id: "4489/2836" + deviceLabel: SMART MAT A75 TW 827 FR E27 + vendorId: 0x1189 + productId: 0x0B14 + deviceProfileName: light-level-colorTemperature + - id: "4489/2514" + deviceLabel: SMART MAT ORBIS DL 200 TW WT + vendorId: 0x1189 + productId: 0x09D2 + deviceProfileName: light-color-level + - id: "4489/2518" + deviceLabel: SMART MAT ORBIS DL 400 TW WT + vendorId: 0x1189 + productId: 0x09D6 + deviceProfileName: light-color-level + - id: "4489/2519" + deviceLabel: SMART MAT ORBIS DL SQ400 TW WT + vendorId: 0x1189 + productId: 0x09D7 + deviceProfileName: light-color-level + - id: "4489/2763" + deviceLabel: SMART MAT A60 RGBW 827 FR B22D + vendorId: 0x1189 + productId: 0x0ACB + deviceProfileName: light-color-level + - id: "4489/2756" + deviceLabel: SMART MAT A100 RGBW 827 FR E27 + vendorId: 0x1189 + productId: 0x0AC4 + deviceProfileName: light-color-level + - id: "4489/3027" + deviceLabel: SMART MAT DECORWALLSWAN300X150TW + vendorId: 0x1189 + productId: 0x0BD3 + deviceProfileName: light-level-colorTemperature + - id: "4489/2994" + deviceLabel: SMART MAT PLANON FL 120X30 TW + vendorId: 0x1189 + productId: 0x0BB2 + deviceProfileName: light-level-colorTemperature + - id: "4489/3032" + deviceLabel: SMART WIFI MAT BATH W400 IP44 TW + vendorId: 0x1189 + productId: 0x0BD8 + deviceProfileName: light-level-colorTemperature + - id: "4489/3026" + deviceLabel: SMART WIFI MAT WAVE300X300IP44TW + vendorId: 0x1189 + productId: 0x0BD2 + deviceProfileName: light-level-colorTemperature + - id: "4489/2957" + deviceLabel: SMART WIFI MAT TRACKSP OSAKA TW + vendorId: 0x1189 + productId: 0x0B8D + deviceProfileName: light-level-colorTemperature + - id: "4489/3025" + deviceLabel: SMART WIFI MAT CYLINDER 450 TW + vendorId: 0x1189 + productId: 0x0BD1 + deviceProfileName: light-level-colorTemperature + - id: "4489/2969" + deviceLabel: SMART MAT PLANON PLUS 120X30TW + vendorId: 0x1189 + productId: 0x0B99 + deviceProfileName: light-level-colorTemperature + - id: "4489/3028" + deviceLabel: SMART MAT DISC 400 IP44 TW + vendorId: 0x1189 + productId: 0x0BD4 + deviceProfileName: light-level-colorTemperature + - id: "4489/2971" + deviceLabel: SMART MAT PLANON FL 80X10TW + vendorId: 0x1189 + productId: 0x0B9B + deviceProfileName: light-level-colorTemperature + - id: "4489/3014" + deviceLabel: SMART MAT PLANONFLSPARK45X45TW + vendorId: 0x1189 + productId: 0x0BC6 + deviceProfileName: light-level-colorTemperature + - id: "4489/3023" + deviceLabel: SMART WIFI MAT MAGNET 600X300 TW + vendorId: 0x1189 + productId: 0x0BCF + deviceProfileName: light-level-colorTemperature + - id: "4489/3021" + deviceLabel: SMART WIFI MAT AQUA280X160IP44TW + vendorId: 0x1189 + productId: 0x0BCD + deviceProfileName: light-level-colorTemperature + - id: "4489/3035" + deviceLabel: SMART WIFI MAT DUPLO 300 IP44 TW + vendorId: 0x1189 + productId: 0x0BDB + deviceProfileName: light-level-colorTemperature + - id: "4489/3024" + deviceLabel: SMART WIFI MAT DUPLO 580 IP44 TW + vendorId: 0x1189 + productId: 0x0BD0 + deviceProfileName: light-level-colorTemperature + - id: "4489/2998" + deviceLabel: SMART WIFI MAT DL SLIM 120 TW + vendorId: 0x1189 + productId: 0x0BB6 + deviceProfileName: light-level-colorTemperature + - id: "4489/3020" + deviceLabel: SMART MATDECORWALLTWIST230X127TW + vendorId: 0x1189 + productId: 0x0BCC + deviceProfileName: light-level-colorTemperature + - id: "4489/3049" + deviceLabel: SMART WIFI MAT TRACKSP CIRCLE TW + vendorId: 0x1189 + productId: 0x0BE9 + deviceProfileName: light-level-colorTemperature + - id: "4489/3029" + deviceLabel: SMART WIFI MAT DUPLO 450 IP44 TW + vendorId: 0x1189 + productId: 0x0BD5 + deviceProfileName: light-level-colorTemperature + - id: "4489/2991" + deviceLabel: SMART MAT PLANON PLUS 60X60 TW + vendorId: 0x1189 + productId: 0x0BAF + deviceProfileName: light-level-colorTemperature + - id: "4489/2985" + deviceLabel: SMART MAT PLANON PLUS 30X30 TW + vendorId: 0x1189 + productId: 0x0BA9 + deviceProfileName: light-level-colorTemperature + - id: "4489/3033" + deviceLabel: SMART WIFI MAT BATH W300 IP44 TW + vendorId: 0x1189 + productId: 0x0BD9 + deviceProfileName: light-level-colorTemperature + - id: "4489/2999" + deviceLabel: SMART WIFI MAT DL SLIM 225 TW + vendorId: 0x1189 + productId: 0x0BB7 + deviceProfileName: light-level-colorTemperature + - id: "4489/3037" + deviceLabel: SMART WIFI MAT MAGNET 300X300 TW + vendorId: 0x1189 + productId: 0x0BDD + deviceProfileName: light-level-colorTemperature + - id: "4489/2990" + deviceLabel: SMART MAT PLANON PLUS 60X30 TW + vendorId: 0x1189 + productId: 0x0BAE + deviceProfileName: light-level-colorTemperature + - id: "4489/3047" + deviceLabel: SMART WIFI MAT TRACKSP OSAKA TW + vendorId: 0x1189 + productId: 0x0BE7 + deviceProfileName: light-level-colorTemperature + - id: "4489/3031" + deviceLabel: SMART WIFI MAT MAGNET 600X300 TW + vendorId: 0x1189 + productId: 0x0BD7 + deviceProfileName: light-level-colorTemperature + - id: "4489/3034" + deviceLabel: SMART WIFI MAT DISC 300 IP44 TW + vendorId: 0x1189 + productId: 0x0BDA + deviceProfileName: light-level-colorTemperature + - id: "4489/3030" + deviceLabel: SMART WIFI MAT MAGNET 450X450 TW + vendorId: 0x1189 + productId: 0x0BD6 + deviceProfileName: light-level-colorTemperature + - id: "4489/3022" + deviceLabel: SMART WIFI MAT AQUA200X200IP44TW + vendorId: 0x1189 + productId: 0x0BCE + deviceProfileName: light-level-colorTemperature + - id: "4489/2993" + deviceLabel: SMART MAT PLANON FL 30X30 TW + vendorId: 0x1189 + productId: 0x0BB1 + deviceProfileName: light-level-colorTemperature + - id: "4489/3048" + deviceLabel: SMART WIFI MAT TRACKSP CIRCLE TW + vendorId: 0x1189 + productId: 0x0BE8 + deviceProfileName: light-level-colorTemperature + - id: "4489/2997" + deviceLabel: SMART WIFI MAT DL SLIM 85 TW WT + vendorId: 0x1189 + productId: 0x0BB5 + deviceProfileName: light-level-colorTemperature + - id: "4489/2949" + deviceLabel: SMART WIFI MAT SPIRAL 500 TW + vendorId: 0x1189 + productId: 0x0B85 + deviceProfileName: light-level-colorTemperature + - id: "4489/3036" + deviceLabel: SMART WFMTDECORWALLSWAN200X200TW + vendorId: 0x1189 + productId: 0x0BDC + deviceProfileName: light-level-colorTemperature + - id: "4489/2995" + deviceLabel: SMART MAT PLANON FL 120X10 TW + vendorId: 0x1189 + productId: 0x0BB3 + deviceProfileName: light-level-colorTemperature + - id: "4489/2987" + deviceLabel: SMART MAT PLANON PLUS 45X45 TW + vendorId: 0x1189 + productId: 0x0BAB + deviceProfileName: light-level-colorTemperature + - id: "4489/3018" + deviceLabel: SMART WFMTDECORWALLWOOD210X110TW + vendorId: 0x1189 + productId: 0x0BCA + deviceProfileName: light-level-colorTemperature + - id: "4489/3019" + deviceLabel: SMART MATDECORWALLTWIST230X127TW + vendorId: 0x1189 + productId: 0x0BCB + deviceProfileName: light-level-colorTemperature + - id: "4489/3055" + deviceLabel: SMART WIFI MAT FLOOD 50W RGBW + vendorId: 0x1189 + productId: 0x0BEF + deviceProfileName: light-color-level + - id: "4489/2970" + deviceLabel: SMART MAT PLANON PLUS60X60RGBTW + vendorId: 0x1189 + productId: 0x0B9A + deviceProfileName: light-color-level + - id: "4489/3057" + deviceLabel: SMART WIFI MAT FLOOD 20W RGBW + vendorId: 0x1189 + productId: 0x0BF1 + deviceProfileName: light-color-level + - id: "4489/2973" + deviceLabel: SMART MAT PLANON PLUS45X45RGBW + vendorId: 0x1189 + productId: 0x0B9D + deviceProfileName: light-color-level + - id: "4489/2974" + deviceLabel: SMART MAT PLANON PLUS60X30RGBW + vendorId: 0x1189 + productId: 0x0B9E + deviceProfileName: light-color-level + - id: "4489/3059" + deviceLabel: SMART WIFI MAT CLARIA 490RGBTW + vendorId: 0x1189 + productId: 0x0BF3 + deviceProfileName: light-color-level + - id: "4489/3058" + deviceLabel: SMART WIFI MAT FLOOD 30W RGBW + vendorId: 0x1189 + productId: 0x0BF2 + deviceProfileName: light-color-level + - id: "4489/2972" + deviceLabel: SMART MAT PLANON PLUS60X60RGBW + vendorId: 0x1189 + productId: 0x0B9C + deviceProfileName: light-color-level + - id: "4489/2984" + deviceLabel: SMART MAT PLANON PLUS120X30RGBW + vendorId: 0x1189 + productId: 0x0BA8 + deviceProfileName: light-color-level + - id: "4489/3001" + deviceLabel: SMART WIFI MAT SP170 110DEGRGBTW + vendorId: 0x1189 + productId: 0x0BB9 + deviceProfileName: light-color-level + - id: "4489/2992" + deviceLabel: SMART MAT PLANON PLUS30X30RGBW + vendorId: 0x1189 + productId: 0x0BB0 + deviceProfileName: light-color-level + - id: "4489/3056" + deviceLabel: SMART WIFI MAT FLOOD 10W RGBW + vendorId: 0x1189 + productId: 0x0BF0 + deviceProfileName: light-color-level + - id: "4489/3016" + deviceLabel: SMART OUTD MAT BEAMADJWALL RGBTW + vendorId: 0x1189 + productId: 0x0BC8 + deviceProfileName: light-color-level + - id: "4489/3000" + deviceLabel: SMART WIFI MAT SP 86 100DEGRGBTW + vendorId: 0x1189 + productId: 0x0BB8 + deviceProfileName: light-color-level + - id: "4489/3003" + deviceLabel: SMART WIFI MAT STEA485RD RGBTW + vendorId: 0x1189 + productId: 0x0BBB + deviceProfileName: light-color-level + - id: "4489/2963" + deviceLabel: SMART WIFI MAT FLOOD 100W RGBW + vendorId: 0x1189 + productId: 0x0B93 + deviceProfileName: light-color-level + - id: "4489/2194" + deviceLabel: SMART WIFI MATTER WALL SWITCH 1G + vendorId: 0x1189 + productId: 0x0892 + deviceProfileName: switch-binary #Shelly - id: "5264/1" deviceLabel: Shelly Plug S MTR Gen3 @@ -2689,6 +3767,26 @@ matterManufacturer: vendorId: 0x100b productId: 0x228F deviceProfileName: light-color-level-2200K-6500K + - id: "4107/8954" + deviceLabel: WiZ Gradient Light Bars + vendorId: 0x100B + productId: 0x22FA + deviceProfileName: light-color-level + - id: "4107/8953" + deviceLabel: WiZ Gradient Light Bars + vendorId: 0x100B + productId: 0x22F9 + deviceProfileName: light-color-level + - id: "4107/8949" + deviceLabel: WiZ Gradient Floor lamp + vendorId: 0x100B + productId: 0x22F5 + deviceProfileName: light-color-level + - id: "4107/8948" + deviceLabel: WiZ Gradient Floor lamp + vendorId: 0x100B + productId: 0x22F4 + deviceProfileName: light-color-level #Zemismart - id: "5020/61154" deviceLabel: Zemismart Inline Module @@ -2760,6 +3858,12 @@ matterManufacturer: vendorId: 0x139C productId: 0x0387 deviceProfileName: matter-bridge +#Zimi + - id: "5410/3" + deviceLabel: Zimi Matter Connect + vendorId: 0x1522 + productId: 0x0003 + deviceProfileName: matter-bridge #TUO - id: "5150/1" deviceLabel: "TUO Smart Button" @@ -2924,13 +4028,66 @@ matterGeneric: deviceTypes: - id: 0x010D # Extended Color Light - id: 0x002B # Fan - deviceProfileName: light-color-level-fan + deviceProfileName: fan-modular - id: "matter/dimmable/light/motion" deviceLabel: Matter Dimmable Light Occupancy Sensor deviceTypes: - id: 0x0101 # Dimmable Light - id: 0x0107 # Occupancy Sensor deviceProfileName: light-level-motion + - id: "matter/camera" + deviceLabel: Matter Camera + deviceTypes: + - id: 0x0142 # Camera + deviceProfileName: camera + - id: "matter/on-off/fan/light" + deviceLabel: Matter OnOff Fan Light + deviceTypes: + - id: 0x002B # Fan + - id: 0x0100 # OnOff Light + deviceProfileName: fan-modular + - id: "matter/dimmable/fan/light" + deviceLabel: Matter Dimmable Fan Light + deviceTypes: + - id: 0x002B # Fan + - id: 0x0101 # Dimmable Light + deviceProfileName: fan-modular + - id: "matter/colorTemperature/fan/light" + deviceLabel: Matter Color Temperature Fan Light + deviceTypes: + - id: 0x002B # Fan + - id: 0x010C # Color Temperature Light + deviceProfileName: fan-modular + - id: "matter/color/fan/light" + deviceLabel: Matter Color Fan Light + deviceTypes: + - id: 0x002B # Fan + - id: 0x010D # Extended Color Light + deviceProfileName: fan-modular + - id: "matter/on-off/fan/plug" + deviceLabel: Matter OnOff Fan Plug + deviceTypes: + - id: 0x002B # Fan + - id: 0x010A # On Off Plug-in Unit + deviceProfileName: fan-modular + - id: "matter/dimmable/fan/plug" + deviceLabel: Matter Dimmable Fan Plug + deviceTypes: + - id: 0x002B # Fan + - id: 0x010B # Dimmable Plug-in Unit + deviceProfileName: fan-modular + - id: "matter/mounted/on-off/control/fan" + deviceLabel: Matter Mounted OnOff Control Fan + deviceTypes: + - id: 0x002B # Fan + - id: 0x010F # Mounted On/Off Control + deviceProfileName: fan-modular + - id: "matter/mounted/dim/load/control/fan" + deviceLabel: Matter Mounted Dimmable Load Control Fan + deviceTypes: + - id: 0x002B # Fan + - id: 0x0110 # Mounted Dimmable Load Control + deviceProfileName: fan-modular matterThing: - id: SmartThings/MatterThing diff --git a/drivers/SmartThings/matter-switch/profiles/3-button-motion.yml b/drivers/SmartThings/matter-switch/profiles/3-button-motion.yml new file mode 100644 index 0000000000..2852dd2ccd --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/3-button-motion.yml @@ -0,0 +1,26 @@ +name: 3-button-motion +components: + - id: main + capabilities: + - id: button + version: 1 + - id: motionSensor + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/profiles/6-button-motion.yml b/drivers/SmartThings/matter-switch/profiles/6-button-motion.yml new file mode 100644 index 0000000000..f191e95fdc --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/6-button-motion.yml @@ -0,0 +1,44 @@ +name: 6-button-motion +components: + - id: main + capabilities: + - id: button + version: 1 + - id: motionSensor + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/profiles/9-button-battery.yml b/drivers/SmartThings/matter-switch/profiles/9-button-battery.yml new file mode 100644 index 0000000000..8aa7df8e39 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/9-button-battery.yml @@ -0,0 +1,63 @@ +name: 9-button-battery +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button7 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button8 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button9 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + diff --git a/drivers/SmartThings/matter-switch/profiles/9-button-batteryLevel.yml b/drivers/SmartThings/matter-switch/profiles/9-button-batteryLevel.yml new file mode 100644 index 0000000000..ae90de2d01 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/9-button-batteryLevel.yml @@ -0,0 +1,62 @@ +name: 9-button-batteryLevel +components: + - id: main + capabilities: + - id: button + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button7 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button8 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button9 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/9-button.yml b/drivers/SmartThings/matter-switch/profiles/9-button.yml new file mode 100644 index 0000000000..3894a40d13 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/9-button.yml @@ -0,0 +1,60 @@ +name: 9-button +components: + - id: main + capabilities: + - id: button + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button7 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button8 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button9 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/camera.yml b/drivers/SmartThings/matter-switch/profiles/camera.yml new file mode 100644 index 0000000000..7f62319984 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/camera.yml @@ -0,0 +1,156 @@ +name: camera +components: + - id: main + capabilities: + - id: webrtc + version: 1 + optional: true + - id: videoCapture2 + version: 1 + optional: true + - id: videoStreamSettings + version: 1 + optional: true + - id: imageCapture + version: 1 + optional: true + - id: mechanicalPanTiltZoom + version: 1 + optional: true + - id: hdr + version: 1 + optional: true + - id: nightVision + version: 1 + optional: true + - id: imageControl + version: 1 + optional: true + - id: audioRecording + version: 1 + optional: true + - id: sounds + version: 1 + optional: true + - id: cameraPrivacyMode + version: 1 + optional: true + - id: zoneManagement + version: 1 + optional: true + - id: localMediaStorage + version: 1 + optional: true + - id: cameraViewportSettings + version: 1 + optional: true + - id: motionSensor + version: 1 + optional: true + - id: vision.clipAnalysis + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Camera + - id: statusLed + optional: true + capabilities: + - id: switch + version: 1 + optional: true + - id: mode + version: 1 + optional: true + - id: speaker + optional: true + capabilities: + - id: audioMute + version: 1 + optional: true + - id: audioVolume + version: 1 + optional: true + - id: microphone + optional: true + capabilities: + - id: audioMute + version: 1 + optional: true + - id: audioVolume + version: 1 + optional: true + - id: doorbell + optional: true + capabilities: + - id: button + version: 1 + optional: true +deviceConfig: + dashboard: + states: + - component: main + capability: imageCapture + version: 1 + values: + - label: "{{___PO_CODE_SAMSUNGELECTRONICS.IM_DEFAULT_IMAGE_CAPTURE}}" + visibleCondition: + component: main + capability: imageCapture + version: 1 + value: captureTime.value + valueType: string + operator: CONTAINS + operand: T + isOffline: false + basicPlus: + - displayType: camera + camera: + image: + component: main + capability: imageCapture + version: 1 + value: image.value + overlayIcons: + - iconUrl: "res://ic_camera_motion_detected" + visibleCondition: + component: main + capability: motionSensor + version: 1 + value: motion.value + valueType: string + operator: EQUALS + operand: "active" + detailView: + - component: main + capability: webrtc + version: 1 + - component: main + capability: mechanicalPanTiltZoom + version: 1 + - component: main + capability: motionSensor + version: 1 + automation: + conditions: + - component: main + capability: motionSensor + version: 1 + actions: + - component: main + capability: videoCapture2 + version: 1 + - component: main + capability: imageCapture + version: 1 + dpInfo: + - os: ios + dpUri: "storyboard://HMVSController/HMVSViewController" + - os: android + dpUri: "plugin://com.samsung.android.plugin.camera" +metadata: + mnmn: SmartThingsEdge + vid: matter-camera diff --git a/drivers/SmartThings/matter-switch/profiles/fan-modular.yml b/drivers/SmartThings/matter-switch/profiles/fan-modular.yml new file mode 100644 index 0000000000..878c9ed615 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/fan-modular.yml @@ -0,0 +1,17 @@ +name: fan-modular +components: +- id: main + capabilities: + - id: fanMode + version: 1 + optional: true + - id: fanSpeedPercent + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Fan + diff --git a/drivers/SmartThings/matter-switch/profiles/ikea-2-button-battery.yml b/drivers/SmartThings/matter-switch/profiles/ikea-2-button-battery.yml new file mode 100644 index 0000000000..0b256f3b65 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/ikea-2-button-battery.yml @@ -0,0 +1,53 @@ +name: ikea-2-button-battery +components: + - id: main + label: Button 1 + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + label: Button 2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +deviceConfig: + icons: + - group: main + iconUrl: 'icon://button_multi' + dashboard: + states: + - component: main + capability: button + version: 1 + detailView: + - component: main + capability: button + version: 1 + - component: main + capability: battery + version: 1 + - component: button2 + capability: button + version: 1 + automation: + conditions: + - component: main + capability: button + version: 1 + - component: main + capability: battery + version: 1 + - component: button2 + capability: button + version: 1 + actions: [] diff --git a/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml new file mode 100644 index 0000000000..166b0a62f1 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml @@ -0,0 +1,81 @@ +name: ikea-scroll +components: + - id: main + label: Group 1 + capabilities: + - id: button + version: 1 + - id: knob + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Button + - id: group2 + label: Group 2 + capabilities: + - id: button + version: 1 + - id: knob + version: 1 + categories: + - name: Button + - id: group3 + label: Group 3 + capabilities: + - id: button + version: 1 + - id: knob + version: 1 + categories: + - name: Button +deviceConfig: + icons: + - group: main + iconUrl: 'icon://button_wheel' + dashboard: + states: + - component: main + capability: button + version: 1 + detailView: + - component: main + capability: button + version: 1 + - component: main + capability: knob + version: 1 + - component: main + capability: battery + version: 1 + - component: group2 + capability: button + version: 1 + - component: group2 + capability: knob + version: 1 + - component: group3 + capability: button + version: 1 + - component: group3 + capability: knob + version: 1 + automation: + conditions: + - component: main + capability: button + version: 1 + - component: main + capability: battery + version: 1 + - component: group2 + capability: button + version: 1 + - component: group3 + capability: button + version: 1 + actions: [] diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-1800K-6500K.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-1800K-6500K.yml index 1638962b8f..a0f5196b12 100755 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-1800K-6500K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-1800K-6500K.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-color-level-1800K-6500K components: - id: main @@ -10,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 1800, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-2000K-7000K.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-2000K-7000K.yml index 3fb0742f0d..7424d241e2 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-2000K-7000K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-2000K-7000K.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-color-level-2000K-7000K components: - id: main @@ -10,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2000, 7000 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-2200K-6500K.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-2200K-6500K.yml index f6c2269431..a098cdd06f 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-2200K-6500K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-2200K-6500K.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-color-level-2200K-6500K components: - id: main @@ -10,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-2700K-6500K.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-2700K-6500K.yml index ebdaa520c1..1b469ae8e8 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-2700K-6500K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-2700K-6500K.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-color-level-2700K-6500K components: - id: main @@ -10,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2700, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-fan.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-fan.yml index 2fabc23bd7..2f91bcb04e 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-fan.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-fan.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2700, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: fanMode diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion-1000K-15000K.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion-1000K-15000K.yml index 0b815134b3..fb7a4fca94 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion-1000K-15000K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion-1000K-15000K.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-color-level-illuminance-motion-1000K-15000K components: - id: main @@ -6,12 +7,16 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 1000, 15000 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: motionSensor diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion.yml index 999e64a047..50f68f60fc 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion.yml @@ -6,12 +6,16 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: motionSensor diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level.yml index 572686ffdd..2f0f673bee 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/light-energy-powerConsumption.yml new file mode 100644 index 0000000000..1f62938b9d --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-energy-powerConsumption.yml @@ -0,0 +1,16 @@ +name: light-energy-powerConsumption +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-2-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-2-button.yml index 7c8b60ef56..6a4fbcc9d0 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-2-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-2-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-3-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-3-button.yml index 59600efd72..9e3106d9de 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-3-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-3-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-4-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-4-button.yml index b49b7f2254..43bcec5955 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-4-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-4-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-5-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-5-button.yml index ee55a6a394..2b98b9784d 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-5-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-5-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-6-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-6-button.yml index 805c97763e..230b2ea341 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-6-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-6-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-7-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-7-button.yml index 5cd1666a5f..13b3ee9443 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-7-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-7-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-8-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-8-button.yml index 4636359e92..a2dc732257 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-8-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-8-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-ColorTemperature-1500-9000k.yml b/drivers/SmartThings/matter-switch/profiles/light-level-ColorTemperature-1500-9000k.yml index e2542bef5c..effe5f93b6 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-ColorTemperature-1500-9000k.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-ColorTemperature-1500-9000k.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-level-colorTemperature-1500k-9000k components: - id: main @@ -10,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 1500, 9000 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-button.yml index 9fc53f642b..8334e32feb 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2200K-6500K.yml b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2200K-6500K.yml index 2348027636..2541fddc09 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2200K-6500K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2200K-6500K.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-level-colorTemperature-2200K-6500K components: - id: main @@ -10,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2700K-6500K.yml b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2700K-6500K.yml index 1a1e957b0c..7c845e8238 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2700K-6500K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2700K-6500K.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-level-colorTemperature-2700K-6500K components: - id: main @@ -10,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2700, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2710k-6500k.yml b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2710k-6500k.yml index 46a6444e92..5ccc67ab4c 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2710k-6500k.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2710k-6500k.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-level-colorTemperature-2710k-6500k components: - id: main @@ -10,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2710, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature.yml b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature.yml index 4d130281d7..5681a97e9e 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/light-level-energy-powerConsumption.yml new file mode 100644 index 0000000000..b2ddcc0a60 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-level-energy-powerConsumption.yml @@ -0,0 +1,24 @@ +name: light-level-energy-powerConsumption +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-motion.yml b/drivers/SmartThings/matter-switch/profiles/light-level-motion.yml index bdea457c21..54ce30abdf 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-motion.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-motion.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: motionSensor version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml index f6c45ed1f7..31c6d44ee1 100755 --- a/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: powerMeter version: 1 - id: energyMeter diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-power.yml b/drivers/SmartThings/matter-switch/profiles/light-level-power.yml new file mode 100644 index 0000000000..86d61c0345 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-level-power.yml @@ -0,0 +1,22 @@ +name: light-level-power +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 + - id: powerMeter + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/profiles/light-level.yml b/drivers/SmartThings/matter-switch/profiles/light-level.yml index e266f497c9..3b286f8262 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-power.yml b/drivers/SmartThings/matter-switch/profiles/light-power.yml new file mode 100644 index 0000000000..27e07d0a49 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-power.yml @@ -0,0 +1,14 @@ +name: light-power +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: powerMeter + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/profiles/m5stack.yml b/drivers/SmartThings/matter-switch/profiles/m5stack.yml deleted file mode 100644 index edf662b462..0000000000 --- a/drivers/SmartThings/matter-switch/profiles/m5stack.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: m5stack -components: -- id: main - capabilities: - - id: switch - version: 1 - - id: switchLevel - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: Switch -- id: switch2 - capabilities: - - id: switch - version: 1 - - id: refresh - version: 1 - categories: - - name: Switch diff --git a/drivers/SmartThings/matter-switch/profiles/plug-button.yml b/drivers/SmartThings/matter-switch/profiles/plug-button.yml deleted file mode 100644 index 0f9e8b1b56..0000000000 --- a/drivers/SmartThings/matter-switch/profiles/plug-button.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: plug-button -components: - - id: main - capabilities: - - id: switch - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: SmartPlug - - id: button - capabilities: - - id: button - version: 1 - categories: - - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/plug-level-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/plug-level-energy-powerConsumption.yml index 86bd861bce..4ae29d2320 100644 --- a/drivers/SmartThings/matter-switch/profiles/plug-level-energy-powerConsumption.yml +++ b/drivers/SmartThings/matter-switch/profiles/plug-level-energy-powerConsumption.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: energyMeter version: 1 - id: powerConsumptionReport diff --git a/drivers/SmartThings/matter-switch/profiles/plug-level-power-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/plug-level-power-energy-powerConsumption.yml index 17e7d6b7a0..477fb8b769 100644 --- a/drivers/SmartThings/matter-switch/profiles/plug-level-power-energy-powerConsumption.yml +++ b/drivers/SmartThings/matter-switch/profiles/plug-level-power-energy-powerConsumption.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: powerMeter version: 1 - id: energyMeter diff --git a/drivers/SmartThings/matter-switch/profiles/plug-level-power.yml b/drivers/SmartThings/matter-switch/profiles/plug-level-power.yml index d175930a92..8bb1c56361 100644 --- a/drivers/SmartThings/matter-switch/profiles/plug-level-power.yml +++ b/drivers/SmartThings/matter-switch/profiles/plug-level-power.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: powerMeter version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/plug-level.yml b/drivers/SmartThings/matter-switch/profiles/plug-level.yml index 0d888b843f..90fa2ece47 100644 --- a/drivers/SmartThings/matter-switch/profiles/plug-level.yml +++ b/drivers/SmartThings/matter-switch/profiles/plug-level.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/switch-2-level.yml b/drivers/SmartThings/matter-switch/profiles/switch-2-level.yml deleted file mode 100644 index 549a766f93..0000000000 --- a/drivers/SmartThings/matter-switch/profiles/switch-2-level.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: switch-2-level -components: -- id: main - capabilities: - - id: switch - version: 1 - - id: switchLevel - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: Switch -- id: switch2 - capabilities: - - id: switch - version: 1 - - id: switchLevel - version: 1 - - id: refresh - version: 1 - categories: - - name: Switch diff --git a/drivers/SmartThings/matter-switch/profiles/switch-2.yml b/drivers/SmartThings/matter-switch/profiles/switch-2.yml index 45fac9402f..f2a1f35e87 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-2.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-2.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: switch-2 components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/switch-3.yml b/drivers/SmartThings/matter-switch/profiles/switch-3.yml index 050675b28c..a9a0756287 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-3.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-3.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: switch-3 components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/switch-4.yml b/drivers/SmartThings/matter-switch/profiles/switch-4.yml index fd81619b44..c6e526a0a7 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-4.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-4.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: switch-4 components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/switch-5.yml b/drivers/SmartThings/matter-switch/profiles/switch-5.yml index 5971405fe5..257da9cdfb 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-5.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-5.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: switch-5 components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/switch-6.yml b/drivers/SmartThings/matter-switch/profiles/switch-6.yml index 882738a9d6..3e5136dc52 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-6.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-6.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: switch-6 components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/switch-7.yml b/drivers/SmartThings/matter-switch/profiles/switch-7.yml index bbc075ffe3..14536f7ccf 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-7.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-7.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: switch-7 components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/switch-color-level.yml b/drivers/SmartThings/matter-switch/profiles/switch-color-level.yml index f1f9e78438..c684f632c9 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-color-level.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-color-level.yml @@ -6,12 +6,16 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/switch-level-colorTemperature.yml b/drivers/SmartThings/matter-switch/profiles/switch-level-colorTemperature.yml index 42e3ef6257..9cba419d02 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-level-colorTemperature.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-level-colorTemperature.yml @@ -6,12 +6,16 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/switch-level.yml b/drivers/SmartThings/matter-switch/profiles/switch-level.yml index 8f3b9f5e5c..827fcdd898 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-level.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-level.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/init.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/init.lua deleted file mode 100644 index 83bc66aa1b..0000000000 --- a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/init.lua +++ /dev/null @@ -1,67 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local ElectricalEnergyMeasurementServerAttributes = require "ElectricalEnergyMeasurement.server.attributes" -local ElectricalEnergyMeasurementTypes = require "ElectricalEnergyMeasurement.types" -local ElectricalEnergyMeasurement = {} - -ElectricalEnergyMeasurement.ID = 0x0091 -ElectricalEnergyMeasurement.NAME = "ElectricalEnergyMeasurement" -ElectricalEnergyMeasurement.server = {} -ElectricalEnergyMeasurement.client = {} -ElectricalEnergyMeasurement.server.attributes = ElectricalEnergyMeasurementServerAttributes:set_parent_cluster(ElectricalEnergyMeasurement) -ElectricalEnergyMeasurement.types = ElectricalEnergyMeasurementTypes - -function ElectricalEnergyMeasurement:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "Accuracy", - [0x0001] = "CumulativeEnergyImported", - [0x0002] = "CumulativeEnergyExported", - [0x0003] = "PeriodicEnergyImported", - [0x0004] = "PeriodicEnergyExported", - [0x0005] = "CumulativeEnergyReset", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - -ElectricalEnergyMeasurement.attribute_direction_map = { - ["Accuracy"] = "server", - ["CumulativeEnergyImported"] = "server", - ["CumulativeEnergyExported"] = "server", - ["PeriodicEnergyImported"] = "server", - ["PeriodicEnergyExported"] = "server", - ["CumulativeEnergyReset"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -ElectricalEnergyMeasurement.FeatureMap = ElectricalEnergyMeasurement.types.Feature - -function ElectricalEnergyMeasurement.are_features_supported(feature, feature_map) - if (ElectricalEnergyMeasurement.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = ElectricalEnergyMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalEnergyMeasurement.NAME)) - end - return ElectricalEnergyMeasurement[direction].attributes[key] -end -ElectricalEnergyMeasurement.attributes = {} -setmetatable(ElectricalEnergyMeasurement.attributes, attribute_helper_mt) - -setmetatable(ElectricalEnergyMeasurement, {__index = cluster_base}) - -return ElectricalEnergyMeasurement - diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua deleted file mode 100644 index 3dc58635e1..0000000000 --- a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local CumulativeEnergyImported = { - ID = 0x0001, - NAME = "CumulativeEnergyImported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", -} - -function CumulativeEnergyImported:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function CumulativeEnergyImported:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function CumulativeEnergyImported:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function CumulativeEnergyImported:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function CumulativeEnergyImported:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function CumulativeEnergyImported:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(CumulativeEnergyImported, {__call = CumulativeEnergyImported.new_value, __index = CumulativeEnergyImported.base_type}) -return CumulativeEnergyImported - diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua deleted file mode 100644 index 753b91ea2d..0000000000 --- a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local PeriodicEnergyImported = { - ID = 0x0003, - NAME = "PeriodicEnergyImported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", -} - -function PeriodicEnergyImported:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function PeriodicEnergyImported:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function PeriodicEnergyImported:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function PeriodicEnergyImported:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function PeriodicEnergyImported:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function PeriodicEnergyImported:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(PeriodicEnergyImported, {__call = PeriodicEnergyImported.new_value, __index = PeriodicEnergyImported.base_type}) -return PeriodicEnergyImported - diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/init.lua deleted file mode 100644 index adfdf42bbf..0000000000 --- a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("ElectricalEnergyMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local ElectricalEnergyMeasurementServerAttributes = {} - -function ElectricalEnergyMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(ElectricalEnergyMeasurementServerAttributes, attr_mt) - -return ElectricalEnergyMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua deleted file mode 100644 index 950b260227..0000000000 --- a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua +++ /dev/null @@ -1,98 +0,0 @@ -local data_types = require "st.matter.data_types" -local StructureABC = require "st.matter.data_types.base_defs.StructureABC" -local EnergyMeasurementStruct = {} -local new_mt = StructureABC.new_mt({NAME = "EnergyMeasurementStruct", ID = data_types.name_to_id_map["Structure"]}) - -EnergyMeasurementStruct.field_defs = { - { - name = "energy", - field_id = 0, - is_nullable = false, - is_optional = false, - data_type = require "st.matter.data_types.Int64", - }, - { - name = "start_timestamp", - field_id = 1, - is_nullable = false, - is_optional = true, - data_type = require "st.matter.data_types.Uint32", - }, - { - name = "end_timestamp", - field_id = 2, - is_nullable = false, - is_optional = true, - data_type = require "st.matter.data_types.Uint32", - }, - { - name = "start_systime", - field_id = 3, - is_nullable = false, - is_optional = true, - data_type = require "st.matter.data_types.Uint64", - }, - { - name = "end_systime", - field_id = 4, - is_nullable = false, - is_optional = true, - data_type = require "st.matter.data_types.Uint64", - }, -} - -EnergyMeasurementStruct.init = function(cls, tbl) - local o = {} - o.elements = {} - o.num_elements = 0 - setmetatable(o, new_mt) - for idx, field_def in ipairs(cls.field_defs) do - if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then - error("Missing non optional or non_nullable field: " .. field_def.name) - else - o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) - o.elements[field_def.name].field_id = field_def.field_id - o.num_elements = o.num_elements + 1 - end - end - return o -end - -EnergyMeasurementStruct.serialize = function(self, buf, include_control, tag) - return data_types['Structure'].serialize(self.elements, buf, include_control, tag) -end - -new_mt.__call = EnergyMeasurementStruct.init -new_mt.__index.serialize = EnergyMeasurementStruct.serialize - -EnergyMeasurementStruct.augment_type = function(self, val) - local elems = {} - local num_elements = 0 - for _, v in pairs(val.elements) do - for _, field_def in ipairs(self.field_defs) do - if field_def.field_id == v.field_id and - field_def.is_nullable and - (v.value == nil and v.elements == nil) then - elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) - num_elements = num_elements + 1 - elseif field_def.field_id == v.field_id and not - (field_def.is_optional and v.value == nil) then - elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) - num_elements = num_elements + 1 - if field_def.element_type ~= nil then - for i, e in ipairs(elems[field_def.name].elements) do - elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) - end - end - end - end - end - val.elements = elems - val.num_elements = num_elements - setmetatable(val, new_mt) -end - -setmetatable(EnergyMeasurementStruct, new_mt) - -return EnergyMeasurementStruct - diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/Feature.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/Feature.lua deleted file mode 100644 index 717ba6a2f3..0000000000 --- a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/Feature.lua +++ /dev/null @@ -1,116 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.IMPORTED_ENERGY = 0x0001 -Feature.EXPORTED_ENERGY = 0x0002 -Feature.CUMULATIVE_ENERGY = 0x0004 -Feature.PERIODIC_ENERGY = 0x0008 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - IMPORTED_ENERGY = 0x0001, - EXPORTED_ENERGY = 0x0002, - CUMULATIVE_ENERGY = 0x0004, - PERIODIC_ENERGY = 0x0008, -} - -Feature.is_imported_energy_set = function(self) - return (self.value & self.IMPORTED_ENERGY) ~= 0 -end - -Feature.set_imported_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.IMPORTED_ENERGY - else - self.value = self.IMPORTED_ENERGY - end -end - -Feature.unset_imported_energy = function(self) - self.value = self.value & (~self.IMPORTED_ENERGY & self.BASE_MASK) -end -Feature.is_exported_energy_set = function(self) - return (self.value & self.EXPORTED_ENERGY) ~= 0 -end - -Feature.set_exported_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.EXPORTED_ENERGY - else - self.value = self.EXPORTED_ENERGY - end -end - -Feature.unset_exported_energy = function(self) - self.value = self.value & (~self.EXPORTED_ENERGY & self.BASE_MASK) -end -Feature.is_cumulative_energy_set = function(self) - return (self.value & self.CUMULATIVE_ENERGY) ~= 0 -end - -Feature.set_cumulative_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.CUMULATIVE_ENERGY - else - self.value = self.CUMULATIVE_ENERGY - end -end - -Feature.unset_cumulative_energy = function(self) - self.value = self.value & (~self.CUMULATIVE_ENERGY & self.BASE_MASK) -end -Feature.is_periodic_energy_set = function(self) - return (self.value & self.PERIODIC_ENERGY) ~= 0 -end - -Feature.set_periodic_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.PERIODIC_ENERGY - else - self.value = self.PERIODIC_ENERGY - end -end - -Feature.unset_periodic_energy = function(self) - self.value = self.value & (~self.PERIODIC_ENERGY & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.IMPORTED_ENERGY | - Feature.EXPORTED_ENERGY | - Feature.CUMULATIVE_ENERGY | - Feature.PERIODIC_ENERGY - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_imported_energy_set = Feature.is_imported_energy_set, - set_imported_energy = Feature.set_imported_energy, - unset_imported_energy = Feature.unset_imported_energy, - is_exported_energy_set = Feature.is_exported_energy_set, - set_exported_energy = Feature.set_exported_energy, - unset_exported_energy = Feature.unset_exported_energy, - is_cumulative_energy_set = Feature.is_cumulative_energy_set, - set_cumulative_energy = Feature.set_cumulative_energy, - unset_cumulative_energy = Feature.unset_cumulative_energy, - is_periodic_energy_set = Feature.is_periodic_energy_set, - set_periodic_energy = Feature.set_periodic_energy, - unset_periodic_energy = Feature.unset_periodic_energy, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/init.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/init.lua deleted file mode 100644 index bb0c39fe0e..0000000000 --- a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/init.lua +++ /dev/null @@ -1,15 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("ElectricalEnergyMeasurement.types." .. key) - end - return types_mt.__types_cache[key] -end - -local ElectricalEnergyMeasurementTypes = {} - -setmetatable(ElectricalEnergyMeasurementTypes, types_mt) - -return ElectricalEnergyMeasurementTypes - diff --git a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/init.lua b/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/init.lua deleted file mode 100644 index 54785d16c6..0000000000 --- a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/init.lua +++ /dev/null @@ -1,94 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local ElectricalPowerMeasurementServerAttributes = require "ElectricalPowerMeasurement.server.attributes" -local ElectricalPowerMeasurementTypes = require "ElectricalPowerMeasurement.types" - -local ElectricalPowerMeasurement = {} - -ElectricalPowerMeasurement.ID = 0x0090 -ElectricalPowerMeasurement.NAME = "ElectricalPowerMeasurement" -ElectricalPowerMeasurement.server = {} -ElectricalPowerMeasurement.client = {} -ElectricalPowerMeasurement.server.attributes = ElectricalPowerMeasurementServerAttributes:set_parent_cluster(ElectricalPowerMeasurement) -ElectricalPowerMeasurement.types = ElectricalPowerMeasurementTypes - -function ElectricalPowerMeasurement:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "PowerMode", - [0x0001] = "NumberOfMeasurementTypes", - [0x0002] = "Accuracy", - [0x0003] = "Ranges", - [0x0004] = "Voltage", - [0x0005] = "ActiveCurrent", - [0x0006] = "ReactiveCurrent", - [0x0007] = "ApparentCurrent", - [0x0008] = "ActivePower", - [0x0009] = "ReactivePower", - [0x000A] = "ApparentPower", - [0x000B] = "RMSVoltage", - [0x000C] = "RMSCurrent", - [0x000D] = "RMSPower", - [0x000E] = "Frequency", - [0x000F] = "HarmonicCurrents", - [0x0010] = "HarmonicPhases", - [0x0011] = "PowerFactor", - [0x0012] = "NeutralCurrent", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - -ElectricalPowerMeasurement.attribute_direction_map = { - ["PowerMode"] = "server", - ["NumberOfMeasurementTypes"] = "server", - ["Accuracy"] = "server", - ["Ranges"] = "server", - ["Voltage"] = "server", - ["ActiveCurrent"] = "server", - ["ReactiveCurrent"] = "server", - ["ApparentCurrent"] = "server", - ["ActivePower"] = "server", - ["ReactivePower"] = "server", - ["ApparentPower"] = "server", - ["RMSVoltage"] = "server", - ["RMSCurrent"] = "server", - ["RMSPower"] = "server", - ["Frequency"] = "server", - ["HarmonicCurrents"] = "server", - ["HarmonicPhases"] = "server", - ["PowerFactor"] = "server", - ["NeutralCurrent"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -ElectricalPowerMeasurement.FeatureMap = ElectricalPowerMeasurement.types.Feature - -function ElectricalPowerMeasurement.are_features_supported(feature, feature_map) - if (ElectricalPowerMeasurement.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = ElectricalPowerMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalPowerMeasurement.NAME)) - end - return ElectricalPowerMeasurement[direction].attributes[key] -end -ElectricalPowerMeasurement.attributes = {} -setmetatable(ElectricalPowerMeasurement.attributes, attribute_helper_mt) - -setmetatable(ElectricalPowerMeasurement, {__index = cluster_base}) - -return ElectricalPowerMeasurement - diff --git a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua b/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua deleted file mode 100644 index 6c34abd2f4..0000000000 --- a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local ActivePower = { - ID = 0x0008, - NAME = "ActivePower", - base_type = require "st.matter.data_types.Int64", -} - -function ActivePower:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function ActivePower:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function ActivePower:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function ActivePower:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function ActivePower:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function ActivePower:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(ActivePower, {__call = ActivePower.new_value, __index = ActivePower.base_type}) -return ActivePower - diff --git a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/init.lua deleted file mode 100644 index 0c30fa8dd4..0000000000 --- a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("ElectricalPowerMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local ElectricalPowerMeasurementServerAttributes = {} - -function ElectricalPowerMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(ElectricalPowerMeasurementServerAttributes, attr_mt) - -return ElectricalPowerMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/Feature.lua b/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/Feature.lua deleted file mode 100644 index cbda4f3478..0000000000 --- a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/Feature.lua +++ /dev/null @@ -1,138 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.DIRECT_CURRENT = 0x0001 -Feature.ALTERNATING_CURRENT = 0x0002 -Feature.POLYPHASE_POWER = 0x0004 -Feature.HARMONICS = 0x0008 -Feature.POWER_QUALITY = 0x0010 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - DIRECT_CURRENT = 0x0001, - ALTERNATING_CURRENT = 0x0002, - POLYPHASE_POWER = 0x0004, - HARMONICS = 0x0008, - POWER_QUALITY = 0x0010, -} - -Feature.is_direct_current_set = function(self) - return (self.value & self.DIRECT_CURRENT) ~= 0 -end - -Feature.set_direct_current = function(self) - if self.value ~= nil then - self.value = self.value | self.DIRECT_CURRENT - else - self.value = self.DIRECT_CURRENT - end -end - -Feature.unset_direct_current = function(self) - self.value = self.value & (~self.DIRECT_CURRENT & self.BASE_MASK) -end -Feature.is_alternating_current_set = function(self) - return (self.value & self.ALTERNATING_CURRENT) ~= 0 -end - -Feature.set_alternating_current = function(self) - if self.value ~= nil then - self.value = self.value | self.ALTERNATING_CURRENT - else - self.value = self.ALTERNATING_CURRENT - end -end - -Feature.unset_alternating_current = function(self) - self.value = self.value & (~self.ALTERNATING_CURRENT & self.BASE_MASK) -end -Feature.is_polyphase_power_set = function(self) - return (self.value & self.POLYPHASE_POWER) ~= 0 -end - -Feature.set_polyphase_power = function(self) - if self.value ~= nil then - self.value = self.value | self.POLYPHASE_POWER - else - self.value = self.POLYPHASE_POWER - end -end - -Feature.unset_polyphase_power = function(self) - self.value = self.value & (~self.POLYPHASE_POWER & self.BASE_MASK) -end -Feature.is_harmonics_set = function(self) - return (self.value & self.HARMONICS) ~= 0 -end - -Feature.set_harmonics = function(self) - if self.value ~= nil then - self.value = self.value | self.HARMONICS - else - self.value = self.HARMONICS - end -end - -Feature.unset_harmonics = function(self) - self.value = self.value & (~self.HARMONICS & self.BASE_MASK) -end -Feature.is_power_quality_set = function(self) - return (self.value & self.POWER_QUALITY) ~= 0 -end - -Feature.set_power_quality = function(self) - if self.value ~= nil then - self.value = self.value | self.POWER_QUALITY - else - self.value = self.POWER_QUALITY - end -end - -Feature.unset_power_quality = function(self) - self.value = self.value & (~self.POWER_QUALITY & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.DIRECT_CURRENT | - Feature.ALTERNATING_CURRENT | - Feature.POLYPHASE_POWER | - Feature.HARMONICS | - Feature.POWER_QUALITY - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_direct_current_set = Feature.is_direct_current_set, - set_direct_current = Feature.set_direct_current, - unset_direct_current = Feature.unset_direct_current, - is_alternating_current_set = Feature.is_alternating_current_set, - set_alternating_current = Feature.set_alternating_current, - unset_alternating_current = Feature.unset_alternating_current, - is_polyphase_power_set = Feature.is_polyphase_power_set, - set_polyphase_power = Feature.set_polyphase_power, - unset_polyphase_power = Feature.unset_polyphase_power, - is_harmonics_set = Feature.is_harmonics_set, - set_harmonics = Feature.set_harmonics, - unset_harmonics = Feature.unset_harmonics, - is_power_quality_set = Feature.is_power_quality_set, - set_power_quality = Feature.set_power_quality, - unset_power_quality = Feature.unset_power_quality, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/init.lua b/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/init.lua deleted file mode 100644 index 16d13a0688..0000000000 --- a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/init.lua +++ /dev/null @@ -1,15 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("ElectricalPowerMeasurement.types." .. key) - end - return types_mt.__types_cache[key] -end - -local ElectricalPowerMeasurementTypes = {} - -setmetatable(ElectricalPowerMeasurementTypes, types_mt) - -return ElectricalPowerMeasurementTypes - diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/init.lua b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/init.lua deleted file mode 100644 index d8ab93412f..0000000000 --- a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/init.lua +++ /dev/null @@ -1,123 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local ValveConfigurationAndControlServerAttributes = require "ValveConfigurationAndControl.server.attributes" -local ValveConfigurationAndControlServerCommands = require "ValveConfigurationAndControl.server.commands" -local ValveConfigurationAndControlTypes = require "ValveConfigurationAndControl.types" -local ValveConfigurationAndControl = {} - -ValveConfigurationAndControl.ID = 0x0081 -ValveConfigurationAndControl.NAME = "ValveConfigurationAndControl" -ValveConfigurationAndControl.server = {} -ValveConfigurationAndControl.client = {} -ValveConfigurationAndControl.server.attributes = ValveConfigurationAndControlServerAttributes:set_parent_cluster(ValveConfigurationAndControl) -ValveConfigurationAndControl.server.commands = ValveConfigurationAndControlServerCommands:set_parent_cluster(ValveConfigurationAndControl) -ValveConfigurationAndControl.types = ValveConfigurationAndControlTypes - -function ValveConfigurationAndControl:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "OpenDuration", - [0x0001] = "DefaultOpenDuration", - [0x0002] = "AutoCloseTime", - [0x0003] = "RemainingDuration", - [0x0004] = "CurrentState", - [0x0005] = "TargetState", - [0x0006] = "CurrentLevel", - [0x0007] = "TargetLevel", - [0x0008] = "DefaultOpenLevel", - [0x0009] = "ValveFault", - [0x000A] = "LevelStep", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - -function ValveConfigurationAndControl:get_server_command_by_id(command_id) - local server_id_map = { - [0x0000] = "Open", - [0x0001] = "Close", - } - if server_id_map[command_id] ~= nil then - return self.server.commands[server_id_map[command_id]] - end - return nil -end - -function ValveConfigurationAndControl:get_event_by_id(event_id) - local event_id_map = { - [0x0000] = "ValveStateChanged", - [0x0001] = "ValveFault", - } - if event_id_map[event_id] ~= nil then - return self.server.events[event_id_map[event_id]] - end - return nil -end - -ValveConfigurationAndControl.attribute_direction_map = { - ["OpenDuration"] = "server", - ["DefaultOpenDuration"] = "server", - ["AutoCloseTime"] = "server", - ["RemainingDuration"] = "server", - ["CurrentState"] = "server", - ["TargetState"] = "server", - ["CurrentLevel"] = "server", - ["TargetLevel"] = "server", - ["DefaultOpenLevel"] = "server", - ["ValveFault"] = "server", - ["LevelStep"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -ValveConfigurationAndControl.command_direction_map = { - ["Open"] = "server", - ["Close"] = "server", -} - -ValveConfigurationAndControl.FeatureMap = ValveConfigurationAndControl.types.Feature - -function ValveConfigurationAndControl.are_features_supported(feature, feature_map) - if (ValveConfigurationAndControl.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = ValveConfigurationAndControl.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, ValveConfigurationAndControl.NAME)) - end - return ValveConfigurationAndControl[direction].attributes[key] -end -ValveConfigurationAndControl.attributes = {} -setmetatable(ValveConfigurationAndControl.attributes, attribute_helper_mt) - -local command_helper_mt = {} -command_helper_mt.__index = function(self, key) - local direction = ValveConfigurationAndControl.command_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown command %s on cluster %s", key, ValveConfigurationAndControl.NAME)) - end - return ValveConfigurationAndControl[direction].commands[key] -end -ValveConfigurationAndControl.commands = {} -setmetatable(ValveConfigurationAndControl.commands, command_helper_mt) - -local event_helper_mt = {} -event_helper_mt.__index = function(self, key) - return ValveConfigurationAndControl.server.events[key] -end -ValveConfigurationAndControl.events = {} -setmetatable(ValveConfigurationAndControl.events, event_helper_mt) - -setmetatable(ValveConfigurationAndControl, {__index = cluster_base}) - -return ValveConfigurationAndControl diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/init.lua deleted file mode 100644 index 237818d98f..0000000000 --- a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/init.lua +++ /dev/null @@ -1,23 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("ValveConfigurationAndControl.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local ValveConfigurationAndControlServerAttributes = {} - -function ValveConfigurationAndControlServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(ValveConfigurationAndControlServerAttributes, attr_mt) - -return ValveConfigurationAndControlServerAttributes diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/init.lua b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/init.lua deleted file mode 100644 index 330e35bba3..0000000000 --- a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/init.lua +++ /dev/null @@ -1,22 +0,0 @@ -local command_mt = {} -command_mt.__command_cache = {} -command_mt.__index = function(self, key) - if command_mt.__command_cache[key] == nil then - local req_loc = string.format("ValveConfigurationAndControl.server.commands.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) - end - return command_mt.__command_cache[key] -end - -local ValveConfigurationAndControlServerCommands = {} - -function ValveConfigurationAndControlServerCommands:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(ValveConfigurationAndControlServerCommands, command_mt) - -return ValveConfigurationAndControlServerCommands diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/Feature.lua b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/Feature.lua deleted file mode 100644 index 7433a99c9e..0000000000 --- a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/Feature.lua +++ /dev/null @@ -1,74 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.TIME_SYNC = 0x0001 -Feature.LEVEL = 0x0002 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - TIME_SYNC = 0x0001, - LEVEL = 0x0002, -} - -Feature.is_time_sync_set = function(self) - return (self.value & self.TIME_SYNC) ~= 0 -end - -Feature.set_time_sync = function(self) - if self.value ~= nil then - self.value = self.value | self.TIME_SYNC - else - self.value = self.TIME_SYNC - end -end - -Feature.unset_time_sync = function(self) - self.value = self.value & (~self.TIME_SYNC & self.BASE_MASK) -end - -Feature.is_level_set = function(self) - return (self.value & self.LEVEL) ~= 0 -end - -Feature.set_level = function(self) - if self.value ~= nil then - self.value = self.value | self.LEVEL - else - self.value = self.LEVEL - end -end - -Feature.unset_level = function(self) - self.value = self.value & (~self.LEVEL & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.TIME_SYNC | - Feature.LEVEL - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_time_sync_set = Feature.is_time_sync_set, - set_time_sync = Feature.set_time_sync, - unset_time_sync = Feature.unset_time_sync, - is_level_set = Feature.is_level_set, - set_level = Feature.set_level, - unset_level = Feature.unset_level, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/init.lua b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/init.lua deleted file mode 100644 index 835167f485..0000000000 --- a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/init.lua +++ /dev/null @@ -1,14 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("ValveConfigurationAndControl.types." .. key) - end - return types_mt.__types_cache[key] -end - -local ValveConfigurationAndControlTypes = {} - -setmetatable(ValveConfigurationAndControlTypes, types_mt) - -return ValveConfigurationAndControlTypes diff --git a/drivers/SmartThings/matter-switch/src/aqara-cube/init.lua b/drivers/SmartThings/matter-switch/src/aqara-cube/init.lua deleted file mode 100644 index 455bac34a4..0000000000 --- a/drivers/SmartThings/matter-switch/src/aqara-cube/init.lua +++ /dev/null @@ -1,258 +0,0 @@ --- 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 capabilities = require "st.capabilities" -local clusters = require "st.matter.clusters" -local device_lib = require "st.device" -local log = require "log" - -local cubeAction = capabilities["stse.cubeAction"] -local cubeFace = capabilities["stse.cubeFace"] - -local COMPONENT_TO_ENDPOINT_MAP_BUTTON = "__component_to_endpoint_map_button" -local DEFERRED_CONFIGURE = "__DEFERRED_CONFIGURE" - --- used in unit testing, since device.profile.id and args.old_st_store.profile.id are always the same --- and this is to avoid the crash of the test case that occurs when try_update_metadata is performed in the device_init stage. -local TEST_CONFIGURE = "__test_configure" -local INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS (MomentarySwitch), but not MSR (MomentarySwitchRelease) - --- after 3 seconds of cubeAction, to automatically change the action status of Plugin UI or Device Card to noAction -local CUBEACTION_TIMER = "__cubeAction_timer" -local CUBEACTION_TIME = 3 - -local function is_aqara_cube(opts, driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER then - local name = string.format("%s", device.manufacturer_info.product_name) - if string.find(name, "Aqara Cube T1 Pro") then - return true - end - end - return false -end - -local callback_timer = function(device) - return function() - device:emit_event(cubeAction.cubeAction("noAction")) - end -end - -local function reset_thread(device) - local timer = device:get_field(CUBEACTION_TIMER) - if timer then - device.thread:cancel_timer(timer) - device:set_field(CUBEACTION_TIMER, nil) - end - device:set_field(CUBEACTION_TIMER, device.thread:call_with_delay(CUBEACTION_TIME, callback_timer(device))) -end - -local function get_field_for_endpoint(device, field, endpoint) - return device:get_field(string.format("%s_%d", field, endpoint)) -end - -local function set_field_for_endpoint(device, field, endpoint, value, persist) - device:set_field(string.format("%s_%d", field, endpoint), value, {persist = persist}) -end - --- The endpoints of each face may increase sequentially, but may increase as in [250, 251, 2, 3, 4, 5] --- and the current device:get_endpoints function is valid only for the former so, adds this function. -local function get_reordered_endpoints(driver, device) - if device.network_type ~= device_lib.NETWORK_TYPE_CHILD then - local MS = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - -- find the default/main endpoint, the device with the lowest EP that supports MS - table.sort(MS) - if MS[6] < (MS[1] + 150) then - -- When the endpoints of each face increase sequentially - -- The lowest EP is the main endpoint - -- as a workaround, it is assumed that the first endpoint number and the last endpoint number are not larger than 150. - return MS - else - -- When the endpoints of each face do not increase sequentially... [250, 251, 2, 3, 4, 5] 250 is the main endpoint. - -- For the situation where a node following these mechanisms has exhausted all available 65535 endpoint addresses for exposed entities, - -- it MAY wrap around to the lowest unused endpoint address (refter to Matter Core Spec 9.2.4. Dynamic Endpoint Allocation) - local ept1 = {} -- First consecutive end points - local ept2 = {} -- Second consecutive end points - local idx1 = 1 - local idx2 = 1 - local flag = 0 - local previous = 0 - for _, ep in ipairs(MS) do - if idx1 == 1 then - ept1[idx1] = ep - else - if flag == 0 - and ep <= (previous + 15) then - -- the endpoint number does not always increase by 1 - -- as a workaround, assume that the next endpoint number is not greater than 15 - ept1[idx1] = ep - else - ept2[idx2] = ep - idx2 = idx2 + 1 - if flag ~= 1 then - flag = 1 - end - end - end - idx1 = idx1 + 1 - previous = ep - end - - local start = #ept2 + 1 - idx1 = 1 - idx2 = start - for i=start, 6 do - ept2[idx2] = ept1[idx1] - idx1 = idx1 + 1 - idx2 = idx2 + 1 - end - return ept2 - end - end -end - -local function endpoint_to_component(device, endpoint) - return "main" -end - --- This is called either on add for parent/child devices, or after the device profile changes for components -local function configure_buttons(device) - if device.network_type ~= device_lib.NETWORK_TYPE_CHILD then - local MS = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - device.log.debug(#MS.." momentary switch endpoints") - for _, ep in ipairs(MS) do - -- device only supports momentary switch, no release events - device.log.debug("configuring for press event only") - set_field_for_endpoint(device, INITIAL_PRESS_ONLY, ep, true, true) - end - end -end - -local function set_configure(driver, device) - local MS = get_reordered_endpoints(driver, device) - local main_endpoint - if #MS > 0 and MS[1] == 0 then -- we shouldn't hit this, but just in case - main_endpoint = MS[2] - elseif #MS > 0 then - main_endpoint = MS[1] -- matches to the non-child device - else - main_endpoint = device.MATTER_DEFAULT_ENDPOINT - end - device.log.debug_with({hub_logs = true}, "The main button endpoint for the Aqara T1 Pro is " .. main_endpoint) - - -- At the moment, we're taking it for granted that all momentary switches only have 2 positions - local current_component_number = 1 - local component_map = {} - for _, ep in ipairs(MS) do -- for each momentary switch endpoint (including main) - log.debug_with({hub_logs = true}, "Configuring endpoint: " .. ep) - -- build the mapping of endpoints to components - component_map[string.format("%d", current_component_number)] = ep - current_component_number = current_component_number + 1 - end - - device:set_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON, component_map, {persist = true}) - device:try_update_metadata({profile = "cube-t1-pro"}) - configure_buttons(device) -end - -local function device_init(driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER then - device:subscribe() - device:set_endpoint_to_component_fn(endpoint_to_component) - - -- when unit testing, call set_configure elsewhere - if not device:get_field(TEST_CONFIGURE) then - set_configure(driver, device) - end - end -end - -local function device_added(driver, device) - if device.network_type ~= device_lib.NETWORK_TYPE_CHILD then - device:set_field(DEFERRED_CONFIGURE, true) - end -end - -local function info_changed(driver, device, event, args) - -- for unit testing - if device:get_field(TEST_CONFIGURE) then - set_configure(driver, device) - end - - if (device.profile.id ~= args.old_st_store.profile.id or device:get_field(TEST_CONFIGURE)) - and device:get_field(DEFERRED_CONFIGURE) - and device.network_type ~= device_lib.NETWORK_TYPE_CHILD then - - reset_thread(device) - device:emit_event(cubeAction.cubeAction("flipToSide1")) - device:emit_event(cubeFace.cubeFace("face1Up")) - - device:set_field(DEFERRED_CONFIGURE, nil) - end -end - --- override do_configure to prevent it running in the main driver -local function do_configure(driver, device) end - --- override driver_switched to prevent it running in the main driver -local function driver_switched(driver, device) end - -local function initial_press_event_handler(driver, device, ib, response) - if get_field_for_endpoint(device, INITIAL_PRESS_ONLY, ib.endpoint_id) then - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON) or {} - local face = 1 - for component, ep in pairs(map) do - if map[component] == ib.endpoint_id then - face = component - break - end - end - - reset_thread(device) - device:emit_event(cubeAction.cubeAction(string.format("flipToSide%d", face))) - device:emit_event(cubeFace.cubeFace(string.format("face%dUp", face))) - end -end - -local function battery_percent_remaining_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) - end -end - -local aqara_cube_handler = { - NAME = "Aqara Cube Handler", - lifecycle_handlers = { - init = device_init, - added = device_added, - infoChanged = info_changed, - doConfigure = do_configure, - driverSwitched = driver_switched - }, - matter_handlers = { - attr = { - [clusters.PowerSource.ID] = { - [clusters.PowerSource.attributes.BatPercentRemaining.ID] = battery_percent_remaining_attr_handler - } - }, - event = { - [clusters.Switch.ID] = { - [clusters.Switch.events.InitialPress.ID] = initial_press_event_handler - } - }, - }, - can_handle = is_aqara_cube -} - -return aqara_cube_handler - diff --git a/drivers/SmartThings/matter-switch/src/embedded-cluster-utils.lua b/drivers/SmartThings/matter-switch/src/embedded-cluster-utils.lua deleted file mode 100644 index 66db6097c7..0000000000 --- a/drivers/SmartThings/matter-switch/src/embedded-cluster-utils.lua +++ /dev/null @@ -1,53 +0,0 @@ -local clusters = require "st.matter.clusters" -local utils = require "st.utils" - --- Include driver-side definitions when lua libs api version is < 11 -local version = require "version" -if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" - clusters.ValveConfigurationAndControl = require "ValveConfigurationAndControl" -end - -local embedded_cluster_utils = {} - -local embedded_clusters = { - [clusters.ElectricalEnergyMeasurement.ID] = clusters.ElectricalEnergyMeasurement, - [clusters.ElectricalPowerMeasurement.ID] = clusters.ElectricalPowerMeasurement, - [clusters.ValveConfigurationAndControl.ID] = clusters.ValveConfigurationAndControl -} - -function embedded_cluster_utils.get_endpoints(device, cluster_id, opts) - -- If using older lua libs and need to check for an embedded cluster feature, - -- we must use the embedded cluster definitions here - if version.api < 11 and embedded_clusters[cluster_id] ~= nil then - local embedded_cluster = embedded_clusters[cluster_id] - local opts = opts or {} - if utils.table_size(opts) > 1 then - device.log.warn_with({hub_logs = true}, "Invalid options for get_endpoints") - return - end - local clus_has_features = function(clus, feature_bitmap) - if not feature_bitmap or not clus then return false end - return embedded_cluster.are_features_supported(feature_bitmap, clus.feature_map) - end - local eps = {} - for _, ep in ipairs(device.endpoints) do - for _, clus in ipairs(ep.clusters) do - if ((clus.cluster_id == cluster_id) - and (opts.feature_bitmap == nil or clus_has_features(clus, opts.feature_bitmap)) - and ((opts.cluster_type == nil and clus.cluster_type == "SERVER" or clus.cluster_type == "BOTH") - or (opts.cluster_type == clus.cluster_type)) - or (cluster_id == nil)) then - table.insert(eps, ep.endpoint_id) - if cluster_id == nil then break end - end - end - end - return eps - else - return device:get_endpoints(cluster_id, opts) - end - end - - return embedded_cluster_utils \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/init.lua new file mode 100644 index 0000000000..fb7a38e63b --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/init.lua @@ -0,0 +1,53 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local DescriptorServerAttributes = require "embedded_clusters.Descriptor.server.attributes" + +local Descriptor = {} + +Descriptor.ID = 0x001D +Descriptor.NAME = "Descriptor" +Descriptor.server = {} +Descriptor.client = {} +Descriptor.server.attributes = DescriptorServerAttributes:set_parent_cluster(Descriptor) + +function Descriptor:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0003] = "PartsList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function Descriptor:get_server_command_by_id(command_id) + local server_id_map = { + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +Descriptor.attribute_direction_map = { + ["PartsList"] = "server", +} + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = Descriptor.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, Descriptor.NAME)) + end + return Descriptor[direction].attributes[key] +end +Descriptor.attributes = {} +setmetatable(Descriptor.attributes, attribute_helper_mt) + +setmetatable(Descriptor, {__index = cluster_base}) + +return Descriptor + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/PartsList.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/PartsList.lua new file mode 100644 index 0000000000..7c5b24d28d --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/PartsList.lua @@ -0,0 +1,78 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local PartsList = { + ID = 0x0003, + NAME = "PartsList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint16", +} + +function PartsList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, PartsList.element_type) + end +end + +function PartsList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function PartsList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function PartsList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function PartsList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function PartsList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function PartsList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(PartsList, {__call = PartsList.new_value, __index = PartsList.base_type}) +return PartsList + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/init.lua new file mode 100644 index 0000000000..8a3d9ea3c2 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.Descriptor.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local DescriptorServerAttributes = {} + +function DescriptorServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(DescriptorServerAttributes, attr_mt) + +return DescriptorServerAttributes + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua new file mode 100644 index 0000000000..0f564673a9 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua @@ -0,0 +1,56 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local ElectricalEnergyMeasurementServerAttributes = require "embedded_clusters.ElectricalEnergyMeasurement.server.attributes" +local ElectricalEnergyMeasurementTypes = require "embedded_clusters.ElectricalEnergyMeasurement.types" +local ElectricalEnergyMeasurement = {} + +ElectricalEnergyMeasurement.ID = 0x0091 +ElectricalEnergyMeasurement.NAME = "ElectricalEnergyMeasurement" +ElectricalEnergyMeasurement.server = {} +ElectricalEnergyMeasurement.client = {} +ElectricalEnergyMeasurement.server.attributes = ElectricalEnergyMeasurementServerAttributes:set_parent_cluster(ElectricalEnergyMeasurement) +ElectricalEnergyMeasurement.types = ElectricalEnergyMeasurementTypes + +function ElectricalEnergyMeasurement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0001] = "CumulativeEnergyImported", + [0x0003] = "PeriodicEnergyImported", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +ElectricalEnergyMeasurement.attribute_direction_map = { + ["CumulativeEnergyImported"] = "server", + ["PeriodicEnergyImported"] = "server", +} + +ElectricalEnergyMeasurement.FeatureMap = ElectricalEnergyMeasurement.types.Feature + +function ElectricalEnergyMeasurement.are_features_supported(feature, feature_map) + if (ElectricalEnergyMeasurement.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ElectricalEnergyMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalEnergyMeasurement.NAME)) + end + return ElectricalEnergyMeasurement[direction].attributes[key] +end +ElectricalEnergyMeasurement.attributes = {} +setmetatable(ElectricalEnergyMeasurement.attributes, attribute_helper_mt) + +setmetatable(ElectricalEnergyMeasurement, {__index = cluster_base}) + +return ElectricalEnergyMeasurement + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua new file mode 100644 index 0000000000..2d41790440 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua @@ -0,0 +1,71 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CumulativeEnergyImported = { + ID = 0x0001, + NAME = "CumulativeEnergyImported", + base_type = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", +} + +function CumulativeEnergyImported:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function CumulativeEnergyImported:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CumulativeEnergyImported:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CumulativeEnergyImported:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CumulativeEnergyImported:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function CumulativeEnergyImported:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(CumulativeEnergyImported, {__call = CumulativeEnergyImported.new_value, __index = CumulativeEnergyImported.base_type}) +return CumulativeEnergyImported + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua new file mode 100644 index 0000000000..5daccf48ab --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua @@ -0,0 +1,71 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local PeriodicEnergyImported = { + ID = 0x0003, + NAME = "PeriodicEnergyImported", + base_type = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", +} + +function PeriodicEnergyImported:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function PeriodicEnergyImported:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function PeriodicEnergyImported:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function PeriodicEnergyImported:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function PeriodicEnergyImported:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function PeriodicEnergyImported:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(PeriodicEnergyImported, {__call = PeriodicEnergyImported.new_value, __index = PeriodicEnergyImported.base_type}) +return PeriodicEnergyImported + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..57bc0d1f72 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.ElectricalEnergyMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local ElectricalEnergyMeasurementServerAttributes = {} + +function ElectricalEnergyMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ElectricalEnergyMeasurementServerAttributes, attr_mt) + +return ElectricalEnergyMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua new file mode 100644 index 0000000000..a4c58a3646 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua @@ -0,0 +1,101 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +local EnergyMeasurementStruct = {} +local new_mt = StructureABC.new_mt({NAME = "EnergyMeasurementStruct", ID = data_types.name_to_id_map["Structure"]}) + +EnergyMeasurementStruct.field_defs = { + { + name = "energy", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Int64", + }, + { + name = "start_timestamp", + field_id = 1, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint32", + }, + { + name = "end_timestamp", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint32", + }, + { + name = "start_systime", + field_id = 3, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint64", + }, + { + name = "end_systime", + field_id = 4, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint64", + }, +} + +EnergyMeasurementStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +EnergyMeasurementStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = EnergyMeasurementStruct.init +new_mt.__index.serialize = EnergyMeasurementStruct.serialize + +EnergyMeasurementStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(EnergyMeasurementStruct, new_mt) + +return EnergyMeasurementStruct + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua new file mode 100644 index 0000000000..e3db76f49d --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua @@ -0,0 +1,31 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.IMPORTED_ENERGY = 0x0001 +Feature.EXPORTED_ENERGY = 0x0002 +Feature.CUMULATIVE_ENERGY = 0x0004 +Feature.PERIODIC_ENERGY = 0x0008 + +function Feature.bits_are_valid(feature) + local max = + Feature.IMPORTED_ENERGY | + Feature.EXPORTED_ENERGY | + Feature.CUMULATIVE_ENERGY | + Feature.PERIODIC_ENERGY + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua new file mode 100644 index 0000000000..ec29b53e05 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua @@ -0,0 +1,18 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.ElectricalEnergyMeasurement.types." .. key) + end + return types_mt.__types_cache[key] +end + +local ElectricalEnergyMeasurementTypes = {} + +setmetatable(ElectricalEnergyMeasurementTypes, types_mt) + +return ElectricalEnergyMeasurementTypes + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua new file mode 100644 index 0000000000..c9061e3cc4 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local ElectricalPowerMeasurementServerAttributes = require "embedded_clusters.ElectricalPowerMeasurement.server.attributes" + +local ElectricalPowerMeasurement = {} + +ElectricalPowerMeasurement.ID = 0x0090 +ElectricalPowerMeasurement.NAME = "ElectricalPowerMeasurement" +ElectricalPowerMeasurement.server = {} +ElectricalPowerMeasurement.client = {} +ElectricalPowerMeasurement.server.attributes = ElectricalPowerMeasurementServerAttributes:set_parent_cluster(ElectricalPowerMeasurement) + +function ElectricalPowerMeasurement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0008] = "ActivePower", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +ElectricalPowerMeasurement.attribute_direction_map = { + ["ActivePower"] = "server", +} + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ElectricalPowerMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalPowerMeasurement.NAME)) + end + return ElectricalPowerMeasurement[direction].attributes[key] +end +ElectricalPowerMeasurement.attributes = {} +setmetatable(ElectricalPowerMeasurement.attributes, attribute_helper_mt) + +setmetatable(ElectricalPowerMeasurement, {__index = cluster_base}) + +return ElectricalPowerMeasurement + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua new file mode 100644 index 0000000000..f1696509f5 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua @@ -0,0 +1,70 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ActivePower = { + ID = 0x0008, + NAME = "ActivePower", + base_type = require "st.matter.data_types.Int64", +} + +function ActivePower:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function ActivePower:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ActivePower:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ActivePower:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ActivePower:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function ActivePower:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(ActivePower, {__call = ActivePower.new_value, __index = ActivePower.base_type}) +return ActivePower diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..6de69b94ff --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.ElectricalPowerMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local ElectricalPowerMeasurementServerAttributes = {} + +function ElectricalPowerMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ElectricalPowerMeasurementServerAttributes, attr_mt) + +return ElectricalPowerMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/init.lua new file mode 100644 index 0000000000..0205e69b1f --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/init.lua @@ -0,0 +1,55 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local PowerTopologyServerAttributes = require "embedded_clusters.PowerTopology.server.attributes" +local PowerTopologyTypes = require "embedded_clusters.PowerTopology.types" + +local PowerTopology = {} + +PowerTopology.ID = 0x009C +PowerTopology.NAME = "PowerTopology" +PowerTopology.server = {} +PowerTopology.client = {} +PowerTopology.server.attributes = PowerTopologyServerAttributes:set_parent_cluster(PowerTopology) +PowerTopology.types = PowerTopologyTypes + +function PowerTopology:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "AvailableEndpoints", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +PowerTopology.attribute_direction_map = { + ["AvailableEndpoints"] = "server", +} + +PowerTopology.FeatureMap = PowerTopology.types.Feature + +function PowerTopology.are_features_supported(feature, feature_map) + if (PowerTopology.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = PowerTopology.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, PowerTopology.NAME)) + end + return PowerTopology[direction].attributes[key] +end +PowerTopology.attributes = {} +setmetatable(PowerTopology.attributes, attribute_helper_mt) + +setmetatable(PowerTopology, {__index = cluster_base}) + +return PowerTopology + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/AvailableEndpoints.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/AvailableEndpoints.lua new file mode 100644 index 0000000000..1536301d11 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/AvailableEndpoints.lua @@ -0,0 +1,72 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AvailableEndpoints = { + ID = 0x0000, + NAME = "AvailableEndpoints", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint16", +} + +function AvailableEndpoints:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function AvailableEndpoints:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AvailableEndpoints:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AvailableEndpoints:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AvailableEndpoints:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AvailableEndpoints:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AvailableEndpoints, {__call = AvailableEndpoints.new_value, __index = AvailableEndpoints.base_type}) +return AvailableEndpoints + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/init.lua new file mode 100644 index 0000000000..ff88697a0e --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/init.lua @@ -0,0 +1,23 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__index = function(self, key) + local req_loc = string.format("embedded_clusters.PowerTopology.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + return raw_def +end + +local PowerTopologyServerAttributes = {} + +function PowerTopologyServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(PowerTopologyServerAttributes, attr_mt) + +return PowerTopologyServerAttributes + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/Feature.lua new file mode 100644 index 0000000000..c2fe8087de --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/Feature.lua @@ -0,0 +1,32 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.NODE_TOPOLOGY = 0x0001 +Feature.TREE_TOPOLOGY = 0x0002 +Feature.SET_TOPOLOGY = 0x0004 +Feature.DYNAMIC_POWER_FLOW = 0x0008 + +function Feature.bits_are_valid(feature) + local max = + Feature.NODE_TOPOLOGY | + Feature.TREE_TOPOLOGY | + Feature.SET_TOPOLOGY | + Feature.DYNAMIC_POWER_FLOW + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/init.lua new file mode 100644 index 0000000000..7609939803 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/init.lua @@ -0,0 +1,14 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local types_mt = {} +types_mt.__index = function(self, key) + return require("embedded_clusters.PowerTopology.types." .. key) +end + +local PowerTopologyTypes = {} + +setmetatable(PowerTopologyTypes, types_mt) + +return PowerTopologyTypes + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua new file mode 100644 index 0000000000..be2141f09c --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua @@ -0,0 +1,84 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local ValveConfigurationAndControlServerAttributes = require "embedded_clusters.ValveConfigurationAndControl.server.attributes" +local ValveConfigurationAndControlServerCommands = require "embedded_clusters.ValveConfigurationAndControl.server.commands" +local ValveConfigurationAndControlTypes = require "embedded_clusters.ValveConfigurationAndControl.types" +local ValveConfigurationAndControl = {} + +ValveConfigurationAndControl.ID = 0x0081 +ValveConfigurationAndControl.NAME = "ValveConfigurationAndControl" +ValveConfigurationAndControl.server = {} +ValveConfigurationAndControl.client = {} +ValveConfigurationAndControl.server.attributes = ValveConfigurationAndControlServerAttributes:set_parent_cluster(ValveConfigurationAndControl) +ValveConfigurationAndControl.server.commands = ValveConfigurationAndControlServerCommands:set_parent_cluster(ValveConfigurationAndControl) +ValveConfigurationAndControl.types = ValveConfigurationAndControlTypes + +function ValveConfigurationAndControl:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0004] = "CurrentState", + [0x0006] = "CurrentLevel", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function ValveConfigurationAndControl:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "Open", + [0x0001] = "Close", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +ValveConfigurationAndControl.attribute_direction_map = { + ["CurrentState"] = "server", + ["CurrentLevel"] = "server", +} + +ValveConfigurationAndControl.command_direction_map = { + ["Open"] = "server", + ["Close"] = "server", +} + +ValveConfigurationAndControl.FeatureMap = ValveConfigurationAndControl.types.Feature + +function ValveConfigurationAndControl.are_features_supported(feature, feature_map) + if (ValveConfigurationAndControl.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ValveConfigurationAndControl.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ValveConfigurationAndControl.NAME)) + end + return ValveConfigurationAndControl[direction].attributes[key] +end +ValveConfigurationAndControl.attributes = {} +setmetatable(ValveConfigurationAndControl.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = ValveConfigurationAndControl.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, ValveConfigurationAndControl.NAME)) + end + return ValveConfigurationAndControl[direction].commands[key] +end +ValveConfigurationAndControl.commands = {} +setmetatable(ValveConfigurationAndControl.commands, command_helper_mt) + +setmetatable(ValveConfigurationAndControl, {__index = cluster_base}) + +return ValveConfigurationAndControl diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua similarity index 93% rename from drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua index 807beba485..c77f0f54ac 100644 --- a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentState.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentState.lua similarity index 88% rename from drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentState.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentState.lua index 76f156d2a7..033d69e0a8 100644 --- a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentState.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentState.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local CurrentState = { ID = 0x0004, NAME = "CurrentState", - base_type = require "ValveConfigurationAndControl.types.ValveStateEnum", + base_type = require "embedded_clusters.ValveConfigurationAndControl.types.ValveStateEnum", } function CurrentState:new_value(...) diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/init.lua new file mode 100644 index 0000000000..ddefc6b205 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/init.lua @@ -0,0 +1,26 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.ValveConfigurationAndControl.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local ValveConfigurationAndControlServerAttributes = {} + +function ValveConfigurationAndControlServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ValveConfigurationAndControlServerAttributes, attr_mt) + +return ValveConfigurationAndControlServerAttributes diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Close.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Close.lua similarity index 96% rename from drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Close.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Close.lua index baee7688ec..263bf0d849 100644 --- a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Close.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Close.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" local Close = {} diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Open.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Open.lua similarity index 97% rename from drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Open.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Open.lua index 3f84040fc0..fd2f62801a 100644 --- a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Open.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Open.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" local Open = {} diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/init.lua new file mode 100644 index 0000000000..86eb2b9546 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/init.lua @@ -0,0 +1,25 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("embedded_clusters.ValveConfigurationAndControl.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local ValveConfigurationAndControlServerCommands = {} + +function ValveConfigurationAndControlServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ValveConfigurationAndControlServerCommands, command_mt) + +return ValveConfigurationAndControlServerCommands diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua new file mode 100644 index 0000000000..bcbc7f0f38 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua @@ -0,0 +1,26 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.TIME_SYNC = 0x0001 +Feature.LEVEL = 0x0002 + +function Feature.bits_are_valid(feature) + local max = + Feature.TIME_SYNC | + Feature.LEVEL + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +setmetatable(Feature, new_mt) + +return Feature diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/ValveStateEnum.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/ValveStateEnum.lua similarity index 91% rename from drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/ValveStateEnum.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/ValveStateEnum.lua index 8b6da46ad8..e445376bb8 100644 --- a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/ValveStateEnum.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/ValveStateEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" local ValveStateEnum = {} diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/init.lua new file mode 100644 index 0000000000..ae8ebed286 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/init.lua @@ -0,0 +1,17 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.ValveConfigurationAndControl.types." .. key) + end + return types_mt.__types_cache[key] +end + +local ValveConfigurationAndControlTypes = {} + +setmetatable(ValveConfigurationAndControlTypes, types_mt) + +return ValveConfigurationAndControlTypes diff --git a/drivers/SmartThings/matter-switch/src/eve-energy/init.lua b/drivers/SmartThings/matter-switch/src/eve-energy/init.lua deleted file mode 100644 index 410ace0c2c..0000000000 --- a/drivers/SmartThings/matter-switch/src/eve-energy/init.lua +++ /dev/null @@ -1,408 +0,0 @@ --- 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. - -------------------------------------------------------------------------------------- --- Definitions -------------------------------------------------------------------------------------- - -local capabilities = require "st.capabilities" -local clusters = require "st.matter.clusters" -local cluster_base = require "st.matter.cluster_base" -local utils = require "st.utils" -local data_types = require "st.matter.data_types" -local device_lib = require "st.device" - -local SWITCH_INITIALIZED = "__switch_intialized" -local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -local ON_OFF_STATES = "ON_OFF_STATES" - -local EVE_MANUFACTURER_ID = 0x130A -local PRIVATE_CLUSTER_ID = 0x130AFC01 - -local PRIVATE_ATTR_ID_WATT = 0x130A000A -local PRIVATE_ATTR_ID_WATT_ACCUMULATED = 0x130A000B -local PRIVATE_ATTR_ID_ACCUMULATED_CONTROL_POINT = 0x130A000E - --- Timer to update the data each minute if the device is on -local RECURRING_POLL_TIMER = "RECURRING_POLL_TIMER" -local TIMER_REPEAT = (1 * 60) -- Run the timer each minute - --- Timer to report the power consumption every 15 minutes to satisfy the ST energy requirement -local RECURRING_REPORT_POLL_TIMER = "RECURRING_REPORT_POLL_TIMER" -local LAST_REPORT_TIME = "LAST_REPORT_TIME" -local LATEST_TOTAL_CONSUMPTION_WH = "LATEST_TOTAL_CONSUMPTION_WH" -local REPORT_TIMEOUT = (15 * 60) -- Report the value each 15 minutes - - -------------------------------------------------------------------------------------- --- Eve specifics -------------------------------------------------------------------------------------- - -local function is_eve_energy_products(opts, driver, device) - -- this sub driver does not support child devices - if device.network_type == device_lib.NETWORK_TYPE_MATTER and - device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID then - return true - end - - return false -end - --- Return a ISO 8061 formatted timestamp in UTC (Z) --- @return e.g. 2022-02-02T08:00:00Z -local function iso8061Timestamp(time) - return os.date("!%Y-%m-%dT%TZ", time) -end - -local function updateEnergyMeter(device, totalConsumptionWh) - -- Remember the total consumption so we can report it every 15 minutes - device:set_field(LATEST_TOTAL_CONSUMPTION_WH, totalConsumptionWh, { persist = true }) - - -- Report the energy consumed - device:emit_event(capabilities.energyMeter.energy({ value = totalConsumptionWh, unit = "Wh" })) -end - - -------------------------------------------------------------------------------------- --- Timer -------------------------------------------------------------------------------------- - -local function requestData(device) - -- Update the Watt usage - local req = cluster_base.read(device, 0x01, PRIVATE_CLUSTER_ID, PRIVATE_ATTR_ID_WATT, nil) - - -- Update the energy consumption - req:merge(cluster_base.read(device, 0x01, PRIVATE_CLUSTER_ID, PRIVATE_ATTR_ID_WATT_ACCUMULATED, nil)) - - device:send(req) -end - -local function create_poll_schedule(device) - -- the poll schedule is only needed for devices that support powerConsumption - if not device:supports_capability(capabilities.powerConsumptionReport) then - return - end - - local poll_timer = device:get_field(RECURRING_POLL_TIMER) - if poll_timer ~= nil then - return - end - - -- The powerConsumption report needs to be updated at least every 15 minutes in order to be included in SmartThings Energy - -- Eve Energy generally report changes every 10 or 17 minutes - local timer = device.thread:call_on_schedule(TIMER_REPEAT, function() - requestData(device) - end, "polling_schedule_timer") - - device:set_field(RECURRING_POLL_TIMER, timer) -end - -local function delete_poll_schedule(device) - local poll_timer = device:get_field(RECURRING_POLL_TIMER) - if poll_timer ~= nil then - device.thread:cancel_timer(poll_timer) - device:set_field(RECURRING_POLL_TIMER, nil) - end -end - - -local function create_poll_report_schedule(device) - -- the poll schedule is only needed for devices that support powerConsumption - if not device:supports_capability(capabilities.powerConsumptionReport) then - return - end - - -- The powerConsumption report needs to be updated at least every 15 minutes in order to be included in SmartThings Energy - local timer = device.thread:call_on_schedule(REPORT_TIMEOUT, function() - local current_time = os.time() - local last_time = device:get_field(LAST_REPORT_TIME) or 0 - local latestTotalConsumptionWH = device:get_field(LATEST_TOTAL_CONSUMPTION_WH) or 0 - - device:set_field(LAST_REPORT_TIME, current_time, { persist = true }) - - -- Calculate the energy consumed between the start and the end time - local previousTotalConsumptionWh = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, - capabilities.powerConsumptionReport.powerConsumption.NAME) - - local deltaEnergyWh = 0.0 - if previousTotalConsumptionWh ~= nil and previousTotalConsumptionWh.energy ~= nil then - deltaEnergyWh = math.max(latestTotalConsumptionWH - previousTotalConsumptionWh.energy, 0.0) - end - - local startTime = iso8061Timestamp(last_time) - local endTime = iso8061Timestamp(current_time - 1) - - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ - start = startTime, - ["end"] = endTime, - deltaEnergy = deltaEnergyWh, - energy = latestTotalConsumptionWH - })) - end, "polling_report_schedule_timer") - - device:set_field(RECURRING_REPORT_POLL_TIMER, timer) -end - - -------------------------------------------------------------------------------------- --- Matter Utilities -------------------------------------------------------------------------------------- - ---- component_to_endpoint helper function to handle situations where ---- device does not have endpoint ids in sequential order from 1 ---- In this case the function returns the lowest endpoint value that isn't 0 -local function find_default_endpoint(device, component) - local eps = device:get_endpoints(clusters.OnOff.ID) - table.sort(eps) - for _, v in ipairs(eps) do - if v ~= 0 then --0 is the matter RootNode endpoint - return v - end - end - device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", - device.MATTER_DEFAULT_ENDPOINT)) - return device.MATTER_DEFAULT_ENDPOINT -end - -local function initialize_switch(driver, device) - local switch_eps = device:get_endpoints(clusters.OnOff.ID) - table.sort(switch_eps) - - -- Since we do not support bindings at the moment, we only want to count On/Off - -- clusters that have been implemented as server. This can be removed when we have - -- support for bindings. - local num_server_eps = 0 - local main_endpoint = find_default_endpoint(device) - for _, ep in ipairs(switch_eps) do - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - num_server_eps = num_server_eps + 1 - if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint - local name = string.format("%s %d", device.label, num_server_eps) - driver:try_create_device( - { - type = "EDGE_CHILD", - label = name, - profile = "plug-binary", - parent_device_id = device.id, - parent_assigned_child_key = string.format("%d", ep), - vendor_provided_label = name - } - ) - end - end - end - - device:set_field(SWITCH_INITIALIZED, true) -end - -local function component_to_endpoint(device, component) - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} - if map[component] then - return map[component] - end - return find_default_endpoint(device, component) -end - -local function endpoint_to_component(device, ep) - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} - for component, endpoint in pairs(map) do - if endpoint == ep then - return component - end - end - return "main" -end - -local function find_child(parent, ep_id) - return parent:get_child_by_parent_assigned_key(string.format("%d", ep_id)) -end - -local function on_off_state(device, endpoint) - local map = device:get_field(ON_OFF_STATES) or {} - if map[endpoint] then - return map[endpoint] - end - - return false -end - -local function set_on_off_state(device, endpoint, value) - local map = device:get_field(ON_OFF_STATES) or {} - - map[endpoint] = value - device:set_field(ON_OFF_STATES, map) -end - - -------------------------------------------------------------------------------------- --- Device Management -------------------------------------------------------------------------------------- - -local function device_init(driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER then - if not device:get_field(COMPONENT_TO_ENDPOINT_MAP) and - not device:get_field(SWITCH_INITIALIZED) then - -- create child devices as needed for multi-switch devices - initialize_switch(driver, device) - end - device:set_component_to_endpoint_fn(component_to_endpoint) - device:set_endpoint_to_component_fn(endpoint_to_component) - device:set_find_child(find_child) - device:subscribe() - - create_poll_schedule(device) - create_poll_report_schedule(device) - end -end - -local function device_added(driver, device) - -- Reset the values - device:emit_event(capabilities.powerMeter.power({ value = 0.0, unit = "W" })) - device:emit_event(capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) -end - -local function device_removed(driver, device) - delete_poll_schedule(device) -end - --- override do_configure to prevent it running in the main driver -local function do_configure(driver, device) end - --- override driver_switched to prevent it running in the main driver -local function driver_switched(driver, device) end - -local function handle_refresh(self, device) - requestData(device) -end - -local function handle_resetEnergyMeter(self, device) - -- 978307200 is the number of seconds from 1 January 1970 to 1 January 2001 - local current_time = os.time() - local current_time_2001 = current_time - 978307200 - if current_time_2001 < 0 then - current_time_2001 = 0 - end - - local last_time = device:get_field(LAST_REPORT_TIME) or 0 - local startTime = iso8061Timestamp(last_time) - local endTime = iso8061Timestamp(current_time - 1) - - -- Reset the consumption on the device - local data = data_types.validate_or_build_type(current_time_2001, data_types.Uint32) - device:send(cluster_base.write(device, 0x01, PRIVATE_CLUSTER_ID, PRIVATE_ATTR_ID_ACCUMULATED_CONTROL_POINT, nil, - data)) - - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ - start = startTime, - ["end"] = endTime, - deltaEnergy = 0, - energy = 0 - })) - - device:set_field(LAST_REPORT_TIME, current_time, { persist = true }) -end - -------------------------------------------------------------------------------------- --- Eve Energy Handler -------------------------------------------------------------------------------------- - -local function on_off_attr_handler(driver, device, ib, response) - if ib.data.value then - set_on_off_state(device, ib.endpoint_id, true) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.on()) - - -- If one of the outlet is on, we should create the poll to monitor the power consumption - create_poll_schedule(device) - else - set_on_off_state(device, ib.endpoint_id, false) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) - - -- Detect if all the outlets are off - local shouldDeletePoll = true - local eps = device:get_endpoints(clusters.OnOff.ID) - for _, v in ipairs(eps) do - local isOutletOn = on_off_state(device, v) - if isOutletOn then - shouldDeletePoll = false - break - end - end - - -- If all the outlet are off, we should delete the poll - if shouldDeletePoll then - -- We want to prevent to read the power reports of the device if the device is off - -- We set here the power to 0 before the read is skipped so that the power is correctly displayed and not using a stale value - device:emit_event(capabilities.powerMeter.power({ value = 0, unit = "W" })) - - -- Stop the timer when the device is off - delete_poll_schedule(device) - end - end -end - -local function watt_attr_handler(driver, device, ib, zb_rx) - if ib.data.value then - local wattValue = ib.data.value - device:emit_event(capabilities.powerMeter.power({ value = wattValue, unit = "W" })) - end -end - -local function watt_accumulated_attr_handler(driver, device, ib, zb_rx) - if ib.data.value then - local totalConsumptionRawValue = ib.data.value - local totalConsumptionWh = utils.round(1000 * totalConsumptionRawValue) - updateEnergyMeter(device, totalConsumptionWh) - end -end - -local eve_energy_handler = { - NAME = "Eve Energy Handler", - lifecycle_handlers = { - init = device_init, - added = device_added, - removed = device_removed, - doConfigure = do_configure, - driverSwitched = driver_switched - }, - matter_handlers = { - attr = { - [clusters.OnOff.ID] = { - [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, - }, - [PRIVATE_CLUSTER_ID] = { - [PRIVATE_ATTR_ID_WATT] = watt_attr_handler, - [PRIVATE_ATTR_ID_WATT_ACCUMULATED] = watt_accumulated_attr_handler - } - }, - }, - capability_handlers = { - [capabilities.refresh.ID] = { - [capabilities.refresh.commands.refresh.NAME] = handle_refresh, - }, - [capabilities.energyMeter.ID] = { - [capabilities.energyMeter.commands.resetEnergyMeter.NAME] = handle_resetEnergyMeter, - }, - }, - supported_capabilities = { - capabilities.switch, - capabilities.powerMeter, - capabilities.energyMeter, - capabilities.powerConsumptionReport - }, - can_handle = is_eve_energy_products -} - -return eve_energy_handler diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 57335bf10b..cac42e4483 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -1,1534 +1,218 @@ --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -local capabilities = require "st.capabilities" -local log = require "log" -local clusters = require "st.matter.clusters" -local im = require "st.matter.interaction_model" local MatterDriver = require "st.matter.driver" -local lua_socket = require "socket" -local utils = require "st.utils" +local capabilities = require "st.capabilities" local device_lib = require "st.device" -local embedded_cluster_utils = require "embedded-cluster-utils" +local clusters = require "st.matter.clusters" +local log = require "log" local version = require "version" +local cfg = require "switch_utils.device_configuration" +local device_cfg = cfg.DeviceCfg +local switch_cfg = cfg.SwitchCfg +local button_cfg = cfg.ButtonCfg +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" +local attribute_handlers = require "switch_handlers.attribute_handlers" +local event_handlers = require "switch_handlers.event_handlers" +local capability_handlers = require "switch_handlers.capability_handlers" +local embedded_cluster_utils = require "switch_utils.embedded_cluster_utils" -- Include driver-side definitions when lua libs api version is < 11 if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" - clusters.ValveConfigurationAndControl = require "ValveConfigurationAndControl" + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" + clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" end -local MOST_RECENT_TEMP = "mostRecentTemp" -local RECEIVED_X = "receivedX" -local RECEIVED_Y = "receivedY" -local HUESAT_SUPPORT = "huesatSupport" -local MIRED_KELVIN_CONVERSION_CONSTANT = 1000000 --- These values are a "sanity check" to check that values we are getting are reasonable -local COLOR_TEMPERATURE_KELVIN_MAX = 15000 -local COLOR_TEMPERATURE_KELVIN_MIN = 1000 -local COLOR_TEMPERATURE_MIRED_MAX = MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MIN -local COLOR_TEMPERATURE_MIRED_MIN = MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MAX -local SWITCH_LEVEL_LIGHTING_MIN = 1 -local CURRENT_HUESAT_ATTR_MIN = 0 -local CURRENT_HUESAT_ATTR_MAX = 254 - --- COMPONENT_TO_ENDPOINT_MAP is here to preserve the endpoint mapping for --- devices that were joined to this driver as MCD devices before the transition --- to join switch devices as parent-child. This value will exist in the device --- table for devices that joined prior to this transition, and is also used for --- button devices that require component mapping. -local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -local ENERGY_MANAGEMENT_ENDPOINT = "__energy_management_endpoint" -local IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" -local COLOR_TEMP_BOUND_RECEIVED_KELVIN = "__colorTemp_bound_received_kelvin" -local COLOR_TEMP_BOUND_RECEIVED_MIRED = "__colorTemp_bound_received_mired" -local COLOR_TEMP_MIN = "__color_temp_min" -local COLOR_TEMP_MAX = "__color_temp_max" -local LEVEL_BOUND_RECEIVED = "__level_bound_received" -local LEVEL_MIN = "__level_min" -local LEVEL_MAX = "__level_max" -local COLOR_MODE = "__color_mode" - -local updated_fields = { - { current_field_name = "__component_to_endpoint_map_button", updated_field_name = COMPONENT_TO_ENDPOINT_MAP }, - { current_field_name = "__switch_intialized", updated_field_name = nil } -} - -local HUE_SAT_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION -local X_Y_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY - -local AGGREGATOR_DEVICE_TYPE_ID = 0x000E -local ON_OFF_LIGHT_DEVICE_TYPE_ID = 0x0100 -local DIMMABLE_LIGHT_DEVICE_TYPE_ID = 0x0101 -local COLOR_TEMP_LIGHT_DEVICE_TYPE_ID = 0x010C -local EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID = 0x010D -local ON_OFF_PLUG_DEVICE_TYPE_ID = 0x010A -local DIMMABLE_PLUG_DEVICE_TYPE_ID = 0x010B -local ON_OFF_SWITCH_ID = 0x0103 -local ON_OFF_DIMMER_SWITCH_ID = 0x0104 -local ON_OFF_COLOR_DIMMER_SWITCH_ID = 0x0105 -local MOUNTED_ON_OFF_CONTROL_ID = 0x010F -local MOUNTED_DIMMABLE_LOAD_CONTROL_ID = 0x0110 -local GENERIC_SWITCH_ID = 0x000F -local ELECTRICAL_SENSOR_ID = 0x0510 -local device_type_profile_map = { - [ON_OFF_LIGHT_DEVICE_TYPE_ID] = "light-binary", - [DIMMABLE_LIGHT_DEVICE_TYPE_ID] = "light-level", - [COLOR_TEMP_LIGHT_DEVICE_TYPE_ID] = "light-level-colorTemperature", - [EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID] = "light-color-level", - [ON_OFF_PLUG_DEVICE_TYPE_ID] = "plug-binary", - [DIMMABLE_PLUG_DEVICE_TYPE_ID] = "plug-level", - [ON_OFF_SWITCH_ID] = "switch-binary", - [ON_OFF_DIMMER_SWITCH_ID] = "switch-level", - [ON_OFF_COLOR_DIMMER_SWITCH_ID] = "switch-color-level", - [MOUNTED_ON_OFF_CONTROL_ID] = "switch-binary", - [MOUNTED_DIMMABLE_LOAD_CONTROL_ID] = "switch-level", -} - -local device_type_attribute_map = { - [ON_OFF_LIGHT_DEVICE_TYPE_ID] = { - clusters.OnOff.attributes.OnOff - }, - [DIMMABLE_LIGHT_DEVICE_TYPE_ID] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel - }, - [COLOR_TEMP_LIGHT_DEVICE_TYPE_ID] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds - }, - [EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY - }, - [ON_OFF_PLUG_DEVICE_TYPE_ID] = { - clusters.OnOff.attributes.OnOff - }, - [DIMMABLE_PLUG_DEVICE_TYPE_ID] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel - }, - [ON_OFF_SWITCH_ID] = { - clusters.OnOff.attributes.OnOff - }, - [ON_OFF_DIMMER_SWITCH_ID] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel - }, - [ON_OFF_COLOR_DIMMER_SWITCH_ID] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY - }, - [GENERIC_SWITCH_ID] = { - clusters.PowerSource.attributes.BatPercentRemaining, - clusters.Switch.events.InitialPress, - clusters.Switch.events.LongPress, - clusters.Switch.events.ShortRelease, - clusters.Switch.events.MultiPressComplete - }, - [ELECTRICAL_SENSOR_ID] = { - clusters.ElectricalPowerMeasurement.attributes.ActivePower, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported - } -} - -local child_device_profile_overrides_per_vendor_id = { - [0x1321] = { - { product_id = 0x000C, target_profile = "switch-binary", initial_profile = "plug-binary" }, - { product_id = 0x000D, target_profile = "switch-binary", initial_profile = "plug-binary" }, - }, - [0x115F] = { - { product_id = 0x1003, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1004, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x1005, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) - { product_id = 0x1006, target_profile = "light-level-power-energy-powerConsumption" }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) - { product_id = 0x1008, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1009, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x100A, target_profile = "light-level-power-energy-powerConsumption" }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) - } -} - -local detect_matter_thing - -local CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" -local FIRST_IMPORT_REPORT_TIMESTAMP = "__first_import_report_timestamp" -local IMPORT_POLL_TIMER_SETTING_ATTEMPTED = "__import_poll_timer_setting_attempted" -local IMPORT_REPORT_TIMEOUT = "__import_report_timeout" -local TOTAL_IMPORTED_ENERGY = "__total_imported_energy" -local LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" -local RECURRING_IMPORT_REPORT_POLL_TIMER = "__recurring_import_report_poll_timer" -local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds -local SUBSCRIPTION_REPORT_OCCURRED = "__subscription_report_occurred" -local CONVERSION_CONST_MILLIWATT_TO_WATT = 1000 -- A milliwatt is 1/1000th of a watt - --- Return an ISO-8061 timestamp in UTC -local function iso8061Timestamp(time) - return os.date("!%Y-%m-%dT%H:%M:%SZ", time) -end - -local function delete_import_poll_schedule(device) - local import_poll_timer = device:get_field(RECURRING_IMPORT_REPORT_POLL_TIMER) - if import_poll_timer then - device.thread:cancel_timer(import_poll_timer) - device:set_field(RECURRING_IMPORT_REPORT_POLL_TIMER, nil) - device:set_field(IMPORT_POLL_TIMER_SETTING_ATTEMPTED, nil) - end +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" end -local function send_import_poll_report(device, latest_total_imported_energy_wh) - local current_time = os.time() - local last_time = device:get_field(LAST_IMPORTED_REPORT_TIMESTAMP) or 0 - device:set_field(LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) - - -- Calculate the energy delta between reports - local energy_delta_wh = 0.0 - local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, - capabilities.powerConsumptionReport.powerConsumption.NAME) - if previous_imported_report and previous_imported_report.energy then - energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) - end - - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - if not device:get_field(ENERGY_MANAGEMENT_ENDPOINT) then - device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ - start = iso8061Timestamp(last_time), - ["end"] = iso8061Timestamp(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) - else - device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT),capabilities.powerConsumptionReport.powerConsumption({ - start = iso8061Timestamp(last_time), - ["end"] = iso8061Timestamp(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) - end -end - -local function create_poll_report_schedule(device) - local import_timer = device.thread:call_on_schedule( - device:get_field(IMPORT_REPORT_TIMEOUT), function() - send_import_poll_report(device, device:get_field(TOTAL_IMPORTED_ENERGY)) - end, "polling_import_report_schedule_timer" - ) - device:set_field(RECURRING_IMPORT_REPORT_POLL_TIMER, import_timer) -end - -local function set_poll_report_timer_and_schedule(device, is_cumulative_report) - local cumul_eps = embedded_cluster_utils.get_endpoints(device, - clusters.ElectricalEnergyMeasurement.ID, - {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY }) - if #cumul_eps == 0 then - device:set_field(CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = true}) - end - if #cumul_eps > 0 and not is_cumulative_report then - return - elseif not device:get_field(SUBSCRIPTION_REPORT_OCCURRED) then - device:set_field(SUBSCRIPTION_REPORT_OCCURRED, true) - elseif not device:get_field(FIRST_IMPORT_REPORT_TIMESTAMP) then - device:set_field(FIRST_IMPORT_REPORT_TIMESTAMP, os.time()) - else - local first_timestamp = device:get_field(FIRST_IMPORT_REPORT_TIMESTAMP) - local second_timestamp = os.time() - local report_interval_secs = second_timestamp - first_timestamp - device:set_field(IMPORT_REPORT_TIMEOUT, math.max(report_interval_secs, MINIMUM_ST_ENERGY_REPORT_INTERVAL)) - -- the poll schedule is only needed for devices that support powerConsumption - -- and enable powerConsumption when energy management is defined in root endpoint(0). - if device:supports_capability(capabilities.powerConsumptionReport) or - device:get_field(ENERGY_MANAGEMENT_ENDPOINT) then - create_poll_report_schedule(device) - end - device:set_field(IMPORT_POLL_TIMER_SETTING_ATTEMPTED, true) - end -end - -local START_BUTTON_PRESS = "__start_button_press" -local TIMEOUT_THRESHOLD = 10 --arbitrary timeout -local HELD_THRESHOLD = 1 --- this is the number of buttons for which we have a static profile already made -local STATIC_BUTTON_PROFILE_SUPPORTED = {1, 2, 3, 4, 5, 6, 7, 8} - --- Some switches will send a MultiPressComplete event as part of a long press sequence. Normally the driver will create a --- button capability event on receipt of MultiPressComplete, but in this case that would result in an extra event because --- the "held" capability event is generated when the LongPress event is received. The IGNORE_NEXT_MPC flag is used --- to tell the driver to ignore MultiPressComplete if it is received after a long press to avoid this extra event. -local IGNORE_NEXT_MPC = "__ignore_next_mpc" - --- These are essentially storing the supported features of a given endpoint --- TODO: add an is_feature_supported_for_endpoint function to matter.device that takes an endpoint -local EMULATE_HELD = "__emulate_held" -- for non-MSR (MomentarySwitchRelease) devices we can emulate this on the software side -local SUPPORTS_MULTI_PRESS = "__multi_button" -- for MSM devices (MomentarySwitchMultiPress), create an event on receipt of MultiPressComplete -local INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS (MomentarySwitch), but not MSR (MomentarySwitchRelease) - -local TEMP_BOUND_RECEIVED = "__temp_bound_received" -local TEMP_MIN = "__temp_min" -local TEMP_MAX = "__temp_max" - -local AQARA_MANUFACTURER_ID = 0x115F -local AQARA_CLIMATE_SENSOR_W100_ID = 0x2004 - ---helper function to create list of multi press values -local function create_multi_press_values_list(size, supportsHeld) - local list = {"pushed", "double"} - if supportsHeld then table.insert(list, "held") end - -- add multi press values of 3 or greater to the list - for i=3, size do - table.insert(list, string.format("pushed_%dx", i)) - end - return list -end - -local function tbl_contains(array, value) - for _, element in ipairs(array) do - if element == value then - return true - end - end - return false -end - -local function get_field_for_endpoint(device, field, endpoint) - return device:get_field(string.format("%s_%d", field, endpoint)) -end - -local function set_field_for_endpoint(device, field, endpoint, value, additional_params) - device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) -end - -local function init_press(device, endpoint) - set_field_for_endpoint(device, START_BUTTON_PRESS, endpoint, lua_socket.gettime(), {persist = false}) -end - -local function emulate_held_event(device, ep) - local now = lua_socket.gettime() - local press_init = get_field_for_endpoint(device, START_BUTTON_PRESS, ep) or now -- if we don't have an init time, assume instant release - if (now - press_init) < TIMEOUT_THRESHOLD then - if (now - press_init) > HELD_THRESHOLD then - device:emit_event_for_endpoint(ep, capabilities.button.button.held({state_change = true})) - else - device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = true})) - end - end - set_field_for_endpoint(device, START_BUTTON_PRESS, ep, nil, {persist = false}) -end - -local function convert_huesat_st_to_matter(val) - return utils.clamp_value(math.floor((val * 0xFE) / 100.0 + 0.5), CURRENT_HUESAT_ATTR_MIN, CURRENT_HUESAT_ATTR_MAX) -end - -local function mired_to_kelvin(value, minOrMax) - if value == 0 then -- shouldn't happen, but has - value = 1 - log.warn(string.format("Received a color temperature of 0 mireds. Using a color temperature of 1 mired to avoid divide by zero")) - end - -- We divide inside the rounding and multiply outside of it because we expect these - -- bounds to be multiples of 100. For the maximum mired value (minimum K value), - -- add 1 before converting and round up to nearest hundreds. For the minimum mired - -- (maximum K value) value, subtract 1 before converting and round down to nearest - -- hundreds. Note that 1 is added/subtracted from the mired value in order to avoid - -- rounding errors from the conversion of Kelvin to mireds. - local kelvin_step_size = 100 - local rounding_value = 0.5 - if minOrMax == COLOR_TEMP_MIN then - return utils.round(MIRED_KELVIN_CONVERSION_CONSTANT / (kelvin_step_size * (value + 1)) + rounding_value) * kelvin_step_size - elseif minOrMax == COLOR_TEMP_MAX then - return utils.round(MIRED_KELVIN_CONVERSION_CONSTANT / (kelvin_step_size * (value - 1)) - rounding_value) * kelvin_step_size - else - log.warn_with({hub_logs = true}, "Attempted to convert temperature unit for an undefined value") - end -end - ---- device_type_supports_button_switch_combination helper function used to check ---- whether the device type for an endpoint is currently supported by a profile for ---- combination button/switch devices. -local function device_type_supports_button_switch_combination(device, endpoint_id) - for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id == endpoint_id then - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == DIMMABLE_LIGHT_DEVICE_TYPE_ID then - for _, fingerprint in ipairs(child_device_profile_overrides_per_vendor_id[0x115F]) do - if device.manufacturer_info.product_id == fingerprint.product_id then - return false -- For Aqara Dimmer Switch with Button. - end - end - return true - end - end - end - end - return false -end - -local function get_first_non_zero_endpoint(endpoints) - table.sort(endpoints) - for _,ep in ipairs(endpoints) do - if ep ~= 0 then -- 0 is the matter RootNode endpoint - return ep - end - end - return nil -end - ---- find_default_endpoint is a helper function to handle situations where ---- device does not have endpoint ids in sequential order from 1 -local function find_default_endpoint(device) - if device.manufacturer_info.vendor_id == AQARA_MANUFACTURER_ID and - device.manufacturer_info.product_id == AQARA_CLIMATE_SENSOR_W100_ID then - -- In case of Aqara Climate Sensor W100, in order to sequentially set the button name to button 1, 2, 3 - return device.MATTER_DEFAULT_ENDPOINT - end - - local switch_eps = device:get_endpoints(clusters.OnOff.ID) - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - - -- Return the first switch endpoint as the default endpoint if no button endpoints are present - if #button_eps == 0 and #switch_eps > 0 then - return get_first_non_zero_endpoint(switch_eps) - end - - -- Return the first button endpoint as the default endpoint if no switch endpoints are present - if #switch_eps == 0 and #button_eps > 0 then - return get_first_non_zero_endpoint(button_eps) - end - - -- If both switch and button endpoints are present, check the device type on the main switch - -- endpoint. If it is not a supported device type, return the first button endpoint as the - -- default endpoint. - if #switch_eps > 0 and #button_eps > 0 then - local main_endpoint = get_first_non_zero_endpoint(switch_eps) - if device_type_supports_button_switch_combination(device, main_endpoint) then - return main_endpoint - else - device.log.warn("The main switch endpoint does not contain a supported device type for a component configuration with buttons") - return get_first_non_zero_endpoint(button_eps) - end - end - - device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) - return device.MATTER_DEFAULT_ENDPOINT -end - -local function component_to_endpoint(device, component) - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} - if map[component] then - return map[component] - end - return find_default_endpoint(device) -end - -local function endpoint_to_component(device, ep) - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} - for component, endpoint in pairs(map) do - if endpoint == ep then - return component - end - end - return "main" -end - -local function check_field_name_updates(device) - for _, field in ipairs(updated_fields) do - if device:get_field(field.current_field_name) then - if field.updated_field_name ~= nil then - device:set_field(field.updated_field_name, device:get_field(field.current_field_name), {persist = true}) - end - device:set_field(field.current_field_name, nil) - end - end -end - -local function assign_child_profile(device, child_ep) - local profile - - for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id == child_ep then - -- Some devices report multiple device types which are a subset of - -- a superset device type (For example, Dimmable Light is a superset of - -- On/Off light). This mostly applies to the four light types, so we will want - -- to match the profile for the superset device type. This can be done by - -- matching to the device type with the highest ID - local id = 0 - for _, dt in ipairs(ep.device_types) do - id = math.max(id, dt.device_type_id) - end - profile = device_type_profile_map[id] - break - end - end - - -- Check if device has an overridden child profile that differs from the profile that would match - -- the child's device type for the following two cases: - -- 1. To add Electrical Sensor only to the first EDGE_CHILD (light-power-energy-powerConsumption) - -- for the Aqara Light Switch H2. The profile of the second EDGE_CHILD for this device is - -- determined in the "for" loop above (e.g., light-binary) - -- 2. The selected profile for the child device matches the initial profile defined in - -- child_device_profile_overrides - for id, vendor in pairs(child_device_profile_overrides_per_vendor_id) do - for _, fingerprint in ipairs(vendor) do - if device.manufacturer_info.product_id == fingerprint.product_id and - ((device.manufacturer_info.vendor_id == AQARA_MANUFACTURER_ID and child_ep == 1) or profile == fingerprint.initial_profile) then - return fingerprint.target_profile - end - end - end +local SwitchLifecycleHandlers = {} - -- default to "switch-binary" if no profile is found - return profile or "switch-binary" -end - -local function configure_buttons(device) - local ms_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - local msr_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_RELEASE}) - local msl_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS}) - local msm_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS}) - - for _, ep in ipairs(ms_eps) do - if device.profile.components[endpoint_to_component(device, ep)] then - device.log.info_with({hub_logs=true}, string.format("Configuring Supported Values for generic switch endpoint %d", ep)) - local supportedButtonValues_event - -- this ordering is important, since MSM & MSL devices must also support MSR - if tbl_contains(msm_eps, ep) then - supportedButtonValues_event = nil -- deferred to the max press handler - device:send(clusters.Switch.attributes.MultiPressMax:read(device, ep)) - set_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ep, true, {persist = true}) - elseif tbl_contains(msl_eps, ep) then - supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}) - elseif tbl_contains(msr_eps, ep) then - supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}) - set_field_for_endpoint(device, EMULATE_HELD, ep, true, {persist = true}) - else -- this switch endpoint only supports momentary switch, no release events - supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}) - set_field_for_endpoint(device, INITIAL_PRESS_ONLY, ep, true, {persist = true}) - end - - if supportedButtonValues_event then - device:emit_event_for_endpoint(ep, supportedButtonValues_event) - end - device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) - else - device.log.info_with({hub_logs=true}, string.format("Component not found for generic switch endpoint %d. Skipping Supported Value configuration", ep)) - end - end -end - -local function find_child(parent, ep_id) - return parent:get_child_by_parent_assigned_key(string.format("%d", ep_id)) -end - -local function build_button_component_map(device, main_endpoint, button_eps) - -- create component mapping on the main profile button endpoints - table.sort(button_eps) - local component_map = {} - component_map["main"] = main_endpoint - for component_num, ep in ipairs(button_eps) do - if ep ~= main_endpoint then - local button_component = "button" - if #button_eps > 1 then - button_component = button_component .. component_num - end - component_map[button_component] = ep - end - end - device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) -end - -local function build_button_profile(device, main_endpoint, num_button_eps) - local profile_name = string.gsub(num_button_eps .. "-button", "1%-", "") -- remove the "1-" in a device with 1 button ep - if device_type_supports_button_switch_combination(device, main_endpoint) then - profile_name = "light-level-" .. profile_name - end - local battery_supported = #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) > 0 - if battery_supported then -- battery profiles are configured later, in power_source_attribute_list_handler - device:send(clusters.PowerSource.attributes.AttributeList:read(device)) - else - device:try_update_metadata({profile = profile_name}) - end -end - -local function build_child_switch_profiles(driver, device, main_endpoint) - local num_switch_server_eps = 0 - local parent_child_device = false - local switch_eps = device:get_endpoints(clusters.OnOff.ID) - table.sort(switch_eps) - for idx, ep in ipairs(switch_eps) do - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - num_switch_server_eps = num_switch_server_eps + 1 - if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint - local name = string.format("%s %d", device.label, num_switch_server_eps) - local child_profile = assign_child_profile(device, ep) - driver:try_create_device( - { - type = "EDGE_CHILD", - label = name, - profile = child_profile, - parent_device_id = device.id, - parent_assigned_child_key = string.format("%d", ep), - vendor_provided_label = name - } - ) - parent_child_device = true - if idx == 1 and string.find(child_profile, "energy") then - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:set_field(ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) - end - end - end - end - - -- If the device is a parent child device, set the find_child function on init. This is persisted because initialize_buttons_and_switches - -- is only run once, but find_child function should be set on each driver init. - if parent_child_device then - device:set_field(IS_PARENT_CHILD_DEVICE, true, {persist = true}) - end - - -- this is needed in initialize_buttons_and_switches - return num_switch_server_eps -end - -local function handle_light_switch_with_onOff_server_clusters(device, main_endpoint) - local cluster_id = 0 - for _, ep in ipairs(device.endpoints) do - -- main_endpoint only supports server cluster by definition of get_endpoints() - if main_endpoint == ep.endpoint_id then - for _, dt in ipairs(ep.device_types) do - -- no device type that is not in the switch subset should be considered. - if (ON_OFF_SWITCH_ID <= dt.device_type_id and dt.device_type_id <= ON_OFF_COLOR_DIMMER_SWITCH_ID) then - cluster_id = math.max(cluster_id, dt.device_type_id) - end - end - break - end - end - - if device_type_profile_map[cluster_id] then - device:try_update_metadata({profile = device_type_profile_map[cluster_id]}) - end -end - -local function initialize_buttons_and_switches(driver, device, main_endpoint) - local profile_found = false - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - build_button_profile(device, main_endpoint, #button_eps) - -- All button endpoints found will be added as additional components in the profile containing the main_endpoint. - -- The resulting endpoint to component map is saved in the COMPONENT_TO_ENDPOINT_MAP field - build_button_component_map(device, main_endpoint, button_eps) - configure_buttons(device) - profile_found = true - end - - -- Without support for bindings, only clusters that are implemented as server are counted. This count is handled - -- while building switch child profiles - local num_switch_server_eps = build_child_switch_profiles(driver, device, main_endpoint) - - -- We do not support the Light Switch device types because they require OnOff to be implemented as 'client', which requires us to support bindings. - -- However, this workaround profiles devices that claim to be Light Switches, but that break spec and implement OnOff as 'server'. - -- Note: since their device type isn't supported, these devices join as a matter-thing. - if num_switch_server_eps > 0 and detect_matter_thing(device) then - handle_light_switch_with_onOff_server_clusters(device, main_endpoint) - profile_found = true +function SwitchLifecycleHandlers.device_added(driver, device) + -- refresh child devices to get an initial attribute state for OnOff in case child device + -- was created after the initial subscription report + if device.network_type == device_lib.NETWORK_TYPE_CHILD then + device:send(clusters.OnOff.attributes.OnOff:read(device)) + elseif device.network_type == device_lib.NETWORK_TYPE_MATTER then + switch_utils.handle_electrical_sensor_info(device) end - return profile_found -end -local function detect_bridge(device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == AGGREGATOR_DEVICE_TYPE_ID then - return true - end - end - end - return false + -- call device init in case init is not called after added due to device caching + SwitchLifecycleHandlers.device_init(driver, device) end -local function device_init(driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER then - check_field_name_updates(device) - device:set_component_to_endpoint_fn(component_to_endpoint) - device:set_endpoint_to_component_fn(endpoint_to_component) - if device:get_field(IS_PARENT_CHILD_DEVICE) then - device:set_find_child(find_child) - end - local main_endpoint = find_default_endpoint(device) - -- ensure subscription to all endpoint attributes- including those mapped to child devices - for idx, ep in ipairs(device.endpoints) do - if ep.endpoint_id ~= main_endpoint then - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - local child_profile = assign_child_profile(device, ep) - if idx == 1 and string.find(child_profile, "energy") then - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:set_field(ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) - end - end - local id = 0 - for _, dt in ipairs(ep.device_types) do - id = math.max(id, dt.device_type_id) - end - for _, attr in pairs(device_type_attribute_map[id] or {}) do - if id == GENERIC_SWITCH_ID and - attr ~= clusters.PowerSource.attributes.BatPercentRemaining and - attr ~= clusters.PowerSource.attributes.BatChargeLevel then - device:add_subscribed_event(attr) - else - device:add_subscribed_attribute(attr) - end - end - end - end +function SwitchLifecycleHandlers.do_configure(driver, device) + if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then + switch_cfg.set_device_control_options(device) + device_cfg.match_profile(driver, device) + elseif device.network_type == device_lib.NETWORK_TYPE_CHILD then + -- because get_parent_device() may cause race conditions if used in init, an initial child subscribe is handled in doConfigure. + -- all future calls to subscribe will be handled by the parent device in init device:subscribe() end end -local function match_profile(driver, device) - local main_endpoint = find_default_endpoint(device) - -- initialize the main device card with buttons if applicable, and create child devices as needed for multi-switch devices. - local profile_found = initialize_buttons_and_switches(driver, device, main_endpoint) - if device:get_field(IS_PARENT_CHILD_DEVICE) then - device:set_find_child(find_child) - end - if profile_found then - return - end - - local fan_eps = device:get_endpoints(clusters.FanControl.ID) - local level_eps = device:get_endpoints(clusters.LevelControl.ID) - local energy_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) - local power_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) - local valve_eps = embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) - local profile_name = nil - local level_support = "" - if #level_eps > 0 then - level_support = "-level" - end - if #energy_eps > 0 and #power_eps > 0 then - profile_name = "plug" .. level_support .. "-power-energy-powerConsumption" - elseif #energy_eps > 0 then - profile_name = "plug" .. level_support .. "-energy-powerConsumption" - elseif #power_eps > 0 then - profile_name = "plug" .. level_support .. "-power" - elseif #valve_eps > 0 then - profile_name = "water-valve" - if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, - {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then - profile_name = profile_name .. "-level" - end - elseif #fan_eps > 0 then - profile_name = "light-color-level-fan" - end - if profile_name then - device:try_update_metadata({ profile = profile_name }) - end -end - -local function do_configure(driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER and not detect_bridge(device) then - match_profile(driver, device) - end -end - -local function driver_switched(driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER and not detect_bridge(device) then - match_profile(driver, device) - end -end - -local function device_removed(driver, device) - log.info("device removed") - delete_import_poll_schedule(device) -end - -local function handle_switch_on(driver, device, cmd) - if type(device.register_native_capability_cmd_handler) == "function" then - device:register_native_capability_cmd_handler(cmd.capability, cmd.command) - end - local endpoint_id = device:component_to_endpoint(cmd.component) - --TODO use OnWithRecallGlobalScene for devices with the LT feature - local req = clusters.OnOff.server.commands.On(device, endpoint_id) - device:send(req) -end - -local function handle_switch_off(driver, device, cmd) - if type(device.register_native_capability_cmd_handler) == "function" then - device:register_native_capability_cmd_handler(cmd.capability, cmd.command) - end - local endpoint_id = device:component_to_endpoint(cmd.component) - local req = clusters.OnOff.server.commands.Off(device, endpoint_id) - device:send(req) -end - -local function handle_set_switch_level(driver, device, cmd) - if type(device.register_native_capability_cmd_handler) == "function" then - device:register_native_capability_cmd_handler(cmd.capability, cmd.command) - end - local endpoint_id = device:component_to_endpoint(cmd.component) - local level = math.floor(cmd.args.level/100.0 * 254) - local req = clusters.LevelControl.server.commands.MoveToLevelWithOnOff(device, endpoint_id, level, cmd.args.rate, 0, 0) - device:send(req) -end - -local TRANSITION_TIME = 0 --1/10ths of a second --- When sent with a command, these options mask and override bitmaps cause the command --- to take effect when the switch/light is off. -local OPTIONS_MASK = 0x01 -local OPTIONS_OVERRIDE = 0x01 - -local function handle_set_color(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local req - local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) - if tbl_contains(huesat_endpoints, endpoint_id) then - local hue = convert_huesat_st_to_matter(cmd.args.color.hue) - local sat = convert_huesat_st_to_matter(cmd.args.color.saturation) - req = clusters.ColorControl.server.commands.MoveToHueAndSaturation(device, endpoint_id, hue, sat, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) - else - local x, y, _ = utils.safe_hsv_to_xy(cmd.args.color.hue, cmd.args.color.saturation) - req = clusters.ColorControl.server.commands.MoveToColor(device, endpoint_id, x, y, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) - end - device:send(req) -end - -local function handle_set_hue(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) - if tbl_contains(huesat_endpoints, endpoint_id) then - local hue = convert_huesat_st_to_matter(cmd.args.hue) - local req = clusters.ColorControl.server.commands.MoveToHue(device, endpoint_id, hue, 0, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) - device:send(req) - else - log.warn("Device does not support huesat features on its color control cluster") - end -end - -local function handle_set_saturation(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) - if tbl_contains(huesat_endpoints, endpoint_id) then - local sat = convert_huesat_st_to_matter(cmd.args.saturation) - local req = clusters.ColorControl.server.commands.MoveToSaturation(device, endpoint_id, sat, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) - device:send(req) - else - log.warn("Device does not support huesat features on its color control cluster") - end -end - -local function handle_set_color_temperature(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local temp_in_kelvin = cmd.args.temperature - local min_temp_kelvin = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_KELVIN..COLOR_TEMP_MIN, endpoint_id) - local max_temp_kelvin = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_KELVIN..COLOR_TEMP_MAX, endpoint_id) - - local temp_in_mired = utils.round(MIRED_KELVIN_CONVERSION_CONSTANT/temp_in_kelvin) - if min_temp_kelvin ~= nil and temp_in_kelvin <= min_temp_kelvin then - temp_in_mired = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_MIRED..COLOR_TEMP_MAX, endpoint_id) - elseif max_temp_kelvin ~= nil and temp_in_kelvin >= max_temp_kelvin then - temp_in_mired = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_MIRED..COLOR_TEMP_MIN, endpoint_id) - end - local req = clusters.ColorControl.server.commands.MoveToColorTemperature(device, endpoint_id, temp_in_mired, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) - device:set_field(MOST_RECENT_TEMP, cmd.args.temperature, {persist = true}) - device:send(req) -end - -local function handle_valve_open(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local req = clusters.ValveConfigurationAndControl.server.commands.Open(device, endpoint_id) - device:send(req) -end - -local function handle_valve_close(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local req = clusters.ValveConfigurationAndControl.server.commands.Close(device, endpoint_id) - device:send(req) -end - -local function handle_set_level(driver, device, cmd) - local commands = clusters.ValveConfigurationAndControl.server.commands - local endpoint_id = device:component_to_endpoint(cmd.component) - local level = cmd.args.level - if not level then - return - elseif level == 0 then - device:send(commands.Close(device, endpoint_id)) - else - device:send(commands.Open(device, endpoint_id, nil, level)) +function SwitchLifecycleHandlers.driver_switched(driver, device) + if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then + switch_utils.handle_electrical_sensor_info(device) -- field settings required for proper setup when switching drivers + device_cfg.match_profile(driver, device) end end -local function set_fan_mode(driver, device, cmd) - local fan_mode_id - if cmd.args.fanMode == capabilities.fanMode.fanMode.low.NAME then - fan_mode_id = clusters.FanControl.attributes.FanMode.LOW - elseif cmd.args.fanMode == capabilities.fanMode.fanMode.medium.NAME then - fan_mode_id = clusters.FanControl.attributes.FanMode.MEDIUM - elseif cmd.args.fanMode == capabilities.fanMode.fanMode.high.NAME then - fan_mode_id = clusters.FanControl.attributes.FanMode.HIGH - elseif cmd.args.fanMode == capabilities.fanMode.fanMode.auto.NAME then - fan_mode_id = clusters.FanControl.attributes.FanMode.AUTO - else - fan_mode_id = clusters.FanControl.attributes.FanMode.OFF - end - if fan_mode_id then - local fan_ep = device:get_endpoints(clusters.FanControl.ID)[1] - device:send(clusters.FanControl.attributes.FanMode:write(device, fan_ep, fan_mode_id)) - end -end - -local function set_fan_speed_percent(driver, device, cmd) - local speed = math.floor(cmd.args.percent) - local fan_ep = device:get_endpoints(clusters.FanControl.ID)[1] - device:send(clusters.FanControl.attributes.PercentSetting:write(device, fan_ep, speed)) -end - --- Fallback handler for responses that dont have their own handler -local function matter_handler(driver, device, response_block) - log.info(string.format("Fallback handler for %s", response_block)) -end - -local function on_off_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.on()) - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) - end - if type(device.register_native_capability_attr_handler) == "function" then - device:register_native_capability_attr_handler("switch", "switch") - end -end - -local function level_attr_handler(driver, device, ib, response) - if ib.data.value ~= nil then - local level = math.floor((ib.data.value / 254.0 * 100) + 0.5) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switchLevel.level(level)) - if type(device.register_native_capability_attr_handler) == "function" then - device:register_native_capability_attr_handler("switchLevel", "level") +function SwitchLifecycleHandlers.info_changed(driver, device, event, args) + if device.profile.id ~= args.old_st_store.profile.id or device:get_field(fields.MODULAR_PROFILE_UPDATED) then + device:set_field(fields.MODULAR_PROFILE_UPDATED, nil) + if device.network_type == device_lib.NETWORK_TYPE_MATTER then + device:subscribe() + button_cfg.configure_buttons(device, + device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + ) + elseif device.network_type == device_lib.NETWORK_TYPE_CHILD then + device:get_parent_device():subscribe() -- parent device required to send subscription requests end end -end - -local function hue_attr_handler(driver, device, ib, response) - if device:get_field(COLOR_MODE) == X_Y_COLOR_MODE or ib.data.value == nil then - return - end - local hue = math.floor((ib.data.value / 0xFE * 100) + 0.5) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(hue)) -end -local function sat_attr_handler(driver, device, ib, response) - if device:get_field(COLOR_MODE) == X_Y_COLOR_MODE or ib.data.value == nil then - return - end - local sat = math.floor((ib.data.value / 0xFE * 100) + 0.5) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(sat)) -end - -local function temp_attr_handler(driver, device, ib, response) - local temp_in_mired = ib.data.value - if temp_in_mired == nil then - return - end - if (temp_in_mired < COLOR_TEMPERATURE_MIRED_MIN or temp_in_mired > COLOR_TEMPERATURE_MIRED_MAX) then - device.log.warn_with({hub_logs = true}, string.format("Device reported color temperature %d mired outside of sane range of %.2f-%.2f", temp_in_mired, COLOR_TEMPERATURE_MIRED_MIN, COLOR_TEMPERATURE_MIRED_MAX)) - return - end - local min_temp_mired = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_MIRED..COLOR_TEMP_MIN, ib.endpoint_id) - local max_temp_mired = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_MIRED..COLOR_TEMP_MAX, ib.endpoint_id) - - local temp = utils.round(MIRED_KELVIN_CONVERSION_CONSTANT/temp_in_mired) - if min_temp_mired ~= nil and temp_in_mired <= min_temp_mired then - temp = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_KELVIN..COLOR_TEMP_MAX, ib.endpoint_id) - elseif max_temp_mired ~= nil and temp_in_mired >= max_temp_mired then - temp = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_KELVIN..COLOR_TEMP_MIN, ib.endpoint_id) - end - - local temp_device = device - if device:get_field(IS_PARENT_CHILD_DEVICE) == true then - temp_device = find_child(device, ib.endpoint_id) or device - end - local most_recent_temp = temp_device:get_field(MOST_RECENT_TEMP) - -- this is to avoid rounding errors from the round-trip conversion of Kelvin to mireds - if most_recent_temp ~= nil and - most_recent_temp <= utils.round(MIRED_KELVIN_CONVERSION_CONSTANT/(temp_in_mired - 1)) and - most_recent_temp >= utils.round(MIRED_KELVIN_CONVERSION_CONSTANT/(temp_in_mired + 1)) then - temp = most_recent_temp - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorTemperature.colorTemperature(temp)) -end - -local mired_bounds_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - local temp_in_mired = ib.data.value - if temp_in_mired == nil then - return - end - if (temp_in_mired < COLOR_TEMPERATURE_MIRED_MIN or temp_in_mired > COLOR_TEMPERATURE_MIRED_MAX) then - device.log.warn_with({hub_logs = true}, string.format("Device reported a color temperature %d mired outside of sane range of %.2f-%.2f", temp_in_mired, COLOR_TEMPERATURE_MIRED_MIN, COLOR_TEMPERATURE_MIRED_MAX)) - return - end - local temp_in_kelvin = mired_to_kelvin(temp_in_mired, minOrMax) - set_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_KELVIN..minOrMax, ib.endpoint_id, temp_in_kelvin) - -- the minimum color temp in kelvin corresponds to the maximum temp in mireds - if minOrMax == COLOR_TEMP_MIN then - set_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_MIRED..COLOR_TEMP_MAX, ib.endpoint_id, temp_in_mired) - else - set_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_MIRED..COLOR_TEMP_MIN, ib.endpoint_id, temp_in_mired) - end - local min = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_KELVIN..COLOR_TEMP_MIN, ib.endpoint_id) - local max = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_KELVIN..COLOR_TEMP_MAX, ib.endpoint_id) - if min ~= nil and max ~= nil then - if min < max then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = min, maximum = max} })) - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min color temperature %d K that is not lower than the reported max color temperature %d K", min, max)) - end - end - end -end - -local level_bounds_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local lighting_endpoints = device:get_endpoints(clusters.LevelControl.ID, {feature_bitmap = clusters.LevelControl.FeatureMap.LIGHTING}) - local lighting_support = tbl_contains(lighting_endpoints, ib.endpoint_id) - -- If the lighting feature is supported then we should check if the reported level is at least 1. - if lighting_support and ib.data.value < SWITCH_LEVEL_LIGHTING_MIN then - device.log.warn_with({hub_logs = true}, string.format("Lighting device reported a switch level %d outside of supported capability range", ib.data.value)) - return - end - -- Convert level from given range of 0-254 to range of 0-100. - local level = utils.round(ib.data.value / 254.0 * 100) - -- If the device supports the lighting feature, the minimum capability level should be 1 so we do not send a 0 value for the level attribute - if lighting_support and level == 0 then - level = 1 - end - set_field_for_endpoint(device, LEVEL_BOUND_RECEIVED..minOrMax, ib.endpoint_id, level) - local min = get_field_for_endpoint(device, LEVEL_BOUND_RECEIVED..LEVEL_MIN, ib.endpoint_id) - local max = get_field_for_endpoint(device, LEVEL_BOUND_RECEIVED..LEVEL_MAX, ib.endpoint_id) - if min ~= nil and max ~= nil then - if min < max then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switchLevel.levelRange({ value = {minimum = min, maximum = max} })) - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min level value %d that is not lower than the reported max level value %d", min, max)) - end - set_field_for_endpoint(device, LEVEL_BOUND_RECEIVED..LEVEL_MAX, ib.endpoint_id, nil) - set_field_for_endpoint(device, LEVEL_BOUND_RECEIVED..LEVEL_MIN, ib.endpoint_id, nil) - end - end -end - -local color_utils = require "color_utils" - -local function x_attr_handler(driver, device, ib, response) - if device:get_field(COLOR_MODE) == HUE_SAT_COLOR_MODE then - return - end - local y = device:get_field(RECEIVED_Y) - --TODO it is likely that both x and y attributes are in the response (not guaranteed though) - -- if they are we can avoid setting fields on the device. - if y == nil then - device:set_field(RECEIVED_X, ib.data.value) - else - local x = ib.data.value - local h, s, _ = color_utils.safe_xy_to_hsv(x, y) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(h)) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(s)) - device:set_field(RECEIVED_Y, nil) - end -end - -local function y_attr_handler(driver, device, ib, response) - if device:get_field(COLOR_MODE) == HUE_SAT_COLOR_MODE then - return - end - local x = device:get_field(RECEIVED_X) - if x == nil then - device:set_field(RECEIVED_Y, ib.data.value) - else - local y = ib.data.value - local h, s, _ = color_utils.safe_xy_to_hsv(x, y) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(h)) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(s)) - device:set_field(RECEIVED_X, nil) - end -end - -local function color_mode_attr_handler(driver, device, ib, response) - if ib.data.value == device:get_field(COLOR_MODE) or (ib.data.value ~= HUE_SAT_COLOR_MODE and ib.data.value ~= X_Y_COLOR_MODE) then - return - end - device:set_field(COLOR_MODE, ib.data.value) - local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - if ib.data.value == HUE_SAT_COLOR_MODE then - req:merge(clusters.ColorControl.attributes.CurrentHue:read()) - req:merge(clusters.ColorControl.attributes.CurrentSaturation:read()) - elseif ib.data.value == X_Y_COLOR_MODE then - req:merge(clusters.ColorControl.attributes.CurrentX:read()) - req:merge(clusters.ColorControl.attributes.CurrentY:read()) - end - if #req.info_blocks > 0 then - device:send(req) - end -end - ---TODO setup configure handler to read this attribute. -local function color_cap_attr_handler(driver, device, ib, response) - if ib.data.value ~= nil then - if ib.data.value & 0x1 then - device:set_field(HUESAT_SUPPORT, true) - end - end -end - -local function illuminance_attr_handler(driver, device, ib, response) - local lux = math.floor(10 ^ ((ib.data.value - 1) / 10000)) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.illuminanceMeasurement.illuminance(lux)) -end - -local function occupancy_attr_handler(driver, device, ib, response) - device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) -end - -local function cumul_energy_imported_handler(driver, device, ib, response) - if ib.data.elements.energy then - local watt_hour_value = ib.data.elements.energy.value / CONVERSION_CONST_MILLIWATT_TO_WATT - device:set_field(TOTAL_IMPORTED_ENERGY, watt_hour_value, {persist = true}) - if ib.endpoint_id ~= 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) - else - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT), capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) - end - end -end - -local function per_energy_imported_handler(driver, device, ib, response) - if ib.data.elements.energy then - local watt_hour_value = ib.data.elements.energy.value / CONVERSION_CONST_MILLIWATT_TO_WATT - local latest_energy_report = device:get_field(TOTAL_IMPORTED_ENERGY) or 0 - local summed_energy_report = latest_energy_report + watt_hour_value - device:set_field(TOTAL_IMPORTED_ENERGY, summed_energy_report, {persist = true}) - device:emit_event(capabilities.energyMeter.energy({ value = summed_energy_report, unit = "Wh" })) - end -end - -local function energy_report_handler_factory(is_cumulative_report) - return function(driver, device, ib, response) - if not device:get_field(IMPORT_POLL_TIMER_SETTING_ATTEMPTED) then - set_poll_report_timer_and_schedule(device, is_cumulative_report) - end - if is_cumulative_report then - cumul_energy_imported_handler(driver, device, ib, response) - elseif device:get_field(CUMULATIVE_REPORTS_NOT_SUPPORTED) then - per_energy_imported_handler(driver, device, ib, response) - end - end -end - -local function initial_press_event_handler(driver, device, ib, response) - if get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then - -- Receipt of an InitialPress event means we do not want to ignore the next MultiPressComplete event - -- or else we would potentially not create the expected button capability event - set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, nil) - elseif get_field_for_endpoint(device, INITIAL_PRESS_ONLY, ib.endpoint_id) then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true})) - elseif get_field_for_endpoint(device, EMULATE_HELD, ib.endpoint_id) then - -- if our button doesn't differentiate between short and long holds, do it in code by keeping track of the press down time - init_press(device, ib.endpoint_id) - end -end - --- if the device distinguishes a long press event, it will always be a "held" --- there's also a "long release" event, but this event is required to come first -local function long_press_event_handler(driver, device, ib, response) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.held({state_change = true})) - if get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then - -- Ignore the next MultiPressComplete event if it is sent as part of this "long press" event sequence - set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, true) - end -end - -local function short_release_event_handler(driver, device, ib, response) - if not get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then - if get_field_for_endpoint(device, EMULATE_HELD, ib.endpoint_id) then - emulate_held_event(device, ib.endpoint_id) - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true})) + if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then + if device.matter_version.software ~= args.old_st_store.matter_version.software then + device_cfg.match_profile(driver, device) end end -end -local function active_power_handler(driver, device, ib, response) - if ib.data.value then - local watt_value = ib.data.value / CONVERSION_CONST_MILLIWATT_TO_WATT - if ib.endpoint_id ~= 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - if type(device.register_native_capability_attr_handler) == "function" then - device:register_native_capability_attr_handler("powerMeter","power") + -- instant update of values after offset preference change + for name, info in pairs(device.preferences or {}) do + if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then + if name == "tempOffset" then + device:send(clusters.TemperatureMeasurement.attributes.MeasuredValue:read(device)) + elseif name == "humidityOffset" then + device:send(clusters.RelativeHumidityMeasurement.attributes.MeasuredValue:read(device)) end - else - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT), capabilities.powerMeter.power({ value = watt_value, unit = "W"})) end end -end -local function valve_state_attr_handler(driver, device, ib, response) - if ib.data.value == 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.valve.valve.closed()) - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.valve.valve.open()) - end end -local function valve_level_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.level.level(ib.data.value)) - end -end - -local function multi_press_complete_event_handler(driver, device, ib, response) - -- in the case of multiple button presses - -- emit number of times, multiple presses have been completed - if ib.data and not get_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id) then - local press_value = ib.data.elements.total_number_of_presses_counted.value - --capability only supports up to 6 presses - if press_value < 7 then - local button_event = capabilities.button.button.pushed({state_change = true}) - if press_value == 2 then - button_event = capabilities.button.button.double({state_change = true}) - elseif press_value > 2 then - button_event = capabilities.button.button(string.format("pushed_%dx", press_value), {state_change = true}) - end - - device:emit_event_for_endpoint(ib.endpoint_id, button_event) - else - log.info(string.format("Number of presses (%d) not supported by capability", press_value)) - end - end - set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, nil) -end - -local function battery_percent_remaining_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) - end -end - -local function battery_charge_level_attr_handler(driver, device, ib, response) - if ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.OK then - device:emit_event(capabilities.batteryLevel.battery.normal()) - elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.WARNING then - device:emit_event(capabilities.batteryLevel.battery.warning()) - elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.CRITICAL then - device:emit_event(capabilities.batteryLevel.battery.critical()) - end -end - -local function power_source_attribute_list_handler(driver, device, ib, response) - local profile_name = "" - - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - for _, attr in ipairs(ib.data.elements) do - -- Re-profile the device if BatPercentRemaining (Attribute ID 0x0C) or - -- BatChargeLevel (Attribute ID 0x0E) is present. - if attr.value == 0x0C then - profile_name = "button-battery" - break - elseif attr.value == 0x0E then - profile_name = "button-batteryLevel" - break - end - end - if profile_name ~= "" then - if #button_eps > 1 then - profile_name = string.format("%d-", #button_eps) .. profile_name +function SwitchLifecycleHandlers.device_init(driver, device) + if device.network_type == device_lib.NETWORK_TYPE_MATTER then + switch_utils.check_field_name_updates(device) + device:set_component_to_endpoint_fn(switch_utils.component_to_endpoint) + device:set_endpoint_to_component_fn(switch_utils.endpoint_to_component) + if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then + device:set_find_child(switch_utils.find_child) end - - if device.manufacturer_info.vendor_id == AQARA_MANUFACTURER_ID and - device.manufacturer_info.product_id == AQARA_CLIMATE_SENSOR_W100_ID then - profile_name = profile_name .. "-temperature-humidity" + if #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) == 0 then + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist = true}) end - device:try_update_metadata({ profile = profile_name }) - end -end - -local function max_press_handler(driver, device, ib, response) - local max = ib.data.value or 1 --get max number of presses - device.log.debug("Device supports "..max.." presses") - -- capability only supports up to 6 presses - if max > 6 then - log.info("Device supports more than 6 presses") - max = 6 - end - local MSL = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS}) - local supportsHeld = tbl_contains(MSL, ib.endpoint_id) - local values = create_multi_press_values_list(max, supportsHeld) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.supportedButtonValues(values, {visibility = {displayed = false}})) -end - -local function info_changed(driver, device, event, args) - if device.profile.id ~= args.old_st_store.profile.id then + device:extend_device("subscribe", switch_utils.subscribe) device:subscribe() - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - if #button_eps > 0 and device.network_type == device_lib.NETWORK_TYPE_MATTER then - configure_buttons(device) - end - end -end -local function device_added(driver, device) - -- refresh child devices to get an initial attribute state for OnOff in case child device - -- was created after the initial subscription report - if device.network_type == device_lib.NETWORK_TYPE_CHILD then - local req = clusters.OnOff.attributes.OnOff:read(device) - device:send(req) - end - - -- call device init in case init is not called after added due to device caching - device_init(driver, device) -end - -local function temperature_attr_handler(driver, device, ib, response) - local measured_value = ib.data.value - if measured_value ~= nil then - local temp = measured_value / 100.0 - local unit = "C" - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperature({value = temp, unit = unit})) - end -end - -local temp_attr_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return + -- device energy reporting must be handled cumulatively, periodically, or by both simultaneously. + -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. + if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID, + {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY}) > 0 then + device:set_field(fields.CUMULATIVE_REPORTS_SUPPORTED, true, {persist = false}) end - local temp = ib.data.value / 100.0 - local unit = "C" - set_field_for_endpoint(device, TEMP_BOUND_RECEIVED..minOrMax, ib.endpoint_id, temp) - local min = get_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MIN, ib.endpoint_id) - local max = get_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MAX, ib.endpoint_id) - if min ~= nil and max ~= nil then - if min < max then - -- Only emit the capability for RPC version >= 5 (unit conversion for - -- temperature range capability is only supported for RPC >= 5) - if version.rpc >= 5 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) - end - set_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MIN, ib.endpoint_id, nil) - set_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MAX, ib.endpoint_id, nil) - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) - end - end - end -end - -local function humidity_attr_handler(driver, device, ib, response) - local measured_value = ib.data.value - if measured_value ~= nil then - local humidity = utils.round(measured_value / 100.0) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) - end -end - -local function fan_mode_handler(driver, device, ib, response) - if ib.data.value == clusters.FanControl.attributes.FanMode.OFF then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("off")) - elseif ib.data.value == clusters.FanControl.attributes.FanMode.LOW then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("low")) - elseif ib.data.value == clusters.FanControl.attributes.FanMode.MEDIUM then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("medium")) - elseif ib.data.value == clusters.FanControl.attributes.FanMode.HIGH then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("high")) - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("auto")) - end -end - -local function fan_mode_sequence_handler(driver, device, ib, response) - local supportedFanModes - if ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH then - supportedFanModes = { - capabilities.fanMode.fanMode.off.NAME, - capabilities.fanMode.fanMode.low.NAME, - capabilities.fanMode.fanMode.medium.NAME, - capabilities.fanMode.fanMode.high.NAME - } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH then - supportedFanModes = { - capabilities.fanMode.fanMode.off.NAME, - capabilities.fanMode.fanMode.low.NAME, - capabilities.fanMode.fanMode.high.NAME - } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO then - supportedFanModes = { - capabilities.fanMode.fanMode.off.NAME, - capabilities.fanMode.fanMode.low.NAME, - capabilities.fanMode.fanMode.medium.NAME, - capabilities.fanMode.fanMode.high.NAME, - capabilities.fanMode.fanMode.auto.NAME - } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH_AUTO then - supportedFanModes = { - capabilities.fanMode.fanMode.off.NAME, - capabilities.fanMode.fanMode.low.NAME, - capabilities.fanMode.fanMode.high.NAME, - capabilities.fanMode.fanMode.auto.NAME - } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_ON_AUTO then - supportedFanModes = { - capabilities.fanMode.fanMode.off.NAME, - capabilities.fanMode.fanMode.high.NAME, - capabilities.fanMode.fanMode.auto.NAME - } - else - supportedFanModes = { - capabilities.fanMode.fanMode.off.NAME, - capabilities.fanMode.fanMode.high.NAME - } end - local event = capabilities.fanMode.supportedFanModes(supportedFanModes, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, event) end -local function fan_speed_percent_attr_handler(driver, device, ib, response) - if ib.data.value == nil or ib.data.value < 0 or ib.data.value > 100 then - return - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(ib.data.value)) +function SwitchLifecycleHandlers.device_removed(driver, device) + device.log.info("device removed") end local matter_driver_template = { lifecycle_handlers = { - init = device_init, - added = device_added, - removed = device_removed, - infoChanged = info_changed, - doConfigure = do_configure, - driverSwitched = driver_switched + added = SwitchLifecycleHandlers.device_added, + doConfigure = SwitchLifecycleHandlers.do_configure, + driverSwitched = SwitchLifecycleHandlers.driver_switched, + infoChanged = SwitchLifecycleHandlers.info_changed, + init = SwitchLifecycleHandlers.device_init, + removed = SwitchLifecycleHandlers.device_removed, }, matter_handlers = { attr = { - [clusters.OnOff.ID] = { - [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, - }, - [clusters.LevelControl.ID] = { - [clusters.LevelControl.attributes.CurrentLevel.ID] = level_attr_handler, - [clusters.LevelControl.attributes.MaxLevel.ID] = level_bounds_handler_factory(LEVEL_MAX), - [clusters.LevelControl.attributes.MinLevel.ID] = level_bounds_handler_factory(LEVEL_MIN), - }, [clusters.ColorControl.ID] = { - [clusters.ColorControl.attributes.CurrentHue.ID] = hue_attr_handler, - [clusters.ColorControl.attributes.CurrentSaturation.ID] = sat_attr_handler, - [clusters.ColorControl.attributes.ColorTemperatureMireds.ID] = temp_attr_handler, - [clusters.ColorControl.attributes.CurrentX.ID] = x_attr_handler, - [clusters.ColorControl.attributes.CurrentY.ID] = y_attr_handler, - [clusters.ColorControl.attributes.ColorMode.ID] = color_mode_attr_handler, - [clusters.ColorControl.attributes.ColorCapabilities.ID] = color_cap_attr_handler, - [clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds.ID] = mired_bounds_handler_factory(COLOR_TEMP_MIN), -- max mireds = min kelvin - [clusters.ColorControl.attributes.ColorTempPhysicalMinMireds.ID] = mired_bounds_handler_factory(COLOR_TEMP_MAX), -- min mireds = max kelvin + [clusters.ColorControl.attributes.ColorCapabilities.ID] = attribute_handlers.color_capabilities_handler, + [clusters.ColorControl.attributes.ColorMode.ID] = attribute_handlers.color_mode_handler, + [clusters.ColorControl.attributes.ColorTemperatureMireds.ID] = attribute_handlers.color_temperature_mireds_handler, + [clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds.ID] = attribute_handlers.color_temp_physical_mireds_bounds_factory(fields.COLOR_TEMP_MIN), -- max mireds = min kelvin + [clusters.ColorControl.attributes.ColorTempPhysicalMinMireds.ID] = attribute_handlers.color_temp_physical_mireds_bounds_factory(fields.COLOR_TEMP_MAX), -- min mireds = max kelvin + [clusters.ColorControl.attributes.CurrentHue.ID] = attribute_handlers.current_hue_handler, + [clusters.ColorControl.attributes.CurrentSaturation.ID] = attribute_handlers.current_saturation_handler, + [clusters.ColorControl.attributes.CurrentX.ID] = attribute_handlers.current_x_handler, + [clusters.ColorControl.attributes.CurrentY.ID] = attribute_handlers.current_y_handler, }, - [clusters.IlluminanceMeasurement.ID] = { - [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = illuminance_attr_handler + [clusters.Descriptor.ID] = { + [clusters.Descriptor.attributes.PartsList.ID] = attribute_handlers.parts_list_handler, }, - [clusters.OccupancySensing.ID] = { - [clusters.OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler, + [clusters.ElectricalEnergyMeasurement.ID] = { + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = attribute_handlers.energy_imported_factory(false), + [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = attribute_handlers.energy_imported_factory(true), }, [clusters.ElectricalPowerMeasurement.ID] = { - [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = active_power_handler, + [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = attribute_handlers.active_power_handler, }, - [clusters.ElectricalEnergyMeasurement.ID] = { - [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = energy_report_handler_factory(true), - [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = energy_report_handler_factory(false), + [clusters.FanControl.ID] = { + [clusters.FanControl.attributes.FanMode.ID] = attribute_handlers.fan_mode_handler, + [clusters.FanControl.attributes.FanModeSequence.ID] = attribute_handlers.fan_mode_sequence_handler, + [clusters.FanControl.attributes.PercentCurrent.ID] = attribute_handlers.percent_current_handler }, - [clusters.ValveConfigurationAndControl.ID] = { - [clusters.ValveConfigurationAndControl.attributes.CurrentState.ID] = valve_state_attr_handler, - [clusters.ValveConfigurationAndControl.attributes.CurrentLevel.ID] = valve_level_attr_handler + [clusters.IlluminanceMeasurement.ID] = { + [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.illuminance_measured_value_handler + }, + [clusters.LevelControl.ID] = { + [clusters.LevelControl.attributes.CurrentLevel.ID] = attribute_handlers.level_control_current_level_handler, + [clusters.LevelControl.attributes.MaxLevel.ID] = attribute_handlers.level_bounds_handler_factory(fields.LEVEL_MAX), + [clusters.LevelControl.attributes.MinLevel.ID] = attribute_handlers.level_bounds_handler_factory(fields.LEVEL_MIN), + }, + [clusters.OccupancySensing.ID] = { + [clusters.OccupancySensing.attributes.Occupancy.ID] = attribute_handlers.occupancy_handler, + }, + [clusters.OnOff.ID] = { + [clusters.OnOff.attributes.OnOff.ID] = attribute_handlers.on_off_attr_handler, }, [clusters.PowerSource.ID] = { - [clusters.PowerSource.attributes.AttributeList.ID] = power_source_attribute_list_handler, - [clusters.PowerSource.attributes.BatChargeLevel.ID] = battery_charge_level_attr_handler, - [clusters.PowerSource.attributes.BatPercentRemaining.ID] = battery_percent_remaining_attr_handler, + [clusters.PowerSource.attributes.AttributeList.ID] = attribute_handlers.power_source_attribute_list_handler, + [clusters.PowerSource.attributes.BatChargeLevel.ID] = attribute_handlers.bat_charge_level_handler, + [clusters.PowerSource.attributes.BatPercentRemaining.ID] = attribute_handlers.bat_percent_remaining_handler, }, - [clusters.Switch.ID] = { - [clusters.Switch.attributes.MultiPressMax.ID] = max_press_handler + [clusters.PowerTopology.ID] = { + [clusters.PowerTopology.attributes.AvailableEndpoints.ID] = attribute_handlers.available_endpoints_handler, }, [clusters.RelativeHumidityMeasurement.ID] = { - [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = humidity_attr_handler + [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.relative_humidity_measured_value_handler + }, + [clusters.Switch.ID] = { + [clusters.Switch.attributes.MultiPressMax.ID] = attribute_handlers.multi_press_max_handler }, [clusters.TemperatureMeasurement.ID] = { - [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = temperature_attr_handler, - [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = temp_attr_handler_factory(TEMP_MIN), - [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = temp_attr_handler_factory(TEMP_MAX), + [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.TEMP_MAX), + [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.temperature_measured_value_handler, + [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.TEMP_MIN), + }, + [clusters.ValveConfigurationAndControl.ID] = { + [clusters.ValveConfigurationAndControl.attributes.CurrentLevel.ID] = attribute_handlers.valve_configuration_current_level_handler, + [clusters.ValveConfigurationAndControl.attributes.CurrentState.ID] = attribute_handlers.valve_configuration_current_state_handler, }, - [clusters.FanControl.ID] = { - [clusters.FanControl.attributes.FanModeSequence.ID] = fan_mode_sequence_handler, - [clusters.FanControl.attributes.FanMode.ID] = fan_mode_handler, - [clusters.FanControl.attributes.PercentCurrent.ID] = fan_speed_percent_attr_handler - } }, event = { [clusters.Switch.ID] = { - [clusters.Switch.events.InitialPress.ID] = initial_press_event_handler, - [clusters.Switch.events.LongPress.ID] = long_press_event_handler, - [clusters.Switch.events.ShortRelease.ID] = short_release_event_handler, - [clusters.Switch.events.MultiPressComplete.ID] = multi_press_complete_event_handler + [clusters.Switch.events.InitialPress.ID] = event_handlers.initial_press_handler, + [clusters.Switch.events.LongPress.ID] = event_handlers.long_press_handler, + [clusters.Switch.events.MultiPressComplete.ID] = event_handlers.multi_press_complete_handler, + [clusters.Switch.events.ShortRelease.ID] = event_handlers.short_release_handler, } }, - fallback = matter_handler, + fallback = switch_utils.matter_handler, }, subscribed_attributes = { - [capabilities.switch.ID] = { - clusters.OnOff.attributes.OnOff + [capabilities.battery.ID] = { + clusters.PowerSource.attributes.BatPercentRemaining, }, - [capabilities.switchLevel.ID] = { - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, + [capabilities.batteryLevel.ID] = { + clusters.PowerSource.attributes.BatChargeLevel, }, [capabilities.colorControl.ID] = { clusters.ColorControl.attributes.ColorMode, @@ -1542,27 +226,28 @@ local matter_driver_template = { clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, }, + [capabilities.energyMeter.ID] = { + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported + }, + [capabilities.fanMode.ID] = { + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, + [capabilities.fanSpeedPercent.ID] = { + clusters.FanControl.attributes.PercentCurrent + }, [capabilities.illuminanceMeasurement.ID] = { clusters.IlluminanceMeasurement.attributes.MeasuredValue }, [capabilities.motionSensor.ID] = { clusters.OccupancySensing.attributes.Occupancy }, - [capabilities.valve.ID] = { - clusters.ValveConfigurationAndControl.attributes.CurrentState - }, [capabilities.level.ID] = { clusters.ValveConfigurationAndControl.attributes.CurrentLevel }, - [capabilities.battery.ID] = { - clusters.PowerSource.attributes.BatPercentRemaining, - }, - [capabilities.batteryLevel.ID] = { - clusters.PowerSource.attributes.BatChargeLevel, - }, - [capabilities.energyMeter.ID] = { - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported + [capabilities.switch.ID] = { + clusters.OnOff.attributes.OnOff }, [capabilities.powerMeter.ID] = { clusters.ElectricalPowerMeasurement.attributes.ActivePower @@ -1570,18 +255,19 @@ local matter_driver_template = { [capabilities.relativeHumidityMeasurement.ID] = { clusters.RelativeHumidityMeasurement.attributes.MeasuredValue }, + [capabilities.switchLevel.ID] = { + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + }, [capabilities.temperatureMeasurement.ID] = { clusters.TemperatureMeasurement.attributes.MeasuredValue, clusters.TemperatureMeasurement.attributes.MinMeasuredValue, clusters.TemperatureMeasurement.attributes.MaxMeasuredValue }, - [capabilities.fanMode.ID] = { - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode + [capabilities.valve.ID] = { + clusters.ValveConfigurationAndControl.attributes.CurrentState }, - [capabilities.fanSpeedPercent.ID] = { - clusters.FanControl.attributes.PercentCurrent - } }, subscribed_events = { [capabilities.button.ID] = { @@ -1592,71 +278,87 @@ local matter_driver_template = { }, }, capability_handlers = { - [capabilities.switch.ID] = { - [capabilities.switch.commands.on.NAME] = handle_switch_on, - [capabilities.switch.commands.off.NAME] = handle_switch_off, - }, - [capabilities.switchLevel.ID] = { - [capabilities.switchLevel.commands.setLevel.NAME] = handle_set_switch_level - }, [capabilities.colorControl.ID] = { - [capabilities.colorControl.commands.setColor.NAME] = handle_set_color, - [capabilities.colorControl.commands.setHue.NAME] = handle_set_hue, - [capabilities.colorControl.commands.setSaturation.NAME] = handle_set_saturation, + [capabilities.colorControl.commands.setColor.NAME] = capability_handlers.handle_set_color, + [capabilities.colorControl.commands.setHue.NAME] = capability_handlers.handle_set_hue, + [capabilities.colorControl.commands.setSaturation.NAME] = capability_handlers.handle_set_saturation, }, [capabilities.colorTemperature.ID] = { - [capabilities.colorTemperature.commands.setColorTemperature.NAME] = handle_set_color_temperature, + [capabilities.colorTemperature.commands.setColorTemperature.NAME] = capability_handlers.handle_set_color_temperature, }, - [capabilities.valve.ID] = { - [capabilities.valve.commands.open.NAME] = handle_valve_open, - [capabilities.valve.commands.close.NAME] = handle_valve_close - }, - [capabilities.level.ID] = { - [capabilities.level.commands.setLevel.NAME] = handle_set_level + [capabilities.energyMeter.ID] = { + [capabilities.energyMeter.commands.resetEnergyMeter.NAME] = capability_handlers.handle_reset_energy_meter, }, [capabilities.fanMode.ID] = { - [capabilities.fanMode.commands.setFanMode.NAME] = set_fan_mode + [capabilities.fanMode.commands.setFanMode.NAME] = capability_handlers.handle_set_fan_mode }, [capabilities.fanSpeedPercent.ID] = { - [capabilities.fanSpeedPercent.commands.setPercent.NAME] = set_fan_speed_percent - } + [capabilities.fanSpeedPercent.commands.setPercent.NAME] = capability_handlers.handle_fan_speed_set_percent + }, + [capabilities.level.ID] = { + [capabilities.level.commands.setLevel.NAME] = capability_handlers.handle_set_level + }, + [capabilities.statelessColorTemperatureStep.ID] = { + [capabilities.statelessColorTemperatureStep.commands.stepColorTemperatureByPercent.NAME] = capability_handlers.handle_step_color_temperature_by_percent, + }, + [capabilities.statelessSwitchLevelStep.ID] = { + [capabilities.statelessSwitchLevelStep.commands.stepLevel.NAME] = capability_handlers.handle_step_level, + }, + [capabilities.switch.ID] = { + [capabilities.switch.commands.off.NAME] = capability_handlers.handle_switch_off, + [capabilities.switch.commands.on.NAME] = capability_handlers.handle_switch_on, + }, + [capabilities.switchLevel.ID] = { + [capabilities.switchLevel.commands.setLevel.NAME] = capability_handlers.handle_switch_set_level + }, + [capabilities.valve.ID] = { + [capabilities.valve.commands.close.NAME] = capability_handlers.handle_valve_close, + [capabilities.valve.commands.open.NAME] = capability_handlers.handle_valve_open, + }, }, supported_capabilities = { - capabilities.switch, - capabilities.switchLevel, + capabilities.audioMute, + capabilities.audioRecording, + capabilities.audioVolume, + capabilities.battery, + capabilities.batteryLevel, + capabilities.button, + capabilities.cameraPrivacyMode, + capabilities.cameraViewportSettings, capabilities.colorControl, capabilities.colorTemperature, + capabilities.energyMeter, + capabilities.fanMode, + capabilities.fanSpeedPercent, + capabilities.hdr, + capabilities.illuminanceMeasurement, + capabilities.imageControl, capabilities.level, + capabilities.localMediaStorage, + capabilities.mechanicalPanTiltZoom, capabilities.motionSensor, - capabilities.illuminanceMeasurement, + capabilities.nightVision, capabilities.powerMeter, - capabilities.energyMeter, capabilities.powerConsumptionReport, - capabilities.valve, - capabilities.button, - capabilities.battery, - capabilities.batteryLevel, - capabilities.temperatureMeasurement, capabilities.relativeHumidityMeasurement, - capabilities.fanMode, - capabilities.fanSpeedPercent + capabilities.sounds, + capabilities.switch, + capabilities.switchLevel, + capabilities.temperatureMeasurement, + capabilities.valve, + capabilities.videoStreamSettings, + capabilities.webrtc, + capabilities.zoneManagement }, sub_drivers = { - require("eve-energy"), - require("aqara-cube"), - require("third-reality-mk1") + switch_utils.lazy_load_if_possible("sub_drivers.aqara_cube"), + switch_utils.lazy_load("sub_drivers.camera"), + switch_utils.lazy_load_if_possible("sub_drivers.eve_energy"), + switch_utils.lazy_load_if_possible("sub_drivers.ikea_scroll"), + switch_utils.lazy_load_if_possible("sub_drivers.third_reality_mk1") } } -function detect_matter_thing(device) - for _, capability in ipairs(matter_driver_template.supported_capabilities) do - if device:supports_capability(capability) then - return false - end - end - return device:supports_capability(capabilities.refresh) -end - local matter_driver = MatterDriver("matter-switch", matter_driver_template) log.info_with({hub_logs=true}, string.format("Starting %s driver, with dispatcher: %s", matter_driver.NAME, matter_driver.matter_dispatcher)) matter_driver:run() diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/can_handle.lua new file mode 100644 index 0000000000..dcecbc86ee --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local device_lib = require "st.device" + +return function(opts, driver, device) + if device.network_type == device_lib.NETWORK_TYPE_MATTER then + local name = string.format("%s", device.manufacturer_info.product_name) + if string.find(name, "Aqara Cube T1 Pro") then + return true, require("sub_drivers.aqara_cube") + end + end + return false +end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua new file mode 100644 index 0000000000..4e6b729fa3 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua @@ -0,0 +1,237 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local device_lib = require "st.device" +local log = require "log" + +local cubeAction = capabilities["stse.cubeAction"] +local cubeFace = capabilities["stse.cubeFace"] + +local COMPONENT_TO_ENDPOINT_MAP_BUTTON = "__component_to_endpoint_map_button" +local DEFERRED_CONFIGURE = "__DEFERRED_CONFIGURE" + +-- used in unit testing, since device.profile.id and args.old_st_store.profile.id are always the same +-- and this is to avoid the crash of the test case that occurs when try_update_metadata is performed in the device_init stage. +local TEST_CONFIGURE = "__test_configure" +local INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS (MomentarySwitch), but not MSR (MomentarySwitchRelease) + +-- after 3 seconds of cubeAction, to automatically change the action status of Plugin UI or Device Card to noAction +local CUBEACTION_TIMER = "__cubeAction_timer" +local CUBEACTION_TIME = 3 + +local callback_timer = function(device) + return function() + device:emit_event(cubeAction.cubeAction("noAction")) + end +end + +local function reset_thread(device) + local timer = device:get_field(CUBEACTION_TIMER) + if timer then + device.thread:cancel_timer(timer) + device:set_field(CUBEACTION_TIMER, nil) + end + device:set_field(CUBEACTION_TIMER, device.thread:call_with_delay(CUBEACTION_TIME, callback_timer(device))) +end + +local function get_field_for_endpoint(device, field, endpoint) + return device:get_field(string.format("%s_%d", field, endpoint)) +end + +local function set_field_for_endpoint(device, field, endpoint, value, persist) + device:set_field(string.format("%s_%d", field, endpoint), value, {persist = persist}) +end + +-- The endpoints of each face may increase sequentially, but may increase as in [250, 251, 2, 3, 4, 5] +-- and the current device:get_endpoints function is valid only for the former so, adds this function. +local function get_reordered_endpoints(driver, device) + if device.network_type ~= device_lib.NETWORK_TYPE_CHILD then + local MS = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + -- find the default/main endpoint, the device with the lowest EP that supports MS + table.sort(MS) + if MS[6] < (MS[1] + 150) then + -- When the endpoints of each face increase sequentially + -- The lowest EP is the main endpoint + -- as a workaround, it is assumed that the first endpoint number and the last endpoint number are not larger than 150. + return MS + else + -- When the endpoints of each face do not increase sequentially... [250, 251, 2, 3, 4, 5] 250 is the main endpoint. + -- For the situation where a node following these mechanisms has exhausted all available 65535 endpoint addresses for exposed entities, + -- it MAY wrap around to the lowest unused endpoint address (refter to Matter Core Spec 9.2.4. Dynamic Endpoint Allocation) + local ept1 = {} -- First consecutive end points + local ept2 = {} -- Second consecutive end points + local idx1 = 1 + local idx2 = 1 + local flag = 0 + local previous = 0 + for _, ep in ipairs(MS) do + if idx1 == 1 then + ept1[idx1] = ep + else + if flag == 0 + and ep <= (previous + 15) then + -- the endpoint number does not always increase by 1 + -- as a workaround, assume that the next endpoint number is not greater than 15 + ept1[idx1] = ep + else + ept2[idx2] = ep + idx2 = idx2 + 1 + if flag ~= 1 then + flag = 1 + end + end + end + idx1 = idx1 + 1 + previous = ep + end + + local start = #ept2 + 1 + idx1 = 1 + idx2 = start + for i=start, 6 do + ept2[idx2] = ept1[idx1] + idx1 = idx1 + 1 + idx2 = idx2 + 1 + end + return ept2 + end + end +end + +local function endpoint_to_component(device, endpoint) + return "main" +end + +-- This is called either on add for parent/child devices, or after the device profile changes for components +local function configure_buttons(device) + if device.network_type ~= device_lib.NETWORK_TYPE_CHILD then + local MS = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + device.log.debug(#MS.." momentary switch endpoints") + for _, ep in ipairs(MS) do + -- device only supports momentary switch, no release events + device.log.debug("configuring for press event only") + set_field_for_endpoint(device, INITIAL_PRESS_ONLY, ep, true, true) + end + end +end + +local function set_configure(driver, device) + local MS = get_reordered_endpoints(driver, device) + local main_endpoint + if #MS > 0 and MS[1] == 0 then -- we shouldn't hit this, but just in case + main_endpoint = MS[2] + elseif #MS > 0 then + main_endpoint = MS[1] -- matches to the non-child device + else + main_endpoint = device.MATTER_DEFAULT_ENDPOINT + end + device.log.debug_with({hub_logs = true}, "The main button endpoint for the Aqara T1 Pro is " .. main_endpoint) + + -- At the moment, we're taking it for granted that all momentary switches only have 2 positions + local current_component_number = 1 + local component_map = {} + for _, ep in ipairs(MS) do -- for each momentary switch endpoint (including main) + log.debug_with({hub_logs = true}, "Configuring endpoint: " .. ep) + -- build the mapping of endpoints to components + component_map[string.format("%d", current_component_number)] = ep + current_component_number = current_component_number + 1 + end + + device:set_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON, component_map, {persist = true}) + device:try_update_metadata({profile = "cube-t1-pro"}) + configure_buttons(device) +end + +local function device_init(driver, device) + if device.network_type == device_lib.NETWORK_TYPE_MATTER then + device:subscribe() + device:set_endpoint_to_component_fn(endpoint_to_component) + + -- when unit testing, call set_configure elsewhere + if not device:get_field(TEST_CONFIGURE) then + set_configure(driver, device) + end + end +end + +local function device_added(driver, device) + if device.network_type ~= device_lib.NETWORK_TYPE_CHILD then + device:set_field(DEFERRED_CONFIGURE, true) + end +end + +local function info_changed(driver, device, event, args) + -- for unit testing + if device:get_field(TEST_CONFIGURE) then + set_configure(driver, device) + end + + if (device.profile.id ~= args.old_st_store.profile.id or device:get_field(TEST_CONFIGURE)) + and device:get_field(DEFERRED_CONFIGURE) + and device.network_type ~= device_lib.NETWORK_TYPE_CHILD then + + reset_thread(device) + device:emit_event(cubeAction.cubeAction("flipToSide1")) + device:emit_event(cubeFace.cubeFace("face1Up")) + + device:set_field(DEFERRED_CONFIGURE, nil) + end +end + +-- override do_configure to prevent it running in the main driver +local function do_configure(driver, device) end + +-- override driver_switched to prevent it running in the main driver +local function driver_switched(driver, device) end + +local function initial_press_event_handler(driver, device, ib, response) + if get_field_for_endpoint(device, INITIAL_PRESS_ONLY, ib.endpoint_id) then + local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON) or {} + local face = 1 + for component, ep in pairs(map) do + if map[component] == ib.endpoint_id then + face = component + break + end + end + + reset_thread(device) + device:emit_event(cubeAction.cubeAction(string.format("flipToSide%d", face))) + device:emit_event(cubeFace.cubeFace(string.format("face%dUp", face))) + end +end + +local function battery_percent_remaining_attr_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) + end +end + +local aqara_cube_handler = { + NAME = "Aqara Cube Handler", + lifecycle_handlers = { + init = device_init, + added = device_added, + infoChanged = info_changed, + doConfigure = do_configure, + driverSwitched = driver_switched + }, + matter_handlers = { + attr = { + [clusters.PowerSource.ID] = { + [clusters.PowerSource.attributes.BatPercentRemaining.ID] = battery_percent_remaining_attr_handler + } + }, + event = { + [clusters.Switch.ID] = { + [clusters.Switch.events.InitialPress.ID] = initial_press_event_handler + } + }, + }, + can_handle = require("sub_drivers.aqara_cube.can_handle") +} + +return aqara_cube_handler + diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua new file mode 100644 index 0000000000..df99d50f94 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua @@ -0,0 +1,402 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local camera_utils = require "sub_drivers.camera.camera_utils.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" +local fields = require "switch_utils.fields" +local utils = require "st.utils" + +local CameraAttributeHandlers = {} + +CameraAttributeHandlers.enabled_state_factory = function(attribute) + return function(driver, device, ib, response) + device:emit_event_for_endpoint(ib, attribute(ib.data.value and "enabled" or "disabled")) + if attribute == capabilities.imageControl.imageFlipHorizontal then + camera_utils.update_supported_attributes(device, ib, capabilities.imageControl, "imageFlipHorizontal") + elseif attribute == capabilities.imageControl.imageFlipVertical then + camera_utils.update_supported_attributes(device, ib, capabilities.imageControl, "imageFlipVertical") + elseif attribute == capabilities.cameraPrivacyMode.hardPrivacyMode then + camera_utils.update_supported_attributes(device, ib, capabilities.cameraPrivacyMode, "hardPrivacyMode") + end + end +end + +CameraAttributeHandlers.night_vision_factory = function(attribute) + return function(driver, device, ib, response) + if camera_fields.tri_state_map[ib.data.value] then + device:emit_event_for_endpoint(ib, attribute(camera_fields.tri_state_map[ib.data.value])) + if attribute == capabilities.nightVision.illumination then + local _ = device:get_latest_state(camera_fields.profile_components.main, capabilities.nightVision.ID, capabilities.nightVision.supportedAttributes.NAME) or + device:emit_event_for_endpoint(ib, capabilities.nightVision.supportedAttributes({"illumination"})) + end + end + end +end + +function CameraAttributeHandlers.image_rotation_handler(driver, device, ib, response) + local degrees = utils.clamp_value(ib.data.value, 0, 359) + device:emit_event_for_endpoint(ib, capabilities.imageControl.imageRotation(degrees)) + camera_utils.update_supported_attributes(device, ib, capabilities.imageControl, "imageRotation") +end + +function CameraAttributeHandlers.two_way_talk_support_handler(driver, device, ib, response) + local two_way_talk_supported = ib.data.value == clusters.CameraAvStreamManagement.types.TwoWayTalkSupportTypeEnum.HALF_DUPLEX or + ib.data.value == clusters.CameraAvStreamManagement.types.TwoWayTalkSupportTypeEnum.FULL_DUPLEX + device:emit_event_for_endpoint(ib, capabilities.webrtc.talkback(two_way_talk_supported)) + if two_way_talk_supported then + device:emit_event_for_endpoint(ib, capabilities.webrtc.talkbackDuplex( + ib.data.value == clusters.CameraAvStreamManagement.types.TwoWayTalkSupportTypeEnum.HALF_DUPLEX and "halfDuplex" or "fullDuplex" + )) + end +end + +function CameraAttributeHandlers.muted_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, capabilities.audioMute.mute(ib.data.value and "muted" or "unmuted")) +end + +function CameraAttributeHandlers.volume_level_handler(driver, device, ib, response) + local component = device:endpoint_to_component(ib) + local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. component) or camera_fields.ABS_VOL_MAX + local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. component) or camera_fields.ABS_VOL_MIN + -- Convert from [min_volume, max_volume] to [0, 100] before emitting capability + local limited_range = max_volume - min_volume + local normalized_volume = utils.round((ib.data.value - min_volume) * 100.0 / limited_range) + device:emit_event_for_endpoint(ib, capabilities.audioVolume.volume(normalized_volume)) +end + +function CameraAttributeHandlers.max_volume_level_handler(driver, device, ib, response) + local component = device:endpoint_to_component(ib) + local max_volume = ib.data.value + local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. component) + if max_volume > camera_fields.ABS_VOL_MAX or (min_volume and max_volume <= min_volume) then + device.log.warn(string.format("Device reported invalid maximum (%d) %s volume level range value", ib.data.value, component)) + max_volume = camera_fields.ABS_VOL_MAX + end + device:set_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. component, max_volume) +end + +function CameraAttributeHandlers.min_volume_level_handler(driver, device, ib, response) + local component = device:endpoint_to_component(ib) + local min_volume = ib.data.value + local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. component) + if min_volume < camera_fields.ABS_VOL_MIN or (max_volume and min_volume >= max_volume) then + device.log.warn(string.format("Device reported invalid minimum (%d) %s volume level range value", ib.data.value, component)) + min_volume = camera_fields.ABS_VOL_MIN + end + device:set_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. component, min_volume) +end + +function CameraAttributeHandlers.status_light_enabled_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, ib.data.value and capabilities.switch.switch.on() or capabilities.switch.switch.off()) +end + +function CameraAttributeHandlers.status_light_brightness_handler(driver, device, ib, response) + local component = device:endpoint_to_component(ib) + local _ = device:get_latest_state(component, capabilities.mode.ID, capabilities.mode.supportedModes.NAME) or + device:emit_event_for_endpoint(ib, capabilities.mode.supportedModes({"low", "medium", "high", "auto"}, {visibility = {displayed = false}})) + local _ = device:get_latest_state(component, capabilities.mode.ID, capabilities.mode.supportedArguments.NAME) or + device:emit_event_for_endpoint(ib, capabilities.mode.supportedArguments({"low", "medium", "high", "auto"}, {visibility = {displayed = false}})) + local mode = "auto" + if ib.data.value == clusters.Global.types.ThreeLevelAutoEnum.LOW then + mode = "low" + elseif ib.data.value == clusters.Global.types.ThreeLevelAutoEnum.MEDIUM then + mode = "medium" + elseif ib.data.value == clusters.Global.types.ThreeLevelAutoEnum.HIGH then + mode = "high" + end + device:emit_event_for_endpoint(ib, capabilities.mode.mode(mode)) +end + +function CameraAttributeHandlers.rate_distortion_trade_off_points_handler(driver, device, ib, response) + if not ib.data.elements then return end + local resolutions = {} + for _, v in ipairs(ib.data.elements) do + local rate_distortion_trade_off_points = v.elements + local width = rate_distortion_trade_off_points.resolution.elements.width.value + local height = rate_distortion_trade_off_points.resolution.elements.height.value + table.insert(resolutions, { + width = width, + height = height + }) + end + device:set_field(camera_fields.SUPPORTED_RESOLUTIONS, resolutions) + local max_encoded_pixel_rate = device:get_field(camera_fields.MAX_ENCODED_PIXEL_RATE) + local max_fps = device:get_field(camera_fields.MAX_FRAMES_PER_SECOND) + if max_encoded_pixel_rate and max_fps and device:get_field(camera_fields.MAX_RESOLUTION) and device:get_field(camera_fields.MIN_RESOLUTION) then + local supported_resolutions = camera_utils.build_supported_resolutions(device, max_encoded_pixel_rate, max_fps) + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(supported_resolutions)) + end +end + +function CameraAttributeHandlers.max_encoded_pixel_rate_handler(driver, device, ib, response) + device:set_field(camera_fields.MAX_ENCODED_PIXEL_RATE, ib.data.value) + local max_fps = device:get_field(camera_fields.MAX_FRAMES_PER_SECOND) + if max_fps and device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) and device:get_field(camera_fields.MAX_RESOLUTION) and device:get_field(camera_fields.MIN_RESOLUTION) then + local supported_resolutions = camera_utils.build_supported_resolutions(device, ib.data.value, max_fps) + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(supported_resolutions)) + end +end + +function CameraAttributeHandlers.video_sensor_parameters_handler(driver, device, ib, response) + if not ib.data.elements then return end + local sensor_width = ib.data.elements.sensor_width.value + local sensor_height = ib.data.elements.sensor_height.value + local max_fps = ib.data.elements.max_fps.value + device:set_field(camera_fields.MAX_RESOLUTION, { + width = sensor_width, + height = sensor_height + }) + device:set_field(camera_fields.MAX_FRAMES_PER_SECOND, max_fps) + device:emit_event_for_endpoint(ib, capabilities.cameraViewportSettings.videoSensorParameters({ + width = sensor_width, + height = sensor_height, + maxFPS = max_fps + })) + local max_encoded_pixel_rate = device:get_field(camera_fields.MAX_ENCODED_PIXEL_RATE) + if max_encoded_pixel_rate and max_fps and device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) and device:get_field(camera_fields.MIN_RESOLUTION) then + local supported_resolutions = camera_utils.build_supported_resolutions(device, max_encoded_pixel_rate, max_fps) + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(supported_resolutions)) + end +end + +function CameraAttributeHandlers.min_viewport_handler(driver, device, ib, response) + if not ib.data.elements then return end + device:emit_event_for_endpoint(ib, capabilities.cameraViewportSettings.minViewportResolution({ + width = ib.data.elements.width.value, + height = ib.data.elements.height.value + })) + device:set_field(camera_fields.MIN_RESOLUTION, { + width = ib.data.elements.width.value, + height = ib.data.elements.height.value + }) + local max_encoded_pixel_rate = device:get_field(camera_fields.MAX_ENCODED_PIXEL_RATE) + local max_fps = device:get_field(camera_fields.MAX_FRAMES_PER_SECOND) + if max_encoded_pixel_rate and max_fps and device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) and device:get_field(camera_fields.MAX_RESOLUTION) then + local supported_resolutions = camera_utils.build_supported_resolutions(device, max_encoded_pixel_rate, max_fps) + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(supported_resolutions)) + end +end + +function CameraAttributeHandlers.allocated_video_streams_handler(driver, device, ib, response) + if not ib.data.elements then return end + local streams = {} + for i, v in ipairs(ib.data.elements) do + local stream = v.elements + local video_stream = { + streamId = stream.video_stream_id.value, + data = { + label = "Stream " .. i, + type = stream.stream_usage.value == clusters.Global.types.StreamUsageEnum.LIVE_VIEW and "liveStream" or "clipRecording", + resolution = { + width = stream.min_resolution.elements.width.value, + height = stream.min_resolution.elements.height.value, + fps = stream.min_frame_rate.value + } + } + } + local viewport = device:get_field(camera_fields.VIEWPORT) + if viewport then + video_stream.data.viewport = viewport + end + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.WATERMARK) then + video_stream.data.watermark = stream.watermark_enabled.value and "enabled" or "disabled" + end + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.ON_SCREEN_DISPLAY) then + video_stream.data.onScreenDisplay = stream.osd_enabled.value and "enabled" or "disabled" + end + table.insert(streams, video_stream) + end + if #streams > 0 then + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.videoStreams(streams)) + end +end + +function CameraAttributeHandlers.viewport_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, capabilities.cameraViewportSettings.defaultViewport({ + upperLeftVertex = { x = ib.data.elements.x1.value, y = ib.data.elements.y1.value }, + lowerRightVertex = { x = ib.data.elements.x2.value, y = ib.data.elements.y2.value }, + })) +end + +function CameraAttributeHandlers.ptz_position_handler(driver, device, ib, response) + local ptz_map = camera_utils.get_ptz_map(device) + local emit_event = function(idx, value) + if value ~= ptz_map[idx].current then + device:emit_event_for_endpoint(ib, ptz_map[idx].attribute( + utils.clamp_value(value, ptz_map[idx].range.minimum, ptz_map[idx].range.maximum) + )) + end + end + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPAN) then + emit_event(camera_fields.PAN_IDX, ib.data.elements.pan.value) + end + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MTILT) then + emit_event(camera_fields.TILT_IDX, ib.data.elements.tilt.value) + end + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MZOOM) then + emit_event(camera_fields.ZOOM_IDX, ib.data.elements.zoom.value) + end +end + +function CameraAttributeHandlers.ptz_presets_handler(driver, device, ib, response) + if not ib.data.elements then return end + local presets = {} + for _, v in ipairs(ib.data.elements) do + local preset = v.elements + local pan, tilt, zoom = 0, 0, 1 + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPAN) then + pan = preset.settings.elements.pan.value + end + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MTILT) then + tilt = preset.settings.elements.tilt.value + end + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MZOOM) then + zoom = preset.settings.elements.zoom.value + end + table.insert(presets, { id = preset.preset_id.value, label = preset.name.value, pan = pan, tilt = tilt, zoom = zoom }) + end + device:emit_event_for_endpoint(ib, capabilities.mechanicalPanTiltZoom.presets(presets)) +end + +function CameraAttributeHandlers.max_presets_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, capabilities.mechanicalPanTiltZoom.maxPresets(ib.data.value)) +end + +function CameraAttributeHandlers.zoom_max_handler(driver, device, ib, response) + if ib.data.value <= camera_fields.ABS_ZOOM_MAX then + device:emit_event_for_endpoint(ib, capabilities.mechanicalPanTiltZoom.zoomRange({ value = { minimum = 1, maximum = ib.data.value } })) + else + device.log.warn(string.format("Device reported invalid maximum zoom (%d)", ib.data.value)) + end +end + +CameraAttributeHandlers.pt_range_handler_factory = function(attribute, limit_field) + return function(driver, device, ib, response) + device:set_field(limit_field, ib.data.value) + local field = string.find(limit_field, "PAN") and "PAN" or "TILT" + local min = device:get_field(camera_fields.pt_range_fields[field].min) + local max = device:get_field(camera_fields.pt_range_fields[field].max) + if min ~= nil and max ~= nil then + local abs_min = field == "PAN" and camera_fields.ABS_PAN_MIN or camera_fields.ABS_TILT_MIN + local abs_max = field == "PAN" and camera_fields.ABS_PAN_MAX or camera_fields.ABS_TILT_MAX + if min < max and min >= abs_min and max <= abs_max then + device:emit_event_for_endpoint(ib, attribute({ value = { minimum = min, maximum = max } })) + device:set_field(camera_fields.pt_range_fields[field].min, nil) + device:set_field(camera_fields.pt_range_fields[field].max, nil) + else + device.log.warn(string.format("Device reported invalid minimum (%d) and maximum (%d) %s " .. + "range values (should be between %d and %d)", min, max, string.lower(field), abs_min, abs_max)) + end + end + end +end + +function CameraAttributeHandlers.max_zones_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, capabilities.zoneManagement.maxZones(ib.data.value)) +end + +function CameraAttributeHandlers.zones_handler(driver, device, ib, response) + if not ib.data.elements then return end + local zones = {} + for _, v in ipairs(ib.data.elements) do + local zone = v.elements + local zone_id = zone.zone_id.value + local zone_type = zone.zone_type.value + local zone_source = zone.zone_source.value + local zone_vertices = {} + if camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.TWO_DIMENSIONAL_CARTESIAN_ZONE) and + zone_type == clusters.ZoneManagement.types.ZoneTypeEnum.TWODCART_ZONE then + local zone_name = zone.two_d_cartesian_zone.elements.name.value + local zone_use = zone.two_d_cartesian_zone.elements.use.value + for _, vertex in pairs(zone.two_d_cartesian_zone.elements.vertices.elements or {}) do + table.insert(zone_vertices, {vertex = {x = vertex.elements.x.value, y = vertex.elements.y.value}}) + end + local zone_uses = { + [clusters.ZoneManagement.types.ZoneUseEnum.MOTION] = "motion", + [clusters.ZoneManagement.types.ZoneUseEnum.FOCUS] = "focus", + [clusters.ZoneManagement.types.ZoneUseEnum.PRIVACY] = "privacy" + } + local zone_color = zone.two_d_cartesian_zone.elements.color and zone.two_d_cartesian_zone.elements.color.value or nil + table.insert(zones, { + id = zone_id, + name = zone_name, + type = "2DCartesian", + polygonVertices = zone_vertices, + source = zone_source == clusters.ZoneManagement.types.ZoneSourceEnum.MFG and "manufacturer" or "user", + use = zone_uses[zone_use], + color = zone_color + }) + else + device.log.warn(string.format("Zone type not currently supported: (%s)", zone_type)) + end + end + device:emit_event_for_endpoint(ib, capabilities.zoneManagement.zones({value = zones})) +end + +function CameraAttributeHandlers.triggers_handler(driver, device, ib, response) + if not ib.data.elements then return end + local triggers = {} + for _, v in ipairs(ib.data.elements) do + local trigger = v.elements + table.insert(triggers, { + zoneId = trigger.zone_id.value, + initialDuration = trigger.initial_duration.value, + augmentationDuration = trigger.augmentation_duration.value, + maxDuration = trigger.max_duration.value, + blindDuration = trigger.blind_duration.value, + sensitivity = camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) and trigger.sensitivity.value + }) + end + device:emit_event_for_endpoint(ib, capabilities.zoneManagement.triggers(triggers)) +end + +function CameraAttributeHandlers.sensitivity_max_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, capabilities.zoneManagement.sensitivityRange({minimum = 1, maximum = ib.data.value}, + {visibility = {displayed = false}})) +end + +function CameraAttributeHandlers.sensitivity_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, capabilities.zoneManagement.sensitivity(ib.data.value, {visibility = {displayed = false}})) +end + +function CameraAttributeHandlers.installed_chime_sounds_handler(driver, device, ib, response) + if not ib.data.elements then return end + local installed_chimes = {} + for _, v in ipairs(ib.data.elements) do + local chime = v.elements + table.insert(installed_chimes, {id = chime.chime_id.value, label = chime.name.value}) + end + device:emit_event_for_endpoint(ib, capabilities.sounds.supportedSounds(installed_chimes, {visibility = {displayed = false}})) +end + +function CameraAttributeHandlers.selected_chime_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, capabilities.sounds.selectedSound(ib.data.value)) +end + +function CameraAttributeHandlers.camera_av_stream_management_attribute_list_handler(driver, device, ib, response) + if not ib.data.elements then return end + local status_light_enabled_present, status_light_brightness_present = false, false + local attribute_ids = {} + for _, attr in ipairs(ib.data.elements) do + if attr.value == clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID then + status_light_enabled_present = true + table.insert(attribute_ids, clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID) + elseif attr.value == clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID then + status_light_brightness_present = true + table.insert(attribute_ids, clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) + end + end + local component_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {} + component_map.statusLed = { + endpoint_id = ib.endpoint_id, + cluster_id = ib.cluster_id, + attribute_ids = attribute_ids, + } + device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist=true}) + camera_cfg.match_profile(device, status_light_enabled_present, status_light_brightness_present) +end + +return CameraAttributeHandlers \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua new file mode 100644 index 0000000000..09134a9757 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua @@ -0,0 +1,365 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local camera_utils = require "sub_drivers.camera.camera_utils.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local utils = require "st.utils" + +local CameraCapabilityHandlers = {} + +CameraCapabilityHandlers.set_enabled_factory = function(attribute) + return function(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(attribute:write(device, endpoint_id, cmd.args.state == "enabled")) + end +end + +CameraCapabilityHandlers.set_night_vision_factory = function(attribute) + return function(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + for i, v in pairs(camera_fields.tri_state_map) do + if v == cmd.args.mode then + device:send(attribute:write(device, endpoint_id, i)) + return + end + end + device.log.warn(string.format("Capability command sent with unknown value: (%s)", cmd.args.mode)) + end +end + +function CameraCapabilityHandlers.handle_set_image_rotation(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local degrees = utils.clamp_value(cmd.args.rotation, 0, 359) + device:send(clusters.CameraAvStreamManagement.attributes.ImageRotation:write(device, endpoint_id, degrees)) +end + +CameraCapabilityHandlers.handle_mute_commands_factory = function(command) + return function(driver, device, cmd) + local attr + if cmd.component == camera_fields.profile_components.speaker then + attr = clusters.CameraAvStreamManagement.attributes.SpeakerMuted + elseif cmd.component == camera_fields.profile_components.microphone then + attr = clusters.CameraAvStreamManagement.attributes.MicrophoneMuted + else + device.log.warn(string.format("Capability command sent from unknown component: (%s)", cmd.component)) + return + end + local endpoint_id = device:component_to_endpoint(cmd.component) + local mute_state = false + if command == capabilities.audioMute.commands.setMute.NAME then + mute_state = cmd.args.state == "muted" + elseif command == capabilities.audioMute.commands.mute.NAME then + mute_state = true + end + device:send(attr:write(device, endpoint_id, mute_state)) + end +end + +function CameraCapabilityHandlers.handle_set_volume(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MAX + local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MIN + -- Convert from [0, 100] to [min_volume, max_volume] before writing attribute + local volume_range = max_volume - min_volume + local volume = utils.round(cmd.args.volume * volume_range / 100.0 + min_volume) + if cmd.component == camera_fields.profile_components.speaker then + device:send(clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:write(device, endpoint_id, volume)) + elseif cmd.component == camera_fields.profile_components.microphone then + device:send(clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:write(device, endpoint_id, volume)) + else + device.log.warn(string.format("Capability command sent from unknown component: (%s)", cmd.component)) + end +end + +function CameraCapabilityHandlers.handle_volume_up(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MAX + local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MIN + local volume = device:get_latest_state(cmd.component, capabilities.audioVolume.ID, capabilities.audioVolume.volume.NAME) + if not volume or volume >= max_volume then return end + -- Convert from [0, 100] to [min_volume, max_volume] before writing attribute + local volume_range = max_volume - min_volume + local converted_volume = utils.round((volume + 1) * volume_range / 100.0 + min_volume) + if cmd.component == camera_fields.profile_components.speaker then + device:send(clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:write(device, endpoint_id, converted_volume)) + elseif cmd.component == camera_fields.profile_components.microphone then + device:send(clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:write(device, endpoint_id, converted_volume)) + end +end + +function CameraCapabilityHandlers.handle_volume_down(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MAX + local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MIN + local volume = device:get_latest_state(cmd.component, capabilities.audioVolume.ID, capabilities.audioVolume.volume.NAME) + if not volume or volume <= min_volume then return end + -- Convert from [0, 100] to [min_volume, max_volume] before writing attribute + local volume_range = max_volume - min_volume + local converted_volume = utils.round((volume - 1) * volume_range / 100.0 + min_volume) + if cmd.component == camera_fields.profile_components.speaker then + device:send(clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:write(device, endpoint_id, converted_volume)) + elseif cmd.component == camera_fields.profile_components.microphone then + device:send(clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:write(device, endpoint_id, converted_volume)) + end +end + +function CameraCapabilityHandlers.handle_set_status_light_mode(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local level_auto_value + if cmd.args.mode == "low" then level_auto_value = "LOW" + elseif cmd.args.mode == "medium" then level_auto_value = "MEDIUM" + elseif cmd.args.mode == "high" then level_auto_value = "HIGH" + elseif cmd.args.mode == "auto" then level_auto_value = "AUTO" end + if not level_auto_value then + device.log.warn(string.format("Invalid mode received from setMode command: %s", cmd.args.mode)) + return + end + device:send(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:write(device, endpoint_id, + clusters.Global.types.ThreeLevelAutoEnum[level_auto_value])) +end + +function CameraCapabilityHandlers.handle_status_led_on(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:write(device, endpoint_id, true)) +end + +function CameraCapabilityHandlers.handle_status_led_off(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:write(device, endpoint_id, false)) +end + +function CameraCapabilityHandlers.handle_audio_recording(driver, device, cmd) + -- TODO: Allocate audio stream if it doesn't exist + local component = device.profile.components[cmd.component] + device:emit_component_event(component, capabilities.audioRecording.audioRecording(cmd.args.state)) +end + +CameraCapabilityHandlers.ptz_relative_move_factory = function(index) + return function (driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local pan_delta = index == camera_fields.PAN_IDX and cmd.args.delta or 0 + local tilt_delta = index == camera_fields.TILT_IDX and cmd.args.delta or 0 + local zoom_delta = index == camera_fields.ZOOM_IDX and cmd.args.delta or 0 + device:send(clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRelativeMove( + device, endpoint_id, pan_delta, tilt_delta, zoom_delta + )) + end +end + +CameraCapabilityHandlers.ptz_set_position_factory = function(command) + return function (driver, device, cmd) + local ptz_map = camera_utils.get_ptz_map(device) + if command == capabilities.mechanicalPanTiltZoom.commands.setPanTiltZoom then + ptz_map[camera_fields.PAN_IDX].current = cmd.args.pan + ptz_map[camera_fields.TILT_IDX].current = cmd.args.tilt + ptz_map[camera_fields.ZOOM_IDX].current = cmd.args.zoom + elseif command == capabilities.mechanicalPanTiltZoom.commands.setPan then + ptz_map[camera_fields.PAN_IDX].current = cmd.args.pan + elseif command == capabilities.mechanicalPanTiltZoom.commands.setTilt then + ptz_map[camera_fields.TILT_IDX].current = cmd.args.tilt + else + ptz_map[camera_fields.ZOOM_IDX].current = cmd.args.zoom + end + for _, v in pairs(ptz_map) do + v.current = utils.clamp_value(v.current, v.range.minimum, v.range.maximum) + end + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSetPosition(device, endpoint_id, + ptz_map[camera_fields.PAN_IDX].current, ptz_map[camera_fields.TILT_IDX].current, ptz_map[camera_fields.ZOOM_IDX].current + )) + end +end + +function CameraCapabilityHandlers.handle_save_preset(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSavePreset( + device, endpoint_id, cmd.args.id, cmd.args.label + )) +end + +function CameraCapabilityHandlers.handle_remove_preset(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRemovePreset(device, endpoint_id, cmd.args.id)) +end + +function CameraCapabilityHandlers.handle_move_to_preset(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZMoveToPreset(device, endpoint_id, cmd.args.id)) +end + +function CameraCapabilityHandlers.handle_new_zone(driver, device, cmd) + local zone_uses = { + ["motion"] = clusters.ZoneManagement.types.ZoneUseEnum.MOTION, + ["focus"] = camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.FOCUSZONES) and + clusters.ZoneManagement.types.ZoneUseEnum.FOCUS or clusters.ZoneManagement.types.ZoneUseEnum.PRIVACY, + ["privacy"] = clusters.ZoneManagement.types.ZoneUseEnum.PRIVACY + } + local vertices = {} + for _, v in pairs(cmd.args.polygonVertices or {}) do + table.insert(vertices, clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = v.value.x, y = v.value.y})) + end + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.ZoneManagement.server.commands.CreateTwoDCartesianZone( + device, endpoint_id, clusters.ZoneManagement.types.TwoDCartesianZoneStruct( + { + name = cmd.args.name, + use = zone_uses[cmd.args.use], + vertices = vertices, + color = cmd.args.color + } + ) + )) +end + +function CameraCapabilityHandlers.handle_update_zone(driver, device, cmd) + local zone_uses = { + ["motion"] = clusters.ZoneManagement.types.ZoneUseEnum.MOTION, + ["focus"] = camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.FOCUSZONES) and + clusters.ZoneManagement.types.ZoneUseEnum.FOCUS or clusters.ZoneManagement.types.ZoneUseEnum.PRIVACY, + ["privacy"] = clusters.ZoneManagement.types.ZoneUseEnum.PRIVACY + } + if not cmd.args.name or not cmd.args.polygonVertices or not cmd.args.use or not cmd.args.color then + local zones = device:get_latest_state( + camera_fields.profile_components.main, capabilities.zoneManagement.ID, capabilities.zoneManagement.zones.NAME + ) or {} + local found_zone = false + for _, v in pairs(zones) do + if v.id == cmd.args.zoneId then + if not cmd.args.name then cmd.args.name = v.name end + if not cmd.args.polygonVertices then cmd.args.polygonVertices = v.polygonVertices end + if not cmd.args.use then cmd.args.use = v.use end + if not cmd.args.color then cmd.args.color = v.color end -- color may be nil, but it is optional in TwoDCartesianZoneStruct + found_zone = true + break + end + end + if not found_zone then + device.log.warn_with({hub_logs = true}, string.format("Zone does not exist, cannot update the zone.")) + return + end + end + local vertices = {} + for _, v in pairs(cmd.args.polygonVertices or {}) do + table.insert(vertices, clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = v.value.x, y = v.value.y})) + end + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.ZoneManagement.server.commands.UpdateTwoDCartesianZone( + device, endpoint_id, cmd.args.zoneId, clusters.ZoneManagement.types.TwoDCartesianZoneStruct( + { + name = cmd.args.name, + use = zone_uses[cmd.args.use], + vertices = vertices, + color = cmd.args.color + } + ) + )) +end + +function CameraCapabilityHandlers.handle_remove_zone(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local triggers = device:get_latest_state( + camera_fields.profile_components.main, capabilities.zoneManagement.ID, capabilities.zoneManagement.triggers.NAME + ) or {} + for _, v in pairs(triggers) do + if v.zoneId == cmd.args.zoneId then + device:send(clusters.ZoneManagement.server.commands.RemoveTrigger(device, endpoint_id, cmd.args.zoneId)) + break + end + end + device:send(clusters.ZoneManagement.server.commands.RemoveZone(device, endpoint_id, cmd.args.zoneId)) +end + +function CameraCapabilityHandlers.handle_create_or_update_trigger(driver, device, cmd) + if not cmd.args.augmentationDuration or not cmd.args.maxDuration or not cmd.args.blindDuration or + (camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) and + not cmd.args.sensitivity) then + local triggers = device:get_latest_state( + camera_fields.profile_components.main, capabilities.zoneManagement.ID, capabilities.zoneManagement.triggers.NAME + ) or {} + local found_trigger = false + for _, v in pairs(triggers) do + if v.zoneId == cmd.args.zoneId then + if not cmd.args.augmentationDuration then cmd.args.augmentationDuration = v.augmentationDuration end + if not cmd.args.maxDuration then cmd.args.maxDuration = v.maxDuration end + if not cmd.args.blindDuration then cmd.args.blindDuration = v.blindDuration end + if camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) and + not cmd.args.sensitivity then + cmd.args.sensitivity = v.sensitivity + end + found_trigger = true + break + end + end + if not found_trigger then + device.log.warn_with({hub_logs = true}, string.format("Missing fields needed to create trigger.")) + return + end + end + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.ZoneManagement.server.commands.CreateOrUpdateTrigger( + device, endpoint_id, clusters.ZoneManagement.types.ZoneTriggerControlStruct( + { + zone_id = cmd.args.zoneId, + initial_duration = cmd.args.initialDuration, + augmentation_duration = cmd.args.augmentationDuration, + max_duration = cmd.args.maxDuration, + blind_duration = cmd.args.blindDuration, + sensitivity = cmd.args.sensitivity + } + ) + )) +end + +function CameraCapabilityHandlers.handle_remove_trigger(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.ZoneManagement.server.commands.RemoveTrigger(device, endpoint_id, cmd.args.zoneId)) +end + +function CameraCapabilityHandlers.handle_set_sensitivity(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + if not camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) then + device:send(clusters.ZoneManagement.attributes.Sensitivity:write(device, endpoint_id, cmd.args.id)) + else + device.log.warn(string.format("Can't set global zone sensitivity setting, per zone sensitivity enabled.")) + end +end + +function CameraCapabilityHandlers.handle_play_sound(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.Chime.server.commands.PlayChimeSound(device, endpoint_id)) +end + +function CameraCapabilityHandlers.handle_set_selected_sound(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.Chime.attributes.SelectedChime:write(device, endpoint_id, cmd.args.id)) +end + +function CameraCapabilityHandlers.handle_set_stream(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local watermark_enabled, on_screen_display_enabled + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.WATERMARK) then + watermark_enabled = cmd.args.watermark == "enabled" + end + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.ON_SCREEN_DISPLAY) then + on_screen_display_enabled = cmd.args.onScreenDisplay == "enabled" + end + device:send(clusters.CameraAvStreamManagement.server.commands.VideoStreamModify(device, endpoint_id, + cmd.args.streamId, watermark_enabled, on_screen_display_enabled + )) +end + +function CameraCapabilityHandlers.handle_set_default_viewport(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.CameraAvStreamManagement.attributes.Viewport:write( + device, endpoint_id, clusters.Global.types.ViewportStruct({ + x1 = cmd.args.upperLeftVertex.x, + x2 = cmd.args.lowerRightVertex.x, + y1 = cmd.args.upperLeftVertex.y, + y2 = cmd.args.lowerRightVertex.y + }) + )) +end + +return CameraCapabilityHandlers diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/event_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/event_handlers.lua new file mode 100644 index 0000000000..02b63bb37f --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/event_handlers.lua @@ -0,0 +1,30 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local capabilities = require "st.capabilities" +local switch_utils = require "switch_utils.utils" + +local CameraEventHandlers = {} + +function CameraEventHandlers.zone_triggered_handler(driver, device, ib, response) + local triggered_zones = device:get_field(camera_fields.TRIGGERED_ZONES) or {} + if not switch_utils.tbl_contains(triggered_zones, ib.data.elements.zone.value) then + table.insert(triggered_zones, {zoneId = ib.data.elements.zone.value}) + device:set_field(camera_fields.TRIGGERED_ZONES, triggered_zones) + device:emit_event_for_endpoint(ib, capabilities.zoneManagement.triggeredZones(triggered_zones)) + end +end + +function CameraEventHandlers.zone_stopped_handler(driver, device, ib, response) + local triggered_zones = device:get_field(camera_fields.TRIGGERED_ZONES) or {} + for i, v in pairs(triggered_zones) do + if v.zoneId == ib.data.elements.zone.value then + table.remove(triggered_zones, i) + device:set_field(camera_fields.TRIGGERED_ZONES, triggered_zones) + device:emit_event_for_endpoint(ib, capabilities.zoneManagement.triggeredZones(triggered_zones)) + end + end +end + +return CameraEventHandlers diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua new file mode 100644 index 0000000000..4f467cbd07 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua @@ -0,0 +1,285 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local button_cfg = require("switch_utils.device_configuration").ButtonCfg +local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local camera_utils = require "sub_drivers.camera.camera_utils.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local device_cfg = require "switch_utils.device_configuration" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" + +local CameraDeviceConfiguration = {} + +function CameraDeviceConfiguration.create_child_devices(driver, device) + local num_floodlight_eps = 0 + local parent_child_device = false + for _, ep in ipairs(device.endpoints or {}) do + if device:supports_server_cluster(clusters.OnOff.ID, ep.endpoint_id) then + local child_profile = device_cfg.SwitchCfg.assign_profile_for_onoff_ep(device, ep.endpoint_id) + if child_profile then + num_floodlight_eps = num_floodlight_eps + 1 + local name = string.format("%s %d", "Floodlight", num_floodlight_eps) + driver:try_create_device( + { + type = "EDGE_CHILD", + label = name, + profile = child_profile, + parent_device_id = device.id, + parent_assigned_child_key = string.format("%d", ep.endpoint_id), + vendor_provided_label = name + } + ) + parent_child_device = true + end + end + end + if parent_child_device then + device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true}) + device:set_find_child(switch_utils.find_child) + end +end + +function CameraDeviceConfiguration.match_profile(device, status_light_enabled_present, status_light_brightness_present) + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + local status_led_component_capabilities = {} + local speaker_component_capabilities = {} + local microphone_component_capabilities = {} + local doorbell_component_capabilities = {} + + local function has_server_cluster_type(cluster) + return cluster.cluster_type == "SERVER" or cluster.cluster_type == "BOTH" + end + + local camera_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) + if #camera_endpoints > 0 then + local camera_ep = switch_utils.get_endpoint_info(device, camera_endpoints[1]) + for _, ep_cluster in pairs(camera_ep.clusters or {}) do + if ep_cluster.cluster_id == clusters.CameraAvStreamManagement.ID and has_server_cluster_type(ep_cluster) then + local clus_has_feature = function(feature_bitmap) + return clusters.CameraAvStreamManagement.are_features_supported(feature_bitmap, ep_cluster.feature_map) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.VIDEO) then + if switch_utils.find_cluster_on_ep(camera_ep, clusters.PushAvStreamTransport.ID, "SERVER") then + table.insert(main_component_capabilities, capabilities.videoCapture2.ID) + end + table.insert(main_component_capabilities, capabilities.cameraViewportSettings.ID) + table.insert(main_component_capabilities, capabilities.videoStreamSettings.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.LOCAL_STORAGE) then + table.insert(main_component_capabilities, capabilities.localMediaStorage.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.AUDIO) then + if switch_utils.find_cluster_on_ep(camera_ep, clusters.PushAvStreamTransport.ID, "SERVER") then + table.insert(main_component_capabilities, capabilities.audioRecording.ID) + end + table.insert(microphone_component_capabilities, capabilities.audioMute.ID) + table.insert(microphone_component_capabilities, capabilities.audioVolume.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.SNAPSHOT) then + table.insert(main_component_capabilities, capabilities.imageCapture.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.PRIVACY) then + table.insert(main_component_capabilities, capabilities.cameraPrivacyMode.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.SPEAKER) then + table.insert(speaker_component_capabilities, capabilities.audioMute.ID) + table.insert(speaker_component_capabilities, capabilities.audioVolume.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.IMAGE_CONTROL) then + table.insert(main_component_capabilities, capabilities.imageControl.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.HIGH_DYNAMIC_RANGE) then + table.insert(main_component_capabilities, capabilities.hdr.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.NIGHT_VISION) then + table.insert(main_component_capabilities, capabilities.nightVision.ID) + end + elseif ep_cluster.cluster_id == clusters.CameraAvSettingsUserLevelManagement.ID and has_server_cluster_type(ep_cluster) then + local clus_has_feature = function(feature_bitmap) + return clusters.CameraAvSettingsUserLevelManagement.are_features_supported(feature_bitmap, ep_cluster.feature_map) + end + if clus_has_feature(clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PAN) or + clus_has_feature(clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_TILT) or + clus_has_feature(clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_ZOOM) then + table.insert(main_component_capabilities, capabilities.mechanicalPanTiltZoom.ID) + end + elseif ep_cluster.cluster_id == clusters.ZoneManagement.ID and has_server_cluster_type(ep_cluster) then + table.insert(main_component_capabilities, capabilities.zoneManagement.ID) + elseif ep_cluster.cluster_id == clusters.OccupancySensing.ID and has_server_cluster_type(ep_cluster) then + table.insert(main_component_capabilities, capabilities.motionSensor.ID) + elseif ep_cluster.cluster_id == clusters.WebRTCTransportProvider.ID and has_server_cluster_type(ep_cluster) and + #device:get_endpoints(clusters.WebRTCTransportRequestor.ID, {cluster_type = "CLIENT"}) > 0 then + table.insert(main_component_capabilities, capabilities.webrtc.ID) + end + end + end + local chime_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CHIME) + if #chime_endpoints > 0 then + table.insert(main_component_capabilities, capabilities.sounds.ID) + end + local doorbell_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) + if #doorbell_endpoints > 0 then + table.insert(doorbell_component_capabilities, capabilities.button.ID) + end + if status_light_enabled_present then + table.insert(status_led_component_capabilities, capabilities.switch.ID) + end + if status_light_brightness_present then + table.insert(status_led_component_capabilities, capabilities.mode.ID) + end + + table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.main, main_component_capabilities}) + if #status_led_component_capabilities > 0 then + table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.statusLed, status_led_component_capabilities}) + end + if #speaker_component_capabilities > 0 then + table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.speaker, speaker_component_capabilities}) + end + if #microphone_component_capabilities > 0 then + table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.microphone, microphone_component_capabilities}) + end + if #doorbell_component_capabilities > 0 then + table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.doorbell, doorbell_component_capabilities}) + end + + if camera_utils.optional_capabilities_list_changed(optional_supported_component_capabilities, device.profile.components) then + device:try_update_metadata({profile = "camera", optional_component_capabilities = optional_supported_component_capabilities}) + if #doorbell_endpoints > 0 then + CameraDeviceConfiguration.update_doorbell_component_map(device, doorbell_endpoints[1]) + button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})) + end + end +end + +local function init_webrtc(device) + if device:supports_capability(capabilities.webrtc) then + -- TODO: Check for individual audio/video and talkback features + local transport_provider_ep_ids = device:get_endpoints(clusters.WebRTCTransportProvider.ID) + device:emit_event_for_endpoint(transport_provider_ep_ids[1], capabilities.webrtc.supportedFeatures({ + value = { + bundle = true, + order = "audio/video", + audio = "sendrecv", + video = "recvonly", + turnSource = "player", + supportTrickleICE = true + } + })) + end +end + +local function init_ptz(device) + if device:supports_capability(capabilities.mechanicalPanTiltZoom) then + local supported_attributes = {} + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPAN) then + table.insert(supported_attributes, "pan") + table.insert(supported_attributes, "panRange") + end + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MTILT) then + table.insert(supported_attributes, "tilt") + table.insert(supported_attributes, "tiltRange") + end + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MZOOM) then + table.insert(supported_attributes, "zoom") + table.insert(supported_attributes, "zoomRange") + end + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPRESETS) then + table.insert(supported_attributes, "presets") + table.insert(supported_attributes, "maxPresets") + end + local av_settings_ep_ids = device:get_endpoints(clusters.CameraAvSettingsUserLevelManagement.ID) + device:emit_event_for_endpoint(av_settings_ep_ids[1], capabilities.mechanicalPanTiltZoom.supportedAttributes(supported_attributes)) + end +end + +local function init_zone_management(device) + if device:supports_capability(capabilities.zoneManagement) then + local supported_features = {} + table.insert(supported_features, "triggerAugmentation") + if camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) then + table.insert(supported_features, "perZoneSensitivity") + end + local zone_management_ep_ids = device:get_endpoints(clusters.ZoneManagement.ID) + device:emit_event_for_endpoint(zone_management_ep_ids[1], capabilities.zoneManagement.supportedFeatures(supported_features)) + end +end + +local function init_local_media_storage(device) + if device:supports_capability(capabilities.localMediaStorage) then + local supported_attributes = {} + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then + table.insert(supported_attributes, "localVideoRecording") + end + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.SNAPSHOT) then + table.insert(supported_attributes, "localSnapshotRecording") + end + local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) + device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.localMediaStorage.supportedAttributes(supported_attributes)) + end +end + +local function init_audio_recording(device) + if device:supports_capability(capabilities.audioRecording) then + local audio_enabled_state = device:get_latest_state( + camera_fields.profile_components.main, capabilities.audioRecording.ID, capabilities.audioRecording.audioRecording.NAME + ) + if audio_enabled_state == nil then + -- Initialize with enabled default if state is unset + local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) + device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.audioRecording.audioRecording("enabled")) + end + end +end + +local function init_video_stream_settings(device) + if device:supports_capability(capabilities.videoStreamSettings) then + local supported_features = {} + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then + table.insert(supported_features, "liveStreaming") + table.insert(supported_features, "clipRecording") + table.insert(supported_features, "perStreamViewports") + end + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.WATERMARK) then + table.insert(supported_features, "watermark") + end + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.ON_SCREEN_DISPLAY) then + table.insert(supported_features, "onScreenDisplay") + end + local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) + device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.videoStreamSettings.supportedFeatures(supported_features)) + end +end + +local function init_camera_privacy_mode(device) + if device:supports_capability(capabilities.cameraPrivacyMode) then + local supported_attributes, supported_commands = {}, {} + table.insert(supported_attributes, "softRecordingPrivacyMode") + table.insert(supported_attributes, "softLivestreamPrivacyMode") + table.insert(supported_commands, "setSoftRecordingPrivacyMode") + table.insert(supported_commands, "setSoftLivestreamPrivacyMode") + local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) + device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.cameraPrivacyMode.supportedAttributes(supported_attributes)) + device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.cameraPrivacyMode.supportedCommands(supported_commands)) + end +end + +function CameraDeviceConfiguration.initialize_camera_capabilities(device) + init_webrtc(device) + init_ptz(device) + init_zone_management(device) + init_local_media_storage(device) + init_audio_recording(device) + init_video_stream_settings(device) + init_camera_privacy_mode(device) +end + +function CameraDeviceConfiguration.update_doorbell_component_map(device, ep) + local component_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {} + component_map.doorbell = ep + device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) +end + +return CameraDeviceConfiguration diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua new file mode 100644 index 0000000000..7598b89893 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua @@ -0,0 +1,50 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" + +local CameraFields = {} + +CameraFields.MAX_ENCODED_PIXEL_RATE = "__max_encoded_pixel_rate" +CameraFields.MAX_FRAMES_PER_SECOND = "__max_frames_per_second" +CameraFields.MAX_VOLUME_LEVEL = "__max_volume_level" +CameraFields.MIN_VOLUME_LEVEL = "__min_volume_level" +CameraFields.SUPPORTED_RESOLUTIONS = "__supported_resolutions" +CameraFields.MAX_RESOLUTION = "__max_resolution" +CameraFields.MIN_RESOLUTION = "__min_resolution" +CameraFields.TRIGGERED_ZONES = "__triggered_zones" +CameraFields.VIEWPORT = "__viewport" + +CameraFields.PAN_IDX = "PAN" +CameraFields.TILT_IDX = "TILT" +CameraFields.ZOOM_IDX = "ZOOM" + +CameraFields.pt_range_fields = { + [CameraFields.PAN_IDX] = { max = "__MAX_PAN" , min = "__MIN_PAN" }, + [CameraFields.TILT_IDX] = { max = "__MAX_TILT" , min = "__MIN_TILT" } +} + +CameraFields.profile_components = { + main = "main", + statusLed = "statusLed", + speaker = "speaker", + microphone = "microphone", + doorbell = "doorbell" +} + +CameraFields.tri_state_map = { + [clusters.CameraAvStreamManagement.types.TriStateAutoEnum.OFF] = "off", + [clusters.CameraAvStreamManagement.types.TriStateAutoEnum.ON] = "on", + [clusters.CameraAvStreamManagement.types.TriStateAutoEnum.AUTO] = "auto" +} + +CameraFields.ABS_PAN_MAX = 180 +CameraFields.ABS_PAN_MIN = -180 +CameraFields.ABS_TILT_MAX = 180 +CameraFields.ABS_TILT_MIN = -180 +CameraFields.ABS_ZOOM_MAX = 100 +CameraFields.ABS_ZOOM_MIN = 1 +CameraFields.ABS_VOL_MAX = 254.0 +CameraFields.ABS_VOL_MIN = 0.0 + +return CameraFields diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua new file mode 100644 index 0000000000..1caa9737bb --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua @@ -0,0 +1,338 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" + +local CameraUtils = {} + +function CameraUtils.component_to_endpoint(device, component) + local camera_eps = device:get_endpoints(clusters.CameraAvStreamManagement.ID) + table.sort(camera_eps) + for _, ep in ipairs(camera_eps or {}) do + if ep ~= 0 then -- 0 is the matter RootNode endpoint + return ep + end + end + return nil +end + +function CameraUtils.update_camera_component_map(device) + local camera_av_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) + if #camera_av_ep_ids > 0 then + -- An assumption here: there is only 1 CameraAvStreamManagement cluster on the device (which is all our profile supports) + local component_map = {} + if CameraUtils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.AUDIO) then + component_map.microphone = { + endpoint_id = camera_av_ep_ids[1], + cluster_id = clusters.CameraAvStreamManagement.ID, + attribute_ids = { + clusters.CameraAvStreamManagement.attributes.MicrophoneMuted.ID, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel.ID, + clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel.ID, + clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel.ID, + }, + } + end + if CameraUtils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then + component_map.speaker = { + endpoint_id = camera_av_ep_ids[1], + cluster_id = clusters.CameraAvStreamManagement.ID, + attribute_ids = { + clusters.CameraAvStreamManagement.attributes.SpeakerMuted.ID, + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel.ID, + clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel.ID, + clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel.ID, + }, + } + end + device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) + end +end + +function CameraUtils.get_ptz_map(device) + local mechanicalPanTiltZoom = capabilities.mechanicalPanTiltZoom + local ptz_map = { + [camera_fields.PAN_IDX] = { + current = device:get_latest_state("main", mechanicalPanTiltZoom.ID, mechanicalPanTiltZoom.pan.NAME), + range = device:get_latest_state("main", mechanicalPanTiltZoom.ID, mechanicalPanTiltZoom.panRange.NAME) or + { minimum = camera_fields.ABS_PAN_MIN, maximum = camera_fields.ABS_PAN_MAX }, + attribute = mechanicalPanTiltZoom.pan + }, + [camera_fields.TILT_IDX] = { + current = device:get_latest_state("main", mechanicalPanTiltZoom.ID, mechanicalPanTiltZoom.tilt.NAME), + range = device:get_latest_state("main", mechanicalPanTiltZoom.ID, mechanicalPanTiltZoom.tiltRange.NAME) or + { minimum = camera_fields.ABS_TILT_MIN, maximum = camera_fields.ABS_TILT_MAX }, + attribute = mechanicalPanTiltZoom.tilt + }, + [camera_fields.ZOOM_IDX] = { + current = device:get_latest_state("main", mechanicalPanTiltZoom.ID, mechanicalPanTiltZoom.zoom.NAME), + range = device:get_latest_state("main", mechanicalPanTiltZoom.ID, mechanicalPanTiltZoom.zoomRange.NAME) or + { minimum = camera_fields.ABS_ZOOM_MIN, maximum = camera_fields.ABS_ZOOM_MAX }, + attribute = mechanicalPanTiltZoom.zoom + } + } + return ptz_map +end + +function CameraUtils.feature_supported(device, cluster_id, feature_flag) + return #device:get_endpoints(cluster_id, { feature_bitmap = feature_flag }) > 0 +end + +function CameraUtils.update_supported_attributes(device, ib, capability, attribute) + local attribute_set = device:get_latest_state( + camera_fields.profile_components.main, capability.ID, capability.supportedAttributes.NAME + ) or {} + if not switch_utils.tbl_contains(attribute_set, attribute) then + local updated_attribute_set = {} + for _, v in ipairs(attribute_set) do + table.insert(updated_attribute_set, v) + end + table.insert(updated_attribute_set, attribute) + device:emit_event_for_endpoint(ib, capability.supportedAttributes(updated_attribute_set)) + end +end + +function CameraUtils.compute_fps(max_encoded_pixel_rate, width, height, max_fps) + local fps_step = 15.0 + local fps = math.min(max_encoded_pixel_rate / (width * height), max_fps) + return math.tointeger(math.floor(fps / fps_step) * fps_step) +end + +function CameraUtils.build_supported_resolutions(device, max_encoded_pixel_rate, max_fps) + local resolutions = {} + local added_resolutions = {} + + local function add_resolution(width, height) + local key = width .. "x" .. height + if not added_resolutions[key] then + local resolution = { width = width, height = height } + resolution.fps = CameraUtils.compute_fps(max_encoded_pixel_rate, width, height, max_fps) + table.insert(resolutions, resolution) + added_resolutions[key] = true + end + end + + local min_resolution = device:get_field(camera_fields.MIN_RESOLUTION) + if min_resolution then + add_resolution(min_resolution.width, min_resolution.height) + end + + local trade_off_resolutions = device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) + for _, v in pairs(trade_off_resolutions or {}) do + add_resolution(v.width, v.height) + end + + local max_resolution = device:get_field(camera_fields.MAX_RESOLUTION) + if max_resolution then + add_resolution(max_resolution.width, max_resolution.height) + end + + return resolutions +end + +function CameraUtils.profile_changed(synced_components, prev_components) + if #synced_components ~= #prev_components then + return true + end + for _, component in pairs(synced_components or {}) do + if (prev_components[component.id] == nil) or + (#component.capabilities ~= #prev_components[component.id].capabilities) then + return true + end + for _, capability in pairs(component.capabilities or {}) do + if prev_components[component.id][capability.id] == nil then + return true + end + end + end + return false +end + +function CameraUtils.optional_capabilities_list_changed(new_component_capability_list, previous_component_capability_list) + local previous_capability_map = {} + local component_sizes = {} + + local previous_component_count = 0 + for component_name, component in pairs(previous_component_capability_list or {}) do + previous_capability_map[component_name] = {} + component_sizes[component_name] = 0 + for _, capability in pairs(component.capabilities or {}) do + if capability.id ~= "firmwareUpdate" and capability.id ~= "refresh" then + previous_capability_map[component_name][capability.id] = true + component_sizes[component_name] = component_sizes[component_name] + 1 + end + end + previous_component_count = previous_component_count + 1 + end + + local number_of_components_counted = 0 + for _, new_component_capabilities in pairs(new_component_capability_list or {}) do + local component_name = new_component_capabilities[1] + local capability_list = new_component_capabilities[2] + + number_of_components_counted = number_of_components_counted + 1 + + if previous_capability_map[component_name] == nil then + return true + end + + for _, capability in ipairs(capability_list) do + if previous_capability_map[component_name][capability] == nil then + return true + end + end + + if #capability_list ~= component_sizes[component_name] then + return true + end + end + + if number_of_components_counted ~= previous_component_count then + return true + end + + return false +end + +function CameraUtils.subscribe(device) + local camera_subscribed_attributes = { + [capabilities.hdr.ID] = { + clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, + clusters.CameraAvStreamManagement.attributes.ImageRotation + }, + [capabilities.nightVision.ID] = { + clusters.CameraAvStreamManagement.attributes.NightVision, + clusters.CameraAvStreamManagement.attributes.NightVisionIllum + }, + [capabilities.imageControl.ID] = { + clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal, + clusters.CameraAvStreamManagement.attributes.ImageFlipVertical + }, + [capabilities.cameraPrivacyMode.ID] = { + clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn + }, + [capabilities.webrtc.ID] = { + clusters.CameraAvStreamManagement.attributes.TwoWayTalkSupport + }, + [capabilities.mechanicalPanTiltZoom.ID] = { + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPresets, + clusters.CameraAvSettingsUserLevelManagement.attributes.MaxPresets, + clusters.CameraAvSettingsUserLevelManagement.attributes.ZoomMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMin, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin + }, + [capabilities.audioMute.ID] = { + clusters.CameraAvStreamManagement.attributes.SpeakerMuted, + clusters.CameraAvStreamManagement.attributes.MicrophoneMuted + }, + [capabilities.audioVolume.ID] = { + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel + }, + [capabilities.mode.ID] = { + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness + }, + [capabilities.switch.ID] = { + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, + clusters.OnOff.attributes.OnOff + }, + [capabilities.videoStreamSettings.ID] = { + clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, + clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams, + clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams + }, + [capabilities.zoneManagement.ID] = { + clusters.ZoneManagement.attributes.MaxZones, + clusters.ZoneManagement.attributes.Zones, + clusters.ZoneManagement.attributes.Triggers, + clusters.ZoneManagement.attributes.SensitivityMax, + clusters.ZoneManagement.attributes.Sensitivity + }, + [capabilities.sounds.ID] = { + clusters.Chime.attributes.InstalledChimeSounds, + clusters.Chime.attributes.SelectedChime + }, + [capabilities.localMediaStorage.ID] = { + clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled, + clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled + }, + [capabilities.cameraViewportSettings.ID] = { + clusters.CameraAvStreamManagement.attributes.MinViewportResolution, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams, + clusters.CameraAvStreamManagement.attributes.Viewport + }, + [capabilities.motionSensor.ID] = { + clusters.OccupancySensing.attributes.Occupancy + }, + [capabilities.switchLevel.ID] = { + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + }, + [capabilities.colorControl.ID] = { + clusters.ColorControl.attributes.ColorMode, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + }, + [capabilities.colorTemperature.ID] = { + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + }, + } + + local camera_subscribed_events = { + [capabilities.zoneManagement.ID] = { + clusters.ZoneManagement.events.ZoneTriggered, + clusters.ZoneManagement.events.ZoneStopped + }, + [capabilities.button.ID] = { + clusters.Switch.events.InitialPress, + clusters.Switch.events.LongPress, + clusters.Switch.events.ShortRelease, + clusters.Switch.events.MultiPressComplete + } + } + + local im = require "st.matter.interaction_model" + + local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) + local devices_seen, capabilities_seen, attributes_seen, events_seen = {}, {}, {}, {} + + if #device:get_endpoints(clusters.CameraAvStreamManagement.ID) > 0 then + local ib = im.InteractionInfoBlock(nil, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.attributes.AttributeList.ID) + subscribe_request:with_info_block(ib) + end + + for _, endpoint_info in ipairs(device.endpoints) do + local checked_device = switch_utils.find_child(device, endpoint_info.endpoint_id) or device + if not devices_seen[checked_device.id] then + switch_utils.populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, + camera_subscribed_attributes, camera_subscribed_events + ) + devices_seen[checked_device.id] = true -- only loop through any device once + end + end + + if #subscribe_request.info_blocks > 0 then + device:send(subscribe_request) + end +end + +return CameraUtils diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/can_handle.lua new file mode 100644 index 0000000000..25a441d641 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local device_lib = require "st.device" + local fields = require "switch_utils.fields" + local switch_utils = require "switch_utils.utils" + if device.network_type == device_lib.NETWORK_TYPE_MATTER then + local version = require "version" + if version.rpc >= 10 and version.api >= 16 and + #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) > 0 then + return true, require("sub_drivers.camera") + end + end + return false +end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua new file mode 100644 index 0000000000..f13589ff41 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua @@ -0,0 +1,207 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +------------------------------------------------------------------------------------- +-- Matter Camera Sub Driver +------------------------------------------------------------------------------------- + +local attribute_handlers = require "sub_drivers.camera.camera_handlers.attribute_handlers" +local button_cfg = require("switch_utils.device_configuration").ButtonCfg +local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" +local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local camera_utils = require "sub_drivers.camera.camera_utils.utils" +local capabilities = require "st.capabilities" +local capability_handlers = require "sub_drivers.camera.camera_handlers.capability_handlers" +local clusters = require "st.matter.clusters" +local event_handlers = require "sub_drivers.camera.camera_handlers.event_handlers" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" + +local CameraLifecycleHandlers = {} + +function CameraLifecycleHandlers.device_init(driver, device) + device:set_component_to_endpoint_fn(camera_utils.component_to_endpoint) + device:set_endpoint_to_component_fn(switch_utils.endpoint_to_component) + device:extend_device("emit_event_for_endpoint", switch_utils.emit_event_for_endpoint) + if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then + device:set_find_child(switch_utils.find_child) + end + device:extend_device("subscribe", camera_utils.subscribe) + device:subscribe() +end + +function CameraLifecycleHandlers.do_configure(driver, device) + camera_utils.update_camera_component_map(device) + if #device:get_endpoints(clusters.CameraAvStreamManagement.ID) == 0 then + camera_cfg.match_profile(device, false, false) + end + camera_cfg.create_child_devices(driver, device) + camera_cfg.initialize_camera_capabilities(device) +end + +function CameraLifecycleHandlers.driver_switched(driver, device) + camera_utils.update_camera_component_map(device) + if #device:get_endpoints(clusters.CameraAvStreamManagement.ID) == 0 then + camera_cfg.match_profile(device, false, false) + end +end + +function CameraLifecycleHandlers.info_changed(driver, device, event, args) + if camera_utils.profile_changed(device.profile.components, args.old_st_store.profile.components) then + camera_cfg.initialize_camera_capabilities(device) + device:subscribe() + if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) > 0 then + button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})) + end + end +end + +function CameraLifecycleHandlers.added() end + +local camera_handler = { + NAME = "Camera Handler", + lifecycle_handlers = { + init = CameraLifecycleHandlers.device_init, + infoChanged = CameraLifecycleHandlers.info_changed, + doConfigure = CameraLifecycleHandlers.do_configure, + driverSwitched = CameraLifecycleHandlers.driver_switched, + added = CameraLifecycleHandlers.added + }, + matter_handlers = { + attr = { + [clusters.CameraAvStreamManagement.ID] = { + [clusters.CameraAvStreamManagement.attributes.HDRModeEnabled.ID] = attribute_handlers.enabled_state_factory(capabilities.hdr.hdr), + [clusters.CameraAvStreamManagement.attributes.NightVision.ID] = attribute_handlers.night_vision_factory(capabilities.nightVision.nightVision), + [clusters.CameraAvStreamManagement.attributes.NightVisionIllum.ID] = attribute_handlers.night_vision_factory(capabilities.nightVision.illumination), + [clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal.ID] = attribute_handlers.enabled_state_factory(capabilities.imageControl.imageFlipHorizontal), + [clusters.CameraAvStreamManagement.attributes.ImageFlipVertical.ID] = attribute_handlers.enabled_state_factory(capabilities.imageControl.imageFlipVertical), + [clusters.CameraAvStreamManagement.attributes.ImageRotation.ID] = attribute_handlers.image_rotation_handler, + [clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled.ID] = attribute_handlers.enabled_state_factory(capabilities.cameraPrivacyMode.softRecordingPrivacyMode), + [clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled.ID] = attribute_handlers.enabled_state_factory(capabilities.cameraPrivacyMode.softLivestreamPrivacyMode), + [clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn.ID] = attribute_handlers.enabled_state_factory(capabilities.cameraPrivacyMode.hardPrivacyMode), + [clusters.CameraAvStreamManagement.attributes.TwoWayTalkSupport.ID] = attribute_handlers.two_way_talk_support_handler, + [clusters.CameraAvStreamManagement.attributes.SpeakerMuted.ID] = attribute_handlers.muted_handler, + [clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel.ID] = attribute_handlers.volume_level_handler, + [clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel.ID] = attribute_handlers.max_volume_level_handler, + [clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel.ID] = attribute_handlers.min_volume_level_handler, + [clusters.CameraAvStreamManagement.attributes.MicrophoneMuted.ID] = attribute_handlers.muted_handler, + [clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel.ID] = attribute_handlers.volume_level_handler, + [clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel.ID] = attribute_handlers.max_volume_level_handler, + [clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel.ID] = attribute_handlers.min_volume_level_handler, + [clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID] = attribute_handlers.status_light_enabled_handler, + [clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID] = attribute_handlers.status_light_brightness_handler, + [clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints.ID] = attribute_handlers.rate_distortion_trade_off_points_handler, + [clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate.ID] = attribute_handlers.max_encoded_pixel_rate_handler, + [clusters.CameraAvStreamManagement.attributes.VideoSensorParams.ID] = attribute_handlers.video_sensor_parameters_handler, + [clusters.CameraAvStreamManagement.attributes.MinViewportResolution.ID] = attribute_handlers.min_viewport_handler, + [clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams.ID] = attribute_handlers.allocated_video_streams_handler, + [clusters.CameraAvStreamManagement.attributes.Viewport.ID] = attribute_handlers.viewport_handler, + [clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled.ID] = attribute_handlers.enabled_state_factory(capabilities.localMediaStorage.localSnapshotRecording), + [clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled.ID] = attribute_handlers.enabled_state_factory(capabilities.localMediaStorage.localVideoRecording), + [clusters.CameraAvStreamManagement.attributes.AttributeList.ID] = attribute_handlers.camera_av_stream_management_attribute_list_handler + }, + [clusters.CameraAvSettingsUserLevelManagement.ID] = { + [clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition.ID] = attribute_handlers.ptz_position_handler, + [clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPresets.ID] = attribute_handlers.ptz_presets_handler, + [clusters.CameraAvSettingsUserLevelManagement.attributes.MaxPresets.ID] = attribute_handlers.max_presets_handler, + [clusters.CameraAvSettingsUserLevelManagement.attributes.ZoomMax.ID] = attribute_handlers.zoom_max_handler, + [clusters.CameraAvSettingsUserLevelManagement.attributes.PanMax.ID] = attribute_handlers.pt_range_handler_factory(capabilities.mechanicalPanTiltZoom.panRange, camera_fields.pt_range_fields[camera_fields.PAN_IDX].max), + [clusters.CameraAvSettingsUserLevelManagement.attributes.PanMin.ID] = attribute_handlers.pt_range_handler_factory(capabilities.mechanicalPanTiltZoom.panRange, camera_fields.pt_range_fields[camera_fields.PAN_IDX].min), + [clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax.ID] = attribute_handlers.pt_range_handler_factory(capabilities.mechanicalPanTiltZoom.tiltRange, camera_fields.pt_range_fields[camera_fields.TILT_IDX].max), + [clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin.ID] = attribute_handlers.pt_range_handler_factory(capabilities.mechanicalPanTiltZoom.tiltRange, camera_fields.pt_range_fields[camera_fields.TILT_IDX].min) + }, + [clusters.ZoneManagement.ID] = { + [clusters.ZoneManagement.attributes.MaxZones.ID] = attribute_handlers.max_zones_handler, + [clusters.ZoneManagement.attributes.Zones.ID] = attribute_handlers.zones_handler, + [clusters.ZoneManagement.attributes.Triggers.ID] = attribute_handlers.triggers_handler, + [clusters.ZoneManagement.attributes.SensitivityMax.ID] = attribute_handlers.sensitivity_max_handler, + [clusters.ZoneManagement.attributes.Sensitivity.ID] = attribute_handlers.sensitivity_handler, + }, + [clusters.Chime.ID] = { + [clusters.Chime.attributes.InstalledChimeSounds.ID] = attribute_handlers.installed_chime_sounds_handler, + [clusters.Chime.attributes.SelectedChime.ID] = attribute_handlers.selected_chime_handler + } + }, + event = { + [clusters.ZoneManagement.ID] = { + [clusters.ZoneManagement.events.ZoneTriggered.ID] = event_handlers.zone_triggered_handler, + [clusters.ZoneManagement.events.ZoneStopped.ID] = event_handlers.zone_stopped_handler + } + } + }, + capability_handlers = { + [capabilities.hdr.ID] = { + [capabilities.hdr.commands.setHdr.NAME] = capability_handlers.set_enabled_factory(clusters.CameraAvStreamManagement.attributes.HDRModeEnabled) + }, + [capabilities.nightVision.ID] = { + [capabilities.nightVision.commands.setNightVision.NAME] = capability_handlers.set_night_vision_factory(clusters.CameraAvStreamManagement.attributes.NightVision), + [capabilities.nightVision.commands.setIllumination.NAME] = capability_handlers.set_night_vision_factory(clusters.CameraAvStreamManagement.attributes.NightVisionIllum) + }, + [capabilities.imageControl.ID] = { + [capabilities.imageControl.commands.setImageFlipHorizontal.NAME] = capability_handlers.set_enabled_factory(clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal), + [capabilities.imageControl.commands.setImageFlipVertical.NAME] = capability_handlers.set_enabled_factory(clusters.CameraAvStreamManagement.attributes.ImageFlipVertical), + [capabilities.imageControl.commands.setImageRotation.NAME] = capability_handlers.handle_set_image_rotation + }, + [capabilities.cameraPrivacyMode.ID] = { + [capabilities.cameraPrivacyMode.commands.setSoftLivestreamPrivacyMode.NAME] = capability_handlers.set_enabled_factory(clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled), + [capabilities.cameraPrivacyMode.commands.setSoftRecordingPrivacyMode.NAME] = capability_handlers.set_enabled_factory(clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled) + }, + [capabilities.audioMute.ID] = { + [capabilities.audioMute.commands.setMute.NAME] = capability_handlers.handle_mute_commands_factory(capabilities.audioMute.commands.setMute.NAME), + [capabilities.audioMute.commands.mute.NAME] = capability_handlers.handle_mute_commands_factory(capabilities.audioMute.commands.mute.NAME), + [capabilities.audioMute.commands.unmute.NAME] = capability_handlers.handle_mute_commands_factory(capabilities.audioMute.commands.unmute.NAME) + }, + [capabilities.audioVolume.ID] = { + [capabilities.audioVolume.commands.setVolume.NAME] = capability_handlers.handle_set_volume, + [capabilities.audioVolume.commands.volumeUp.NAME] = capability_handlers.handle_volume_up, + [capabilities.audioVolume.commands.volumeDown.NAME] = capability_handlers.handle_volume_down + }, + [capabilities.mode.ID] = { + [capabilities.mode.commands.setMode.NAME] = capability_handlers.handle_set_status_light_mode + }, + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = capability_handlers.handle_status_led_on, + [capabilities.switch.commands.off.NAME] = capability_handlers.handle_status_led_off + }, + [capabilities.audioRecording.ID] = { + [capabilities.audioRecording.commands.setAudioRecording.NAME] = capability_handlers.handle_audio_recording + }, + [capabilities.mechanicalPanTiltZoom.ID] = { + [capabilities.mechanicalPanTiltZoom.commands.panRelative.NAME] = capability_handlers.ptz_relative_move_factory(camera_fields.PAN_IDX), + [capabilities.mechanicalPanTiltZoom.commands.tiltRelative.NAME] = capability_handlers.ptz_relative_move_factory(camera_fields.TILT_IDX), + [capabilities.mechanicalPanTiltZoom.commands.zoomRelative.NAME] = capability_handlers.ptz_relative_move_factory(camera_fields.ZOOM_IDX), + [capabilities.mechanicalPanTiltZoom.commands.setPan.NAME] = capability_handlers.ptz_set_position_factory(capabilities.mechanicalPanTiltZoom.commands.setPan), + [capabilities.mechanicalPanTiltZoom.commands.setTilt.NAME] = capability_handlers.ptz_set_position_factory(capabilities.mechanicalPanTiltZoom.commands.setTilt), + [capabilities.mechanicalPanTiltZoom.commands.setZoom.NAME] = capability_handlers.ptz_set_position_factory(capabilities.mechanicalPanTiltZoom.commands.setZoom), + [capabilities.mechanicalPanTiltZoom.commands.setPanTiltZoom.NAME] = capability_handlers.ptz_set_position_factory(capabilities.mechanicalPanTiltZoom.commands.setPanTiltZoom), + [capabilities.mechanicalPanTiltZoom.commands.savePreset.NAME] = capability_handlers.handle_save_preset, + [capabilities.mechanicalPanTiltZoom.commands.removePreset.NAME] = capability_handlers.handle_remove_preset, + [capabilities.mechanicalPanTiltZoom.commands.moveToPreset.NAME] = capability_handlers.handle_move_to_preset + }, + [capabilities.zoneManagement.ID] = { + [capabilities.zoneManagement.commands.newZone.NAME] = capability_handlers.handle_new_zone, + [capabilities.zoneManagement.commands.updateZone.NAME] = capability_handlers.handle_update_zone, + [capabilities.zoneManagement.commands.removeZone.NAME] = capability_handlers.handle_remove_zone, + [capabilities.zoneManagement.commands.createOrUpdateTrigger.NAME] = capability_handlers.handle_create_or_update_trigger, + [capabilities.zoneManagement.commands.removeTrigger.NAME] = capability_handlers.handle_remove_trigger, + [capabilities.zoneManagement.commands.setSensitivity.NAME] = capability_handlers.handle_set_sensitivity + }, + [capabilities.sounds.ID] = { + [capabilities.sounds.commands.playSound.NAME] = capability_handlers.handle_play_sound, + [capabilities.sounds.commands.setSelectedSound.NAME] = capability_handlers.handle_set_selected_sound + }, + [capabilities.videoStreamSettings.ID] = { + [capabilities.videoStreamSettings.commands.setStream.NAME] = capability_handlers.handle_set_stream + }, + [capabilities.cameraViewportSettings.ID] = { + [capabilities.cameraViewportSettings.commands.setDefaultViewport.NAME] = capability_handlers.handle_set_default_viewport + }, + [capabilities.localMediaStorage.ID] = { + [capabilities.localMediaStorage.commands.setLocalSnapshotRecording.NAME] = capability_handlers.set_enabled_factory(clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled), + [capabilities.localMediaStorage.commands.setLocalVideoRecording.NAME] = capability_handlers.set_enabled_factory(clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled) + } + }, + can_handle = require("sub_drivers.camera.can_handle") +} + +return camera_handler diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/can_handle.lua new file mode 100644 index 0000000000..369b93b8de --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/can_handle.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local device_lib = require "st.device" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" + +return function(opts, driver, device) + local EVE_MANUFACTURER_ID = 0x130A + -- this sub driver does NOT support child devices, and ONLY supports Eve devices + -- that do NOT support the Electrical Sensor device type + if device.network_type == device_lib.NETWORK_TYPE_MATTER and + device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID and + #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR) == 0 then + return true, require("sub_drivers.eve_energy") + end + return false +end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua new file mode 100644 index 0000000000..8222fc1378 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua @@ -0,0 +1,361 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +------------------------------------------------------------------------------------- +-- Definitions +------------------------------------------------------------------------------------- + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local cluster_base = require "st.matter.cluster_base" +local st_utils = require "st.utils" +local data_types = require "st.matter.data_types" +local device_lib = require "st.device" + +local SWITCH_INITIALIZED = "__switch_intialized" +local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" +local ON_OFF_STATES = "ON_OFF_STATES" + +local PRIVATE_CLUSTER_ID = 0x130AFC01 + +local PRIVATE_ATTR_ID_WATT = 0x130A000A +local PRIVATE_ATTR_ID_WATT_ACCUMULATED = 0x130A000B +local PRIVATE_ATTR_ID_ACCUMULATED_CONTROL_POINT = 0x130A000E + +-- Timer to update the data each minute if the device is on +local RECURRING_POLL_TIMER = "RECURRING_POLL_TIMER" +local TIMER_REPEAT = (1 * 60) -- Run the timer each minute + +local LAST_REPORT_TIME = "LAST_REPORT_TIME" +local LATEST_TOTAL_CONSUMPTION_WH = "LATEST_TOTAL_CONSUMPTION_WH" +local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds + + +------------------------------------------------------------------------------------- +-- Eve specifics +------------------------------------------------------------------------------------- + +-- Return a ISO 8061 formatted timestamp in UTC (Z) +-- @return e.g. 2022-02-02T08:00:00Z +local function epoch_to_iso8601(time) + return os.date("!%Y-%m-%dT%TZ", time) +end + +local function updateEnergyMeter(device, totalConsumptionWh) + -- Remember the total consumption so we can report it every 15 minutes + device:set_field(LATEST_TOTAL_CONSUMPTION_WH, totalConsumptionWh, { persist = true }) + + -- Report the energy consumed + device:emit_event(capabilities.energyMeter.energy({ value = totalConsumptionWh, unit = "Wh" })) +end + + +------------------------------------------------------------------------------------- +-- Timer +------------------------------------------------------------------------------------- + +local function requestData(device) + -- Update the Watt usage + local req = cluster_base.read(device, 0x01, PRIVATE_CLUSTER_ID, PRIVATE_ATTR_ID_WATT, nil) + + -- Update the energy consumption + req:merge(cluster_base.read(device, 0x01, PRIVATE_CLUSTER_ID, PRIVATE_ATTR_ID_WATT_ACCUMULATED, nil)) + + device:send(req) +end + +local function create_poll_schedule(device) + -- the poll schedule is only needed for devices that support powerConsumption + if not device:supports_capability(capabilities.powerConsumptionReport) then + return + end + + local poll_timer = device:get_field(RECURRING_POLL_TIMER) + if poll_timer ~= nil then + return + end + + -- The powerConsumption report needs to be updated at least every 15 minutes in order to be included in SmartThings Energy + -- Eve Energy generally report changes every 10 or 17 minutes + local timer = device.thread:call_on_schedule(TIMER_REPEAT, function() + requestData(device) + end, "polling_schedule_timer") + + device:set_field(RECURRING_POLL_TIMER, timer) +end + +local function delete_poll_schedule(device) + local poll_timer = device:get_field(RECURRING_POLL_TIMER) + if poll_timer ~= nil then + device.thread:cancel_timer(poll_timer) + device:set_field(RECURRING_POLL_TIMER, nil) + end +end + +local function report_power_consumption_to_st_energy(device, latest_total_imported_energy_wh) + local current_time = os.time() + local last_time = device:get_field(LAST_REPORT_TIME) or 0 + + -- Ensure that the previous report was sent at least 15 minutes ago + if MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then + return + end + + device:set_field(LAST_REPORT_TIME, current_time, { persist = true }) + + -- Calculate the energy delta between reports + local energy_delta_wh = 0.0 + local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, + capabilities.powerConsumptionReport.powerConsumption.NAME) + if previous_imported_report and previous_imported_report.energy then + energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) + end + + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + local component = device.profile.components["main"] + device:emit_component_event(component, capabilities.powerConsumptionReport.powerConsumption({ + start = epoch_to_iso8601(last_time), + ["end"] = epoch_to_iso8601(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) +end + + +------------------------------------------------------------------------------------- +-- Matter Utilities +------------------------------------------------------------------------------------- + +--- component_to_endpoint helper function to handle situations where +--- device does not have endpoint ids in sequential order from 1 +--- In this case the function returns the lowest endpoint value that isn't 0 +local function find_default_endpoint(device, component) + local eps = device:get_endpoints(clusters.OnOff.ID) + table.sort(eps) + for _, v in ipairs(eps) do + if v ~= 0 then --0 is the matter RootNode endpoint + return v + end + end + device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", + device.MATTER_DEFAULT_ENDPOINT)) + return device.MATTER_DEFAULT_ENDPOINT +end + +local function initialize_switch(driver, device) + local switch_eps = device:get_endpoints(clusters.OnOff.ID) + table.sort(switch_eps) + + -- Since we do not support bindings at the moment, we only want to count On/Off + -- clusters that have been implemented as server. This can be removed when we have + -- support for bindings. + local num_server_eps = 0 + local main_endpoint = find_default_endpoint(device) + for _, ep in ipairs(switch_eps) do + if device:supports_server_cluster(clusters.OnOff.ID, ep) then + num_server_eps = num_server_eps + 1 + if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint + local name = string.format("%s %d", device.label, num_server_eps) + driver:try_create_device( + { + type = "EDGE_CHILD", + label = name, + profile = "plug-binary", + parent_device_id = device.id, + parent_assigned_child_key = string.format("%d", ep), + vendor_provided_label = name + } + ) + end + end + end + + device:set_field(SWITCH_INITIALIZED, true) +end + +local function component_to_endpoint(device, component) + local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} + if map[component] then + return map[component] + end + return find_default_endpoint(device, component) +end + +local function endpoint_to_component(device, ep) + local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} + for component, endpoint in pairs(map) do + if endpoint == ep then + return component + end + end + return "main" +end + +local function find_child(parent, ep_id) + return parent:get_child_by_parent_assigned_key(string.format("%d", ep_id)) +end + +local function on_off_state(device, endpoint) + local map = device:get_field(ON_OFF_STATES) or {} + if map[endpoint] then + return map[endpoint] + end + + return false +end + +local function set_on_off_state(device, endpoint, value) + local map = device:get_field(ON_OFF_STATES) or {} + + map[endpoint] = value + device:set_field(ON_OFF_STATES, map) +end + + +------------------------------------------------------------------------------------- +-- Device Management +------------------------------------------------------------------------------------- + +local function device_init(driver, device) + if device.network_type == device_lib.NETWORK_TYPE_MATTER then + if not device:get_field(COMPONENT_TO_ENDPOINT_MAP) and + not device:get_field(SWITCH_INITIALIZED) then + -- create child devices as needed for multi-switch devices + initialize_switch(driver, device) + end + device:set_component_to_endpoint_fn(component_to_endpoint) + device:set_endpoint_to_component_fn(endpoint_to_component) + device:set_find_child(find_child) + device:subscribe() + + create_poll_schedule(device) + end +end + +local function device_added(driver, device) + -- Reset the values + device:emit_event(capabilities.powerMeter.power({ value = 0.0, unit = "W" })) + device:emit_event(capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) +end + +local function device_removed(driver, device) + delete_poll_schedule(device) +end + +-- override do_configure to prevent it running in the main driver +local function do_configure(driver, device) end + +-- override driver_switched to prevent it running in the main driver +local function driver_switched(driver, device) end + +local function handle_refresh(self, device) + requestData(device) +end + +local function handle_resetEnergyMeter(self, device) + local current_time = os.time() + + -- 978307200 is the number of seconds from 1 January 1970 to 1 January 2001 + local current_time_2001 = current_time - 978307200 + if current_time_2001 < 0 then + current_time_2001 = 0 + end + + -- Reset the consumption on the device + local data = data_types.validate_or_build_type(current_time_2001, data_types.Uint32) + device:send(cluster_base.write(device, 0x01, PRIVATE_CLUSTER_ID, PRIVATE_ATTR_ID_ACCUMULATED_CONTROL_POINT, nil, + data)) +end + +------------------------------------------------------------------------------------- +-- Eve Energy Handler +------------------------------------------------------------------------------------- + +local function on_off_attr_handler(driver, device, ib, response) + if ib.data.value then + set_on_off_state(device, ib.endpoint_id, true) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.on()) + + -- If one of the outlet is on, we should create the poll to monitor the power consumption + create_poll_schedule(device) + else + set_on_off_state(device, ib.endpoint_id, false) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) + + -- Detect if all the outlets are off + local shouldDeletePoll = true + local eps = device:get_endpoints(clusters.OnOff.ID) + for _, v in ipairs(eps) do + local isOutletOn = on_off_state(device, v) + if isOutletOn then + shouldDeletePoll = false + break + end + end + + -- If all the outlet are off, we should delete the poll + if shouldDeletePoll then + -- We want to prevent to read the power reports of the device if the device is off + -- We set here the power to 0 before the read is skipped so that the power is correctly displayed and not using a stale value + device:emit_event(capabilities.powerMeter.power({ value = 0, unit = "W" })) + + -- Stop the timer when the device is off + delete_poll_schedule(device) + end + end +end + +local function watt_attr_handler(driver, device, ib, zb_rx) + if ib.data.value then + local wattValue = ib.data.value + device:emit_event(capabilities.powerMeter.power({ value = wattValue, unit = "W" })) + end +end + +local function watt_accumulated_attr_handler(driver, device, ib, zb_rx) + if ib.data.value then + local totalConsumptionRawValue = ib.data.value + local totalConsumptionWh = st_utils.round(1000 * totalConsumptionRawValue) + updateEnergyMeter(device, totalConsumptionWh) + report_power_consumption_to_st_energy(device, totalConsumptionWh) + end +end + +local eve_energy_handler = { + NAME = "Eve Energy Handler", + lifecycle_handlers = { + init = device_init, + added = device_added, + removed = device_removed, + doConfigure = do_configure, + driverSwitched = driver_switched + }, + matter_handlers = { + attr = { + [clusters.OnOff.ID] = { + [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, + }, + [PRIVATE_CLUSTER_ID] = { + [PRIVATE_ATTR_ID_WATT] = watt_attr_handler, + [PRIVATE_ATTR_ID_WATT_ACCUMULATED] = watt_accumulated_attr_handler + } + }, + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = handle_refresh, + }, + [capabilities.energyMeter.ID] = { + [capabilities.energyMeter.commands.resetEnergyMeter.NAME] = handle_resetEnergyMeter, + }, + }, + supported_capabilities = { + capabilities.switch, + capabilities.powerMeter, + capabilities.energyMeter, + capabilities.powerConsumptionReport + }, + can_handle = require("sub_drivers.eve_energy.can_handle") +} + +return eve_energy_handler diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/can_handle.lua new file mode 100644 index 0000000000..1245fae5c9 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local switch_utils = require "switch_utils.utils" + +return function(opts, driver, device) + if switch_utils.get_product_override_field(device, "is_ikea_scroll") then + return true, require("sub_drivers.ikea_scroll") + end + return false +end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua new file mode 100644 index 0000000000..32b23ccaae --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua @@ -0,0 +1,61 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" +local switch_utils = require "switch_utils.utils" +local scroll_utils = require "sub_drivers.ikea_scroll.scroll_utils.utils" +local scroll_cfg = require "sub_drivers.ikea_scroll.scroll_utils.device_configuration" +local event_handlers = require "sub_drivers.ikea_scroll.scroll_handlers.event_handlers" + +local IkeaScrollLifecycleHandlers = {} + +-- prevent main driver device_added handling from running +function IkeaScrollLifecycleHandlers.device_added(driver, device) +end + +function IkeaScrollLifecycleHandlers.device_init(driver, device) + device:set_endpoint_to_component_fn(switch_utils.endpoint_to_component) + device:extend_device("subscribe", scroll_utils.subscribe) + device:subscribe() +end + +function IkeaScrollLifecycleHandlers.do_configure(driver, device) + scroll_cfg.match_profile(driver, device) +end + +function IkeaScrollLifecycleHandlers.driver_switched(driver, device) + scroll_cfg.match_profile(driver, device) +end + +function IkeaScrollLifecycleHandlers.info_changed(driver, device, event, args) + if device.profile.id ~= args.old_st_store.profile.id then + scroll_cfg.configure_buttons(device) + device:subscribe() + end +end + + +-- DEVICE TEMPLATE -- + +local ikea_scroll_handler = { + NAME = "Ikea Scroll Handler", + lifecycle_handlers = { + added = IkeaScrollLifecycleHandlers.device_added, + doConfigure = IkeaScrollLifecycleHandlers.do_configure, + driverSwitched = IkeaScrollLifecycleHandlers.driver_switched, + infoChanged = IkeaScrollLifecycleHandlers.info_changed, + init = IkeaScrollLifecycleHandlers.device_init, + }, + matter_handlers = { + event = { + [clusters.Switch.ID] = { + [clusters.Switch.events.InitialPress.ID] = event_handlers.initial_press_handler, + [clusters.Switch.events.MultiPressOngoing.ID] = event_handlers.multi_press_ongoing_handler, + [clusters.Switch.events.MultiPressComplete.ID] = event_handlers.multi_press_complete_handler, + } + } + }, + can_handle = require("sub_drivers.ikea_scroll.can_handle") +} + +return ikea_scroll_handler diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_handlers/event_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_handlers/event_handlers.lua new file mode 100644 index 0000000000..da9b1c0392 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_handlers/event_handlers.lua @@ -0,0 +1,61 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local st_utils = require "st.utils" +local capabilities = require "st.capabilities" +local switch_utils = require "switch_utils.utils" +local generic_event_handlers = require "switch_handlers.event_handlers" +local scroll_fields = require "sub_drivers.ikea_scroll.scroll_utils.fields" + +local IkeaScrollEventHandlers = {} + +local function rotate_amount_event_helper(device, endpoint_id, num_presses_to_handle) + -- to cut down on checks, we can assume that if the endpoint is not in ENDPOINTS_UP_SCROLL, it is in ENDPOINTS_DOWN_SCROLL + local scroll_direction = switch_utils.tbl_contains(scroll_fields.ENDPOINTS_UP_SCROLL, endpoint_id) and 1 or -1 + local scroll_amount = st_utils.clamp_value(scroll_direction * scroll_fields.PER_SCROLL_EVENT_ROTATION * num_presses_to_handle, -100, 100) + device:emit_event_for_endpoint(endpoint_id, capabilities.knob.rotateAmount(scroll_amount, {state_change = true})) +end + +-- Used by ENDPOINTS_UP_SCROLL and ENDPOINTS_DOWN_SCROLL, not ENDPOINTS_PUSH +function IkeaScrollEventHandlers.multi_press_ongoing_handler(driver, device, ib, response) + if switch_utils.tbl_contains(scroll_fields.ENDPOINTS_PUSH, ib.endpoint_id) then + -- Ignore MultiPressOngoing events from push endpoints. + device.log.debug("Received MultiPressOngoing event from push endpoint, ignoring.") + else + local cur_num_presses_counted = ib.data and ib.data.elements and ib.data.elements.current_number_of_presses_counted.value or 0 + local num_presses_to_handle = cur_num_presses_counted - (device:get_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED) or 0) + if num_presses_to_handle > 0 then + device:set_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED, cur_num_presses_counted) + rotate_amount_event_helper(device, ib.endpoint_id, num_presses_to_handle) + end + end +end + +function IkeaScrollEventHandlers.multi_press_complete_handler(driver, device, ib, response) + if switch_utils.tbl_contains(scroll_fields.ENDPOINTS_PUSH, ib.endpoint_id) then + generic_event_handlers.multi_press_complete_handler(driver, device, ib, response) + else + local total_num_presses_counted = ib.data and ib.data.elements and ib.data.elements.total_number_of_presses_counted.value or 0 + local num_presses_to_handle = total_num_presses_counted - (device:get_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED) or 0) + if num_presses_to_handle > 0 then + rotate_amount_event_helper(device, ib.endpoint_id, num_presses_to_handle) + end + -- reset the LATEST_NUMBER_OF_PRESSES_COUNTED to nil at the end of a MultiPress chain. + device:set_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED, nil) + end +end + +function IkeaScrollEventHandlers.initial_press_handler(driver, device, ib, response) + if switch_utils.tbl_contains(scroll_fields.ENDPOINTS_PUSH, ib.endpoint_id) then + generic_event_handlers.initial_press_handler(driver, device, ib, response) + else + -- the magic number "1" occurs in this handler since the InitialPress event represents the first press. + local latest_presses_counted = device:get_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED) or 0 + if latest_presses_counted == 0 then + device:set_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED, 1) + rotate_amount_event_helper(device, ib.endpoint_id, 1) + end + end +end + +return IkeaScrollEventHandlers diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua new file mode 100644 index 0000000000..457804d495 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua @@ -0,0 +1,38 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" +local switch_utils = require "switch_utils.utils" +local switch_fields = require "switch_utils.fields" +local scroll_fields = require "sub_drivers.ikea_scroll.scroll_utils.fields" + +local IkeaScrollConfiguration = {} + +function IkeaScrollConfiguration.build_button_component_map(device) + local component_map = { + main = {scroll_fields.ENDPOINTS_PUSH[1], scroll_fields.ENDPOINTS_UP_SCROLL[1], scroll_fields.ENDPOINTS_DOWN_SCROLL[1]}, + group2 = {scroll_fields.ENDPOINTS_PUSH[2], scroll_fields.ENDPOINTS_UP_SCROLL[2], scroll_fields.ENDPOINTS_DOWN_SCROLL[2]}, + group3 = {scroll_fields.ENDPOINTS_PUSH[3], scroll_fields.ENDPOINTS_UP_SCROLL[3], scroll_fields.ENDPOINTS_DOWN_SCROLL[3]}, + } + device:set_field(switch_fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) +end + +function IkeaScrollConfiguration.configure_buttons(device) + for _, ep in ipairs(scroll_fields.ENDPOINTS_PUSH) do + device:send(clusters.Switch.attributes.MultiPressMax:read(device, ep)) + switch_utils.set_field_for_endpoint(device, switch_fields.SUPPORTS_MULTI_PRESS, ep, true, {persist = true}) + device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) + end + for _, ep in ipairs(scroll_fields.ENDPOINTS_UP_SCROLL) do -- and by extension, ENDPOINTS_DOWN_SCROLL + device:emit_event_for_endpoint(ep, capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}})) + end +end + +function IkeaScrollConfiguration.match_profile(driver, device) + device:try_update_metadata({profile = "ikea-scroll"}) + IkeaScrollConfiguration.build_button_component_map(device) + IkeaScrollConfiguration.configure_buttons(device) +end + +return IkeaScrollConfiguration diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/fields.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/fields.lua new file mode 100644 index 0000000000..5e98a829c0 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/fields.lua @@ -0,0 +1,45 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local st_utils = require "st.utils" +local clusters = require "st.matter.clusters" + +local IkeaScrollFields = {} + +-- PowerSource supported on Root Node +IkeaScrollFields.ENDPOINT_POWER_SOURCE = 0 + +-- Generic Switch Endpoints used for basic push functionality +IkeaScrollFields.ENDPOINTS_PUSH = {3, 6, 9} + +-- Generic Switch Endpoints used for Up Scroll functionality +IkeaScrollFields.ENDPOINTS_UP_SCROLL = {1, 4, 7} + +-- Generic Switch Endpoints used for Down Scroll functionality +IkeaScrollFields.ENDPOINTS_DOWN_SCROLL = {2, 5, 8} + +-- Maximum number of presses at a time +IkeaScrollFields.MAX_SCROLL_PRESSES = 18 + +-- Amount to rotate per scroll event +IkeaScrollFields.PER_SCROLL_EVENT_ROTATION = st_utils.round(1 / IkeaScrollFields.MAX_SCROLL_PRESSES * 100) + +-- Field to track the latest number of presses counted during a single scroll event sequence +IkeaScrollFields.LATEST_NUMBER_OF_PRESSES_COUNTED = "__latest_number_of_presses_counted" + +-- Required Events for the ENDPOINTS_PUSH. +IkeaScrollFields.switch_press_subscribed_events = { + clusters.Switch.events.InitialPress.ID, + clusters.Switch.events.MultiPressComplete.ID, + clusters.Switch.events.LongPress.ID, +} + +-- Required Events for the ENDPOINTS_UP_SCROLL and ENDPOINTS_DOWN_SCROLL. Adds a +-- MultiPressOngoing subscription to handle step functionality in real-time +IkeaScrollFields.switch_scroll_subscribed_events = { + clusters.Switch.events.InitialPress.ID, + clusters.Switch.events.MultiPressOngoing.ID, + clusters.Switch.events.MultiPressComplete.ID, +} + +return IkeaScrollFields diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua new file mode 100644 index 0000000000..9a9f95228b --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua @@ -0,0 +1,38 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local im = require "st.matter.interaction_model" +local clusters = require "st.matter.clusters" +local scroll_fields = require "sub_drivers.ikea_scroll.scroll_utils.fields" + +local IkeaScrollUtils = {} + +-- override subscribe function in the main driver +function IkeaScrollUtils.subscribe(device) + local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) + for _, ep_push in ipairs(scroll_fields.ENDPOINTS_PUSH) do + for _, switch_event in ipairs(scroll_fields.switch_press_subscribed_events) do + local ib = im.InteractionInfoBlock(ep_push, clusters.Switch.ID, nil, switch_event) + subscribe_request:with_info_block(ib) + end + end + for _, ep_up in ipairs(scroll_fields.ENDPOINTS_UP_SCROLL) do + for _, switch_event in ipairs(scroll_fields.switch_scroll_subscribed_events) do + local ib = im.InteractionInfoBlock(ep_up, clusters.Switch.ID, nil, switch_event) + subscribe_request:with_info_block(ib) + end + end + for _, ep_down in ipairs(scroll_fields.ENDPOINTS_DOWN_SCROLL) do + for _, switch_event in ipairs(scroll_fields.switch_scroll_subscribed_events) do + local ib = im.InteractionInfoBlock(ep_down, clusters.Switch.ID, nil, switch_event) + subscribe_request:with_info_block(ib) + end + end + local ib = im.InteractionInfoBlock( + scroll_fields.ENDPOINT_POWER_SOURCE, clusters.PowerSource.ID, clusters.PowerSource.attributes.BatPercentRemaining.ID + ) + subscribe_request:with_info_block(ib) + device:send(subscribe_request) +end + +return IkeaScrollUtils \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/can_handle.lua new file mode 100644 index 0000000000..f3f5342989 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local device_lib = require "st.device" + +return function(opts, driver, device) + local THIRD_REALITY_MK1_FINGERPRINT = { vendor_id = 0x1407, product_id = 0x1388 } + if device.network_type == device_lib.NETWORK_TYPE_MATTER and + device.manufacturer_info.vendor_id == THIRD_REALITY_MK1_FINGERPRINT.vendor_id and + device.manufacturer_info.product_id == THIRD_REALITY_MK1_FINGERPRINT.product_id then + return true, require("sub_drivers.third_reality_mk1") + end + return false +end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/init.lua new file mode 100644 index 0000000000..1572886089 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/init.lua @@ -0,0 +1,113 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local im = require "st.matter.interaction_model" + +local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" + +------------------------------------------------------------------------------------- +-- Third Reality MK1 specifics +------------------------------------------------------------------------------------- + +local function endpoint_to_component(device, ep) + local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} + for component, endpoint in pairs(map) do + if endpoint == ep then + return component + end + end + return "main" +end + +-- override subscribe function to prevent subscribing to additional events from the main driver +local function subscribe(device) + local ib = im.InteractionInfoBlock(nil, clusters.Switch.ID, nil, clusters.Switch.events.InitialPress.ID) + local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) + subscribe_request:with_info_block(ib) + device:send(subscribe_request) +end + +local function configure_buttons(device) + local ms_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + for _, ep in ipairs(ms_eps) do + if device.profile.components[endpoint_to_component(device, ep)] then + device.log.info(string.format("Configuring Supported Values for generic switch endpoint %d", ep)) + local supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ep, supportedButtonValues_event) + device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) + else + device.log.info(string.format("Component not found for generic switch endpoint %d. Skipping Supported Value configuration", ep)) + end + end +end + +local function build_button_component_map(device) + local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + table.sort(button_eps) + local component_map = {} + component_map["main"] = button_eps[1] + for component_num = 2, 12 do + component_map["F" .. component_num] = button_eps[component_num] + end + device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) +end + +local function device_init(driver, device) + device:set_endpoint_to_component_fn(endpoint_to_component) + device:extend_device("subscribe", subscribe) + device:subscribe() +end + +-- override device_added to prevent it running in the main driver +local function device_added(driver, device) end + +local function info_changed(driver, device, event, args) + if device.profile.id ~= args.old_st_store.profile.id then + configure_buttons(device) + device:subscribe() + end +end + +local function match_profile(driver, device) + device:try_update_metadata({profile = "12-button-keyboard"}) + build_button_component_map(device) + configure_buttons(device) +end + +local function do_configure(driver, device) + match_profile(driver, device) +end + +local function driver_switched(driver, device) + match_profile(driver, device) +end + +local function initial_press_event_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true})) +end + +local third_reality_mk1_handler = { + NAME = "ThirdReality MK1 Handler", + lifecycle_handlers = { + init = device_init, + added = device_added, + infoChanged = info_changed, + doConfigure = do_configure, + driverSwitched = driver_switched + }, + matter_handlers = { + event = { + [clusters.Switch.ID] = { + [clusters.Switch.events.InitialPress.ID] = initial_press_event_handler + } + } + }, + supported_capabilities = { + capabilities.button + }, + can_handle = require("sub_drivers.third_reality_mk1.can_handle") +} + +return third_reality_mk1_handler diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua new file mode 100644 index 0000000000..ae76709be8 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -0,0 +1,537 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local version = require "version" +local im = require "st.matter.interaction_model" +local st_utils = require "st.utils" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" +local color_utils = require "switch_utils.color_utils" +local cfg = require "switch_utils.device_configuration" +local device_cfg = cfg.DeviceCfg + +-- Include driver-side definitions when lua libs api version is < 11 +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" +end + +local AttributeHandlers = {} + +-- [[ ON OFF CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.on_off_attr_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.on()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) + end + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("switch", "switch") + end +end + + +-- [[ LEVEL CONTROL CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.level_control_current_level_handler(driver, device, ib, response) + if ib.data.value ~= nil then + local level = ib.data.value + if level > 0 then + level = math.max(1, st_utils.round(level / 254.0 * 100)) + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switchLevel.level(level)) + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("switchLevel", "level") + end + end +end + +function AttributeHandlers.level_bounds_handler_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local lighting_endpoints = device:get_endpoints(clusters.LevelControl.ID, {feature_bitmap = clusters.LevelControl.FeatureMap.LIGHTING}) + local lighting_support = switch_utils.tbl_contains(lighting_endpoints, ib.endpoint_id) + -- If the lighting feature is supported then we should check if the reported level is at least 1. + if lighting_support and ib.data.value < fields.SWITCH_LEVEL_LIGHTING_MIN then + device.log.warn_with({hub_logs = true}, string.format("Lighting device reported a switch level %d outside of supported capability range", ib.data.value)) + return + end + -- Convert level from given range of 0-254 to range of 0-100. + local level = st_utils.round(ib.data.value / 254.0 * 100) + -- If the device supports the lighting feature, the minimum capability level should be 1 so we do not send a 0 value for the level attribute + if lighting_support and level == 0 then + level = 1 + end + switch_utils.set_field_for_endpoint(device, fields.LEVEL_BOUND_RECEIVED..minOrMax, ib.endpoint_id, level) + local min = switch_utils.get_field_for_endpoint(device, fields.LEVEL_BOUND_RECEIVED..fields.LEVEL_MIN, ib.endpoint_id) + local max = switch_utils.get_field_for_endpoint(device, fields.LEVEL_BOUND_RECEIVED..fields.LEVEL_MAX, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switchLevel.levelRange({ value = {minimum = min, maximum = max} })) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min level value %d that is not lower than the reported max level value %d", min, max)) + end + switch_utils.set_field_for_endpoint(device, fields.LEVEL_BOUND_RECEIVED..fields.LEVEL_MAX, ib.endpoint_id, nil) + switch_utils.set_field_for_endpoint(device, fields.LEVEL_BOUND_RECEIVED..fields.LEVEL_MIN, ib.endpoint_id, nil) + end + end +end + + +-- [[ COLOR CONTROL CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.current_hue_handler(driver, device, ib, response) + if device:get_field(fields.COLOR_MODE) ~= fields.X_Y_COLOR_MODE and ib.data.value ~= nil then + local hue = math.floor((ib.data.value / 0xFE * 100) + 0.5) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(hue)) + end + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("colorControl", "hue") + end +end + +function AttributeHandlers.current_saturation_handler(driver, device, ib, response) + if device:get_field(fields.COLOR_MODE) ~= fields.X_Y_COLOR_MODE and ib.data.value ~= nil then + local sat = math.floor((ib.data.value / 0xFE * 100) + 0.5) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(sat)) + end + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("colorControl", "saturation") + end +end + +function AttributeHandlers.color_temperature_mireds_handler(driver, device, ib, response) + local temp_in_mired = ib.data.value + if temp_in_mired == nil then + return + end + if (temp_in_mired < fields.COLOR_TEMPERATURE_MIRED_MIN or temp_in_mired > fields.COLOR_TEMPERATURE_MIRED_MAX) then + device.log.warn_with({hub_logs = true}, string.format("Device reported color temperature %d mired outside of sane range of %.2f-%.2f", temp_in_mired, fields.COLOR_TEMPERATURE_MIRED_MIN, fields.COLOR_TEMPERATURE_MIRED_MAX)) + return + end + local min_temp_mired = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MIN, ib.endpoint_id) + local max_temp_mired = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MAX, ib.endpoint_id) + + local temp = st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT/temp_in_mired) + if min_temp_mired ~= nil and temp_in_mired <= min_temp_mired then + temp = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MAX, ib.endpoint_id) + elseif max_temp_mired ~= nil and temp_in_mired >= max_temp_mired then + temp = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MIN, ib.endpoint_id) + end + + local temp_device = device + if device:get_field(fields.IS_PARENT_CHILD_DEVICE) == true then + temp_device = switch_utils.find_child(device, ib.endpoint_id) or device + end + local most_recent_temp = temp_device:get_field(fields.MOST_RECENT_TEMP) + -- this is to avoid rounding errors from the round-trip conversion of Kelvin to mireds + if most_recent_temp ~= nil and + most_recent_temp <= st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT/(temp_in_mired - 1)) and + most_recent_temp >= st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT/(temp_in_mired + 1)) then + temp = most_recent_temp + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorTemperature.colorTemperature(temp)) +end + +function AttributeHandlers.current_x_handler(driver, device, ib, response) + if device:get_field(fields.COLOR_MODE) == clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION then + return + end + local y = device:get_field(fields.RECEIVED_Y) + --TODO it is likely that both x and y attributes are in the response (not guaranteed though) + -- if they are we can avoid setting fields on the device. + if y == nil then + device:set_field(fields.RECEIVED_X, ib.data.value) + else + local x = ib.data.value + local h, s, _ = color_utils.safe_xy_to_hsv(x, y, nil) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(h)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(s)) + device:set_field(fields.RECEIVED_Y, nil) + end +end + +function AttributeHandlers.current_y_handler(driver, device, ib, response) + if device:get_field(fields.COLOR_MODE) == clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION then + return + end + local x = device:get_field(fields.RECEIVED_X) + if x == nil then + device:set_field(fields.RECEIVED_Y, ib.data.value) + else + local y = ib.data.value + local h, s, _ = color_utils.safe_xy_to_hsv(x, y, nil) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(h)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(s)) + device:set_field(fields.RECEIVED_X, nil) + end +end + +function AttributeHandlers.color_mode_handler(driver, device, ib, response) + if ib.data.value == device:get_field(fields.COLOR_MODE) + or (ib.data.value ~= clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION + and ib.data.value ~= clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) then + return + end + device:set_field(fields.COLOR_MODE, ib.data.value) + local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + if ib.data.value == clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION then + req:merge(clusters.ColorControl.attributes.CurrentHue:read()) + req:merge(clusters.ColorControl.attributes.CurrentSaturation:read()) + elseif ib.data.value == clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY then + req:merge(clusters.ColorControl.attributes.CurrentX:read()) + req:merge(clusters.ColorControl.attributes.CurrentY:read()) + end + if #req.info_blocks > 0 then + device:send(req) + end +end + +--TODO setup configure handler to read this attribute. +function AttributeHandlers.color_capabilities_handler(driver, device, ib, response) + if ib.data.value ~= nil then + if ib.data.value & 0x1 then + device:set_field(fields.HUESAT_SUPPORT, true) + end + end +end + +function AttributeHandlers.color_temp_physical_mireds_bounds_factory(minOrMax) + return function(driver, device, ib, response) + local temp_in_mired = ib.data.value + if temp_in_mired == nil then + return + end + if (temp_in_mired < fields.COLOR_TEMPERATURE_MIRED_MIN or temp_in_mired > fields.COLOR_TEMPERATURE_MIRED_MAX) then + device.log.warn_with({hub_logs = true}, string.format("Device reported a color temperature %d mired outside of sane range of %.2f-%.2f", temp_in_mired, fields.COLOR_TEMPERATURE_MIRED_MIN, fields.COLOR_TEMPERATURE_MIRED_MAX)) + return + end + local temp_in_kelvin = switch_utils.mired_to_kelvin(temp_in_mired, minOrMax) + switch_utils.set_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..minOrMax, ib.endpoint_id, temp_in_kelvin) + -- the minimum color temp in kelvin corresponds to the maximum temp in mireds + if minOrMax == fields.COLOR_TEMP_MIN then + switch_utils.set_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MAX, ib.endpoint_id, temp_in_mired) + else + switch_utils.set_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MIN, ib.endpoint_id, temp_in_mired) + end + local min = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MIN, ib.endpoint_id) + local max = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MAX, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = min, maximum = max} })) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min color temperature %d K that is not lower than the reported max color temperature %d K", min, max)) + end + end + end +end + + +-- [[ ILLUMINANCE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.illuminance_measured_value_handler(driver, device, ib, response) + local lux = math.floor(10 ^ ((ib.data.value - 1) / 10000)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.illuminanceMeasurement.illuminance(lux)) +end + + +-- [[ OCCUPANCY CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.occupancy_handler(driver, device, ib, response) + device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) +end + + +-- [[ ELECTRICAL POWER MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.active_power_handler(driver, device, ib, response) + if ib.data.value then + local watt_value = ib.data.value / 1000 -- convert received milliwatt to watt + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + end + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("powerMeter","power") + end +end + + +-- [[ VALVE CONFIGURATION AND CONTROL CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.valve_configuration_current_state_handler(driver, device, ib, response) + if ib.data.value == 0 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.valve.valve.closed()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.valve.valve.open()) + end +end + +function AttributeHandlers.valve_configuration_current_level_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.level.level(ib.data.value)) + end +end + + +-- [[ ELECTRICAL ENERGY MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.energy_imported_factory(is_periodic_report) + return function(driver, device, ib, response) + if version.api < 11 then + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct:augment_type(ib.data) + end + if ib.data.elements.energy then + local energy_imported_wh = ib.data.elements.energy.value / 1000 -- convert received milliwatt-hour to watt-hour + if is_periodic_report then + -- handle this report only if cumulative reports are not supported + if device:get_field(fields.CUMULATIVE_REPORTS_SUPPORTED) then return end + local energy_meter_latest_state = switch_utils.get_latest_state_for_endpoint( + device, ib, capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME + ) or 0 + energy_imported_wh = energy_imported_wh + energy_meter_latest_state + else + -- the field containing the offset may be associated with a child device + local field_device = switch_utils.find_child(device, ib.endpoint_id) or device + local energy_meter_offset = field_device:get_field(fields.ENERGY_METER_OFFSET) or 0.0 + energy_imported_wh = energy_imported_wh - energy_meter_offset + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = energy_imported_wh, unit = "Wh" })) + switch_utils.report_power_consumption_to_st_energy(device, ib.endpoint_id, energy_imported_wh) + else + device.log.warn("Received data from the energy imported attribute does not include a numerical energy value") + end + end +end + + +-- [[ POWER TOPOLOGY CLUSTER ATTRIBUTES ]] -- + +--- AvailableEndpoints: This attribute SHALL indicate the list of endpoints capable of +--- providing power to and/or consuming power from the endpoint hosting this server. +--- +--- In the case there are multiple endpoints supporting the PowerTopology cluster with +--- SET feature, all AvailableEndpoints responses must be handled before profiling. +function AttributeHandlers.available_endpoints_handler(driver, device, ib, response) + local set_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS) + for i, set_ep_info in pairs(set_topology_eps or {}) do + if ib.endpoint_id == set_ep_info.endpoint_id then + -- since EP response is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table + switch_utils.remove_field_index(device, fields.ELECTRICAL_SENSOR_EPS, i) + local available_endpoints_ids = {} + for _, element in pairs(ib.data.elements or {}) do + table.insert(available_endpoints_ids, element.value) + end + -- set the required profile elements ("-power", etc.) to one of these EP IDs for later profiling. + -- set an assigned child key in the case this will emit events on an EDGE_CHILD device + switch_utils.set_fields_for_electrical_sensor_endpoint(device, set_ep_info, available_endpoints_ids) + break + end + end + if #set_topology_eps == 0 then -- in other words, all AvailableEndpoints attribute responses have been handled + device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.SET_TOPOLOGY, {persist=true}) + device_cfg.match_profile(driver, device) + end +end + + +-- [[ DESCRIPTOR CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.parts_list_handler(driver, device, ib, response) + local tree_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS) + for i, tree_ep_info in pairs(tree_topology_eps or {}) do + if ib.endpoint_id == tree_ep_info.endpoint_id then + -- since EP response is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table + switch_utils.remove_field_index(device, fields.ELECTRICAL_SENSOR_EPS, i) + local associated_endpoints_ids = {} + for _, element in pairs(ib.data.elements or {}) do + table.insert(associated_endpoints_ids, element.value) + end + -- set the required profile elements ("-power", etc.) to one of these EP IDs for later profiling. + -- set an assigned child key in the case this will emit events on an EDGE_CHILD device + switch_utils.set_fields_for_electrical_sensor_endpoint(device, tree_ep_info, associated_endpoints_ids) + break + end + end + if #tree_topology_eps == 0 then -- in other words, all PartsList attribute responses for TREE Electrical Sensor EPs have been handled + device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.TREE_TOPOLOGY, {persist=true}) + device_cfg.match_profile(driver, device) + end +end + + +-- [[ POWER SOURCE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.bat_percent_remaining_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) + end +end + +function AttributeHandlers.bat_charge_level_handler(driver, device, ib, response) + if ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.OK then + device:emit_event(capabilities.batteryLevel.battery.normal()) + elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.WARNING then + device:emit_event(capabilities.batteryLevel.battery.warning()) + elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.CRITICAL then + device:emit_event(capabilities.batteryLevel.battery.critical()) + end +end + +function AttributeHandlers.power_source_attribute_list_handler(driver, device, ib, response) + local previous_battery_support = device:get_field(fields.profiling_data.BATTERY_SUPPORT) + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist=true}) + for _, attr in ipairs(ib.data.elements or {}) do + if attr.value == clusters.PowerSource.attributes.BatPercentRemaining.ID then + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_PERCENTAGE, {persist=true}) + break + elseif attr.value == clusters.PowerSource.attributes.BatChargeLevel.ID and + device:get_field(fields.profiling_data.BATTERY_SUPPORT) ~= fields.battery_support.BATTERY_PERCENTAGE then -- don't overwrite if percentage support is already detected + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_LEVEL, {persist=true}) + end + end + if not previous_battery_support or previous_battery_support ~= device:get_field(fields.profiling_data.BATTERY_SUPPORT) then + device_cfg.match_profile(driver, device) + end +end + + +-- [[ SWITCH CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.multi_press_max_handler(driver, device, ib, response) + local max = ib.data.value or 1 --get max number of presses + device.log.debug("Device supports "..max.." presses") + -- capability only supports up to 6 presses + if max > 6 then + device.log.info("Device supports more than 6 presses") + max = 6 + end + local MSL = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS}) + local supportsHeld = switch_utils.tbl_contains(MSL, ib.endpoint_id) + local values = switch_utils.create_multi_press_values_list(max, supportsHeld) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.supportedButtonValues(values, {visibility = {displayed = false}})) +end + + +-- [[ TEMPERATURE MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.temperature_measured_value_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local temp = measured_value / 100.0 + local unit = "C" + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperature({value = temp, unit = unit})) + end +end + +function AttributeHandlers.temperature_measured_value_bounds_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local temp = ib.data.value / 100.0 + local unit = "C" + switch_utils.set_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..minOrMax, ib.endpoint_id, temp) + local min = switch_utils.get_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MIN, ib.endpoint_id) + local max = switch_utils.get_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MAX, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + -- Only emit the capability for RPC version >= 5 (unit conversion for + -- temperature range capability is only supported for RPC >= 5) + if version.rpc >= 5 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) + end + switch_utils.set_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MIN, ib.endpoint_id, nil) + switch_utils.set_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MAX, ib.endpoint_id, nil) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) + end + end + end +end + + +-- [[ RELATIVE HUMIDITY MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.relative_humidity_measured_value_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local humidity = st_utils.round(measured_value / 100.0) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) + end +end + + +-- [[ FAN CONTROL CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.fan_mode_handler(driver, device, ib, response) + if ib.data.value == clusters.FanControl.attributes.FanMode.OFF then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("off")) + elseif ib.data.value == clusters.FanControl.attributes.FanMode.LOW then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("low")) + elseif ib.data.value == clusters.FanControl.attributes.FanMode.MEDIUM then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("medium")) + elseif ib.data.value == clusters.FanControl.attributes.FanMode.HIGH then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("high")) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("auto")) + end +end + +function AttributeHandlers.fan_mode_sequence_handler(driver, device, ib, response) + local supportedFanModes + if ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH then + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.medium.NAME, + capabilities.fanMode.fanMode.high.NAME + } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH then + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.high.NAME + } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO then + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.medium.NAME, + capabilities.fanMode.fanMode.high.NAME, + capabilities.fanMode.fanMode.auto.NAME + } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH_AUTO then + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.high.NAME, + capabilities.fanMode.fanMode.auto.NAME + } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_ON_AUTO then + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.high.NAME, + capabilities.fanMode.fanMode.auto.NAME + } + else + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.high.NAME + } + end + local event = capabilities.fanMode.supportedFanModes(supportedFanModes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.percent_current_handler(driver, device, ib, response) + if ib.data.value == nil or ib.data.value < 0 or ib.data.value > 100 then + return + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(ib.data.value)) +end + +return AttributeHandlers diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua new file mode 100644 index 0000000000..3c65e3e8c0 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua @@ -0,0 +1,218 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local st_utils = require "st.utils" +local version = require "version" +local switch_utils = require "switch_utils.utils" +local fields = require "switch_utils.fields" + +local CapabilityHandlers = {} + +-- Include driver-side definitions when lua libs api version is < 11 +if version.api < 11 then + clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" +end + +-- [[ SWITCH CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_switch_on(driver, device, cmd) + if type(device.register_native_capability_cmd_handler) == "function" then + device:register_native_capability_cmd_handler(cmd.capability, cmd.command) + end + local endpoint_id = device:component_to_endpoint(cmd.component) + --TODO use OnWithRecallGlobalScene for devices with the LT feature + device:send(clusters.OnOff.server.commands.On(device, endpoint_id)) +end + +function CapabilityHandlers.handle_switch_off(driver, device, cmd) + if type(device.register_native_capability_cmd_handler) == "function" then + device:register_native_capability_cmd_handler(cmd.capability, cmd.command) + end + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.OnOff.server.commands.Off(device, endpoint_id)) +end + + +-- [[ SWITCH LEVEL CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_switch_set_level(driver, device, cmd) + if type(device.register_native_capability_cmd_handler) == "function" then + device:register_native_capability_cmd_handler(cmd.capability, cmd.command) + end + local endpoint_id = device:component_to_endpoint(cmd.component) + local level = st_utils.round(cmd.args.level/100.0 * 254) + device:send(clusters.LevelControl.server.commands.MoveToLevelWithOnOff(device, endpoint_id, level, cmd.args.rate, 0, 0)) +end + + +-- [[ STATELESS SWITCH LEVEL STEP CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_step_level(driver, device, cmd) + local step_size = st_utils.round((cmd.args and cmd.args.stepSize or 0)/100.0 * 254) + if step_size == 0 then return end + local endpoint_id = device:component_to_endpoint(cmd.component) + local step_mode = step_size > 0 and clusters.LevelControl.types.StepMode.UP or clusters.LevelControl.types.StepMode.DOWN + device:send(clusters.LevelControl.server.commands.Step(device, endpoint_id, step_mode, math.abs(step_size), fields.TRANSITION_TIME_FAST, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF)) +end + + +-- [[ COLOR CONTROL CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_set_color(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req + local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) + if switch_utils.tbl_contains(huesat_endpoints, endpoint_id) then + local hue = switch_utils.convert_huesat_st_to_matter(cmd.args.color.hue) + local sat = switch_utils.convert_huesat_st_to_matter(cmd.args.color.saturation) + req = clusters.ColorControl.server.commands.MoveToHueAndSaturation(device, endpoint_id, hue, sat, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) + else + local x, y, _ = st_utils.safe_hsv_to_xy(cmd.args.color.hue, cmd.args.color.saturation) + req = clusters.ColorControl.server.commands.MoveToColor(device, endpoint_id, x, y, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) + end + device:send(req) +end + +function CapabilityHandlers.handle_set_hue(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) + if switch_utils.tbl_contains(huesat_endpoints, endpoint_id) then + local hue = switch_utils.convert_huesat_st_to_matter(cmd.args.hue) + local req = clusters.ColorControl.server.commands.MoveToHue(device, endpoint_id, hue, 0, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) + device:send(req) + else + device.log.warn("Device does not support huesat features on its color control cluster") + end +end + +function CapabilityHandlers.handle_set_saturation(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) + if switch_utils.tbl_contains(huesat_endpoints, endpoint_id) then + local sat = switch_utils.convert_huesat_st_to_matter(cmd.args.saturation) + local req = clusters.ColorControl.server.commands.MoveToSaturation(device, endpoint_id, sat, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) + device:send(req) + else + device.log.warn("Device does not support huesat features on its color control cluster") + end +end + + +-- [[ COLOR TEMPERATURE CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_set_color_temperature(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local temp_in_kelvin = cmd.args.temperature + local min_temp_kelvin = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MIN, endpoint_id) + local max_temp_kelvin = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MAX, endpoint_id) + + local temp_in_mired = st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT/temp_in_kelvin) + if min_temp_kelvin ~= nil and temp_in_kelvin <= min_temp_kelvin then + temp_in_mired = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MAX, endpoint_id) + elseif max_temp_kelvin ~= nil and temp_in_kelvin >= max_temp_kelvin then + temp_in_mired = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MIN, endpoint_id) + end + local req = clusters.ColorControl.server.commands.MoveToColorTemperature(device, endpoint_id, temp_in_mired, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) + device:set_field(fields.MOST_RECENT_TEMP, cmd.args.temperature, {persist = true}) + device:send(req) +end + + +-- [[ STATELESS COLOR TEMPERATURE STEP CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_step_color_temperature_by_percent(driver, device, cmd) + local step_percent_change = cmd.args and cmd.args.stepSize or 0 + if step_percent_change == 0 then return end + local endpoint_id = device:component_to_endpoint(cmd.component) + -- before the Matter 1.3 lua libs update (HUB FW 55), there was no ColorControl StepModeEnum type defined + local step_mode = step_percent_change > 0 and (clusters.ColorControl.types.StepModeEnum and clusters.ColorControl.types.StepModeEnum.DOWN or 3) or (clusters.ColorControl.types.StepModeEnum and clusters.ColorControl.types.StepModeEnum.UP or 1) + local min_mireds = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MIN, endpoint_id) or fields.COLOR_TEMPERATURE_MIRED_MIN -- default min mireds + local max_mireds = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MAX, endpoint_id) or fields.COLOR_TEMPERATURE_MIRED_MAX -- default max mireds + local step_size_in_mireds = st_utils.round((max_mireds - min_mireds) * (math.abs(step_percent_change)/100.0)) + device:send(clusters.ColorControl.server.commands.StepColorTemperature(device, endpoint_id, step_mode, step_size_in_mireds, fields.TRANSITION_TIME_FAST, min_mireds, max_mireds, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF)) +end + + +-- [[ VALVE CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_valve_open(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.ValveConfigurationAndControl.server.commands.Open(device, endpoint_id)) +end + +function CapabilityHandlers.handle_valve_close(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.ValveConfigurationAndControl.server.commands.Close(device, endpoint_id)) +end + + +-- [[ LEVEL CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_set_level(driver, device, cmd) + local commands = clusters.ValveConfigurationAndControl.server.commands + local endpoint_id = device:component_to_endpoint(cmd.component) + local level = cmd.args.level + if not level then + return + elseif level == 0 then + device:send(commands.Close(device, endpoint_id)) + else + device:send(commands.Open(device, endpoint_id, nil, level)) + end +end + + +-- [[ FAN MODE CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_set_fan_mode(driver, device, cmd) + local fan_mode_id + if cmd.args.fanMode == capabilities.fanMode.fanMode.low.NAME then + fan_mode_id = clusters.FanControl.attributes.FanMode.LOW + elseif cmd.args.fanMode == capabilities.fanMode.fanMode.medium.NAME then + fan_mode_id = clusters.FanControl.attributes.FanMode.MEDIUM + elseif cmd.args.fanMode == capabilities.fanMode.fanMode.high.NAME then + fan_mode_id = clusters.FanControl.attributes.FanMode.HIGH + elseif cmd.args.fanMode == capabilities.fanMode.fanMode.auto.NAME then + fan_mode_id = clusters.FanControl.attributes.FanMode.AUTO + else + fan_mode_id = clusters.FanControl.attributes.FanMode.OFF + end + if fan_mode_id then + local fan_ep = device:get_endpoints(clusters.FanControl.ID)[1] + device:send(clusters.FanControl.attributes.FanMode:write(device, fan_ep, fan_mode_id)) + end +end + + +-- [[ FAN SPEED PERCENT CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_fan_speed_set_percent(driver, device, cmd) + local speed = math.floor(cmd.args.percent) + local fan_ep = device:get_endpoints(clusters.FanControl.ID)[1] + device:send(clusters.FanControl.attributes.PercentSetting:write(device, fan_ep, speed)) +end + + +-- [[ ENERGY METER CAPABILITY COMMANDS ]] -- + +--- +--- If a Cumulative Reporting device, this will store the most recent energy meter reading, and all subsequent reports will have this value subtracted +--- from the value reported. Matter, like Zigbee and unlike Z-Wave, does not provide a way to reset the value to zero, so this is an attempt at a workaround. +--- In the case it is a Periodic Reporting device, the reports do not need to be offset, so setting the current energy to 0.0 will do the same thing. +--- +function CapabilityHandlers.handle_reset_energy_meter(driver, device, cmd) + local energy_meter_latest_state = device:get_latest_state(cmd.component, capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME) or 0.0 + if energy_meter_latest_state ~= 0.0 then + device:emit_component_event(device.profile.components[cmd.component], capabilities.energyMeter.energy({value = 0.0, unit = "Wh"})) + -- note: field containing cumulative reports supported is only set on the parent device + local field_device = device:get_parent_device() or device + if field_device:get_field(fields.CUMULATIVE_REPORTS_SUPPORTED) then + local current_offset = device:get_field(fields.ENERGY_METER_OFFSET) or 0.0 + device:set_field(fields.ENERGY_METER_OFFSET, current_offset + energy_meter_latest_state, {persist=true}) + end + end +end + +return CapabilityHandlers diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/event_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/event_handlers.lua new file mode 100644 index 0000000000..16177b38f4 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/event_handlers.lua @@ -0,0 +1,82 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local lua_socket = require "socket" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" + +local EventHandlers = {} + + +-- [[ SWITCH CLUSTER EVENTS ]] -- + +function EventHandlers.initial_press_handler(driver, device, ib, response) + if switch_utils.get_field_for_endpoint(device, fields.SUPPORTS_MULTI_PRESS, ib.endpoint_id) then + -- Receipt of an InitialPress event means we do not want to ignore the next MultiPressComplete event + -- or else we would potentially not create the expected button capability event + switch_utils.set_field_for_endpoint(device, fields.IGNORE_NEXT_MPC, ib.endpoint_id, nil) + elseif switch_utils.get_field_for_endpoint(device, fields.INITIAL_PRESS_ONLY, ib.endpoint_id) then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true})) + elseif switch_utils.get_field_for_endpoint(device, fields.EMULATE_HELD, ib.endpoint_id) then + -- if our button doesn't differentiate between short and long holds, do it in code by keeping track of the press down time + switch_utils.set_field_for_endpoint(device, fields.START_BUTTON_PRESS, ib.endpoint_id, lua_socket.gettime(), {persist = false}) + end +end + +-- if the device distinguishes a long press event, it will always be a "held" +-- there's also a "long release" event, but this event is required to come first +function EventHandlers.long_press_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.held({state_change = true})) + if switch_utils.get_field_for_endpoint(device, fields.SUPPORTS_MULTI_PRESS, ib.endpoint_id) then + -- Ignore the next MultiPressComplete event if it is sent as part of this "long press" event sequence + switch_utils.set_field_for_endpoint(device, fields.IGNORE_NEXT_MPC, ib.endpoint_id, true) + end +end + +function EventHandlers.multi_press_complete_handler(driver, device, ib, response) + -- in the case of multiple button presses + -- emit number of times, multiple presses have been completed + if ib.data and not switch_utils.get_field_for_endpoint(device, fields.IGNORE_NEXT_MPC, ib.endpoint_id) then + local press_value = ib.data.elements.total_number_of_presses_counted.value + --capability only supports up to 6 presses + if press_value < 7 then + local button_event = capabilities.button.button.pushed({state_change = true}) + if press_value == 2 then + button_event = capabilities.button.button.double({state_change = true}) + elseif press_value > 2 then + button_event = capabilities.button.button(string.format("pushed_%dx", press_value), {state_change = true}) + end + + device:emit_event_for_endpoint(ib.endpoint_id, button_event) + else + device.log.info(string.format("Number of presses (%d) not supported by capability", press_value)) + end + end + switch_utils.set_field_for_endpoint(device, fields.IGNORE_NEXT_MPC, ib.endpoint_id, nil) +end + +local function emulate_held_event(device, ep) + local now = lua_socket.gettime() + local press_init = switch_utils.get_field_for_endpoint(device, fields.START_BUTTON_PRESS, ep) or now -- if we don't have an init time, assume instant release + if (now - press_init) < fields.TIMEOUT_THRESHOLD then + if (now - press_init) > fields.HELD_THRESHOLD then + device:emit_event_for_endpoint(ep, capabilities.button.button.held({state_change = true})) + else + device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = true})) + end + end + switch_utils.set_field_for_endpoint(device, fields.START_BUTTON_PRESS, ep, nil, {persist = false}) +end + +function EventHandlers.short_release_handler(driver, device, ib, response) + if not switch_utils.get_field_for_endpoint(device, fields.SUPPORTS_MULTI_PRESS, ib.endpoint_id) then + if switch_utils.get_field_for_endpoint(device, fields.EMULATE_HELD, ib.endpoint_id) then + emulate_held_event(device, ib.endpoint_id) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true})) + end + end +end + +return EventHandlers \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/color_utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/color_utils.lua similarity index 88% rename from drivers/SmartThings/matter-switch/src/color_utils.lua rename to drivers/SmartThings/matter-switch/src/switch_utils/color_utils.lua index ce73b1eecd..7e4c231a30 100644 --- a/drivers/SmartThings/matter-switch/src/color_utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/color_utils.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + --TODO remove the usage of these color utils once 0.48.x has been distributed -- to all hubs. local color_utils = {} @@ -46,9 +49,9 @@ end --- Convert from x/y/Y to Hue/Saturation --- If every value is missing then [x, y, Y] = [0, 0, 1] --- ---- @param x number red in range [0x0000, 0xFFFF] ---- @param y number green in range [0x0000, 0xFFFF] ---- @param Y number blue in range [0x0000, 0xFFFF] +--- @param x number|nil red in range [0x0000, 0xFFFF] +--- @param y number|nil green in range [0x0000, 0xFFFF] +--- @param Y number|nil blue in range [0x0000, 0xFFFF] --- @returns number, number equivalent hue, saturation, level each in range [0,100]% color_utils.safe_xy_to_hsv = function(x, y, Y) local safe_x = x ~= nil and x / 65536 or 0 diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua new file mode 100644 index 0000000000..18219ef459 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -0,0 +1,263 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local version = require "version" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" +local embedded_cluster_utils = require "switch_utils.embedded_cluster_utils" + +-- Include driver-side definitions when lua libs api version is < 11 +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" +end + +local DeviceConfiguration = {} +local ChildConfiguration = {} +local SwitchDeviceConfiguration = {} +local ButtonDeviceConfiguration = {} +local FanDeviceConfiguration = {} + +function ChildConfiguration.create_or_update_child_devices(driver, device, server_cluster_ep_ids, default_endpoint_id, assign_profile_fn) + if #server_cluster_ep_ids == 1 and server_cluster_ep_ids[1] == default_endpoint_id then -- no children will be created + return + end + + table.sort(server_cluster_ep_ids) + for device_num, ep_id in ipairs(server_cluster_ep_ids) do + if ep_id ~= default_endpoint_id then -- don't create a child device that maps to the main endpoint + local label_and_name = string.format("%s %d", device.label, device_num) + local child_profile, _ = assign_profile_fn(device, ep_id, true) + local existing_child_device = device:get_field(fields.IS_PARENT_CHILD_DEVICE) and switch_utils.find_child(device, ep_id) + if not existing_child_device then + driver:try_create_device({ + type = "EDGE_CHILD", + label = label_and_name, + profile = child_profile, + parent_device_id = device.id, + parent_assigned_child_key = string.format("%d", ep_id), + vendor_provided_label = label_and_name + }) + else + existing_child_device:try_update_metadata({ + profile = child_profile + }) + end + end + end + + -- Persist so that the find_child function is always set on each driver init. + device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true}) + device:set_find_child(switch_utils.find_child) +end + +function FanDeviceConfiguration.assign_profile_for_fan_ep(device, server_fan_ep_id) + local ep_info = switch_utils.get_endpoint_info(device, server_fan_ep_id) + local fan_cluster_info = switch_utils.find_cluster_on_ep(ep_info, clusters.FanControl.ID) + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + + if clusters.FanControl.are_features_supported(clusters.FanControl.types.Feature.MULTI_SPEED, fan_cluster_info.feature_map) then + table.insert(main_component_capabilities, capabilities.fanSpeedPercent.ID) + -- only fanMode can trigger AUTO, so a multi-speed fan still requires this capability if it supports AUTO + if clusters.FanControl.are_features_supported(clusters.FanControl.types.Feature.AUTO, fan_cluster_info.feature_map) then + table.insert(main_component_capabilities, capabilities.fanMode.ID) + end + else -- MULTI_SPEED is not supported + table.insert(main_component_capabilities, capabilities.fanMode.ID) + end + + table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + return "fan-modular", optional_supported_component_capabilities +end + + +function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_onoff_ep_id, is_child_device) + local ep_info = switch_utils.get_endpoint_info(device, server_onoff_ep_id) + + -- per spec, the Switch device types support OnOff as CLIENT, though some vendors break spec and support it as SERVER. + local primary_dt_id = switch_utils.find_max_subset_device_type(ep_info, fields.DEVICE_TYPE_ID.LIGHT) + or switch_utils.find_max_subset_device_type(ep_info, fields.DEVICE_TYPE_ID.SWITCH) + or switch_utils.find_primary_device_type(ep_info) + + local generic_profile = fields.device_type_profile_map[primary_dt_id] + + local static_electrical_tags = switch_utils.get_field_for_endpoint(device, fields.ELECTRICAL_TAGS, server_onoff_ep_id) + if static_electrical_tags ~= nil then + -- profiles like 'light-binary' and 'plug-binary' should drop the '-binary' and become 'light-power', 'plug-energy-powerConsumption', etc. + generic_profile = string.gsub(generic_profile, "-binary", "") .. static_electrical_tags + end + + if is_child_device and generic_profile == switch_utils.get_product_override_field(device, "initial_profile") then + generic_profile = switch_utils.get_product_override_field(device, "target_profile") or generic_profile + end + + -- if no supported device type is found, return switch-binary as a generic "OnOff EP" profile + return generic_profile or "switch-binary" +end + +-- Per the spec, these attributes are "meant to be changed only during commissioning." +function SwitchDeviceConfiguration.set_device_control_options(device) + for _, ep in ipairs(device.endpoints) do + -- before the Matter 1.3 lua libs update (HUB FW 54), OptionsBitmap was defined as LevelControlOptions + if switch_utils.find_cluster_on_ep(ep, clusters.LevelControl.ID) then + device:send(clusters.LevelControl.attributes.Options:write(device, ep.endpoint_id, clusters.LevelControl.types.LevelControlOptions.EXECUTE_IF_OFF)) + end + -- before the Matter 1.4 lua libs update (HUB FW 56), there was no OptionsBitmap type defined + if switch_utils.find_cluster_on_ep(ep, clusters.ColorControl.ID) then + local excute_if_off_bit = clusters.ColorControl.types.OptionsBitmap and clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF or 0x0001 + device:send(clusters.ColorControl.attributes.Options:write(device, ep.endpoint_id, excute_if_off_bit)) + end + end +end + +function ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, num_button_eps) + local profile_name = string.gsub(num_button_eps .. "-button", "1%-", "") -- remove the "1-" in a device with 1 button ep + if switch_utils.device_type_supports_button_switch_combination(device, default_endpoint_id) then + profile_name = "light-level-" .. profile_name + end + local motion_eps = device:get_endpoints(clusters.OccupancySensing.ID) + if #motion_eps > 0 and (num_button_eps == 3 or num_button_eps == 6) then -- only these two devices are handled + profile_name = profile_name .. "-motion" + end + local battery_support = device:get_field(fields.profiling_data.BATTERY_SUPPORT) + if battery_support == fields.battery_support.BATTERY_PERCENTAGE then + profile_name = profile_name .. "-battery" + elseif battery_support == fields.battery_support.BATTERY_LEVEL then + profile_name = profile_name .. "-batteryLevel" + end + if switch_utils.get_product_override_field(device, "is_climate_sensor_w100") then + profile_name = "3-button-battery-temperature-humidity" + end + if switch_utils.get_product_override_field(device, "is_ikea_dual_button") then + profile_name = "ikea-2-button-battery" + end + return profile_name +end + +function ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, button_eps) + -- create component mapping on the main profile button endpoints + table.sort(button_eps) + local component_map = {} + component_map["main"] = default_endpoint_id + for component_num, ep in ipairs(button_eps) do + if ep ~= default_endpoint_id then + local button_component = "button" + if #button_eps > 1 then + button_component = button_component .. component_num + end + component_map[button_component] = ep + end + end + device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) +end + + +function ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep_ids) + local msr_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_RELEASE}) + local msl_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS}) + local msm_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS}) + + for _, ep in ipairs(momentary_switch_ep_ids or {}) do + if device.profile.components[switch_utils.endpoint_to_component(device, ep)] then + device.log.info_with({hub_logs=true}, string.format("Configuring Supported Values for generic switch endpoint %d", ep)) + local supportedButtonValues_event + -- this ordering is important, since MSM & MSL devices must also support MSR + if switch_utils.tbl_contains(msm_eps, ep) then + supportedButtonValues_event = nil -- deferred to the max press handler + device:send(clusters.Switch.attributes.MultiPressMax:read(device, ep)) + switch_utils.set_field_for_endpoint(device, fields.SUPPORTS_MULTI_PRESS, ep, true, {persist = true}) + elseif switch_utils.tbl_contains(msl_eps, ep) then + supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}) + elseif switch_utils.tbl_contains(msr_eps, ep) then + supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}) + switch_utils.set_field_for_endpoint(device, fields.EMULATE_HELD, ep, true, {persist = true}) + else -- this switch endpoint only supports momentary switch, no release events + supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}) + switch_utils.set_field_for_endpoint(device, fields.INITIAL_PRESS_ONLY, ep, true, {persist = true}) + end + + if supportedButtonValues_event then + device:emit_event_for_endpoint(ep, supportedButtonValues_event) + end + device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) + else + device.log.info_with({hub_logs=true}, string.format("Component not found for generic switch endpoint %d. Skipping Supported Value configuration", ep)) + end + end +end + + +-- [[ PROFILE MATCHING AND CONFIGURATIONS ]] -- + +local function profiling_data_still_required(device) + for _, field in pairs(fields.profiling_data) do + if device:get_field(field) == nil then + return true -- data still required if a field is nil + end + end + return false +end + +function DeviceConfiguration.match_profile(driver, device) + if profiling_data_still_required(device) then return end + + local default_endpoint_id = switch_utils.find_default_endpoint(device) + local optional_component_capabilities + local updated_profile + + if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) > 0 then + updated_profile = "water-valve" + if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, + {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then + updated_profile = updated_profile .. "-level" + end + end + + local server_onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) -- get_endpoints defaults to return EPs supporting SERVER or BOTH + if #server_onoff_ep_ids > 0 then + ChildConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id, SwitchDeviceConfiguration.assign_profile_for_onoff_ep) + end + + if switch_utils.tbl_contains(server_onoff_ep_ids, default_endpoint_id) then + updated_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, default_endpoint_id) + local generic_profile = function(s) return string.find(updated_profile or "", s, 1, true) end + if generic_profile("light-level") and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then + updated_profile = "light-level-motion" + elseif switch_utils.check_switch_category_vendor_overrides(device) then + -- check whether the overwrite should be over "plug" or "light" based on the current profile + local overwrite_category = string.find(updated_profile, "plug") and "plug" or "light" + updated_profile = string.gsub(updated_profile, overwrite_category, "switch") + elseif generic_profile("light-level-colorTemperature") or generic_profile("light-color-level") then + -- ignore attempts to dynamically profile light-level-colorTemperature and light-color-level devices for now, since + -- these may lose fingerprinted Kelvin ranges when dynamically profiled. + return + end + end + + local fan_device_type_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN) + if #fan_device_type_ep_ids > 0 then + updated_profile, optional_component_capabilities = FanDeviceConfiguration.assign_profile_for_fan_ep(device, default_endpoint_id) + device:set_field(fields.MODULAR_PROFILE_UPDATED, true) + end + + -- initialize the main device card with buttons if applicable + local momentary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #momentary_switch_ep_ids) then + updated_profile = ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #momentary_switch_ep_ids) + -- All button endpoints found will be added as additional components in the profile containing the default_endpoint_id. + ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, momentary_switch_ep_ids) + ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep_ids) + end + + device:try_update_metadata({ profile = updated_profile, optional_component_capabilities = optional_component_capabilities }) +end + +return { + DeviceCfg = DeviceConfiguration, + SwitchCfg = SwitchDeviceConfiguration, + ButtonCfg = ButtonDeviceConfiguration +} diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/embedded_cluster_utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/embedded_cluster_utils.lua new file mode 100644 index 0000000000..8f52a1488c --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/switch_utils/embedded_cluster_utils.lua @@ -0,0 +1,56 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" +local utils = require "st.utils" + +-- Include driver-side definitions when lua libs api version is < 11 +local version = require "version" +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" +end + +local embedded_cluster_utils = {} + +local embedded_clusters = { + [clusters.ElectricalEnergyMeasurement.ID] = clusters.ElectricalEnergyMeasurement, + [clusters.ElectricalPowerMeasurement.ID] = clusters.ElectricalPowerMeasurement, + [clusters.ValveConfigurationAndControl.ID] = clusters.ValveConfigurationAndControl +} + +function embedded_cluster_utils.get_endpoints(device, cluster_id, opts) + -- If using older lua libs and need to check for an embedded cluster feature, + -- we must use the embedded cluster definitions here + if version.api < 11 and embedded_clusters[cluster_id] ~= nil then + local embedded_cluster = embedded_clusters[cluster_id] + local opts = opts or {} + if utils.table_size(opts) > 1 then + device.log.warn_with({hub_logs = true}, "Invalid options for get_endpoints") + return + end + local clus_has_features = function(clus, feature_bitmap) + if not feature_bitmap or not clus then return false end + return embedded_cluster.are_features_supported(feature_bitmap, clus.feature_map) + end + local eps = {} + for _, ep in ipairs(device.endpoints) do + for _, clus in ipairs(ep.clusters) do + if ((clus.cluster_id == cluster_id) + and (opts.feature_bitmap == nil or clus_has_features(clus, opts.feature_bitmap)) + and ((opts.cluster_type == nil and clus.cluster_type == "SERVER" or clus.cluster_type == "BOTH") + or (opts.cluster_type == clus.cluster_type)) + or (cluster_id == nil)) then + table.insert(eps, ep.endpoint_id) + if cluster_id == nil then break end + end + end + end + return eps + else + return device:get_endpoints(cluster_id, opts) + end + end + + return embedded_cluster_utils \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua new file mode 100644 index 0000000000..2b315c6528 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -0,0 +1,201 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local st_utils = require "st.utils" + +local SwitchFields = {} + +SwitchFields.MOST_RECENT_TEMP = "mostRecentTemp" +SwitchFields.RECEIVED_X = "receivedX" +SwitchFields.RECEIVED_Y = "receivedY" +SwitchFields.HUESAT_SUPPORT = "huesatSupport" + +SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT = 1000000 + +-- These values are a "sanity check" to check that values we are getting are reasonable +local COLOR_TEMPERATURE_KELVIN_MAX = 15000 +local COLOR_TEMPERATURE_KELVIN_MIN = 1000 +SwitchFields.COLOR_TEMPERATURE_MIRED_MAX = st_utils.round(SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MIN) +SwitchFields.COLOR_TEMPERATURE_MIRED_MIN = st_utils.round(SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MAX) + +SwitchFields.SWITCH_LEVEL_LIGHTING_MIN = 1 +SwitchFields.CURRENT_HUESAT_ATTR_MIN = 0 +SwitchFields.CURRENT_HUESAT_ATTR_MAX = 254 + +SwitchFields.DEVICE_TYPE_ID = { + AGGREGATOR = 0x000E, + BRIDGED_NODE = 0x0013, + CAMERA = 0x0142, + CHIME = 0x0146, + DIMMABLE_PLUG_IN_UNIT = 0x010B, + DOORBELL = 0x0143, + ELECTRICAL_SENSOR = 0x0510, + FAN = 0x002B, + GENERIC_SWITCH = 0x000F, + MOUNTED_ON_OFF_CONTROL = 0x010F, + MOUNTED_DIMMABLE_LOAD_CONTROL = 0x0110, + ON_OFF_PLUG_IN_UNIT = 0x010A, + LIGHT = { + ON_OFF = 0x0100, + DIMMABLE = 0x0101, + COLOR_TEMPERATURE = 0x010C, + EXTENDED_COLOR = 0x010D, + }, + SWITCH = { + ON_OFF_LIGHT = 0x0103, + DIMMER = 0x0104, + COLOR_DIMMER = 0x0105, + }, +} + +SwitchFields.device_type_profile_map = { + [SwitchFields.DEVICE_TYPE_ID.LIGHT.ON_OFF] = "light-binary", + [SwitchFields.DEVICE_TYPE_ID.LIGHT.DIMMABLE] = "light-level", + [SwitchFields.DEVICE_TYPE_ID.LIGHT.COLOR_TEMPERATURE] = "light-level-colorTemperature", + [SwitchFields.DEVICE_TYPE_ID.LIGHT.EXTENDED_COLOR] = "light-color-level", + [SwitchFields.DEVICE_TYPE_ID.SWITCH.ON_OFF_LIGHT] = "switch-binary", + [SwitchFields.DEVICE_TYPE_ID.SWITCH.DIMMER] = "switch-level", + [SwitchFields.DEVICE_TYPE_ID.SWITCH.COLOR_DIMMER] = "switch-color-level", + [SwitchFields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT] = "plug-binary", + [SwitchFields.DEVICE_TYPE_ID.DIMMABLE_PLUG_IN_UNIT] = "plug-level", + [SwitchFields.DEVICE_TYPE_ID.MOUNTED_ON_OFF_CONTROL] = "switch-binary", + [SwitchFields.DEVICE_TYPE_ID.MOUNTED_DIMMABLE_LOAD_CONTROL] = "switch-level", +} + +-- COMPONENT_TO_ENDPOINT_MAP is here to preserve the endpoint mapping for +-- devices that were joined to this driver as MCD devices before the transition +-- to join switch devices as parent-child. This value will exist in the device +-- table for devices that joined prior to this transition, and is also used for +-- button devices that require component mapping. +SwitchFields.COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" +SwitchFields.IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" + +--- If the ASSIGNED_CHILD_KEY field is populated for an endpoint, it should be +--- used as the key in the get_child_by_parent_assigned_key() function. This allows +--- multiple endpoints to associate with the same child device, though right now child +--- devices are keyed using only one endpoint id. +SwitchFields.ASSIGNED_CHILD_KEY = "__assigned_child_key" + +SwitchFields.COLOR_TEMP_BOUND_RECEIVED_KELVIN = "__colorTemp_bound_received_kelvin" +SwitchFields.COLOR_TEMP_BOUND_RECEIVED_MIRED = "__colorTemp_bound_received_mired" +SwitchFields.COLOR_TEMP_MIN = "__color_temp_min" +SwitchFields.COLOR_TEMP_MAX = "__color_temp_max" +SwitchFields.LEVEL_BOUND_RECEIVED = "__level_bound_received" +SwitchFields.LEVEL_MIN = "__level_min" +SwitchFields.LEVEL_MAX = "__level_max" +SwitchFields.COLOR_MODE = "__color_mode" + +SwitchFields.SUBSCRIBED_ATTRIBUTES_KEY = "__subscribed_attributes" + +SwitchFields.updated_fields = { + { current_field_name = "__component_to_endpoint_map_button", updated_field_name = SwitchFields.COMPONENT_TO_ENDPOINT_MAP }, + { current_field_name = "__switch_intialized", updated_field_name = nil }, + { current_field_name = "__energy_management_endpoint", updated_field_name = nil }, + { current_field_name = "__total_imported_energy", updated_field_name = nil }, +} + +SwitchFields.vendor_overrides = { + [0x115F] = { -- AQARA_MANUFACTURER_ID + [0x1006] = { ignore_combo_switch_button = true }, -- 3 Buttons(Generic Switch), 1 Channel (Dimmable Light) + [0x100A] = { ignore_combo_switch_button = true }, -- 1 Buttons(Generic Switch), 1 Channel (Dimmable Light) + [0x2004] = { is_climate_sensor_w100 = true }, -- Climate Sensor W100, requires unique profile + }, + [0x117C] = { -- IKEA_MANUFACTURER_ID + [0x8000] = { is_ikea_scroll = true }, -- BILRESA scroll wheel + [0x8001] = { is_ikea_dual_button = true}, -- BILRESA dual button + }, + [0x1189] = { -- LEDVANCE_MANUFACTURER_ID + [0x0891] = { target_profile = "switch-binary", initial_profile = "light-binary" }, + }, + [0x1321] = { -- SONOFF_MANUFACTURER_ID + [0x000C] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, + [0x000D] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, + }, +} + +SwitchFields.switch_category_vendor_overrides = { + [0x1432] = -- Elko + {0x1000}, + [0x130A] = -- Eve + {0x005D, 0x0043}, + [0x1339] = -- GE + {0x007D, 0x0074, 0x0075}, + [0x1372] = -- Innovation Matters + {0x0002}, + [0x1189] = -- Ledvance + {0x0891, 0x0892}, + [0x1021] = -- Legrand + {0x0005}, + [0x109B] = -- Leviton + {0x1001, 0x1000, 0x100B, 0x100E, 0x100C, 0x100D, 0x1009, 0x1003, 0x1004, 0x1002}, + [0x142B] = -- LeTianPai + {0x1004, 0x1003, 0x1002}, + [0x1509] = -- SmartSetup + {0x0004, 0x0001}, + [0x1321] = -- SONOFF + {0x000B, 0x000C, 0x000D}, + [0x147F] = -- U-Tec + {0x0004}, + [0x139C] = -- Zemismart + {0xEEE2, 0xAB08, 0xAB31, 0xAB04, 0xAB01, 0xAB43, 0xAB02, 0xAB03, 0xAB05} +} + +--- stores a table of endpoints that support the Electrical Sensor device type, used during profiling +--- in AvailableEndpoints and PartsList handlers for SET and TREE PowerTopology features, respectively +SwitchFields.ELECTRICAL_SENSOR_EPS = "__electrical_sensor_eps" + +--- used in tandem with an EP ID. Stores the required electrical tags "-power", "-energy-powerConsumption", etc. +--- for an Electrical Sensor EP with a "primary" endpoint, used during device profiling. +SwitchFields.ELECTRICAL_TAGS = "__electrical_tags" + +SwitchFields.MODULAR_PROFILE_UPDATED = "__modular_profile_updated" + +SwitchFields.profiling_data = { + POWER_TOPOLOGY = "__power_topology", + BATTERY_SUPPORT = "__battery_support", +} + +SwitchFields.battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_LEVEL = "BATTERY_LEVEL", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE", +} + +SwitchFields.ENERGY_METER_OFFSET = "__energy_meter_offset" +SwitchFields.CUMULATIVE_REPORTS_SUPPORTED = "__cumulative_reports_supported" +SwitchFields.LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" +SwitchFields.MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds + +SwitchFields.START_BUTTON_PRESS = "__start_button_press" +SwitchFields.TIMEOUT_THRESHOLD = 10 --arbitrary timeout +SwitchFields.HELD_THRESHOLD = 1 + +-- this is the number of buttons for which we have a static profile already made +SwitchFields.STATIC_BUTTON_PROFILE_SUPPORTED = {1, 2, 3, 4, 5, 6, 7, 8, 9} + +-- Some switches will send a MultiPressComplete event as part of a long press sequence. Normally the driver will create a +-- button capability event on receipt of MultiPressComplete, but in this case that would result in an extra event because +-- the "held" capability event is generated when the LongPress event is received. The IGNORE_NEXT_MPC flag is used +-- to tell the driver to ignore MultiPressComplete if it is received after a long press to avoid this extra event. +SwitchFields.IGNORE_NEXT_MPC = "__ignore_next_mpc" + +-- These are essentially storing the supported features of a given endpoint +-- TODO: add an is_feature_supported_for_endpoint function to matter.device that takes an endpoint +SwitchFields.EMULATE_HELD = "__emulate_held" -- for non-MSR (MomentarySwitchRelease) devices we can emulate this on the software side +SwitchFields.SUPPORTS_MULTI_PRESS = "__multi_button" -- for MSM devices (MomentarySwitchMultiPress), create an event on receipt of MultiPressComplete +SwitchFields.INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS (MomentarySwitch), but not MSR (MomentarySwitchRelease) + +SwitchFields.TEMP_BOUND_RECEIVED = "__temp_bound_received" +SwitchFields.TEMP_MIN = "__temp_min" +SwitchFields.TEMP_MAX = "__temp_max" + +SwitchFields.TRANSITION_TIME = 0 -- number of 10ths of a second +SwitchFields.TRANSITION_TIME_FAST = 3 -- 0.3 seconds + +-- For Level/Color Control cluster commands, this field indicates which bits in the OptionsOverride field are valid. In this case, we specify that the ExecuteIfOff option (bit 1) may be overridden. +SwitchFields.OPTIONS_MASK = 0x01 +-- the OptionsOverride field's first bit overrides the ExecuteIfOff option, defining whether the command should take effect when the device is off. +SwitchFields.HANDLE_COMMAND_IF_OFF = 0x01 +SwitchFields.IGNORE_COMMAND_IF_OFF = 0x00 + +return SwitchFields diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua new file mode 100644 index 0000000000..0592d9a342 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -0,0 +1,518 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local MatterDriver = require "st.matter.driver" +local fields = require "switch_utils.fields" +local st_utils = require "st.utils" +local version = require "version" +local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" +local im = require "st.matter.interaction_model" +local log = require "log" + +-- Include driver-side definitions when lua libs api version is < 11 +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" +end + +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end + +local utils = {} + +function utils.tbl_contains(array, value) + if value == nil then return false end + for _, element in pairs(array or {}) do + if element == value then + return true + end + end + return false +end + +function utils.convert_huesat_st_to_matter(val) + return st_utils.clamp_value(math.floor((val * 0xFE) / 100.0 + 0.5), fields.CURRENT_HUESAT_ATTR_MIN, fields.CURRENT_HUESAT_ATTR_MAX) +end + +function utils.get_field_for_endpoint(device, field, endpoint) + return device:get_field(string.format("%s_%d", field, endpoint)) +end + +function utils.set_field_for_endpoint(device, field, endpoint, value, additional_params) + device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) +end + +function utils.remove_field_index(device, field_name, index) + local new_table = device:get_field(field_name) + if type(new_table) == "table" then + new_table[index] = nil -- remove value associated with index from table + device:set_field(field_name, new_table) + end +end + +function utils.mired_to_kelvin(value, minOrMax) + if value == 0 then -- shouldn't happen, but has + value = 1 + log.warn(string.format("Received a color temperature of 0 mireds. Using a color temperature of 1 mired to avoid divide by zero")) + end + -- We divide inside the rounding and multiply outside of it because we expect these + -- bounds to be multiples of 100. For the maximum mired value (minimum K value), + -- add 1 before converting and round up to nearest hundreds. For the minimum mired + -- (maximum K value) value, subtract 1 before converting and round down to nearest + -- hundreds. Note that 1 is added/subtracted from the mired value in order to avoid + -- rounding errors from the conversion of Kelvin to mireds. + local kelvin_step_size = 100 + local rounding_value = 0.5 + if minOrMax == fields.COLOR_TEMP_MIN then + return st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT / (kelvin_step_size * (value + 1)) + rounding_value) * kelvin_step_size + elseif minOrMax == fields.COLOR_TEMP_MAX then + return st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT / (kelvin_step_size * (value - 1)) - rounding_value) * kelvin_step_size + else + log.warn_with({hub_logs = true}, "Attempted to convert temperature unit for an undefined value") + end +end + +function utils.get_product_override_field(device, override_key) + if device.manufacturer_info + and fields.vendor_overrides[device.manufacturer_info.vendor_id] + and fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id] + then + return fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id][override_key] + end +end + +function utils.check_field_name_updates(device) + for _, field in ipairs(fields.updated_fields) do + if device:get_field(field.current_field_name) then + if field.updated_field_name ~= nil then + device:set_field(field.updated_field_name, device:get_field(field.current_field_name), {persist = true}) + end + device:set_field(field.current_field_name, nil) + end + end +end + +function utils.check_switch_category_vendor_overrides(device) + for _, product_id in ipairs(fields.switch_category_vendor_overrides[device.manufacturer_info.vendor_id] or {}) do + if device.manufacturer_info.product_id == product_id then + return true + end + end +end + +--- device_type_supports_button_switch_combination helper function used to check +--- whether the device type for an endpoint is currently supported by a profile for +--- combination button/switch devices. +function utils.device_type_supports_button_switch_combination(device, endpoint_id) + if utils.get_product_override_field(device, "ignore_combo_switch_button") then + return false + end + local dimmable_eps = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.LIGHT.DIMMABLE) + return utils.tbl_contains(dimmable_eps, endpoint_id) +end + +--- Some devices report multiple device types which are a subset of a superset +--- device type (Ex. Dimmable Light is a superset of On/Off Light). We should map +--- to the largest superset device type supported. +--- This can be done by matching to the device type with the highest ID +--- note: that superset device types have a higher ID than those of their subset +--- is heuristic and could therefore break in the future, were the spec expanded +function utils.find_max_subset_device_type(ep, device_type_set) + if ep.endpoint_id == 0 then return end -- EP-scoped device types not permitted on Root Node + local primary_dt_id = -1 + for _, dt in ipairs(ep.device_types) do + -- only device types in the subset should be considered. + if utils.tbl_contains(device_type_set, dt.device_type_id) then + primary_dt_id = math.max(primary_dt_id, dt.device_type_id) + end + end + return (primary_dt_id > 0) and primary_dt_id or nil +end + +--- Lights and Switches are Device Types that have Superset-style functionality +--- For all other device types, this function should be used to identify the primary device type +function utils.find_primary_device_type(ep_info) + for _, dt in ipairs(ep_info.device_types) do + if dt.device_type_id ~= fields.DEVICE_TYPE_ID.BRIDGED_NODE then + -- if this is not a bridged node, return the first device type seen + return dt.device_type_id + end + end +end + +--- find_default_endpoint is a helper function to handle situations where +--- device does not have endpoint ids in sequential order from 1 +function utils.find_default_endpoint(device) + -- Buttons should not be set on the main component for the Aqara Climate Sensor W100, + if utils.get_product_override_field(device, "is_climate_sensor_w100") then + return device.MATTER_DEFAULT_ENDPOINT + end + + local onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) + local momentary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + local fan_endpoint_ids = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN) + + local get_first_non_zero_endpoint = function(endpoints) + table.sort(endpoints) + for _,ep in ipairs(endpoints) do + if ep ~= 0 then -- 0 is the matter RootNode endpoint + return ep + end + end + return nil + end + + -- Return the first fan endpoint as the default endpoint if any is found + if #fan_endpoint_ids > 0 then + return get_first_non_zero_endpoint(fan_endpoint_ids) + end + + -- Return the first onoff endpoint as the default endpoint if no momentary switch endpoints are present + if #momentary_switch_ep_ids == 0 and #onoff_ep_ids > 0 then + return get_first_non_zero_endpoint(onoff_ep_ids) + end + + -- Return the first momentary switch endpoint as the default endpoint if no onoff endpoints are present + if #onoff_ep_ids == 0 and #momentary_switch_ep_ids > 0 then + return get_first_non_zero_endpoint(momentary_switch_ep_ids) + end + + -- If both onoff and momentary switch endpoints are present, check the device type on the first onoff + -- endpoint. If it is not a supported device type, return the first momentary switch endpoint as the + -- default endpoint. + if #onoff_ep_ids > 0 and #momentary_switch_ep_ids > 0 then + local default_endpoint_id = get_first_non_zero_endpoint(onoff_ep_ids) + if utils.device_type_supports_button_switch_combination(device, default_endpoint_id) then + return default_endpoint_id + else + device.log.warn("The main switch endpoint does not contain a supported device type for a component configuration with buttons") + return get_first_non_zero_endpoint(momentary_switch_ep_ids) + end + end + + device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) + return device.MATTER_DEFAULT_ENDPOINT +end + +function utils.component_to_endpoint(device, component) + local map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {} + if map[component] then + return map[component] + end + return utils.find_default_endpoint(device) +end + +--- An extension of the library function endpoint_to_component, used to support a mapping scheme +--- that optionally includes cluster and attribute ids so that multiple components can be mapped +--- to a single endpoint. This extension also handles the case that multiple endpoints map to the +--- same component +--- +--- @param device any a Matter device object +--- @param ep_info number|table either an ep_id or a table { endpoint_id, optional(cluster_id), optional(attribute_id) } +--- where cluster_id is required for an attribute_id to be handled. +--- @return string component +function utils.endpoint_to_component(device, ep_info) + if type(ep_info) == "number" then + ep_info = { endpoint_id = ep_info } + end + for component, map_info in pairs(device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {}) do + if type(map_info) == "number" and map_info == ep_info.endpoint_id then + return component + elseif type(map_info) == "table" then + if type(map_info.endpoint_id) == "number" then + map_info = {map_info} + end + for _, ep_map_info in ipairs(map_info) do + if type(ep_map_info) == "number" and ep_map_info == ep_info.endpoint_id then + return component + elseif type(ep_map_info) == "table" and ep_map_info.endpoint_id == ep_info.endpoint_id + and (not ep_map_info.cluster_id or (ep_map_info.cluster_id == ep_info.cluster_id + and (not ep_map_info.attribute_ids or utils.tbl_contains(ep_map_info.attribute_ids, ep_info.attribute_id)))) then + return component + end + end + end + end + return "main" +end + +--- An extension of the library function emit_event_for_endpoint, used to support devices with +--- multiple components mapped to the same endpoint. This is handled by extending the parameters to optionally +--- include a cluster id and attribute id for more specific routing +--- +--- @param device any a Matter device object +--- @param ep_info number|table endpoint_id or an ib (the ib data includes endpoint_id, cluster_id, and attribute_id fields) +--- @param event any a capability event object +function utils.emit_event_for_endpoint(device, ep_info, event) + if type(ep_info) == "number" then + ep_info = { endpoint_id = ep_info } + end + if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then + local child = utils.find_child(device, ep_info.endpoint_id) + if child ~= nil then + child:emit_event(event) + return + end + end + local comp_id = utils.endpoint_to_component(device, ep_info) + local comp = device.profile.components[comp_id] + device:emit_component_event(comp, event) +end + +function utils.find_child(parent_device, ep_id) + local assigned_key = utils.get_field_for_endpoint(parent_device, fields.ASSIGNED_CHILD_KEY, ep_id) or ep_id + return parent_device:get_child_by_parent_assigned_key(string.format("%d", assigned_key)) +end + +function utils.get_endpoint_info(device, endpoint_id) + for _, ep in ipairs(device.endpoints) do + if ep.endpoint_id == endpoint_id then return ep end + end + return {} +end + +function utils.find_cluster_on_ep(ep, cluster_id, opts) + opts = opts or {} + local clus_has_features = function(cluster, checked_feature) + return (cluster.feature_map & checked_feature) == checked_feature + end + for _, cluster in ipairs(ep.clusters) do + if ((cluster.cluster_id == cluster_id) + and (opts.feature_bitmap == nil or clus_has_features(cluster, opts.feature_bitmap)) + and ((opts.cluster_type == nil and cluster.cluster_type == "SERVER" or cluster.cluster_type == "BOTH") + or (opts.cluster_type == cluster.cluster_type)) + or (cluster_id == nil)) then + return cluster + end + end +end + +-- Fallback handler for responses that dont have their own handler +function utils.matter_handler(driver, device, response_block) + device.log.info(string.format("Fallback handler for %s", response_block)) +end + +-- get a list of endpoints for a specified device type. +function utils.get_endpoints_by_device_type(device, device_type_id, opts) + opts = opts or {} + local dt_eps = {} + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == device_type_id then + if opts.with_info then + table.insert(dt_eps, ep) + else + table.insert(dt_eps, ep.endpoint_id) + end + break + end + end + end + return dt_eps +end + +--helper function to create list of multi press values +function utils.create_multi_press_values_list(size, supportsHeld) + local list = {"pushed", "double"} + if supportsHeld then table.insert(list, "held") end + -- add multi press values of 3 or greater to the list + for i=3, size do + table.insert(list, string.format("pushed_%dx", i)) + end + return list +end + +function utils.detect_bridge(device) + return #utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.AGGREGATOR) > 0 +end + +--- Generalizes the 'get_latest_state' function to be callable with extra endpoint information, described below, +--- without directly specifying the expected component. See the 'get_latest_state' definition for more +--- information about parameters and expected functionality otherwise. +--- +--- @param endpoint_info number|table an endpoint id or an ib (the ib data includes endpoint_id, cluster_id, and attribute_id fields) +function utils.get_latest_state_for_endpoint(device, endpoint_info, capability_id, attribute_id, default_value, default_state_table) + if type(endpoint_info) == "number" then + endpoint_info = { endpoint_id = endpoint_info } + end + + local component = device:endpoint_to_component(endpoint_info) + local state_device = utils.find_child(device, endpoint_info.endpoint_id) or device + return state_device:get_latest_state(component, capability_id, attribute_id, default_value, default_state_table) +end + +function utils.report_power_consumption_to_st_energy(device, endpoint_id, total_imported_energy_wh) + local current_time = os.time() + local last_time = device:get_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP) or 0 + + -- Ensure that the previous report was sent at least 15 minutes ago + if fields.MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then + return + end + device:set_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) + + local previous_imported_report = utils.get_latest_state_for_endpoint(device, endpoint_id, capabilities.powerConsumptionReport.ID, + capabilities.powerConsumptionReport.powerConsumption.NAME, { energy = total_imported_energy_wh }) -- default value if nil + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + local epoch_to_iso8601 = function(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) end -- Return an ISO-8061 timestamp from UTC + device:emit_event_for_endpoint(endpoint_id, capabilities.powerConsumptionReport.powerConsumption({ + start = epoch_to_iso8601(last_time), + ["end"] = epoch_to_iso8601(current_time - 1), + deltaEnergy = total_imported_energy_wh - previous_imported_report.energy, + energy = total_imported_energy_wh + })) +end + +--- sets fields for handling EPs with the Electrical Sensor device type +--- +--- @param device table a Matter device object +--- @param electrical_sensor_ep table an EP object that includes an Electrical Sensor device type +--- @param associated_endpoint_ids table EP IDs that are associated with the Electrical Sensor EP +--- @return boolean +function utils.set_fields_for_electrical_sensor_endpoint(device, electrical_sensor_ep, associated_endpoint_ids) + if #associated_endpoint_ids == 0 then + return false + else + local tags = "" + if utils.find_cluster_on_ep(electrical_sensor_ep, clusters.ElectricalPowerMeasurement.ID) then tags = tags.."-power" end + if utils.find_cluster_on_ep(electrical_sensor_ep, clusters.ElectricalEnergyMeasurement.ID) then tags = tags.."-energy-powerConsumption" end + -- note: using the lowest valued EP ID here is arbitrary (not spec defined) and is done to create internal consistency + -- Ex. for the NODE topology, electrical capabilities will then be associated with the default (aka lowest ID'd) OnOff EP + table.sort(associated_endpoint_ids) + local primary_associated_ep_id = associated_endpoint_ids[1] + -- map the required electrical tags for this electrical sensor EP with the first associated EP ID, used later during profling. + utils.set_field_for_endpoint(device, fields.ELECTRICAL_TAGS, primary_associated_ep_id, tags) + utils.set_field_for_endpoint(device, fields.ASSIGNED_CHILD_KEY, electrical_sensor_ep.endpoint_id, string.format("%d", primary_associated_ep_id), { persist = true }) + return true + end +end + +function utils.handle_electrical_sensor_info(device) + local electrical_sensor_eps = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR, { with_info = true }) + if #electrical_sensor_eps == 0 then + -- no Electrical Sensor EPs are supported. Set profiling data to false and return + device:set_field(fields.profiling_data.POWER_TOPOLOGY, false, {persist=true}) + return + end + + -- check the feature map for the first (or only) Electrical Sensor EP + local endpoint_power_topology_cluster = utils.find_cluster_on_ep(electrical_sensor_eps[1], clusters.PowerTopology.ID) or {} + local endpoint_power_topology_feature_map = endpoint_power_topology_cluster.feature_map or 0 + if clusters.PowerTopology.are_features_supported(clusters.PowerTopology.types.Feature.SET_TOPOLOGY, endpoint_power_topology_feature_map) then + device:set_field(fields.ELECTRICAL_SENSOR_EPS, electrical_sensor_eps) -- assume any other stored EPs also have a SET topology + local available_eps_req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) -- SET read + for _, ep in ipairs(electrical_sensor_eps) do + available_eps_req:merge(clusters.PowerTopology.attributes.AvailableEndpoints:read(device, ep.endpoint_id)) + end + device:send(available_eps_req) + return + elseif clusters.PowerTopology.are_features_supported(clusters.PowerTopology.types.Feature.TREE_TOPOLOGY, endpoint_power_topology_feature_map) then + device:set_field(fields.ELECTRICAL_SENSOR_EPS, electrical_sensor_eps) -- assume any other stored EPs also have a TREE topology + local parts_list_req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) -- TREE read + for _, ep in ipairs(electrical_sensor_eps) do + parts_list_req:merge(clusters.Descriptor.attributes.PartsList:read(device, ep.endpoint_id)) + end + device:send(parts_list_req) + return + elseif clusters.PowerTopology.are_features_supported(clusters.PowerTopology.types.Feature.NODE_TOPOLOGY, endpoint_power_topology_feature_map) then + -- EP has a NODE topology, so there is only ONE Electrical Sensor EP + device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.NODE_TOPOLOGY, {persist=true}) + if utils.set_fields_for_electrical_sensor_endpoint(device, electrical_sensor_eps[1], device:get_endpoints(clusters.OnOff.ID)) == false then + device.log.warn("Electrical Sensor EP with NODE topology found, but no OnOff EPs exist. Electrical Sensor capabilities will not be exposed.") + end + return + end +end + +function utils.lazy_load(sub_driver_name) + if version.api >= 16 then + return MatterDriver.lazy_load_sub_driver_v2(sub_driver_name) + end +end + +function utils.lazy_load_if_possible(sub_driver_name) + if version.api >= 16 then + return MatterDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return MatterDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end + +--- helper for the switch subscribe override, which adds to a subscribed request for a checked device +--- +--- @param checked_device any a Matter device object, either a parent or child device, so not necessarily the same as device +--- @param subscribe_request table a subscribe request that will be appended to as needed for the device +--- @param capabilities_seen table a list of capabilities that have already been checked by previously handled devices +--- @param attributes_seen table a list of attributes that have already been checked +--- @param events_seen table a list of events that have already been checked +--- @param subscribed_attributes table key-value pairs mapping capability ids to subscribed attributes +--- @param subscribed_events table key-value pairs mapping capability ids to subscribed events +function utils.populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, subscribed_attributes, subscribed_events) + for _, component in pairs(checked_device.st_store.profile.components) do + for _, capability in pairs(component.capabilities) do + if not capabilities_seen[capability.id] then + for _, attr in ipairs(subscribed_attributes[capability.id] or {}) do + local cluster_id = attr.cluster or attr._cluster.ID + local attr_id = attr.ID or attr.attribute + if not attributes_seen[cluster_id] or not attributes_seen[cluster_id][attr_id] then + local ib = im.InteractionInfoBlock(nil, cluster_id, attr_id) + subscribe_request:with_info_block(ib) + attributes_seen[cluster_id] = attributes_seen[cluster_id] or {} + attributes_seen[cluster_id][attr_id] = ib + end + end + for _, event in ipairs(subscribed_events[capability.id] or {}) do + local cluster_id = event.cluster or event._cluster.ID + local event_id = event.ID or event.event + if not events_seen[cluster_id] or not events_seen[cluster_id][event_id] then + local ib = im.InteractionInfoBlock(nil, cluster_id, nil, event_id) + subscribe_request:with_info_block(ib) + events_seen[cluster_id] = events_seen[cluster_id] or {} + events_seen[cluster_id][event_id] = ib + end + end + capabilities_seen[capability.id] = true -- only loop through any capability once + end + end + end +end + +--- create and send a subscription request by checking all devices, accounting for both parent and child devices +--- +--- @param device any a Matter device object +function utils.subscribe(device) + local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) + local devices_seen, capabilities_seen, attributes_seen, events_seen = {}, {}, {}, {} + + for _, endpoint_info in ipairs(device.endpoints) do + local checked_device = utils.find_child(device, endpoint_info.endpoint_id) or device + if not devices_seen[checked_device.id] then + utils.populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, + device.driver.subscribed_attributes, device.driver.subscribed_events + ) + devices_seen[checked_device.id] = true -- only loop through any device once + end + end + -- The refresh capability command handler in the lua libs uses this key to determine which attributes to read. Note + -- that only attributes_seen needs to be saved here, and not events_seen, since the refresh handler only checks + -- attributes and not events. + device:set_field(fields.SUBSCRIBED_ATTRIBUTES_KEY, attributes_seen) + + -- If the type of battery support has not yet been determined, add the PowerSource AttributeList to the list of + -- subscribed attributes in order to determine which if any battery capability should be used. + if device:get_field(fields.profiling_data.BATTERY_SUPPORT) == nil then + local ib = im.InteractionInfoBlock(nil, clusters.PowerSource.ID, clusters.PowerSource.attributes.AttributeList.ID) + subscribe_request:with_info_block(ib) + end + + if #subscribe_request.info_blocks > 0 then + device:send(subscribe_request) + end +end + +return utils diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua index 830a9b99f8..d47c666166 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua @@ -1,31 +1,17 @@ --- Copyright 2024 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. +-- Copyright © 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -local test = require "integration_test" -local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" -local utils = require "st.utils" -local dkjson = require "dkjson" -local uint32 = require "st.matter.data_types.Uint32" - local clusters = require "st.matter.generated.zap_clusters" -local button_attr = capabilities.button.button +local t_utils = require "integration_test.utils" +local test = require "integration_test" +local uint32 = require "st.matter.data_types.Uint32" -- Mock a 3-button device with temperature and humidity sensor local aqara_mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("3-button-battery-temperature-humidity.yml"), manufacturer_info = {vendor_id = 0x115F, product_id = 0x2004, product_name = "Aqara Climate Sensor W100"}, + matter_version = {hardware = 1, software = 1}, label = "Climate Sensor W100", device_id = "00000000-1111-2222-3333-000000000001", endpoints = { @@ -111,17 +97,20 @@ local aqara_mock_device = test.mock_device.build_test_matter_device({ local function configure_buttons() test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 3)}) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", button_attr.pushed({state_change = false}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", capabilities.button.button.pushed({state_change = false}))) test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 4)}) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 5)}) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", capabilities.button.button.pushed({state_change = false}))) end local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(aqara_mock_device) local cluster_subscribe_list = { + clusters.PowerSource.server.attributes.AttributeList, clusters.PowerSource.server.attributes.BatPercentRemaining, clusters.TemperatureMeasurement.attributes.MeasuredValue, clusters.TemperatureMeasurement.attributes.MinMeasuredValue, @@ -140,30 +129,23 @@ local function test_init() end end - test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) - local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() - test.socket.matter:__expect_send({aqara_mock_device.id, read_attribute_list}) - configure_buttons() - aqara_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(aqara_mock_device) - test.set_rpc_version(5) - test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) - local device_info_copy = utils.deep_copy(aqara_mock_device.raw_st_data) - device_info_copy.profile.id = "3-button-battery-temperature-humidity" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "infoChanged", device_info_json }) + test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "init" }) test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) - configure_buttons() + + test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) + aqara_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) local function update_profile() - test.socket.matter:__queue_receive({aqara_mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data(aqara_mock_device, 6, {uint32(0x0C)})}) + test.socket.matter:__queue_receive({aqara_mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( + aqara_mock_device, 6, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)} + )}) + configure_buttons() aqara_mock_device:expect_metadata_update({ profile = "3-button-battery-temperature-humidity" }) end @@ -324,7 +306,7 @@ test.register_coroutine_test( clusters.Switch.events.MultiPressComplete:build_test_event_report(aqara_mock_device, 4, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1}) } ) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", button_attr.double({state_change = true}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", capabilities.button.button.double({state_change = true}))) end ) @@ -344,7 +326,7 @@ test.register_coroutine_test( clusters.Switch.events.LongPress:build_test_event_report(aqara_mock_device, 3, {new_position = 1}) } ) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", button_attr.held({state_change = true}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", capabilities.button.button.held({state_change = true}))) test.socket.matter:__queue_receive( { aqara_mock_device.id, @@ -370,7 +352,7 @@ test.register_coroutine_test( clusters.Switch.events.LongPress:build_test_event_report(aqara_mock_device, 5, {new_position = 1}) } ) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", button_attr.held({state_change = true}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", capabilities.button.button.held({state_change = true}))) test.socket.matter:__queue_receive( { aqara_mock_device.id, @@ -391,7 +373,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send( - aqara_mock_device:generate_test_message("button1", button_attr.double({state_change = true})) + aqara_mock_device:generate_test_message("button1", capabilities.button.button.double({state_change = true})) ) end ) @@ -475,4 +457,3 @@ test.register_coroutine_test( ) test.run_registered_tests() - diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_cube.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_cube.lua index e641cb88ca..3c377255ab 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_cube.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_cube.lua @@ -1,3 +1,6 @@ +-- Copyright © 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.add_package_capability("cubeAction.yml") test.add_package_capability("cubeFace.yml") diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua index 369689e181..35f4cd9ce7 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua @@ -1,27 +1,20 @@ --- Copyright 2024 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. +-- Copyright © 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" local utils = require "st.utils" local dkjson = require "dkjson" - local clusters = require "st.matter.clusters" local button_attr = capabilities.button.button +local version = require "version" -local DEFERRED_CONFIGURE = "__DEFERRED_CONFIGURE" +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" +end local aqara_parent_ep = 4 local aqara_child1_ep = 1 @@ -30,6 +23,7 @@ local aqara_child2_ep = 2 local aqara_mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("4-button.yml"), manufacturer_info = {vendor_id = 0x115F, product_id = 0x1009, product_name = "Aqara Light Switch H2"}, + matter_version = {hardware = 1, software = 1}, label = "Aqara Light Switch", device_id = "00000000-1111-2222-3333-000000000001", endpoints = { @@ -38,7 +32,8 @@ local aqara_mock_device = test.mock_device.build_test_matter_device({ clusters = { {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, {cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 2 }, - {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 5 } + {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 5 }, + {cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 1 } -- NODE_TOPOLOGY }, device_types = { {device_type_id = 0x0016, device_type_revision = 1}, -- RootNode @@ -126,6 +121,8 @@ local cumulative_report_val_19 = { end_timestamp = 0, start_systime = 0, end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 } local cumulative_report_val_29 = { @@ -134,6 +131,8 @@ local cumulative_report_val_29 = { end_timestamp = 0, start_systime = 0, end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 } local cumulative_report_val_39 = { @@ -142,6 +141,8 @@ local cumulative_report_val_39 = { end_timestamp = 0, start_systime = 0, end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 } local function configure_buttons() @@ -159,7 +160,8 @@ local function configure_buttons() end local function test_init() - local opts = { persist = true } + test.disable_startup_messages() + test.mock_device.add_test_device(aqara_mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.Switch.server.events.InitialPress, @@ -176,13 +178,16 @@ local function test_init() subscribe_request:merge(cluster:subscribe(aqara_mock_device)) end end + + -- Test added -> doConfigure logic + test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) + test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "init" }) test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) - test.mock_devices_api._expected_device_updates[aqara_mock_device.device_id] = "00000000-1111-2222-3333-000000000001" - test.mock_devices_api._expected_device_updates[1] = {device_id = "00000000-1111-2222-3333-000000000001"} - test.mock_devices_api._expected_device_updates[1].metadata = {deviceId="00000000-1111-2222-3333-000000000001", profileReference="4-button"} + configure_buttons() + aqara_mock_device:expect_metadata_update({ profile = "4-button" }) aqara_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(aqara_mock_device) -- to test powerConsumptionReport test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "create_poll_report_schedule") @@ -206,11 +211,6 @@ local function test_init() parent_assigned_child_key = string.format("%d", aqara_child2_ep) }) - test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) - configure_buttons() - test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) - - aqara_mock_device:set_field(DEFERRED_CONFIGURE, true, opts) local device_info_copy = utils.deep_copy(aqara_mock_device.raw_st_data) device_info_copy.profile.id = "4-button" local device_info_json = dkjson.encode(device_info_copy) @@ -275,10 +275,8 @@ test.register_coroutine_test( function() test.socket.matter:__queue_receive( { - -- don't use "aqara_mock_children[aqara_child1_ep].id," - -- because energy management is at the root endpoint. aqara_mock_device.id, - clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_device, 1, 17000) + clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_device, 0, 17000) } ) @@ -287,10 +285,12 @@ test.register_coroutine_test( aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.powerMeter.power({value = 17.0, unit="W"})) ) + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + test.socket.matter:__queue_receive( { aqara_mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_19) + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 0, cumulative_report_val_19) } ) @@ -298,12 +298,19 @@ test.register_coroutine_test( aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) ) - -- in order to do powerConsumptionReport, CumulativeEnergyImported must be called twice. - -- This is because related variable settings are required in set_poll_report_timer_and_schedule(). + test.socket.capability:__expect_send( + aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 19.0 + })) + ) + test.socket.matter:__queue_receive( { aqara_mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_29) + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 0, cumulative_report_val_29) } ) @@ -311,11 +318,16 @@ test.register_coroutine_test( aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) ) + -- don't send a powerConsumptionReport event, 15 minutes have not passed since the last one. + + test.wait_for_events() + test.mock_time.advance_time(1500) + test.socket.matter:__queue_receive( { aqara_mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - aqara_mock_device, 1, cumulative_report_val_39 + aqara_mock_device, 0, cumulative_report_val_39 ) } ) @@ -324,13 +336,11 @@ test.register_coroutine_test( aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.energyMeter.energy({ value = 39.0, unit = "Wh" })) ) - -- to test powerConsumptionReport - test.mock_time.advance_time(2000) test.socket.capability:__expect_send( aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:33:19Z", - deltaEnergy = 0.0, + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:40:00Z", + deltaEnergy = 20.0, energy = 39.0 })) ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua deleted file mode 100644 index bf791d906b..0000000000 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ /dev/null @@ -1,751 +0,0 @@ --- Copyright 2024 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 capabilities = require "st.capabilities" -local t_utils = require "integration_test.utils" - -local clusters = require "st.matter.clusters" - -clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" -clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" - -local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("plug-level-power-energy-powerConsumption.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, - }, - device_types = { - { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode - } - }, - { - endpoint_id = 1, - clusters = { - { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, - { cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", feature_map = 0, }, - }, - device_types = { - { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor - } - }, - { - endpoint_id = 2, - clusters = { - { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} - }, - device_types = { - { device_type_id = 0x010A, device_type_revision = 1 } -- OnOff Plug - } - }, - }, -}) - - -local mock_device_periodic = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("plug-energy-powerConsumption.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, - }, - device_types = { - { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode - } - }, - { - endpoint_id = 1, - clusters = { - { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 10, }, - }, - device_types = { - { device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor - } - }, - }, -}) - -local subscribed_attributes_periodic = { - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, -} -local subscribed_attributes = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ElectricalPowerMeasurement.attributes.ActivePower, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, -} - -local cumulative_report_val_19 = { - energy = 19000, - start_timestamp = 0, - end_timestamp = 0, - start_systime = 0, - end_systime = 0, -} - -local cumulative_report_val_29 = { - energy = 29000, - start_timestamp = 0, - end_timestamp = 0, - start_systime = 0, - end_systime = 0, -} - -local cumulative_report_val_39 = { - energy = 39000, - start_timestamp = 0, - end_timestamp = 0, - start_systime = 0, - end_systime = 0, -} - -local periodic_report_val_23 = { - energy = 23000, - start_timestamp = 0, - end_timestamp = 0, - start_systime = 0, - end_systime = 0, -} - -local function test_init() - local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) - for i, cluster in ipairs(subscribed_attributes) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) - -- to test powerConsumptionReport - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "create_poll_report_schedule") -end -test.set_test_init_function(test_init) - -local function test_init_periodic() - local subscribe_request = subscribed_attributes_periodic[1]:subscribe(mock_device_periodic) - for i, cluster in ipairs(subscribed_attributes_periodic) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_periodic)) - end - end - test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) - test.mock_device.add_test_device(mock_device_periodic) - -- to test powerConsumptionReport - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "create_poll_report_schedule") -end - -test.register_message_test( - "On command should send the appropriate commands", - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "switch", component = "main", command = "on", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, 2) - } - } - } -) - -test.register_message_test( - "Off command should send the appropriate commands", - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "off" } - } - }, - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "switch", component = "main", command = "off", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.Off(mock_device, 2) - } - } - } -) - -test.register_message_test( - "Active power measurement should generate correct messages", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ElectricalPowerMeasurement.server.attributes.ActivePower:build_test_report_data(mock_device, 1, 17000) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.powerMeter.power({value = 17.0, unit="W"})) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } - } - } - } -) - -test.register_coroutine_test( - "Cumulative Energy measurement should generate correct messages", - function() - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_19 - ) - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_19 - ) - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_29 - ) - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_39 - ) - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 39.0, unit = "Wh" })) - ) - test.mock_time.advance_time(2000) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:33:19Z", - deltaEnergy = 0.0, - energy = 39.0 - })) - ) - end -) - -test.register_message_test( - "Periodic Energy as subordinate to Cumulative Energy measurement should not generate any messages", - { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data(mock_device, 1, periodic_report_val_23) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data(mock_device, 1, periodic_report_val_23) - } - }, - } -) - -test.register_coroutine_test( - "Periodic Energy measurement should generate correct messages", - function() - test.socket.matter:__queue_receive( - { - mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( - mock_device_periodic, 1, periodic_report_val_23 - ) - } - ) - test.socket.capability:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 23.0, unit="Wh"})) - ) - test.socket.matter:__queue_receive( - { - mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( - mock_device_periodic, 1, periodic_report_val_23 - ) - } - ) - test.socket.capability:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 46.0, unit="Wh"})) - ) - test.socket.matter:__queue_receive( - { - mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( - mock_device_periodic, 1, periodic_report_val_23 - ) - } - ) - test.socket.capability:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 69.0, unit="Wh"})) - ) - test.mock_time.advance_time(2000) - test.socket.capability:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:33:19Z", - deltaEnergy = 0.0, - energy = 69.0 - })) - ) - end, - { test_init = test_init_periodic } -) - -local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds - -test.register_coroutine_test( - "Generated poll timer (<15 minutes) gets correctly set", function() - - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_19 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) - ) - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_19 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) - ) - test.wait_for_events() - test.mock_time.advance_time(899) - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_29 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) - ) - test.wait_for_events() - local report_import_poll_timer = mock_device:get_field("__recurring_import_report_poll_timer") - local import_timer_length = mock_device:get_field("__import_report_timeout") - assert(report_import_poll_timer ~= nil, "report_import_poll_timer should exist") - assert(import_timer_length ~= nil, "import_timer_length should exist") - assert(import_timer_length == MINIMUM_ST_ENERGY_REPORT_INTERVAL, "import_timer should min_interval") - end -) - -test.register_coroutine_test( - "Generated poll timer (>15 minutes) gets correctly set", function() - - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_19 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) - ) - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_19 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) - ) - test.wait_for_events() - test.mock_time.advance_time(2000) - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_29 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:33:19Z", - deltaEnergy = 0.0, - energy = 29.0 - })) - ) - test.wait_for_events() - local report_import_poll_timer = mock_device:get_field("__recurring_import_report_poll_timer") - local import_timer_length = mock_device:get_field("__import_report_timeout") - assert(report_import_poll_timer ~= nil, "report_import_poll_timer should exist") - assert(import_timer_length ~= nil, "import_timer_length should exist") - assert(import_timer_length == 2000, "import_timer should min_interval") - end -) - -test.register_coroutine_test( - "Check when the device is removed", function() - - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_19 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) - ) - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_19 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) - ) - test.wait_for_events() - test.mock_time.advance_time(2000) - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_29 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:33:19Z", - deltaEnergy = 0.0, - energy = 29.0 - })) - ) - test.wait_for_events() - local report_import_poll_timer = mock_device:get_field("__recurring_import_report_poll_timer") - local import_timer_length = mock_device:get_field("__import_report_timeout") - assert(report_import_poll_timer ~= nil, "report_import_poll_timer should exist") - assert(import_timer_length ~= nil, "import_timer_length should exist") - assert(import_timer_length == 2000, "import_timer should min_interval") - - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "removed" }) - test.wait_for_events() - report_import_poll_timer = mock_device:get_field("__recurring_import_report_poll_timer") - import_timer_length = mock_device:get_field("__import_report_timeout") - assert(report_import_poll_timer == nil, "report_import_poll_timer should exist") - assert(import_timer_length == nil, "import_timer_length should exist") - end -) - -test.register_coroutine_test( - "Generated periodic import energy device poll timer (<15 minutes) gets correctly set", function() - test.socket["matter"]:__queue_receive( - { - mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( - mock_device_periodic, 1, periodic_report_val_23 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 23.0, unit = "Wh" })) - ) - test.socket["matter"]:__queue_receive( - { - mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( - mock_device_periodic, 1, periodic_report_val_23 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 46.0, unit = "Wh" })) - ) - test.wait_for_events() - test.mock_time.advance_time(899) - test.socket["matter"]:__queue_receive( - { - mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( - mock_device_periodic, 1, periodic_report_val_23 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 69.0, unit = "Wh" })) - ) - test.wait_for_events() - local report_import_poll_timer = mock_device_periodic:get_field("__recurring_import_report_poll_timer") - local import_timer_length = mock_device_periodic:get_field("__import_report_timeout") - assert(report_import_poll_timer ~= nil, "report_import_poll_timer should exist") - assert(import_timer_length ~= nil, "import_timer_length should exist") - assert(import_timer_length == MINIMUM_ST_ENERGY_REPORT_INTERVAL, "import_timer should min_interval") - end, - { test_init = test_init_periodic } -) - -test.register_coroutine_test( - "Generated periodic import energy device poll timer (>15 minutes) gets correctly set", function() - test.socket["matter"]:__queue_receive( - { - mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( - mock_device_periodic, 1, periodic_report_val_23 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 23.0, unit = "Wh" })) - ) - test.socket["matter"]:__queue_receive( - { - mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( - mock_device_periodic, 1, periodic_report_val_23 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 46.0, unit = "Wh" })) - ) - test.wait_for_events() - test.mock_time.advance_time(2000) - test.socket["matter"]:__queue_receive( - { - mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( - mock_device_periodic, 1, periodic_report_val_23 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 69.0, unit = "Wh" })) - ) - test.socket["capability"]:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ - deltaEnergy=0.0, - ["end"] = "1970-01-01T00:33:19Z", - energy=69.0, - start="1970-01-01T00:00:00Z" - })) - ) - test.wait_for_events() - local report_import_poll_timer = mock_device_periodic:get_field("__recurring_import_report_poll_timer") - local import_timer_length = mock_device_periodic:get_field("__import_report_timeout") - assert(report_import_poll_timer ~= nil, "report_import_poll_timer should exist") - assert(import_timer_length ~= nil, "import_timer_length should exist") - assert(import_timer_length == 2000, "import_timer should min_interval") - end, - { test_init = test_init_periodic } -) - -test.register_coroutine_test( - "Test profile change on init for Electrical Sensor device type", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end, - { test_init = test_init } -) - -test.register_coroutine_test( - "Test profile change on init for only Periodic Electrical Sensor device type", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "doConfigure" }) - mock_device_periodic:expect_metadata_update({ profile = "plug-energy-powerConsumption" }) - mock_device_periodic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end, - { test_init = test_init_periodic } -) - -test.register_message_test( - "Set level command should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "switchLevel", component = "main", command = "setLevel", args = {20,20} } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switchLevel", capability_cmd_id = "setLevel" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 2, math.floor(20/100.0 * 254), 20, 0 ,0) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 2) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 2, 50) - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.switchLevel.level(20)) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 2, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } - } - }, - } -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua new file mode 100644 index 0000000000..8e3ad613da --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua @@ -0,0 +1,738 @@ +-- Copyright © 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" +local version = require "version" +local st_utils = require "st.utils" + +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" +end + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("plug-level-power-energy-powerConsumption.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, + { cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", feature_map = 0, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + } + }, + { + endpoint_id = 2, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + { device_type_id = 0x010B, device_type_revision = 1 }, -- Dimmable Plug In Unit + } + }, + { + endpoint_id = 3, + clusters = { + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + } + }, + { + endpoint_id = 4, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + { cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + }, + device_types = { + { device_type_id = 0x010B, device_type_revision = 1 }, -- Dimmable Plug In Unit + } + }, + }, +}) + + +local mock_device_periodic = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("plug-energy-powerConsumption.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 10, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, } -- SET_TOPOLOGY + }, + device_types = { + { device_type_id = 0x010A, device_type_revision = 1 }, -- OnOff Plug + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + } + }, + }, +}) + +local subscribed_attributes_periodic = { + clusters.OnOff.attributes.OnOff, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, +} +local subscribed_attributes = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ElectricalPowerMeasurement.attributes.ActivePower, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, +} + +local cumulative_report_val_19 = { + energy = 19000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +local cumulative_report_val_29 = { + energy = 29000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +local cumulative_report_val_39 = { + energy = 39000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +local periodic_report_val_23 = { + energy = 23000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +local mock_child = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("plug-level-energy-powerConsumption.yml"), + device_network_id = string.format("%s:%d", mock_device.id, 4), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 4) +}) + +local function test_init() + test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_child) + + local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) + for i, cluster in ipairs(subscribed_attributes) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + local read_req = clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device.id, 1) + read_req:merge(clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device.id, 3)) + test.socket.matter:__expect_send({ mock_device.id, read_req }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) +end +test.set_test_init_function(test_init) + +local function test_init_periodic() + test.mock_device.add_test_device(mock_device_periodic) + local subscribe_request = subscribed_attributes_periodic[1]:subscribe(mock_device_periodic) + for i, cluster in ipairs(subscribed_attributes_periodic) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_periodic)) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "added" }) + local read_req = clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device_periodic.id, 1) + test.socket.matter:__expect_send({ mock_device_periodic.id, read_req }) + test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "init" }) + test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) + test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) +end + +test.register_message_test( + "On command should send the appropriate commands", + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "main", command = "on", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, 2) + } + } + } +) + +test.register_message_test( + "Off command should send the appropriate commands", + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "off" } + } + }, + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "main", command = "off", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.Off(mock_device, 2) + } + } + } +) + +test.register_message_test( + "Active power measurement should generate correct messages", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ElectricalPowerMeasurement.server.attributes.ActivePower:build_test_report_data(mock_device, 1, 17000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerMeter.power({value = 17.0, unit="W"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } + } + } +) + +test.register_coroutine_test( + "Cumulative Energy measurement should generate correct messages", + function() + + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_19 + ) + } + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 19.0 + })) + ) + + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_29 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) + ) + + test.wait_for_events() + test.mock_time.advance_time(1500) + + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_39 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 39.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:40:00Z", + deltaEnergy = 20.0, + energy = 39.0 + })) + ) + end +) + +test.register_coroutine_test( + "Periodic Energy as subordinate to Cumulative Energy measurement should not generate any messages", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device, 1, periodic_report_val_23 + ) + } + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device, 1, periodic_report_val_23 + ) + } + ) + end +) + +test.register_coroutine_test( + "Periodic Energy measurement should generate correct messages", + function() + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + test.socket.matter:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device_periodic, 1, periodic_report_val_23 + ) + } + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 23.0, unit="Wh"})) + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 23.0 + })) + ) + test.socket.matter:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device_periodic, 1, periodic_report_val_23 + ) + } + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 46.0, unit="Wh"})) + ) + test.wait_for_events() + test.mock_time.advance_time(2000) + test.socket.matter:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device_periodic, 1, periodic_report_val_23 + ) + } + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 69.0, unit="Wh"})) + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:48:20Z", + deltaEnergy = 46.0, + energy = 69.0 + })) + ) + end, + { test_init = test_init_periodic } +) + +test.register_coroutine_test( + "Test profile change on init for Electrical Sensor device type", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 2, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 4, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 1, {uint32(2)})}) + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 3, {uint32(4)})}) + mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "nil 2", + profile = "plug-level-energy-powerConsumption", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 4) + }) + end, + { test_init = test_init } +) + +test.register_coroutine_test( + "Test profile change on init for only Periodic Electrical Sensor device type", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "doConfigure" }) + mock_device_periodic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device_periodic.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device_periodic, 1, {uint32(1)})}) + mock_device_periodic:expect_metadata_update({ profile = "plug-energy-powerConsumption" }) + end, + { test_init = test_init_periodic } +) + +test.register_coroutine_test( + "Test resetEnergyMeter command on parent and child for CumulativeEnergyImported", + function() + + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + + -- this block needs to run to set the requisite fields. It is tested on its own elsewhere + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 2, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 4, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 1, {uint32(2)})}) + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 3, {uint32(4)})}) + mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "nil 2", + profile = "plug-level-energy-powerConsumption", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 4) + }) + -- end of block + + -- Initial Parent Energy Report + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_19 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 19.0 + })) + ) + + -- Initial Child Energy Report + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 3, cumulative_report_val_19 + ) + } + ) + test.socket.capability:__expect_send( + mock_child:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) + ) + -- no powerConsumptionReport will be emitted now, since it has not been 15 minutes since the previous report (even though it was the parent). + + + test.wait_for_events() + test.mock_time.advance_time(1500) + + + -- Parent call to resetEnergyMeter + test.socket.capability:__queue_receive({mock_device.id, { capability = "energyMeter", component = "main", command = "resetEnergyMeter", args = {}}}) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) + ) + -- Child call to resetEnergyMeter + test.socket.capability:__queue_receive({mock_child.id, { capability = "energyMeter", component = "main", command = "resetEnergyMeter", args = {}}}) + test.socket.capability:__expect_send( + mock_child:generate_test_message("main", capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) + ) + + test.wait_for_events() + + -- Second Child Energy Report + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 3, cumulative_report_val_39 + ) + } + ) + test.socket.capability:__expect_send( + mock_child:generate_test_message("main", capabilities.energyMeter.energy({ value = 20.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_child:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:40:00Z", + deltaEnergy = 0.0, + energy = 20.0 + })) + ) + + -- Second Parent Energy Report + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_39 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 20.0, unit = "Wh" })) + ) + -- no powerConsumptionReport will be emitted now, since it has not been 15 minutes since the previous report (even though it was the child). + end, + { test_init = test_init } +) + +test.register_coroutine_test( + "Test resetEnergyMeter command on device for PeriodicEnergyImported", + function() + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + test.socket.matter:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device_periodic, 1, periodic_report_val_23 + ) + } + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 23.0, unit="Wh"})) + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 23.0 + })) + ) + test.socket.matter:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device_periodic, 1, periodic_report_val_23 + ) + } + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 46.0, unit="Wh"})) + ) + + test.wait_for_events() + test.mock_time.advance_time(2000) + + test.socket.capability:__queue_receive({mock_device_periodic.id, { capability = "energyMeter", component = "main", command = "resetEnergyMeter", args = {}}}) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) + ) + + test.wait_for_events() + + test.socket.matter:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device_periodic, 1, cumulative_report_val_19 + ) + } + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 19.0, unit="Wh"})) + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:48:20Z", + deltaEnergy = -4.0, + energy = 19.0 + })) + ) + end, + { test_init = test_init_periodic } +) + +test.register_message_test( + "Set level command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switchLevel", component = "main", command = "setLevel", args = {20,20} } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "switchLevel", capability_cmd_id = "setLevel" } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 2, st_utils.round(20/100.0 * 254), 20, 0 ,0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 2) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 2, 50) + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switchLevel.level(20)) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 2, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua new file mode 100644 index 0000000000..b548bb819b --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua @@ -0,0 +1,394 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" +local version = require "version" +local st_utils = require "st.utils" + +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" +end + +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("plug-level-power-energy-powerConsumption.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, + { cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", feature_map = 0, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 2, }, -- TREE_TOPOLOGY + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + } + }, + { + endpoint_id = 2, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug + } + }, + { + endpoint_id = 3, + clusters = { + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 2, }, -- TREE_TOPOLOGY + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + } + }, + { + endpoint_id = 4, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + { cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + }, + device_types = { + { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug + } + }, + }, +}) + +local subscribed_attributes = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ElectricalPowerMeasurement.attributes.ActivePower, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, +} + +local cumulative_report_val_19 = { + energy = 19000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +local cumulative_report_val_29 = { + energy = 29000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +local cumulative_report_val_39 = { + energy = 39000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +local function test_init() + test.mock_device.add_test_device(mock_device) + local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) + for i, cluster in ipairs(subscribed_attributes) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + local read_req = clusters.Descriptor.attributes.PartsList:read(mock_device.id, 1) + read_req:merge(clusters.Descriptor.attributes.PartsList:read(mock_device.id, 3)) + test.socket.matter:__expect_send({ mock_device.id, read_req }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) +end +test.set_test_init_function(test_init) + +test.register_message_test( + "On command should send the appropriate commands", + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "main", command = "on", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, 2) + } + } + } +) + +test.register_message_test( + "Off command should send the appropriate commands", + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "off" } + } + }, + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "main", command = "off", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.Off(mock_device, 2) + } + } + } +) + +test.register_message_test( + "Active power measurement should generate correct messages", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ElectricalPowerMeasurement.server.attributes.ActivePower:build_test_report_data(mock_device, 1, 17000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerMeter.power({value = 17.0, unit="W"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } + } + } +) + +test.register_coroutine_test( + "Cumulative Energy measurement should generate correct messages", + function() + + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_19 + ) + } + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 19.0 + })) + ) + + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_29 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) + ) + + test.wait_for_events() + test.mock_time.advance_time(1500) + + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_39 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 39.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:40:00Z", + deltaEnergy = 20.0, + energy = 39.0 + })) + ) + end +) + +test.register_coroutine_test( + "Test profile change on init for Electrical Sensor device type", + function() + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 2, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 4, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device.id, clusters.Descriptor.attributes.PartsList:build_test_report_data(mock_device, 1, {uint32(2)})}) + test.socket.matter:__queue_receive({ mock_device.id, clusters.Descriptor.attributes.PartsList:build_test_report_data(mock_device, 3, {uint32(4)})}) + mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "nil 2", + profile = "plug-level-energy-powerConsumption", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 4) + }) + end, + { test_init = test_init } +) + +test.register_message_test( + "Set level command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switchLevel", component = "main", command = "setLevel", args = {20,20} } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "switchLevel", capability_cmd_id = "setLevel" } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 2, st_utils.round(20/100.0 * 254), 20, 0 ,0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 2) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 2, 50) + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switchLevel.level(20)) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 2, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua b/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua index 991593953e..a189b1e5fc 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua @@ -1,21 +1,11 @@ --- Copyright 2023 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. +-- Copyright © 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local data_types = require "st.matter.data_types" +local fields = require "switch_utils.fields" local clusters = require "st.matter.clusters" local cluster_base = require "st.matter.cluster_base" @@ -64,6 +54,89 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) +local mock_device_electrical_sensor = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("plug-energy-powerConsumption.yml"), + manufacturer_info = { + vendor_id = 0x130A, + product_id = 0x0050, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, --u32 bitmap + }, + { + cluster_id = PRIVATE_CLUSTER_ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, --u32 bitmap + } + }, + device_types = { + { device_type_id = 0x010A, device_type_revision = 1 } -- On/Off Plug + } + }, + { + endpoint_id = 2, + clusters = { + { + cluster_id = clusters.ElectricalEnergyMeasurement.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY, + }, + { + cluster_id = clusters.PowerTopology.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.PowerTopology.types.Feature.NODE_TOPOLOGY, + } + }, + device_types = { + { device_type_id = fields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR, device_type_revision = 1 } + } + } + } +}) + +local function test_init_electrical_sensor() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_electrical_sensor) + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_electrical_sensor) + for i, clus in ipairs(cluster_subscribe_list) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_electrical_sensor)) end + end + + test.socket.device_lifecycle:__queue_receive({ mock_device_electrical_sensor.id, "added" }) + test.socket.matter:__expect_send({mock_device_electrical_sensor.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_electrical_sensor.id, "init" }) + test.socket.matter:__expect_send({mock_device_electrical_sensor.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_electrical_sensor.id, "doConfigure" }) + mock_device_electrical_sensor:expect_metadata_update({ profile = "plug-energy-powerConsumption" }) + mock_device_electrical_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + local function test_init() local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, @@ -164,9 +237,6 @@ test.register_coroutine_test( local poll_timer = mock_device:get_field("RECURRING_POLL_TIMER") assert(poll_timer ~= nil, "poll_timer should exist") - local report_poll_timer = mock_device:get_field("RECURRING_REPORT_POLL_TIMER") - assert(report_poll_timer ~= nil, "report_poll_timer should exist") - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "removed" }) test.wait_for_events() end @@ -291,6 +361,15 @@ test.register_coroutine_test( mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 50000, unit = "Wh" })) ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T16:39:59Z", + deltaEnergy = 0.0, + energy = 50000, + })) + ) + test.wait_for_events() end ) @@ -316,16 +395,6 @@ test.register_coroutine_test( test.socket.matter:__expect_send({ mock_device.id, cluster_base.write(mock_device, 0x01, PRIVATE_CLUSTER_ID, PRIVATE_ATTR_ID_ACCUMULATED_CONTROL_POINT, nil, data) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.powerConsumptionReport.powerConsumption({ - energy = 0, - deltaEnergy = 0, - start = "1970-01-01T00:00:00Z", - ["end"] = "2001-01-01T00:00:00Z" - })) - ) - test.wait_for_events() end ) @@ -368,23 +437,6 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 0, unit = "W" })) ) - - test.wait_for_events() - -- after 15 minutes, the device should still report power consumption even when off - test.mock_time.advance_time(60 * 15) -- Ensure that the timer created in create_poll_schedule triggers - - - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.powerConsumptionReport.powerConsumption({ - energy = 0, - deltaEnergy = 0.0, - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" - })) - ) - - test.wait_for_events() end, { test_init = function() @@ -406,4 +458,94 @@ test.register_coroutine_test( } ) +local cumulative_report_val_19 = { + energy = 19000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +local cumulative_report_val_29 = { + energy = 29000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +local cumulative_report_val_39 = { + energy = 39000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +test.register_coroutine_test( + "Cumulative Energy measurement should generate correct messages", + function() + local mock_device = mock_device_electrical_sensor + + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_19 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 19.0 + })) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_29 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) + ) + test.wait_for_events() + test.mock_time.advance_time(1500) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_39 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 39.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:40:00Z", + deltaEnergy = 20.0, + energy = 39.0 + })) + ) + end, { test_init = test_init_electrical_sensor } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua b/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua new file mode 100644 index 0000000000..4629d69a5f --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua @@ -0,0 +1,754 @@ +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" + +local mock_ikea_scroll = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("ikea-scroll.yml"), + manufacturer_info = {vendor_id = 0x117C, product_id = 0x8000, product_name = "Ikea Scroll"}, + label = "Ikea Scroll", + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = {{ + cluster_id = clusters.Switch.ID, + feature_map = + clusters.Switch.types.Feature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + cluster_type = "SERVER" + },}, + device_types = {{device_type_id = 0x000F, device_type_revision = 1}} -- GENERIC SWITCH + }, + { + endpoint_id = 2, + clusters = {{ + cluster_id = clusters.Switch.ID, + feature_map = + clusters.Switch.types.Feature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + cluster_type = "SERVER" + },}, + device_types = {{device_type_id = 0x000F, device_type_revision = 1}} -- GENERIC SWITCH + }, + { + endpoint_id = 3, + clusters = {{ + cluster_id = clusters.Switch.ID, + feature_map = + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER"}, + }, + device_types = {{device_type_id = 0x000F, device_type_revision = 1}} -- GENERIC SWITCH + }, + { + endpoint_id = 4, + clusters = {{ + cluster_id = clusters.Switch.ID, + feature_map = + clusters.Switch.types.Feature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + cluster_type = "SERVER" + },}, + device_types = {{device_type_id = 0x000F, device_type_revision = 1}} -- GENERIC SWITCH + }, + { + endpoint_id = 5, + clusters = {{ + cluster_id = clusters.Switch.ID, + feature_map = + clusters.Switch.types.Feature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + cluster_type = "SERVER" + },}, + device_types = {{device_type_id = 0x000F, device_type_revision = 1}} -- GENERIC SWITCH + }, + { + endpoint_id = 6, + clusters = {{ + cluster_id = clusters.Switch.ID, + feature_map = + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER"}, + }, + device_types = {{device_type_id = 0x000F, device_type_revision = 1}} -- GENERIC SWITCH + }, + { + endpoint_id = 7, + clusters = {{ + cluster_id = clusters.Switch.ID, + feature_map = + clusters.Switch.types.Feature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + cluster_type = "SERVER" + },}, + device_types = {{device_type_id = 0x000F, device_type_revision = 1}} -- GENERIC SWITCH + }, + { + endpoint_id = 8, + clusters = {{ + cluster_id = clusters.Switch.ID, + feature_map = + clusters.Switch.types.Feature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + cluster_type = "SERVER" + },}, + device_types = {{device_type_id = 0x000F, device_type_revision = 1}} -- GENERIC SWITCH + }, + { + endpoint_id = 9, + clusters = {{ + cluster_id = clusters.Switch.ID, + feature_map = + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER"}, + }, + device_types = {{device_type_id = 0x000F, device_type_revision = 1}} -- GENERIC SWITCH + }, + } +}) + +local ENDPOINTS_PUSH = { 3, 6, 9 } +local ENDPOINTS_SCROLL = {1, 2, 4, 5, 7, 8} + +-- the ikea scroll subdriver has overriden subscribe behavior +local function ikea_scroll_subscribe() + local CLUSTER_SUBSCRIBE_LIST_PUSH ={ + clusters.Switch.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.MultiPressComplete, + } + local CLUSTER_SUBSCRIBE_LIST_SCROLL = { + clusters.Switch.events.InitialPress, + clusters.Switch.server.events.MultiPressOngoing, + clusters.Switch.server.events.MultiPressComplete, + } + local subscribe_request = CLUSTER_SUBSCRIBE_LIST_PUSH[1]:subscribe(mock_ikea_scroll, ENDPOINTS_PUSH[1]) + for _, ep_press in ipairs(ENDPOINTS_PUSH) do + for _, event in ipairs(CLUSTER_SUBSCRIBE_LIST_PUSH) do + subscribe_request:merge(event:subscribe(mock_ikea_scroll, ep_press)) + end + end + for _, ep_press in ipairs(ENDPOINTS_SCROLL) do + for _, event in ipairs(CLUSTER_SUBSCRIBE_LIST_SCROLL) do + subscribe_request:merge(event:subscribe(mock_ikea_scroll, ep_press)) + end + end + subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_ikea_scroll, 0)) + return subscribe_request +end + +local function expect_configure_buttons() + local button_attr = capabilities.button.button + test.socket.matter:__expect_send({mock_ikea_scroll.id, clusters.Switch.attributes.MultiPressMax:read(mock_ikea_scroll, 3)}) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("main", button_attr.pushed({state_change = false}))) + test.socket.matter:__expect_send({mock_ikea_scroll.id, clusters.Switch.attributes.MultiPressMax:read(mock_ikea_scroll, 6)}) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group2", button_attr.pushed({state_change = false}))) + test.socket.matter:__expect_send({mock_ikea_scroll.id, clusters.Switch.attributes.MultiPressMax:read(mock_ikea_scroll, 9)}) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group3", button_attr.pushed({state_change = false}))) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("main", capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group2", capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group3", capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}}))) +end + +local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_ikea_scroll) + local subscribe_request = ikea_scroll_subscribe() + + test.socket.device_lifecycle:__queue_receive({ mock_ikea_scroll.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_ikea_scroll.id, "init" }) + test.socket.matter:__expect_send({mock_ikea_scroll.id, subscribe_request}) + + mock_ikea_scroll:expect_metadata_update({ profile = "ikea-scroll" }) + mock_ikea_scroll:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + expect_configure_buttons() + test.socket.device_lifecycle:__queue_receive({ mock_ikea_scroll.id, "doConfigure" }) +end +test.set_test_init_function(test_init) + +test.register_message_test( + "Ensure Ikea Scroll Button initialization works as expected", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_ikea_scroll, 3, 3 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.button.supportedButtonValues({"pushed", "double", "held", "pushed_3x"}, {visibility = {displayed = false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_ikea_scroll, 6, 3 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.button.supportedButtonValues({"pushed", "double", "held", "pushed_3x"}, {visibility = {displayed = false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_ikea_scroll, 9, 3 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group3", + capabilities.button.supportedButtonValues({"pushed", "double", "held", "pushed_3x"}, {visibility = {displayed = false}})) + }, + } +) + +test.register_message_test( + "Ikea Scroll Positive rotateAmount events on main are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {current_number_of_presses_counted = 2, new_position = 2} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {current_number_of_presses_counted = 5, new_position = 5} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(18, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {new_position = 5, total_number_of_presses_counted = 5, previous_position = 0} + ) + }, + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {current_number_of_presses_counted = 2, new_position = 2} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {current_number_of_presses_counted = 5, new_position = 5} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(18, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {new_position = 5, total_number_of_presses_counted = 5, previous_position = 0} + ) + }, + } + } +) + +test.register_message_test( + "Ikea Scroll Negative rotateAmount events on main are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(-6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {current_number_of_presses_counted = 2, new_position = 2} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(-6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {current_number_of_presses_counted = 5, new_position = 5} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(-18, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {new_position = 5, total_number_of_presses_counted = 5, previous_position = 0} + ) + }, + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(-6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {current_number_of_presses_counted = 2, new_position = 2} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(-6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {current_number_of_presses_counted = 5, new_position = 5} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(-18, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {new_position = 5, total_number_of_presses_counted = 5, previous_position = 0} + ) + }, + } + } +) + +test.register_message_test( + "Ikea Scroll Positive rotateAmount events on group2 are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[3], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.knob.rotateAmount(6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[3], {current_number_of_presses_counted = 2, new_position = 2} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.knob.rotateAmount(6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[3], {current_number_of_presses_counted = 5, new_position = 5} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.knob.rotateAmount(18, {state_change = true})) + } + } +) + +test.register_message_test( + "Ikea Scroll Negative rotateAmount events on group2 are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[4], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.knob.rotateAmount(-6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[4], {current_number_of_presses_counted = 2, new_position = 2} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.knob.rotateAmount(-6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[4], {current_number_of_presses_counted = 5, new_position = 5} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.knob.rotateAmount(-18, {state_change = true})) + } + } +) + +test.register_message_test( + "Ikea Scroll Positive rotateAmount events on group3 are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[5], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group3", + capabilities.knob.rotateAmount(6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[5], {current_number_of_presses_counted = 2, new_position = 2} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group3", + capabilities.knob.rotateAmount(6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[5], {current_number_of_presses_counted = 5, new_position = 5} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group3", + capabilities.knob.rotateAmount(18, {state_change = true})) + } + } +) + +test.register_message_test( + "Ikea Scroll Negative rotateAmount events on group3 are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[6], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group3", + capabilities.knob.rotateAmount(-6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[6], {current_number_of_presses_counted = 2, new_position = 2} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group3", + capabilities.knob.rotateAmount(-6, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[6], {current_number_of_presses_counted = 5, new_position = 5} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group3", + capabilities.knob.rotateAmount(-18, {state_change = true})) + } + } +) + +test.register_message_test( + "Ikea Scroll Long Press Push events on main are handled correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_PUSH[1], {new_position = 1} + ) + }, + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_PUSH[1], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.button.button.held({state_change = true})) + }, + } +) + +test.register_message_test( + "Ikea Scroll MultiPressComplete Push events on group2 are handled correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_PUSH[2], {new_position = 1} + ) + }, + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_PUSH[2], {total_number_of_presses_counted = 1, previous_position = 0} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.button.button.pushed({state_change = true})) + }, + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua b/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua index 22fdbb316a..abc98591fc 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua @@ -1,25 +1,15 @@ --- Copyright 2022 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" +local st_utils = require "st.utils" local clusters = require "st.matter.clusters" local TRANSITION_TIME = 0 local OPTIONS_MASK = 0x01 -local OPTIONS_OVERRIDE = 0x01 +local HANDLE_COMMAND_IF_OFF = 0x01 local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("light-color-level-illuminance-motion.yml"), @@ -91,29 +81,35 @@ local function set_color_mode(device, endpoint, color_mode) test.socket.matter:__expect_send({device.id, read_req}) end -local function test_init() - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.IlluminanceMeasurement.attributes.MeasuredValue, - clusters.OccupancySensing.attributes.Occupancy - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end +local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.IlluminanceMeasurement.attributes.MeasuredValue, + clusters.OccupancySensing.attributes.Occupancy +} + +local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) +for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) end +end + +local function test_init() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + -- the following subscribe is due to the init event sent by the test framework. test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION) @@ -121,28 +117,9 @@ end test.set_test_init_function(test_init) local function test_init_x_y_color_mode() - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.IlluminanceMeasurement.attributes.MeasuredValue, - clusters.OccupancySensing.attributes.Occupancy - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) @@ -232,7 +209,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 1, math.floor(20/100.0 * 254), 20, 0 ,0) + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 1, st_utils.round(20/100.0 * 254), 20, 0 ,0) } }, { @@ -342,7 +319,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -366,6 +343,14 @@ test.register_message_test( direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.hue(50)) }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } + } + }, { channel = "matter", direction = "receive", @@ -378,7 +363,15 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.saturation(50)) - } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } + } + }, } ) @@ -398,7 +391,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToHue(mock_device, 1, hue, 0, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToHue(mock_device, 1, hue, 0, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, } @@ -420,7 +413,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToSaturation(mock_device, 1, sat, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToSaturation(mock_device, 1, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, } @@ -442,7 +435,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua index 54eb21dc88..20e2545349 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -70,19 +59,12 @@ local mock_bridge = test.mock_device.build_test_matter_device({ } }) -local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff -} - local function test_init_mock_bridge() - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_bridge) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_bridge)) - end - end - test.socket.matter:__expect_send({mock_bridge.id, subscribe_request}) test.mock_device.add_test_device(mock_bridge) + test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "init" }) + test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "doConfigure" }) + mock_bridge:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.register_coroutine_test( diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua index f34f432ec7..f84c29c4c5 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -1,17 +1,49 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" local button_attr = capabilities.button.button local utils = require "st.utils" local dkjson = require "dkjson" local uint32 = require "st.matter.data_types.Uint32" ---mock the actual device local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("button.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER", + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + } + } +}) + +local mock_device_battery = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("button-battery.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, endpoints = { { endpoint_id = 0, @@ -43,177 +75,258 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) --- add device for each mock device -local CLUSTER_SUBSCRIBE_LIST ={ - clusters.PowerSource.server.attributes.BatPercentRemaining, - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete, -} - -local function configure_buttons() - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) +local function expect_configure_buttons(device) + test.socket.capability:__expect_send(device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(device:generate_test_message("main", button_attr.pushed({state_change = false}))) +end + +local function update_profile() + test.socket.matter:__queue_receive({mock_device_battery.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( + mock_device_battery, 1, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)} + )}) + expect_configure_buttons(mock_device_battery) + mock_device_battery:expect_metadata_update({ profile = "button-battery" }) end local function test_init() + local CLUSTER_SUBSCRIBE_LIST = { + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + test.disable_startup_messages() test.mock_device.add_test_device(mock_device) - local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() - test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) - configure_buttons() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "buttons-battery" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - configure_buttons() + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + expect_configure_buttons(mock_device) + mock_device:expect_metadata_update({ profile = "button" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_battery() + local CLUSTER_SUBSCRIBE_LIST_BATTERY = { + clusters.PowerSource.server.attributes.AttributeList, + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + + local subscribe_request = CLUSTER_SUBSCRIBE_LIST_BATTERY[1]:subscribe(mock_device_battery) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_BATTERY) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_battery)) end + end + + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_battery) + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "added" }) + test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "init" }) + test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "doConfigure" }) + mock_device_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) +test.register_coroutine_test( + "Simulate the profile change update taking affect and the device info changing", + function() + test.socket.matter:__set_channel_ordering("relaxed") + update_profile() + test.wait_for_events() + local device_info_copy = utils.deep_copy(mock_device_battery.raw_st_data) + device_info_copy.profile.id = "buttons-battery" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "infoChanged", device_info_json }) + -- due to the AttributeList being processed in update_profile, setting profiling_data.BATTERY_SUPPORT, + -- subsequent subscriptions will not include AttributeList. + local UPDATED_CLUSTER_SUBSCRIBE_LIST = { + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + local updated_subscribe_request = UPDATED_CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device_battery) + for i, clus in ipairs(UPDATED_CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then updated_subscribe_request:merge(clus:subscribe(mock_device_battery)) end + end + test.socket.matter:__expect_send({mock_device_battery.id, updated_subscribe_request}) + expect_configure_buttons(mock_device_battery) + end, + { test_init = test_init_battery } +) + +test.register_coroutine_test( + "Handle received BatPercentRemaining from device.", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device_battery.id, + clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( + mock_device_battery, 1, 150 + ) + } + ) + test.socket.capability:__expect_send( + mock_device_battery:generate_test_message( + "main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5)) + ) + ) + end, + { test_init = test_init_battery } +) + test.register_message_test( "Handle single press sequence, no hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1} --move to position 1? - ), + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} --move to position 1 + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press } -} ) test.register_message_test( "Handle single press sequence, with hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ), + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) } -} ) test.register_message_test( "Handle release after short press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device, 1, {previous_position = 1} - ) - } - }, - { -- this is a double event because the test device in this test shouldn't support the above event - -- but we handle it anyway - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 1, {previous_position = 1} + ) + } + }, + { -- this is a double event because the test device in this test shouldn't support the above event + -- but we handle it anyway + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, } ) test.register_message_test( "Handle release after long press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongRelease:build_test_event_report( - mock_device, 1, {previous_position = 1} - ) - } - }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 1, {previous_position = 1} + ) + } + }, } ) @@ -282,124 +395,129 @@ test.register_message_test( test.register_message_test( "Handle double press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ) - } - }, - { -- again, on a device that reports that it supports double press, this event - -- will not be generated. See a multi-button test file for that case - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 1, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.double({state_change = true})) - }, - -} + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ) + } + }, + { -- again, on a device that reports that it supports double press, this event + -- will not be generated. See a multi-button test file for that case + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 1, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.double({state_change = true})) + }, + } ) test.register_message_test( "Handle multi press for 4 times", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1, total_number_of_presses_counted = 1, previous_position = 0} - ) - } - }, - { -- again, on a device that reports that it supports double press, this event - -- will not be generated. See the multi-button test file for that case - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 1, {new_position = 1, total_number_of_presses_counted = 4, previous_position = 0} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed_4x({state_change = true})) - }, - -} -) - -test.register_message_test( - "Handle received BatPercentRemaining from device.", { { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( - mock_device, 1, 150 - ), - }, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1, total_number_of_presses_counted = 1, previous_position = 0} + ) + } + }, + { -- again, on a device that reports that it supports double press, this event + -- will not be generated. See the multi-button test file for that case + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 1, {new_position = 1, total_number_of_presses_counted = 4, previous_position = 0} + ) + } }, { channel = "capability", direction = "send", - message = mock_device:generate_test_message( - "main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5)) - ), + message = mock_device:generate_test_message("main", button_attr.pushed_4x({state_change = true})) }, } ) +local function reset_battery_profiling_info() + local fields = require "switch_utils.fields" + mock_device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist=true}) +end + test.register_coroutine_test( - "Test profile change to button-battery when battery percent remaining attribute (attribute ID 12) is available", + "Test profile does not change to button-battery when battery percent remaining attribute (attribute ID 12) is not available", function() + reset_battery_profiling_info() + test.wait_for_events() test.socket.matter:__queue_receive( { mock_device.id, - clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32(12)}) + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32(10)}) } ) - mock_device:expect_metadata_update({ profile = "button-battery" }) end ) test.register_coroutine_test( - "Test profile does not change to button-battery when battery percent remaining attribute (attribute ID 12) is not available", + "Test profile change to button-batteryLevel when battery percent remaining attribute (attribute ID 14) is available", function() + reset_battery_profiling_info() + test.wait_for_events() test.socket.matter:__queue_receive( { mock_device.id, - clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32(10)}) + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32( + clusters.PowerSource.attributes.BatChargeLevel.ID + )}) + } + ) + expect_configure_buttons(mock_device) + mock_device:expect_metadata_update({ profile = "button-batteryLevel" }) + end +) + +test.register_coroutine_test( + "Test profile change to button-battery when battery percent remaining attribute (attribute ID 12) is available", + function() + reset_battery_profiling_info() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32( + clusters.PowerSource.attributes.BatPercentRemaining.ID + )}) } ) + expect_configure_buttons(mock_device) + mock_device:expect_metadata_update({ profile = "button-battery" }) end ) --- run the tests test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua new file mode 100644 index 0000000000..facc6ea984 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -0,0 +1,2010 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local test = require "integration_test" +local uint32 = require "st.matter.data_types.Uint32" + +test.disable_startup_messages() + +local CAMERA_EP, FLOODLIGHT_EP, CHIME_EP, DOORBELL_EP = 1, 2, 3, 4 + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("camera.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = CAMERA_EP, + clusters = { + { + cluster_id = clusters.CameraAvStreamManagement.ID, + feature_map = clusters.CameraAvStreamManagement.types.Feature.VIDEO | + clusters.CameraAvStreamManagement.types.Feature.PRIVACY | + clusters.CameraAvStreamManagement.types.Feature.AUDIO | + clusters.CameraAvStreamManagement.types.Feature.LOCAL_STORAGE | + clusters.CameraAvStreamManagement.types.Feature.PRIVACY | + clusters.CameraAvStreamManagement.types.Feature.SPEAKER | + clusters.CameraAvStreamManagement.types.Feature.IMAGE_CONTROL | + clusters.CameraAvStreamManagement.types.Feature.SPEAKER | + clusters.CameraAvStreamManagement.types.Feature.HIGH_DYNAMIC_RANGE | + clusters.CameraAvStreamManagement.types.Feature.NIGHT_VISION | + clusters.CameraAvStreamManagement.types.Feature.WATERMARK | + clusters.CameraAvStreamManagement.types.Feature.ON_SCREEN_DISPLAY, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.CameraAvSettingsUserLevelManagement.ID, + feature_map = clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PAN | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_TILT | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_ZOOM | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PRESETS, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.PushAvStreamTransport.ID, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.ZoneManagement.ID, + feature_map = clusters.ZoneManagement.types.Feature.TWO_DIMENSIONAL_CARTESIAN_ZONE | + clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.WebRTCTransportProvider.ID, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.WebRTCTransportRequestor.ID, + cluster_type = "CLIENT" + }, + { + cluster_id = clusters.OccupancySensing.ID, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x0142, device_type_revision = 1} -- Camera + } + }, + { + endpoint_id = FLOODLIGHT_EP, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30} + }, + device_types = { + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light + } + }, + { + endpoint_id = CHIME_EP, + clusters = { + { + cluster_id = clusters.Chime.ID, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x0146, device_type_revision = 1} -- Chime + } + }, + { + endpoint_id = DOORBELL_EP, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER", + } + }, + device_types = { + {device_type_id = 0x0143, device_type_revision = 1} -- Doorbell + } + } + } +}) + +local subscribe_request +local subscribed_attributes = { + clusters.CameraAvStreamManagement.attributes.AttributeList, + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, +} + +local function test_init() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + local floodlight_child_device_data = { + profile = t_utils.get_profile_definition("light-color-level.yml"), + device_network_id = string.format("%s:%d", mock_device.id, FLOODLIGHT_EP), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", FLOODLIGHT_EP) + } + test.mock_device.add_test_device(test.mock_device.build_test_child_device(floodlight_child_device_data)) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Floodlight 1", + profile = "light-color-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", FLOODLIGHT_EP) + }) + subscribe_request = subscribed_attributes[1]:subscribe(mock_device) + for i, attr in ipairs(subscribed_attributes) do + if i > 1 then subscribe_request:merge(attr:subscribe(mock_device)) end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +test.set_test_init_function(test_init) + +local additional_subscribed_attributes = { + clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, + clusters.CameraAvStreamManagement.attributes.ImageRotation, + clusters.CameraAvStreamManagement.attributes.NightVision, + clusters.CameraAvStreamManagement.attributes.NightVisionIllum, + clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal, + clusters.CameraAvStreamManagement.attributes.ImageFlipVertical, + clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn, + clusters.CameraAvStreamManagement.attributes.TwoWayTalkSupport, + clusters.CameraAvStreamManagement.attributes.SpeakerMuted, + clusters.CameraAvStreamManagement.attributes.MicrophoneMuted, + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness, + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, + clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, + clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled, + clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled, + clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams, + clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams, + clusters.CameraAvStreamManagement.attributes.Viewport, + clusters.CameraAvStreamManagement.attributes.MinViewportResolution, + clusters.CameraAvStreamManagement.attributes.AttributeList, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPresets, + clusters.CameraAvSettingsUserLevelManagement.attributes.MaxPresets, + clusters.CameraAvSettingsUserLevelManagement.attributes.ZoomMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMin, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin, + clusters.Chime.attributes.InstalledChimeSounds, + clusters.Chime.attributes.SelectedChime, + clusters.ZoneManagement.attributes.MaxZones, + clusters.ZoneManagement.attributes.Zones, + clusters.ZoneManagement.attributes.Triggers, + clusters.ZoneManagement.attributes.SensitivityMax, + clusters.ZoneManagement.attributes.Sensitivity, + clusters.ZoneManagement.events.ZoneTriggered, + clusters.ZoneManagement.events.ZoneStopped, + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.OccupancySensing.attributes.Occupancy, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete +} + +local expected_metadata = { + optional_component_capabilities = { + { + "main", + { + "videoCapture2", + "cameraViewportSettings", + "videoStreamSettings", + "localMediaStorage", + "audioRecording", + "cameraPrivacyMode", + "imageControl", + "hdr", + "nightVision", + "mechanicalPanTiltZoom", + "zoneManagement", + "webrtc", + "motionSensor", + "sounds", + } + }, + { + "statusLed", + { + "switch", + "mode" + } + }, + { + "speaker", + { + "audioMute", + "audioVolume" + } + }, + { + "microphone", + { + "audioMute", + "audioVolume" + } + }, + { + "doorbell", + { + "button" + } + } + }, + profile = "camera" +} + +local function update_device_profile() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP, { + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID), + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) + }) + }) + mock_device:expect_metadata_update(expected_metadata) + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) + test.wait_for_events() + local updated_device_profile = t_utils.get_profile_definition( + "camera.yml", {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} + ) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = updated_device_profile })) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.webrtc.supportedFeatures( + {audio="sendrecv", bundle=true, order="audio/video", supportTrickleICE=true, turnSource="player", video="recvonly"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.supportedAttributes( + {"pan", "panRange", "tilt", "tiltRange", "zoom", "zoomRange", "presets", "maxPresets"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.supportedFeatures( + {"triggerAugmentation", "perZoneSensitivity"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.localMediaStorage.supportedAttributes( + {"localVideoRecording"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.audioRecording.audioRecording("enabled")) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.videoStreamSettings.supportedFeatures( + {"liveStreaming", "clipRecording", "perStreamViewports", "watermark", "onScreenDisplay"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.cameraPrivacyMode.supportedAttributes( + {"softRecordingPrivacyMode", "softLivestreamPrivacyMode"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.cameraPrivacyMode.supportedCommands( + {"setSoftRecordingPrivacyMode", "setSoftLivestreamPrivacyMode"} + )) + ) + for _, attr in ipairs(additional_subscribed_attributes) do + subscribe_request:merge(attr:subscribe(mock_device)) + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false}))) +end + +-- Matter Handler UTs + +test.register_coroutine_test( + "Reports mapping to EnabledState capability data type should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + local cluster_to_capability_map = { + {cluster = clusters.CameraAvStreamManagement.server.attributes.HDRModeEnabled, capability = capabilities.hdr.hdr}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.ImageFlipHorizontal, capability = capabilities.imageControl.imageFlipHorizontal}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.ImageFlipVertical, capability = capabilities.imageControl.imageFlipVertical}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.SoftRecordingPrivacyModeEnabled, capability = capabilities.cameraPrivacyMode.softRecordingPrivacyMode}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.SoftLivestreamPrivacyModeEnabled, capability = capabilities.cameraPrivacyMode.softLivestreamPrivacyMode}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.HardPrivacyModeOn, capability = capabilities.cameraPrivacyMode.hardPrivacyMode}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.LocalSnapshotRecordingEnabled, capability = capabilities.localMediaStorage.localSnapshotRecording}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.LocalVideoRecordingEnabled, capability = capabilities.localMediaStorage.localVideoRecording} + } + for _, v in ipairs(cluster_to_capability_map) do + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", v.capability("enabled")) + ) + if v.capability == capabilities.imageControl.imageFlipHorizontal then + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.imageControl.supportedAttributes({"imageFlipHorizontal"})) + ) + elseif v.capability == capabilities.imageControl.imageFlipVertical then + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.imageControl.supportedAttributes({"imageFlipHorizontal", "imageFlipVertical"})) + ) + elseif v.capability == capabilities.cameraPrivacyMode.hardPrivacyMode then + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.cameraPrivacyMode.supportedAttributes({"softRecordingPrivacyMode", "softLivestreamPrivacyMode", "hardPrivacyMode"})) + ) + end + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, false) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", v.capability("disabled")) + ) + end + end +) + +test.register_coroutine_test( + "Night Vision reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + local cluster_to_capability_map = { + {cluster = clusters.CameraAvStreamManagement.server.attributes.NightVision, capability = capabilities.nightVision.nightVision}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.NightVisionIllum, capability = capabilities.nightVision.illumination} + } + for _, v in ipairs(cluster_to_capability_map) do + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TriStateAutoEnum.OFF) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", v.capability("off")) + ) + if v.capability == capabilities.nightVision.illumination then + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.nightVision.supportedAttributes({"illumination"})) + ) + end + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TriStateAutoEnum.ON) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", v.capability("on")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TriStateAutoEnum.AUTO) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", v.capability("auto")) + ) + end + end +) + +test.register_coroutine_test( + "Image Rotation reports should generate appropriate events", + function() + local utils = require "st.utils" + update_device_profile() + test.wait_for_events() + local first_value = true + for angle = 0, 400, 50 do + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.ImageRotation:build_test_report_data(mock_device, CAMERA_EP, angle) + }) + local clamped_angle = utils.clamp_value(angle, 0, 359) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.imageControl.imageRotation(clamped_angle)) + ) + if first_value then + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.imageControl.supportedAttributes({"imageRotation"})) + ) + first_value = false + end + end + end +) + +test.register_coroutine_test( + "Two Way Talk Support reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.TwoWayTalkSupport:build_test_report_data( + mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TwoWayTalkSupportTypeEnum.HALF_DUPLEX + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.webrtc.talkback(true)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.webrtc.talkbackDuplex("halfDuplex")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.TwoWayTalkSupport:build_test_report_data( + mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TwoWayTalkSupportTypeEnum.FULL_DUPLEX + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.webrtc.talkback(true)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.webrtc.talkbackDuplex("fullDuplex")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.TwoWayTalkSupport:build_test_report_data( + mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TwoWayTalkSupportTypeEnum.NOT_SUPPORTED + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.webrtc.talkback(false)) + ) + end +) + +test.register_coroutine_test( + "Muted reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + local cluster_to_component_map = { + {cluster = clusters.CameraAvStreamManagement.server.attributes.SpeakerMuted, component = "speaker"}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.MicrophoneMuted, component = "microphone"} + } + for _, v in ipairs(cluster_to_component_map) do + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message(v.component, capabilities.audioMute.mute("muted")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, false) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message(v.component, capabilities.audioMute.mute("unmuted")) + ) + end + end +) + +test.register_coroutine_test( + "Volume Level reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + local max_vol = 200 + local min_vol = 0 + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.SpeakerMaxLevel:build_test_report_data(mock_device, CAMERA_EP, max_vol) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.SpeakerMinLevel:build_test_report_data(mock_device, CAMERA_EP, min_vol) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.MicrophoneMaxLevel:build_test_report_data(mock_device, CAMERA_EP, max_vol) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.MicrophoneMinLevel:build_test_report_data(mock_device, CAMERA_EP, min_vol) + }) + test.wait_for_events() + local cluster_to_component_map = { + { cluster = clusters.CameraAvStreamManagement.server.attributes.SpeakerVolumeLevel, component = "speaker"}, + { cluster = clusters.CameraAvStreamManagement.server.attributes.MicrophoneVolumeLevel, component = "microphone"} + } + for _, v in ipairs(cluster_to_component_map) do + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, 130) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message(v.component, capabilities.audioVolume.volume(65)) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, 64) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message(v.component, capabilities.audioVolume.volume(32)) + ) + end + end +) + +test.register_coroutine_test( + "Status Light Enabled reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:build_test_report_data(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.switch.switch.on()) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:build_test_report_data(mock_device, CAMERA_EP, false) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.switch.switch.off()) + ) + end +) + +test.register_coroutine_test( + "Status Light Brightness reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( + mock_device, CAMERA_EP, clusters.Global.types.ThreeLevelAutoEnum.LOW) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.supportedModes( + {"low", "medium", "high", "auto"}, {visibility = {displayed = false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.supportedArguments( + {"low", "medium", "high", "auto"}, {visibility = {displayed = false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.mode("low")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( + mock_device, CAMERA_EP, clusters.Global.types.ThreeLevelAutoEnum.MEDIUM) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.mode("medium")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( + mock_device, CAMERA_EP, clusters.Global.types.ThreeLevelAutoEnum.HIGH) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.mode("high")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( + mock_device, CAMERA_EP, clusters.Global.types.ThreeLevelAutoEnum.AUTO) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.mode("auto")) + ) + end +) + +local function receive_rate_distortion_trade_off_points() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints:build_test_report_data( + mock_device, CAMERA_EP, { + clusters.CameraAvStreamManagement.types.RateDistortionTradeOffPointsStruct({ + codec = clusters.CameraAvStreamManagement.types.VideoCodecEnum.H264, + resolution = clusters.CameraAvStreamManagement.types.VideoResolutionStruct({ + width = 1920, + height = 1080 + }), + min_bit_rate = 5000000 + }), + clusters.CameraAvStreamManagement.types.RateDistortionTradeOffPointsStruct({ + codec = clusters.CameraAvStreamManagement.types.VideoCodecEnum.HEVC, + resolution = clusters.CameraAvStreamManagement.types.VideoResolutionStruct({ + width = 3840, + height = 2160 + }), + min_bit_rate = 20000000 + }) + } + ) + }) +end + +local function receive_max_encoded_pixel_rate() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate:build_test_report_data( + mock_device, CAMERA_EP, 124416000) -- 1080p @ 60 fps or 4K @ 15 fps + }) +end + +local function receive_min_viewport() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.MinViewportResolution:build_test_report_data( + mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.VideoResolutionStruct({ + width = 1920, + height = 1080 + }) + ) + }) +end + +local function receive_video_sensor_params() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams:build_test_report_data( + mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.VideoSensorParamsStruct({ + sensor_width = 7360, + sensor_height = 4912, + max_fps = 60, + max_hdrfps = 30 + }) + ) + }) +end + +local function emit_min_viewport() + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.cameraViewportSettings.minViewportResolution({ + width = 1920, + height = 1080, + })) + ) +end + +local function emit_video_sensor_parameters() + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.cameraViewportSettings.videoSensorParameters({ + width = 7360, + height = 4912, + maxFPS = 60 + })) + ) +end + +local function emit_supported_resolutions() + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.videoStreamSettings.supportedResolutions({ + { + width = 1920, + height = 1080, + fps = 60 + }, + { + width = 3840, + height = 2160, + fps = 15 + }, + { + width = 7360, + height = 4912, + fps = 0 + } + })) + ) +end + +-- Test receiving RateDistortionTradeOffPoints, MaxEncodedPixelRate, and VideoSensorParams in various orders +-- to ensure that cameraViewportSettings and videoStreamSettings capabilities are updated as expected. Note that +-- cameraViewportSettings.videoSensorParameters is set in the VideoSensorParams handler and +-- videoStreamSettings.supportedResolutions is emitted after all three attributes are received. + +test.register_coroutine_test( + "Rate Distortion Trade Off Points, MaxEncodedPixelRate, MinViewport, VideoSensorParams reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + receive_rate_distortion_trade_off_points() + receive_max_encoded_pixel_rate() + receive_min_viewport() + emit_min_viewport() + receive_video_sensor_params() + emit_video_sensor_parameters() + emit_supported_resolutions() + end +) + +test.register_coroutine_test( + "Rate Distortion Trade Off Points, MinViewport, VideoSensorParams, MaxEncodedPixelRate reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + receive_rate_distortion_trade_off_points() + receive_min_viewport() + emit_min_viewport() + receive_video_sensor_params() + emit_video_sensor_parameters() + receive_max_encoded_pixel_rate() + emit_supported_resolutions() + end +) + +test.register_coroutine_test( + "MaxEncodedPixelRate, MinViewport, VideoSensorParams, Rate Distortion Trade Off Points reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + receive_max_encoded_pixel_rate() + receive_min_viewport() + emit_min_viewport() + receive_video_sensor_params() + emit_video_sensor_parameters() + receive_rate_distortion_trade_off_points() + emit_supported_resolutions() + end +) + +test.register_coroutine_test( + "PTZ Position reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMax:build_test_report_data(mock_device, CAMERA_EP, 150) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMin:build_test_report_data(mock_device, CAMERA_EP, -150) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.panRange({value = {minimum = -150, maximum = 150}})) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax:build_test_report_data(mock_device, CAMERA_EP, 80) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin:build_test_report_data(mock_device, CAMERA_EP, -80) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.tiltRange({value = {minimum = -80, maximum = 80}})) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.ZoomMax:build_test_report_data(mock_device, CAMERA_EP, 70) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.zoomRange({value = {minimum = 1, maximum = 70}})) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition:build_test_report_data( + mock_device, CAMERA_EP, {pan = 10, tilt = 20, zoom = 30}) + }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.pan(10)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.tilt(20)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.zoom(30)) + ) + end +) + +test.register_coroutine_test( + "PTZ Presets reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPresets:build_test_report_data( + mock_device, CAMERA_EP, {{preset_id = 1, name = "Preset 1", settings = {pan = 10, tilt = 20, zoom = 30}}, + {preset_id = 2, name = "Preset 2", settings = {pan = -55, tilt = 80, zoom = 60}}} + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.presets({ + { id = 1, label = "Preset 1", pan = 10, tilt = 20, zoom = 30}, + { id = 2, label = "Preset 2", pan = -55, tilt = 80, zoom = 60} + })) + ) + end +) + +test.register_coroutine_test( + "Max Presets reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.MaxPresets:build_test_report_data(mock_device, CAMERA_EP, 10) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.maxPresets(10)) + ) + end +) + +test.register_coroutine_test( + "Max Zones reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.MaxZones:build_test_report_data(mock_device, CAMERA_EP, 10) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.maxZones(10)) + ) + end +) + +test.register_coroutine_test( + "Zones reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.Zones:build_test_report_data( + mock_device, CAMERA_EP, { + clusters.ZoneManagement.types.ZoneInformationStruct({ + zone_id = 1, + zone_type = clusters.ZoneManagement.types.ZoneTypeEnum.TWODCART_ZONE, + zone_source = clusters.ZoneManagement.types.ZoneSourceEnum.MFG, + two_d_cartesian_zone = clusters.ZoneManagement.types.TwoDCartesianZoneStruct({ + name = "Zone 1", + use = clusters.ZoneManagement.types.ZoneUseEnum.MOTION, + vertices = { + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({ x = 0, y = 0 }), + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({ x = 1920, y = 1080 }) + }, + color = "#FFFFFF" + }) + }) + } + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.zones({ + { + id = 1, + name = "Zone 1", + type = "2DCartesian", + polygonVertices = { + {vertex = {x = 0, y = 0}}, + {vertex = {x = 1920, y = 1080}} + }, + source = "manufacturer", + use = "motion", + color = "#FFFFFF" + } + })) + ) + end +) + +test.register_coroutine_test( + "Triggers reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.Triggers:build_test_report_data( + mock_device, CAMERA_EP, { + clusters.ZoneManagement.types.ZoneTriggerControlStruct({ + zone_id = 1, + initial_duration = 8, + augmentation_duration = 4, + max_duration = 20, + blind_duration = 3, + sensitivity = 4 + }) + } + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.triggers({ + { + zoneId = 1, + initialDuration = 8, + augmentationDuration = 4, + maxDuration = 20, + blindDuration = 3, + sensitivity = 4 + } + })) + ) + end +) + +test.register_coroutine_test( + "Sensitivity reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.SensitivityMax:build_test_report_data(mock_device, CAMERA_EP, 7) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.sensitivityRange({ minimum = 1, maximum = 7}, + {visibility = {displayed = false}})) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.Sensitivity:build_test_report_data(mock_device, CAMERA_EP, 5) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.sensitivity(5, {visibility = {displayed = false}})) + ) + end +) + +test.register_coroutine_test( + "Chime reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Chime.attributes.InstalledChimeSounds:build_test_report_data(mock_device, CAMERA_EP, { + clusters.Chime.types.ChimeSoundStruct({chime_id = 1, name = "Sound 1"}), + clusters.Chime.types.ChimeSoundStruct({chime_id = 2, name = "Sound 2"}) + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.sounds.supportedSounds({ + {id = 1, label = "Sound 1"}, + {id = 2, label = "Sound 2"}, + }, {visibility = {displayed = false}})) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Chime.attributes.SelectedChime:build_test_report_data(mock_device, CAMERA_EP, 2) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.sounds.selectedSound(2))) + end +) + +-- Event Handler UTs + +test.register_coroutine_test( + "Zone events should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.events.ZoneTriggered:build_test_event_report(mock_device, CAMERA_EP, { + zone = 2, + reason = clusters.ZoneManagement.types.ZoneEventTriggeredReasonEnum.MOTION + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.triggeredZones({{zoneId = 2}})) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.events.ZoneTriggered:build_test_event_report(mock_device, CAMERA_EP, { + zone = 3, + reason = clusters.ZoneManagement.types.ZoneEventTriggeredReasonEnum.MOTION + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.triggeredZones({{zoneId = 2}, {zoneId = 3}})) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.events.ZoneStopped:build_test_event_report(mock_device, CAMERA_EP, { + zone = 2, + reason = clusters.ZoneManagement.types.ZoneEventStoppedReasonEnum.ACTION_STOPPED + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.triggeredZones({{zoneId = 3}})) + ) + end +) + +test.register_coroutine_test( + "Button events should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.server.events.InitialPress:build_test_event_report(mock_device, DOORBELL_EP, {new_position = 1}) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.server.events.MultiPressComplete:build_test_event_report(mock_device, DOORBELL_EP, { + new_position = 1, + total_number_of_presses_counted = 2, + previous_position = 0 + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("doorbell", capabilities.button.button.double({state_change = true})) + ) + end +) + +-- Capability Handler UTs + +test.register_coroutine_test( + "Set night vision commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + local command_to_attribute_map = { + ["setNightVision"] = clusters.CameraAvStreamManagement.attributes.NightVision, + ["setIllumination"] = clusters.CameraAvStreamManagement.attributes.NightVisionIllum + } + for cmd, attr in pairs(command_to_attribute_map) do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "nightVision", component = "main", command = cmd, args = { "off" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, attr:write(mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TriStateAutoEnum.OFF) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "nightVision", component = "main", command = cmd, args = { "on" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, attr:write(mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TriStateAutoEnum.ON) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "nightVision", component = "main", command = cmd, args = { "auto" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, attr:write(mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TriStateAutoEnum.AUTO) + }) + end + end +) + +test.register_coroutine_test( + "Set enabled commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + local command_to_attribute_map = { + ["setHdr"] = { capability = "hdr", attr = clusters.CameraAvStreamManagement.attributes.HDRModeEnabled}, + ["setImageFlipHorizontal"] = { capability = "imageControl", attr = clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal}, + ["setImageFlipVertical"] = { capability = "imageControl", attr = clusters.CameraAvStreamManagement.attributes.ImageFlipVertical}, + ["setSoftLivestreamPrivacyMode"] = { capability = "cameraPrivacyMode", attr = clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled}, + ["setSoftRecordingPrivacyMode"] = { capability = "cameraPrivacyMode", attr = clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled}, + ["setLocalSnapshotRecording"] = { capability = "localMediaStorage", attr = clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled}, + ["setLocalVideoRecording"] = { capability = "localMediaStorage", attr = clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled} + } + for i, v in pairs(command_to_attribute_map) do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = v.capability, component = "main", command = i, args = { "enabled" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, v.attr:write(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = v.capability, component = "main", command = i, args = { "disabled" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, v.attr:write(mock_device, CAMERA_EP, false) + }) + end + end +) + +test.register_coroutine_test( + "Set image rotation command should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "imageControl", component = "main", command = "setImageRotation", args = { 10 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.ImageRotation:write(mock_device, CAMERA_EP, 10) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "imageControl", component = "main", command = "setImageRotation", args = { 257 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.ImageRotation:write(mock_device, CAMERA_EP, 257) + }) + end +) + +test.register_coroutine_test( + "Set mute commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "speaker", command = "setMute", args = { "muted" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.SpeakerMuted:write(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "speaker", command = "setMute", args = { "unmuted" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.SpeakerMuted:write(mock_device, CAMERA_EP, false) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "speaker", command = "mute", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.SpeakerMuted:write(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "speaker", command = "unmute", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.SpeakerMuted:write(mock_device, CAMERA_EP, false) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "microphone", command = "setMute", args = { "muted" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.MicrophoneMuted:write(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "microphone", command = "setMute", args = { "unmuted" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.MicrophoneMuted:write(mock_device, CAMERA_EP, false) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "microphone", command = "mute", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.MicrophoneMuted:write(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "microphone", command = "unmute", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.MicrophoneMuted:write(mock_device, CAMERA_EP, false) + }) + end +) + +test.register_coroutine_test( + "Set Volume command should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + local max_vol = 200 + local min_vol = 5 + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.SpeakerMaxLevel:build_test_report_data(mock_device, CAMERA_EP, max_vol) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.SpeakerMinLevel:build_test_report_data(mock_device, CAMERA_EP, min_vol) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.MicrophoneMaxLevel:build_test_report_data(mock_device, CAMERA_EP, max_vol) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.MicrophoneMinLevel:build_test_report_data(mock_device, CAMERA_EP, min_vol) + }) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioVolume", component = "speaker", command = "setVolume", args = { 0 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:write(mock_device, CAMERA_EP, 5) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioVolume", component = "speaker", command = "setVolume", args = { 35 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:write(mock_device, CAMERA_EP, 73) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioVolume", component = "microphone", command = "setVolume", args = { 77 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:write(mock_device, CAMERA_EP, 155) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioVolume", component = "microphone", command = "setVolume", args = { 100 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:write(mock_device, CAMERA_EP, 200) + }) + + ---- test volumeUp command + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:build_test_report_data(mock_device, CAMERA_EP, 103) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("speaker", capabilities.audioVolume.volume(50)) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioVolume", component = "speaker", command = "volumeUp", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:write(mock_device, CAMERA_EP, 104) + }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:build_test_report_data(mock_device, CAMERA_EP, 104) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("speaker", capabilities.audioVolume.volume(51)) + ) + + -- test volumeDown command + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:build_test_report_data(mock_device, CAMERA_EP, 200) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("microphone", capabilities.audioVolume.volume(100)) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioVolume", component = "microphone", command = "volumeDown", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:write(mock_device, CAMERA_EP, 198) + }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:build_test_report_data(mock_device, CAMERA_EP, 198) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("microphone", capabilities.audioVolume.volume(99)) + ) + end +) + +test.register_coroutine_test( + "Set Mode command should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + local mode_to_enum_map = { + ["low"] = clusters.Global.types.ThreeLevelAutoEnum.LOW, + ["medium"] = clusters.Global.types.ThreeLevelAutoEnum.MEDIUM, + ["high"] = clusters.Global.types.ThreeLevelAutoEnum.HIGH, + ["auto"] = clusters.Global.types.ThreeLevelAutoEnum.AUTO + } + for i, v in pairs(mode_to_enum_map) do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "speaker", command = "setMode", args = { i } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:write(mock_device, CAMERA_EP, v) + }) + end + end +) + +test.register_coroutine_test( + "Set Status LED commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "switch", component = "statusLed", command = "on", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:write(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "switch", component = "statusLed", command = "off", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:write(mock_device, CAMERA_EP, false) + }) + end +) + +test.register_coroutine_test( + "Set Relative PTZ commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mechanicalPanTiltZoom", component = "main", command = "panRelative", args = { 10 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRelativeMove(mock_device, CAMERA_EP, 10, 0, 0) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mechanicalPanTiltZoom", component = "main", command = "tiltRelative", args = { -35 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRelativeMove(mock_device, CAMERA_EP, 0, -35, 0) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mechanicalPanTiltZoom", component = "main", command = "zoomRelative", args = { 80 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRelativeMove(mock_device, CAMERA_EP, 0, 0, 80) + }) + end +) + +test.register_coroutine_test( + "Set PTZ commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mechanicalPanTiltZoom", component = "main", command = "setPanTiltZoom", args = { 10, 20, 30 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSetPosition(mock_device, CAMERA_EP, 10, 20, 30) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition:build_test_report_data( + mock_device, CAMERA_EP, {pan = 10, tilt = 20, zoom = 30}) + }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.pan(10)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.tilt(20)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.zoom(30)) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mechanicalPanTiltZoom", component = "main", command = "setPan", args = { 50 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSetPosition(mock_device, CAMERA_EP, 50, 20, 30) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition:build_test_report_data( + mock_device, CAMERA_EP, {pan = 50, tilt = 20, zoom = 30}) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.pan(50)) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mechanicalPanTiltZoom", component = "main", command = "setTilt", args = { -44 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSetPosition(mock_device, CAMERA_EP, 50, -44, 30) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition:build_test_report_data( + mock_device, CAMERA_EP, {pan = 50, tilt = -44, zoom = 30}) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.tilt(-44)) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mechanicalPanTiltZoom", component = "main", command = "setZoom", args = { 5 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSetPosition(mock_device, CAMERA_EP, 50, -44, 5) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition:build_test_report_data( + mock_device, CAMERA_EP, {pan = 50, tilt = -44, zoom = 5}) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.zoom(5)) + ) + end +) + +test.register_coroutine_test( + "Preset commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mechanicalPanTiltZoom", component = "main", command = "savePreset", args = { 1, "Preset 1" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSavePreset(mock_device, CAMERA_EP, 1, "Preset 1") + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mechanicalPanTiltZoom", component = "main", command = "removePreset", args = { 1 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRemovePreset(mock_device, CAMERA_EP, 1) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mechanicalPanTiltZoom", component = "main", command = "moveToPreset", args = { 2 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZMoveToPreset(mock_device, CAMERA_EP, 2) + }) + end +) + +test.register_coroutine_test( + "Sound commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "sounds", component = "main", command = "setSelectedSound", args = { 1 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.Chime.attributes.SelectedChime:write(mock_device, CAMERA_EP, 1) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "sounds", component = "main", command = "playSound", args = {} }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.Chime.server.commands.PlayChimeSound(mock_device, CAMERA_EP) + }) + end +) + +test.register_coroutine_test( + "Zone Management zone commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + local use_map = { + ["motion"] = clusters.ZoneManagement.types.ZoneUseEnum.MOTION, + ["focus"] = clusters.ZoneManagement.types.ZoneUseEnum.FOCUS, + ["privacy"] = clusters.ZoneManagement.types.ZoneUseEnum.PRIVACY + } + for i, v in pairs(use_map) do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "newZone", args = { + i .. " zone", {{value = {x = 0, y = 0}}, {value = {x = 1920, y = 1080}} }, i, "#FFFFFF" + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.CreateTwoDCartesianZone(mock_device, CAMERA_EP, + clusters.ZoneManagement.types.TwoDCartesianZoneStruct( + { + name = i .. " zone", + use = v, + vertices = { + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 0, y = 0}), + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 1920, y = 1080}) + }, + color = "#FFFFFF" + } + ) + ) + }) + end + local zone_id = 1 + for i, v in pairs(use_map) do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "updateZone", args = { + zone_id, "updated " .. i .. " zone", {{value = {x = 50, y = 50}}, {value = {x = 1000, y = 1000}} }, i, "red" + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.UpdateTwoDCartesianZone(mock_device, CAMERA_EP, + zone_id, + clusters.ZoneManagement.types.TwoDCartesianZoneStruct( + { + name = "updated " .. i .. " zone", + use = v, + vertices = { + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({ x = 50, y = 50 }), + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({ x = 1000, y = 1000 }) + }, + color = "red" + } + ) + ) + }) + zone_id = zone_id + 1 + end + for i = 1, 3 do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "removeZone", args = { i } } + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.RemoveZone(mock_device, CAMERA_EP, i) + }) + end + end +) + +test.register_coroutine_test( + "Zone Management zone commands should send the appropriate commands - missing optional color argument", + function() + update_device_profile() + test.wait_for_events() + local use_map = { + ["motion"] = clusters.ZoneManagement.types.ZoneUseEnum.MOTION, + ["focus"] = clusters.ZoneManagement.types.ZoneUseEnum.FOCUS, + ["privacy"] = clusters.ZoneManagement.types.ZoneUseEnum.PRIVACY + } + for i, v in pairs(use_map) do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "newZone", args = { + i .. " zone", {{value = {x = 0, y = 0}}, {value = {x = 1920, y = 1080}} }, i + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.CreateTwoDCartesianZone(mock_device, CAMERA_EP, + clusters.ZoneManagement.types.TwoDCartesianZoneStruct( + { + name = i .. " zone", + use = v, + vertices = { + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 0, y = 0}), + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 1920, y = 1080}) + }, + } + ) + ) + }) + end + local zone_id = 1 + for i, v in pairs(use_map) do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "updateZone", args = { + zone_id, "updated " .. i .. " zone", {{value = {x = 50, y = 50}}, {value = {x = 1000, y = 1000}} }, i, "red" + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.UpdateTwoDCartesianZone(mock_device, CAMERA_EP, + zone_id, + clusters.ZoneManagement.types.TwoDCartesianZoneStruct( + { + name = "updated " .. i .. " zone", + use = v, + vertices = { + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({ x = 50, y = 50 }), + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({ x = 1000, y = 1000 }) + }, + color = "red" + } + ) + ) + }) + zone_id = zone_id + 1 + end + for i = 1, 3 do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "removeZone", args = { i } } + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.RemoveZone(mock_device, CAMERA_EP, i) + }) + end + end +) + +test.register_coroutine_test( + "Zone Management trigger commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + + -- Create the trigger + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "createOrUpdateTrigger", args = { + 1, 10, 3, 15, 3, 5 + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.CreateOrUpdateTrigger(mock_device, CAMERA_EP, { + zone_id = 1, + initial_duration = 10, + augmentation_duration = 3, + max_duration = 15, + blind_duration = 3, + sensitivity = 5 + }) + }) + + -- The device reports the Triggers attribute with the newly created trigger and the capability is updated + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.Triggers:build_test_report_data( + mock_device, CAMERA_EP, { + clusters.ZoneManagement.types.ZoneTriggerControlStruct({ + zone_id = 1, initial_duration = 10, augmentation_duration = 3, max_duration = 15, blind_duration = 3, sensitivity = 5 + }) + } + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.triggers({{ + zoneId = 1, initialDuration = 10, augmentationDuration = 3, maxDuration = 15, blindDuration = 3, sensitivity = 5 + }})) + ) + test.wait_for_events() + + -- Update trigger, note that some arguments are optional. In this case, + -- blindDuration is not specified in the capability command. + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "createOrUpdateTrigger", args = { + 1, 8, 7, 25, 3, 1 + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.CreateOrUpdateTrigger(mock_device, CAMERA_EP, { + zone_id = 1, + initial_duration = 8, + augmentation_duration = 7, + max_duration = 25, + blind_duration = 3, + sensitivity = 1 + }) + }) + + -- Remove the trigger + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "removeTrigger", args = { 1 } } + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.RemoveTrigger(mock_device, CAMERA_EP, 1) + }) + end +) + +test.register_coroutine_test( + "Removing a zone with an existing trigger should send RemoveTrigger followed by RemoveZone", + function() + update_device_profile() + test.wait_for_events() + + -- Create a zone + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "newZone", args = { + "motion zone", {{value = {x = 0, y = 0}}, {value = {x = 1920, y = 1080}}}, "motion", "#FFFFFF" + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.CreateTwoDCartesianZone(mock_device, CAMERA_EP, + clusters.ZoneManagement.types.TwoDCartesianZoneStruct({ + name = "motion zone", + use = clusters.ZoneManagement.types.ZoneUseEnum.MOTION, + vertices = { + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 0, y = 0}), + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 1920, y = 1080}) + }, + color = "#FFFFFF" + }) + ) + }) + + -- Create a trigger + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "createOrUpdateTrigger", args = { + 1, 10, 3, 15, 3, 5 + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.CreateOrUpdateTrigger(mock_device, CAMERA_EP, { + zone_id = 1, + initial_duration = 10, + augmentation_duration = 3, + max_duration = 15, + blind_duration = 3, + sensitivity = 5 + }) + }) + + -- Receive the Triggers attribute update from the device reflecting the new trigger + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.Triggers:build_test_report_data( + mock_device, CAMERA_EP, { + clusters.ZoneManagement.types.ZoneTriggerControlStruct({ + zone_id = 1, initial_duration = 10, augmentation_duration = 3, + max_duration = 15, blind_duration = 3, sensitivity = 5 + }) + } + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.triggers({{ + zoneId = 1, initialDuration = 10, augmentationDuration = 3, + maxDuration = 15, blindDuration = 3, sensitivity = 5 + }})) + ) + test.wait_for_events() + + -- Receive removeZone command: since a trigger exists for zone 1, RemoveTrigger is sent first, then RemoveZone + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "removeZone", args = { 1 } } + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.RemoveTrigger(mock_device, CAMERA_EP, 1) + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.RemoveZone(mock_device, CAMERA_EP, 1) + }) + test.wait_for_events() + + -- Receive the updated Zones attribute from the device with the zone removed + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.Zones:build_test_report_data(mock_device, CAMERA_EP, {}) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.zones({value = {}})) + ) + end +) + +test.register_coroutine_test( + "Stream management commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "videoStreamSettings", component = "main", command = "setStream", args = { + 3, + "liveStream", + "Stream 3", + { width = 1920, height = 1080, fps = 30 }, + { upperLeftVertex = {x = 0, y = 0}, lowerRightVertex = {x = 1920, y = 1080} }, + "enabled", + "disabled" + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.server.commands.VideoStreamModify(mock_device, CAMERA_EP, + 3, true, false + ) + }) + end +) + +test.register_coroutine_test( + "Stream management setStream command should modify an existing stream", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams:build_test_report_data( + mock_device, CAMERA_EP, { + clusters.CameraAvStreamManagement.types.VideoStreamStruct({ + video_stream_id = 1, + stream_usage = clusters.Global.types.StreamUsageEnum.LIVE_VIEW, + video_codec = clusters.CameraAvStreamManagement.types.VideoCodecEnum.H264, + min_frame_rate = 30, + max_frame_rate = 60, + min_resolution = clusters.CameraAvStreamManagement.types.VideoResolutionStruct({width = 640, height = 360}), + max_resolution = clusters.CameraAvStreamManagement.types.VideoResolutionStruct({width = 640, height = 360}), + min_bit_rate = 10000, + max_bit_rate = 10000, + key_frame_interval = 4000, + watermark_enabled = true, + osd_enabled = false, + reference_count = 0 + }) + } + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.videoStreamSettings.videoStreams({ + { + streamId = 1, + data = { + label = "Stream 1", + type = "liveStream", + resolution = { + width = 640, + height = 360, + fps = 30 + }, + watermark = "enabled", + onScreenDisplay = "disabled" + } + } + })) + ) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "videoStreamSettings", component = "main", command = "setStream", args = { + 1, + "liveStream", + "Stream 1", + { width = 640, height = 360, fps = 30 }, + { upperLeftVertex = {x = 0, y = 0}, lowerRightVertex = {x = 640, y = 360} }, + "disabled", + "enabled" + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.server.commands.VideoStreamModify(mock_device, CAMERA_EP, + 1, false, true + ) + }) + end +) + +test.register_coroutine_test( + "Camera profile should not update for an unchanged Status Light AttributeList report", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP, { + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID), + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) + }) + }) + end +) + +test.register_coroutine_test( + "Camera profile should update for a changed Status Light AttributeList report", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP, { + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID) + }) + }) + local updated_expected_metadata = { + optional_component_capabilities = { + { "main", + { "videoCapture2", "cameraViewportSettings", "videoStreamSettings", "localMediaStorage", "audioRecording", + "cameraPrivacyMode", "imageControl", "hdr", "nightVision", "mechanicalPanTiltZoom", "zoneManagement", + "webrtc", "motionSensor", "sounds", } + }, + { "statusLed", + { "switch" } -- only switch capability remains + }, + { "speaker", + { "audioMute", "audioVolume" } + }, + { "microphone", + { "audioMute", "audioVolume" } + }, + { "doorbell", + { "button" } + } + }, + profile = "camera" + } + mock_device:expect_metadata_update(updated_expected_metadata) + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false}))) + end +) + +-- run the tests +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua index 7d3211bdfb..e1af3ad52b 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.matter.generated.zap_clusters" @@ -20,18 +9,22 @@ local version = require "version" local TRANSITION_TIME = 0 local OPTIONS_MASK = 0x01 -local OPTIONS_OVERRIDE = 0x01 +local HANDLE_COMMAND_IF_OFF = 0x01 local mock_device_ep1 = 1 local mock_device_ep2 = 2 local mock_device = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("light-color-level-fan.yml"), + label = "Matter Fan Light", + profile = t_utils.get_profile_definition("fan-modular.yml", {}), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, }, + matter_version = { + software = 1, + hardware = 1, + }, endpoints = { { endpoint_id = 0, @@ -45,7 +38,7 @@ local mock_device = test.mock_device.build_test_matter_device({ { endpoint_id = mock_device_ep1, clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, }, @@ -83,16 +76,46 @@ local CLUSTER_SUBSCRIBE_LIST ={ clusters.FanControl.attributes.PercentCurrent, } +local mock_child = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("light-color-level.yml"), + device_network_id = string.format("%s:%d", mock_device.id, 4), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", mock_device_ep1) +}) + local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_child) local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) -- since all fan capabilities are optional, nothing is initially subscribed to + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "light-color-level-fan" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, mock_device_ep1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Fan Light 1", + profile = "light-color-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", mock_device_ep1) + }) + mock_device:expect_metadata_update({ profile = "fan-modular", optional_component_capabilities = {{"main", {"fanSpeedPercent", "fanMode"}}} }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + local updated_device_profile = t_utils.get_profile_definition("fan-modular.yml", + {enabled_optional_capabilities = {{"main", {"fanSpeedPercent", "fanMode"}}}} + ) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = updated_device_profile })) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) end test.set_test_init_function(test_init) @@ -101,7 +124,7 @@ test.register_coroutine_test( "Switch capability should send the appropriate commands", function() test.socket.capability:__queue_receive( { - mock_device.id, + mock_child.id, { capability = "switch", component = "main", command = "on", args = { } } } ) @@ -109,14 +132,14 @@ test.register_coroutine_test( test.socket.devices:__expect_send( { "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } + { device_uuid = mock_child.id, capability_id = "switch", capability_cmd_id = "on" } } ) end test.socket.matter:__expect_send( { mock_device.id, - clusters.OnOff.server.commands.On(mock_device, 1) + clusters.OnOff.server.commands.On(mock_device, mock_device_ep1) } ) test.socket.matter:__queue_receive( @@ -131,9 +154,8 @@ test.register_coroutine_test( { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } } ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( + mock_child:generate_test_message( "main", capabilities.switch.switch.on() ) ) @@ -147,7 +169,7 @@ test.register_message_test( channel = "capability", direction = "receive", message = { - mock_device.id, + mock_child.id, { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } } }, @@ -156,7 +178,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device_ep1, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device_ep1, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -178,7 +200,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) + message = mock_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) }, } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua index 624a3ab205..33002a8203 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua @@ -1,22 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local utils = require "st.utils" local dkjson = require "dkjson" - local clusters = require "st.matter.generated.zap_clusters" local button_attr = capabilities.button.button +local uint32 = require "st.matter.data_types.Uint32" --- Mock a 5-button device using endpoints non-consecutive endpoints local mock_device = test.mock_device.build_test_matter_device( { - profile = t_utils.get_profile_definition("5-button-battery.yml"), -- on a real device we would switch to this, rather than fingerprint to it + profile = t_utils.get_profile_definition("5-button.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, endpoints = { { endpoint_id = 0, clusters = {}, - device_types = {} + device_types = { + {device_type_id = 0x0016, device_type_revision = 1}, -- RootNode + } }, { endpoint_id = 10, @@ -26,7 +31,6 @@ local mock_device = test.mock_device.build_test_matter_device( feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, cluster_type = "SERVER" }, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} }, device_types = { {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch @@ -87,107 +91,279 @@ local mock_device = test.mock_device.build_test_matter_device( } }, }, -} -) +}) --- add device for each mock device -local CLUSTER_SUBSCRIBE_LIST ={ - clusters.PowerSource.server.attributes.BatPercentRemaining, - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete, -} +local mock_device_battery = test.mock_device.build_test_matter_device( + { + profile = t_utils.get_profile_definition("5-button-battery.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = 0, + clusters = {}, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1}, -- RootNode + } + }, + { + endpoint_id = 10, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + }, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 20, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_RELEASE, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 30, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 50, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 60, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + }, + }) -local function configure_buttons() - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) +local function expect_configure_buttons(device) + test.socket.capability:__expect_send(device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(device:generate_test_message("main", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) + test.socket.capability:__expect_send(device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(device:generate_test_message("button2", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) + test.socket.capability:__expect_send(device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(device:generate_test_message("button3", button_attr.pushed({state_change = false}))) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 50)}) - test.socket.capability:__expect_send(mock_device:generate_test_message("button4", button_attr.pushed({state_change = false}))) + test.socket.matter:__expect_send({device.id, clusters.Switch.attributes.MultiPressMax:read(device, 50)}) + test.socket.capability:__expect_send(device:generate_test_message("button4", button_attr.pushed({state_change = false}))) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 60)}) - test.socket.capability:__expect_send(mock_device:generate_test_message("button5", button_attr.pushed({state_change = false}))) + test.socket.matter:__expect_send({device.id, clusters.Switch.attributes.MultiPressMax:read(device, 60)}) + test.socket.capability:__expect_send(device:generate_test_message("button5", button_attr.pushed({state_change = false}))) end +local function update_profile() + test.socket.matter:__queue_receive({mock_device_battery.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( + mock_device_battery, 10, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)} + )}) + expect_configure_buttons(mock_device_battery) + mock_device_battery:expect_metadata_update({ profile = "5-button-battery" }) +end + +-- All messages queued and expectations set are done before the driver is actually run local function test_init() + local CLUSTER_SUBSCRIBE_LIST = { + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end + + -- we dont want the integration test framework to generate init/doConfigure, we are doing that here + -- so we can set the proper expectations on those events. + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) -- make sure the cache is populated + + -- added sets a bunch of fields on the device, and calls init test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device) - local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() - test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) - configure_buttons() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + -- init results in subscription interaction test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "5-buttons-battery" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - configure_buttons() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + --doConfigure sets the provisioning state to provisioned + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + expect_configure_buttons(mock_device) + mock_device:expect_metadata_update({ profile = "5-button" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_battery() + local CLUSTER_SUBSCRIBE_LIST = { + clusters.PowerSource.server.attributes.AttributeList, + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device_battery) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_battery)) end + end + + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_battery) + + test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "added" }) + + test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "init" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "doConfigure" }) + mock_device_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) +test.register_coroutine_test( + "Simulate the profile change update taking affect and the device info changing", + function() + test.socket.matter:__set_channel_ordering("relaxed") + update_profile() + test.wait_for_events() + local device_info_copy = utils.deep_copy(mock_device_battery.raw_st_data) + device_info_copy.profile.id = "5-buttons-battery" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "infoChanged", device_info_json }) + -- due to the AttributeList being processed in update_profile, setting profiling_data.BATTERY_SUPPORT, + -- subsequent subscriptions will not include AttributeList. + local UPDATED_CLUSTER_SUBSCRIBE_LIST = { + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + local updated_subscribe_request = UPDATED_CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device_battery) + for i, clus in ipairs(UPDATED_CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then updated_subscribe_request:merge(clus:subscribe(mock_device_battery)) end + end + test.socket.matter:__expect_send({mock_device_battery.id, updated_subscribe_request}) + expect_configure_buttons(mock_device_battery) + end, + { test_init = test_init_battery } +) + +test.register_coroutine_test( + "Handle received BatPercentRemaining from device.", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device_battery.id, + clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( + mock_device_battery, 10, 150 + ) + } + ) + test.socket.capability:__expect_send( + mock_device_battery:generate_test_message( + "main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5)) + ) + ) + end, + { test_init = test_init_battery } +) + test.register_message_test( "Handle single press sequence, no hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} --move to position 1? - ), + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} --move to position 1 + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press } -} ) test.register_message_test( "Handle single press sequence for short release-supported button", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 20, {new_position = 1} --move to position 1? - ), - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device, 20, {previous_position = 0} --move to position 1? - ), + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 20, {new_position = 1} --move to position 1 + ), + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 20, {previous_position = 0} --move to position 1 + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button2", button_attr.pushed({state_change = true})) --should send initial press } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("button2", button_attr.pushed({state_change = true})) --should send initial press } -} ) test.register_coroutine_test( @@ -344,117 +520,117 @@ test.register_coroutine_test( test.register_message_test( "Handle single press sequence, with hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ), + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) } -} ) test.register_message_test( "Handle release after short press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device, 10, {previous_position = 1} - ) - } - }, - { -- this is a double event because the test device in this test shouldn't support the above event - -- but we handle it anyway - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 10, {previous_position = 1} + ) + } + }, + { -- this is a double event because the test device in this test shouldn't support the above event + -- but we handle it anyway + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, } ) test.register_message_test( "Handle release after long press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongRelease:build_test_event_report( - mock_device, 10, {previous_position = 1} - ) - } - }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 10, {previous_position = 1} + ) + } + }, } ) @@ -523,161 +699,136 @@ test.register_message_test( test.register_message_test( "Handle double press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ) - } - }, - { -- again, on a device that reports that it supports double press, this event - -- will not be generated. See a multi-button test file for that case - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 10, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.double({state_change = true})) - }, - -} + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { -- again, on a device that reports that it supports double press, this event + -- will not be generated. See a multi-button test file for that case + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 10, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.double({state_change = true})) + }, + } ) test.register_message_test( "Handle multi press for 4 times", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ) - } - }, - { -- again, on a device that reports that it supports double press, this event - -- will not be generated. See a multi-button test file for that case - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 10, {new_position = 1, total_number_of_presses_counted = 4, previous_position=0} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed_4x({state_change = true})) - }, - -} -) - -test.register_message_test( - "Receiving a max press attribute of 2 should emit correct event", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.attributes.MultiPressMax:build_test_report_data( - mock_device, 50, 2 - ) - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("button4", - capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) - }, - } + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { -- again, on a device that reports that it supports double press, this event + -- will not be generated. See a multi-button test file for that case + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 10, {new_position = 1, total_number_of_presses_counted = 4, previous_position=0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed_4x({state_change = true})) + }, + } ) test.register_message_test( - "Handle received BatPercentRemaining from device.", { + "Receiving a max press attribute of 2 should emit correct event", { { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( - mock_device, 10, 150 - ), + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 50, 2 + ) }, }, { channel = "capability", direction = "send", - message = mock_device:generate_test_message( - "main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5)) - ), + message = mock_device:generate_test_message("button4", + capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) }, } ) - test.register_message_test( "Handle a long press including MultiPressComplete", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 60, {new_position = 1} - ) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 60, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("button5", button_attr.held({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} - ) + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button5", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} + ) + } } + -- no double event } - -- no double event -} ) test.register_message_test( @@ -688,7 +839,7 @@ test.register_message_test( message = { mock_device.id, clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 60, {new_position = 1} + mock_device, 60, {new_position = 1} ) } }, @@ -698,7 +849,7 @@ test.register_message_test( message = { mock_device.id, clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 60, {new_position = 1} + mock_device, 60, {new_position = 1} ) } }, @@ -713,7 +864,7 @@ test.register_message_test( message = { mock_device.id, clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 60, {new_position = 1} + mock_device, 60, {new_position = 1} ) } }, @@ -723,7 +874,7 @@ test.register_message_test( message = { mock_device.id, clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} + mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} ) } }, @@ -734,5 +885,60 @@ test.register_message_test( } } ) --- run the tests + +local function reset_battery_profiling_info() + local fields = require "switch_utils.fields" + mock_device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist=true}) +end + +test.register_coroutine_test( + "Test profile does not change to button-battery when battery percent remaining attribute (attribute ID 12) is not available", + function() + reset_battery_profiling_info() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 10, {uint32(10)}) + } + ) + end +) + +test.register_coroutine_test( + "Test profile change to button-batteryLevel when battery percent remaining attribute (attribute ID 14) is available", + function() + reset_battery_profiling_info() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 10, {uint32( + clusters.PowerSource.attributes.BatChargeLevel.ID + )}) + } + ) + expect_configure_buttons(mock_device) + mock_device:expect_metadata_update({ profile = "5-button-batteryLevel" }) + end +) + +test.register_coroutine_test( + "Test profile change to button-battery when battery percent remaining attribute (attribute ID 12) is available", + function() + reset_battery_profiling_info() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 10, {uint32( + clusters.PowerSource.attributes.BatPercentRemaining.ID + )}) + } + ) + expect_configure_buttons(mock_device) + mock_device:expect_metadata_update({ profile = "5-button-battery" }) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua new file mode 100644 index 0000000000..dca5f20645 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua @@ -0,0 +1,769 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local utils = require "st.utils" +local dkjson = require "dkjson" + +local clusters = require "st.matter.generated.zap_clusters" +local button_attr = capabilities.button.button + +local mock_device = test.mock_device.build_test_matter_device( + { + profile = t_utils.get_profile_definition("6-button-motion.yml"), -- on a real device we would switch to this, rather than fingerprint to it + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = 0, + clusters = {}, + device_types = {} + }, + { + endpoint_id = 10, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 20, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_RELEASE, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 30, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 40, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 50, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 60, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 70, + clusters = { + {cluster_id = clusters.OccupancySensing.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0107, device_type_revision = 1} -- OccupancySensor + } + }, + }, +} +) + +-- add device for each mock device +local CLUSTER_SUBSCRIBE_LIST ={ + clusters.OccupancySensing.attributes.Occupancy, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, +} + +local function expect_configure_buttons() + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) + + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) + + test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) + + test.socket.capability:__expect_send(mock_device:generate_test_message("button4", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button4", button_attr.pushed({state_change = false}))) + + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 50)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("button5", button_attr.pushed({state_change = false}))) + + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 60)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("button6", button_attr.pushed({state_change = false}))) +end + +-- All messages queued and expectations set are done before the driver is actually run +local function test_init() + -- we dont want the integration test framework to generate init/doConfigure, we are doing that here + -- so we can set the proper expectations on those events. + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) -- make sure the cache is populated + + -- added sets a bunch of fields on the device, and calls init + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + -- init results in subscription interaction + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + --doConfigure sets the provisioning state to provisioned + mock_device:expect_metadata_update({ profile = "6-button-motion" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + expect_configure_buttons() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + -- simulate the profile change update taking affect and the device info changing + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "6-buttons-motion" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + expect_configure_buttons() +end +test.set_test_init_function(test_init) + +test.register_message_test( + "Handle single press sequence, no hold", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} --move to position 1? + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press + } +} +) + +test.register_message_test( + "Handle single press sequence for short release-supported button", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 20, {new_position = 1} --move to position 1? + ), + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 20, {previous_position = 0} --move to position 1? + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button2", button_attr.pushed({state_change = true})) --should send initial press + } +} +) + +test.register_coroutine_test( + "Handle single press sequence for emulated hold on short-release-only button", + function () + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 20, {new_position = 1} + ) + }) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 20, {previous_position = 0} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.held({state_change = true}))) + end +) + +test.register_coroutine_test( + "Handle single press sequence for a long hold on long-release-capable button", -- only a long press event should generate a held event + function () + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 30, {new_position = 1} + ) + }) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 30, {previous_position = 0} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = true}))) + end +) + +test.register_coroutine_test( + "Handle single press sequence for a long hold on multi button", -- pushes should only be generated from multiPressComplete events + function () + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 50, {new_position = 1} + ) + }) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 50, {previous_position = 0} + ) + }) + end +) + +test.register_coroutine_test( + "Handle single press sequence for a multi press on multi button", + function () + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 60, {previous_position = 0} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_device, 60, {new_position = 1, current_number_of_presses_counted = 2} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 60, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button6", button_attr.double({state_change = true}))) + end +) + +test.register_coroutine_test( + "Handle long press sequence for a long hold on long-release-capable button", -- only a long press event should generate a held event + function () + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 40, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 40, {new_position = 1} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button4", button_attr.held({state_change = true}))) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 40, {previous_position = 0} + ) + }) + end +) + +test.register_coroutine_test( + "Occupancy reports should generate correct messages", + function () + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 70, 1) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.active())) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 70, 0) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) + end +) + +test.register_coroutine_test( + "Handle long press sequence for a long hold on multi button", + function () + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button6", button_attr.held({state_change = true}))) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 60, {previous_position = 0} + ) + }) + end +) + +test.register_message_test( + "Handle single press sequence, with hold", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) + } +} +) + +test.register_message_test( + "Handle release after short press", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 10, {previous_position = 1} + ) + } + }, + { -- this is a double event because the test device in this test shouldn't support the above event + -- but we handle it anyway + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + } +) + +test.register_message_test( + "Handle release after long press", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 10, {previous_position = 1} + ) + } + }, + } +) + +test.register_message_test( + "Receiving a max press attribute of 2 should emit correct event", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 10, 2 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) + }, + } +) + +test.register_message_test( + "Receiving a max press attribute of 3 should emit correct event", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 60, 3 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button6", + capabilities.button.supportedButtonValues({"pushed", "double", "held", "pushed_3x"}, {visibility = {displayed = false}})) + }, + } +) + +test.register_message_test( + "Receiving a max press attribute of greater than 6 should only emit up to pushed_6x", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 10, 7 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.button.supportedButtonValues({"pushed", "double", "pushed_3x", "pushed_4x", "pushed_5x", "pushed_6x"}, {visibility = {displayed = false}})) + }, + } +) + +test.register_message_test( + "Handle double press", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { -- again, on a device that reports that it supports double press, this event + -- will not be generated. See a multi-button test file for that case + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 10, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.double({state_change = true})) + }, + +} +) + +test.register_message_test( + "Handle multi press for 4 times", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { -- again, on a device that reports that it supports double press, this event + -- will not be generated. See a multi-button test file for that case + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 10, {new_position = 1, total_number_of_presses_counted = 4, previous_position=0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed_4x({state_change = true})) + }, + +} +) + +test.register_message_test( + "Receiving a max press attribute of 2 should emit correct event", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 50, 2 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button5", + capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) + }, + } +) + +test.register_message_test( + "Handle a long press including MultiPressComplete", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button6", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} + ) + } + } + -- no double event +} +) + +test.register_message_test( + "Handle long press followed by single press", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button6", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button6", button_attr.pushed({state_change = true})) + } + } +) +-- run the tests +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua index 139c519dbf..db1ae04bfb 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua @@ -1,14 +1,15 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" -local dkjson = require "dkjson" local clusters = require "st.matter.generated.zap_clusters" local TRANSITION_TIME = 0 local OPTIONS_MASK = 0x01 -local OPTIONS_OVERRIDE = 0x01 +local HANDLE_COMMAND_IF_OFF = 0x01 local button_attr = capabilities.button.button @@ -25,6 +26,7 @@ local mock_device = test.mock_device.build_test_matter_device({ vendor_id = 0x0000, product_id = 0x0000, }, + matter_version = {hardware = 1, software = 1}, endpoints = { { endpoint_id = 0, @@ -100,11 +102,12 @@ local mock_device = test.mock_device.build_test_matter_device({ local mock_device_mcd_unsupported_switch_device_type = test.mock_device.build_test_matter_device({ label = "Matter Switch", - profile = t_utils.get_profile_definition("matter-thing.yml"), + profile = t_utils.get_profile_definition("button.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, }, + matter_version = {hardware = 1, software = 1}, endpoints = { { endpoint_id = 0, @@ -163,7 +166,18 @@ local child_data = { local mock_child = test.mock_device.build_test_child_device(child_data) -- add device for each mock device -local CLUSTER_SUBSCRIBE_LIST ={ +local CLUSTER_SUBSCRIBE_LIST_NO_CHILD ={ + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, +} + +local CLUSTER_SUBSCRIBE_LIST_WITH_CHILD ={ clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, @@ -175,13 +189,14 @@ local CLUSTER_SUBSCRIBE_LIST ={ clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, clusters.Switch.server.events.InitialPress, clusters.Switch.server.events.LongPress, clusters.Switch.server.events.ShortRelease, clusters.Switch.server.events.MultiPressComplete, } -local function configure_buttons() +local function expect_configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("button1", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("button1", button_attr.pushed({state_change = false}))) @@ -193,22 +208,25 @@ local function configure_buttons() end local function test_init() - local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) - for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) -- make sure the cache is populated + + -- added sets a bunch of fields on the device, and calls init + local subscribe_request = CLUSTER_SUBSCRIBE_LIST_NO_CHILD[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_NO_CHILD) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep5, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, mock_device_ep5, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) mock_device:expect_metadata_update({ profile = "light-level-3-button" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "3-button" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - configure_buttons() - test.mock_device.add_test_device(mock_device) - test.mock_device.add_test_device(mock_child) mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", @@ -216,37 +234,16 @@ local function test_init() parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", mock_device_ep5) }) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - configure_buttons() - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + expect_configure_buttons() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) end +-- All messages queued and expectations set are done before the driver is actually run local function test_init_mcd_unsupported_switch_device_type() - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete, - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_mcd_unsupported_switch_device_type) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_mcd_unsupported_switch_device_type)) - end - end - test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "doConfigure" }) - mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ profile = "2-button" }) - mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - mock_device_mcd_unsupported_switch_device_type:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 1", - profile = "switch-binary", - parent_device_id = mock_device_mcd_unsupported_switch_device_type.id, - parent_assigned_child_key = string.format("%d", 7) - }) - test.mock_device.add_test_device(mock_device_mcd_unsupported_switch_device_type) + -- we dont want the integration test framework to generate init/doConfigure, we are doing that here + -- so we can set the proper expectations on those events. + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_mcd_unsupported_switch_device_type) -- make sure the cache is populated end test.set_test_init_function(test_init) @@ -358,69 +355,151 @@ test.register_coroutine_test( end ) -test.register_message_test( +test.register_coroutine_test( "Switch child device: Set color temperature should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_child.id, - { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device_ep5, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, mock_device_ep5) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, mock_device_ep5, 556) - } - }, - { - channel = "capability", - direction = "send", - message = mock_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) - }, - } + function() + test.mock_device.add_test_device(mock_child) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_child.id, + { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device_ep5, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, mock_device_ep5) + }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, mock_device_ep5, 556) + }) + test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800))) + end ) test.register_coroutine_test( "Test MCD configuration not including switch for unsupported switch device type, create child device instead", function() - end, + local unsup_mock_device = mock_device_mcd_unsupported_switch_device_type + -- added sets a bunch of fields on the device, and calls init + local CLUSTER_SUBSCRIBE_LIST = { + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(unsup_mock_device) + for _, cluster in ipairs(CLUSTER_SUBSCRIBE_LIST) do + subscribe_request:merge(cluster:subscribe(unsup_mock_device)) + end + test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "added" }) + test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "init" }) + test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "doConfigure" }) + unsup_mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 1", + profile = "switch-binary", + parent_device_id = unsup_mock_device.id, + parent_assigned_child_key = string.format("%d", 7) + }) + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) + unsup_mock_device:expect_metadata_update({ profile = "2-button" }) + unsup_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + test.wait_for_events() + + local updated_device_profile = t_utils.get_profile_definition("2-button.yml") + test.socket.device_lifecycle:__queue_receive(unsup_mock_device:generate_info_changed({ profile = updated_device_profile })) + + local CLUSTER_SUBSCRIBE_LIST = { + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(unsup_mock_device) + for i, cluster in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(unsup_mock_device)) + end + end + test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) + + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) + + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) + end, { test_init = test_init_mcd_unsupported_switch_device_type } ) test.register_coroutine_test( "Test driver switched event", function() + test.mock_device.add_test_device(mock_child) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + local subscribe_request = CLUSTER_SUBSCRIBE_LIST_WITH_CHILD[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_WITH_CHILD) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "driverSwitched" }) + mock_child:expect_metadata_update({ profile = "light-color-level" }) mock_device:expect_metadata_update({ profile = "light-level-3-button" }) - configure_buttons() - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "light-color-level", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", mock_device_ep5) - }) + expect_configure_buttons() + end +) + +test.register_coroutine_test( + "Test info changed event with parent device profile update", + function() + local subscribe_request = CLUSTER_SUBSCRIBE_LIST_NO_CHILD[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_NO_CHILD) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + local updated_device_profile = t_utils.get_profile_definition("light-level-3-button.yml") + updated_device_profile.id = "updated device profile id" + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = updated_device_profile })) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + expect_configure_buttons() + end +) + +test.register_coroutine_test( + "Test info changed event with matter_version update", + function() + test.mock_device.add_test_device(mock_child) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 } })) -- bump sw to 2 + mock_child:expect_metadata_update({ profile = "light-color-level" }) + mock_device:expect_metadata_update({ profile = "light-level-3-button" }) + expect_configure_buttons() + end +) + +test.register_coroutine_test( + "Test child device initialization, and that subscriptions are initialized correctly", + function () + test.mock_device.add_test_device(mock_child) + test.socket.matter:__expect_send({mock_device.id, clusters.OnOff.attributes.OnOff:read(mock_device)}) + test.socket.device_lifecycle:__queue_receive({ mock_child.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_child.id, "init" }) + local subscribe_request = CLUSTER_SUBSCRIBE_LIST_WITH_CHILD[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_WITH_CHILD) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + mock_child:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ mock_child.id, "doConfigure" }) end ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua new file mode 100644 index 0000000000..de2afe3bd3 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua @@ -0,0 +1,123 @@ +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local utils = require "st.utils" +local dkjson = require "dkjson" +local clusters = require "st.matter.clusters" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("3-button-battery-temperature-humidity.yml"), + matter_version = {hardware = 1, software = 1}, + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0302, device_type_revision = 1}, + } + }, + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "BOTH"}, + }, + device_types = { + {device_type_id = 0x0307, device_type_revision = 1}, + } + }, + } +}) + +local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + + local cluster_subscribe_list = { + clusters.Switch.events.InitialPress, + clusters.Switch.events.LongPress, + clusters.Switch.events.ShortRelease, + clusters.Switch.events.MultiPressComplete, + + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, + + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, + clusters.PowerSource.attributes.BatPercentRemaining + } + + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "3-button-battery-temperature-humidity" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json}) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + +end + +test.register_coroutine_test("Read appropriate attribute values after tempOffset preference change", function() + local report = clusters.TemperatureMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device,1, 2000) + mock_device.st_store.preferences = {tempOffset = "0"} + + test.socket.matter:__queue_receive({mock_device.id, report}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.temperatureMeasurement.temperature({ + value = 20.0, + unit = "C" + }))) + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {tempOffset = "5"}})) + test.socket.matter:__expect_send({mock_device.id, clusters.TemperatureMeasurement.attributes.MeasuredValue:read(mock_device)}) + + test.wait_for_events() + + test.socket.matter:__queue_receive({mock_device.id, report}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.temperatureMeasurement.temperature({ + value = 20.0, + unit = "C" + }))) +end) + +test.register_coroutine_test("Read appropriate attribute values after humidityOffset preference change", function() + local report = clusters.RelativeHumidityMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device,2, 2000) + mock_device.st_store.preferences = {humidityOffset = "0"} + + test.socket.matter:__queue_receive({mock_device.id, report}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.relativeHumidityMeasurement.humidity({ + value = 20 + }))) + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {humidityOffset = "5"}})) + test.socket.matter:__expect_send({mock_device.id, clusters.RelativeHumidityMeasurement.attributes.MeasuredValue:read(mock_device)}) + + test.wait_for_events() + + test.socket.matter:__queue_receive({mock_device.id, report}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.relativeHumidityMeasurement.humidity({ + value = 20 + }))) +end) + +test.set_test_init_function(test_init) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua index 84ccc9e478..6cdf530dbf 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -1,25 +1,15 @@ --- Copyright 2022 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" +local st_utils = require "st.utils" local clusters = require "st.matter.clusters" local TRANSITION_TIME = 0 local OPTIONS_MASK = 0x01 -local OPTIONS_OVERRIDE = 0x01 +local HANDLE_COMMAND_IF_OFF = 0x01 local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("switch-color-level.yml"), @@ -77,6 +67,52 @@ local mock_device_no_hue_sat = test.mock_device.build_test_matter_device({ } }) +local mock_device_color_temp = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 1}, -- On/Off Light + {device_type_id = 0x010C, device_type_revision = 1} -- Color Temperature Light + } + } + } +}) + +local mock_device_extended_color = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("light-color-level.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 1}, -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 1}, -- Dimmable Light + {device_type_id = 0x010C, device_type_revision = 1}, -- Color Temperature Light + {device_type_id = 0x010D, device_type_revision = 1}, -- Extended Color Light + } + } + } +}) + local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -90,6 +126,7 @@ local cluster_subscribe_list = { clusters.ColorControl.attributes.ColorTemperatureMireds, clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.ColorMode, } local function set_color_mode(device, endpoint, color_mode) @@ -116,6 +153,11 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + -- note that since disable_startup_messages is not explicitly called here, + -- the following subscribe is due to the init event sent by the test framework. test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION) @@ -129,6 +171,9 @@ local function test_init_x_y_color_mode() subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) @@ -141,11 +186,92 @@ local function test_init_no_hue_sat() subscribe_request:merge(cluster:subscribe(mock_device_no_hue_sat)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device_no_hue_sat.id, "added" }) + test.socket.matter:__expect_send({mock_device_no_hue_sat.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device_no_hue_sat.id, subscribe_request}) test.mock_device.add_test_device(mock_device_no_hue_sat) set_color_mode(mock_device_no_hue_sat, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) end + +local cluster_subscribe_list_color_temp = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds +} + +local function test_init_color_temp() + test.mock_device.add_test_device(mock_device_color_temp) + local subscribe_request = cluster_subscribe_list_color_temp[1]:subscribe(mock_device_color_temp) + for i, cluster in ipairs(cluster_subscribe_list_color_temp) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_color_temp)) + end + end + + test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "added" }) + test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "init" }) + test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_color_temp.id, + clusters.LevelControl.attributes.Options:write(mock_device_color_temp, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + test.socket.matter:__expect_send({ + mock_device_color_temp.id, + clusters.ColorControl.attributes.Options:write(mock_device_color_temp, 1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + mock_device_color_temp:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) +end + +local function test_init_extended_color() + test.mock_device.add_test_device(mock_device_extended_color) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_extended_color) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_extended_color)) + end + end + test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "init" }) + test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_extended_color.id, + clusters.LevelControl.attributes.Options:write(mock_device_extended_color, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + test.socket.matter:__expect_send({ + mock_device_extended_color.id, + clusters.ColorControl.attributes.Options:write(mock_device_extended_color, 1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + mock_device_extended_color:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) +end + +test.register_message_test( + "Test that Color Temperature Light device does not switch profiles", + {}, + { test_init = test_init_color_temp } +) + +test.register_message_test( + "Test that Extended Color Light device does not switch profiles", + {}, + { test_init = test_init_extended_color } +) + test.register_message_test( "On command should send the appropriate commands", { @@ -230,7 +356,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 1, math.floor(20/100.0 * 254), 20, 0 ,0) + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 1, st_utils.round(20/100.0 * 254), 20, 0 ,0) } }, { @@ -301,7 +427,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_device:generate_test_message("main", capabilities.switchLevel.level(math.floor((50 / 254.0 * 100) + 0.5))) + message = mock_device:generate_test_message("main", capabilities.switchLevel.level(st_utils.round((50 / 254.0 * 100) + 0.5))) }, { channel = "devices", @@ -325,7 +451,7 @@ test.register_coroutine_test( test.socket.matter:__expect_send( { mock_device_no_hue_sat.id, - clusters.ColorControl.server.commands.MoveToColor(mock_device_no_hue_sat, 1, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColor(mock_device_no_hue_sat, 1, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } ) test.socket.matter:__queue_receive( @@ -387,7 +513,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -411,6 +537,14 @@ test.register_message_test( direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.hue(50)) }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } + } + }, { channel = "matter", direction = "receive", @@ -423,7 +557,15 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.saturation(50)) - } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } + } + }, } ) @@ -453,7 +595,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -477,6 +619,14 @@ test.register_message_test( direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.hue(100)) }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } + } + }, { channel = "matter", direction = "receive", @@ -489,7 +639,15 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.saturation(100)) - } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } + } + }, } ) @@ -511,7 +669,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToHue(mock_device, 1, hue, 0, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToHue(mock_device, 1, hue, 0, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, } @@ -533,7 +691,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToSaturation(mock_device, 1, sat, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToSaturation(mock_device, 1, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, } @@ -555,7 +713,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -827,7 +985,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 165, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 165, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -843,7 +1001,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 365, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 365, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } } } @@ -970,6 +1128,12 @@ test.register_coroutine_test( "main", capabilities.colorControl.hue(100) ) ) + test.socket.devices:__expect_send( + { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } + } + ) test.socket.matter:__queue_receive( { mock_device.id, @@ -981,6 +1145,12 @@ test.register_coroutine_test( "main", capabilities.colorControl.saturation(100) ) ) + test.socket.devices:__expect_send( + { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } + } + ) end, { test_init = test_init_x_y_color_mode } ) @@ -1027,4 +1197,19 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Refresh necessary attributes", + function() + test.socket.capability:__queue_receive( + {mock_device.id, {capability = "refresh", component = "main", command = "refresh", args = {}}} + ) + local read_request = cluster_subscribe_list[1]:read(mock_device) + for i, attr in ipairs(cluster_subscribe_list) do + if i > 1 then read_request:merge(attr:read(mock_device)) end + end + test.socket.matter:__expect_send({mock_device.id, read_request}) + test.wait_for_events() + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index a35552007a..5513c915b7 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -1,14 +1,22 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" +test.disable_startup_messages() + local mock_device_onoff = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("matter-thing.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, }, + matter_version = { + hardware = 1, + software = 1, + }, endpoints = { { endpoint_id = 0, @@ -88,6 +96,35 @@ local mock_device_dimmer = test.mock_device.build_test_matter_device({ } }) +local mock_device_switch_vendor_override = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("switch-binary.yml"), + manufacturer_info = { + vendor_id = 0x109B, + product_id = 0x1001, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, + }, + device_types = { + {device_type_id = 0x010A, device_type_revision = 1} -- OnOff PlugIn Unit + } + } + } +}) + + local mock_device_color_dimmer = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("matter-thing.yml"), manufacturer_info = { @@ -139,7 +176,7 @@ local mock_device_mounted_on_off_control = test.mock_device.build_test_matter_de endpoint_id = 7, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "CLIENT", feature_map = 2}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, }, device_types = { @@ -169,7 +206,7 @@ local mock_device_mounted_dimmable_load_control = test.mock_device.build_test_ma endpoint_id = 7, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "CLIENT", feature_map = 2}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, }, device_types = { @@ -381,12 +418,7 @@ local mock_device_light_level_motion = test.mock_device.build_test_matter_device { endpoint_id = 1, clusters = { - { - cluster_id = clusters.OnOff.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = 0, --u32 bitmap - }, + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} }, device_types = { @@ -406,15 +438,17 @@ local mock_device_light_level_motion = test.mock_device.build_test_matter_device }) local function test_init_parent_child_switch_types() - local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_parent_child_switch_types) - test.socket.matter:__expect_send({mock_device_parent_child_switch_types.id, subscribe_request}) - + test.mock_device.add_test_device(mock_device_parent_child_switch_types) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_parent_child_switch_types.id, + clusters.LevelControl.attributes.Options:write(mock_device_parent_child_switch_types, 7, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) mock_device_parent_child_switch_types:expect_metadata_update({ profile = "switch-level" }) mock_device_parent_child_switch_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_parent_child_switch_types) - mock_device_parent_child_switch_types:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", @@ -426,6 +460,8 @@ end local function test_init_onoff() test.mock_device.add_test_device(mock_device_onoff) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "doConfigure" }) mock_device_onoff:expect_metadata_update({ profile = "switch-binary" }) mock_device_onoff:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -433,32 +469,60 @@ end local function test_init_onoff_client() test.mock_device.add_test_device(mock_device_onoff_client) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_client.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_client.id, "init" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_client.id, "doConfigure" }) + mock_device_onoff_client:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_parent_client_child_server() - local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_parent_client_child_server) - test.socket.matter:__expect_send({mock_device_parent_client_child_server.id, subscribe_request}) + test.mock_device.add_test_device(mock_device_parent_client_child_server) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "init" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "doConfigure" }) mock_device_parent_client_child_server:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_client_child_server:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_parent_client_child_server) end local function test_init_dimmer() test.mock_device.add_test_device(mock_device_dimmer) + test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_dimmer.id, + clusters.LevelControl.attributes.Options:write(mock_device_dimmer, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) mock_device_dimmer:expect_metadata_update({ profile = "switch-level" }) mock_device_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_color_dimmer() test.mock_device.add_test_device(mock_device_color_dimmer) + test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "doConfigure" }) mock_device_color_dimmer:expect_metadata_update({ profile = "switch-color-level" }) mock_device_color_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end +local function test_init_switch_vendor_override() + test.mock_device.add_test_device(mock_device_switch_vendor_override) + local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_switch_vendor_override) + test.socket.device_lifecycle:__queue_receive({ mock_device_switch_vendor_override.id, "added" }) + test.socket.matter:__expect_send({mock_device_switch_vendor_override.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_switch_vendor_override.id, "init" }) + test.socket.matter:__expect_send({mock_device_switch_vendor_override.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_switch_vendor_override.id, "doConfigure" }) + mock_device_switch_vendor_override:expect_metadata_update({ profile = "switch-binary" }) + mock_device_switch_vendor_override:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + local function test_init_mounted_on_off_control() + test.mock_device.add_test_device(mock_device_mounted_on_off_control) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, } @@ -468,15 +532,28 @@ local function test_init_mounted_on_off_control() subscribe_request:merge(cluster:subscribe(mock_device_mounted_on_off_control)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "added" }) + test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "init" }) test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_mounted_on_off_control.id, + clusters.LevelControl.attributes.Options:write(mock_device_mounted_on_off_control, 7, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + mock_device_mounted_on_off_control:expect_metadata_update({ profile = "switch-binary" }) mock_device_mounted_on_off_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_mounted_on_off_control) end local function test_init_mounted_dimmable_load_control() + test.mock_device.add_test_device(mock_device_mounted_dimmable_load_control) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.LevelControl.attributes.MaxLevel, } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_mounted_dimmable_load_control) for i, cluster in ipairs(cluster_subscribe_list) do @@ -484,46 +561,54 @@ local function test_init_mounted_dimmable_load_control() subscribe_request:merge(cluster:subscribe(mock_device_mounted_dimmable_load_control)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "added" }) test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "init" }) + test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_mounted_dimmable_load_control.id, + clusters.LevelControl.attributes.Options:write(mock_device_mounted_dimmable_load_control, 7, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + mock_device_mounted_dimmable_load_control:expect_metadata_update({ profile = "switch-level" }) mock_device_mounted_dimmable_load_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_mounted_dimmable_load_control) end local function test_init_water_valve() test.mock_device.add_test_device(mock_device_water_valve) + test.socket.device_lifecycle:__queue_receive({ mock_device_water_valve.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_water_valve.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_water_valve.id, "doConfigure" }) mock_device_water_valve:expect_metadata_update({ profile = "water-valve-level" }) mock_device_water_valve:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_parent_child_different_types() + test.mock_device.add_test_device(mock_device_parent_child_different_types) local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY + clusters.OnOff.attributes.OnOff } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_parent_child_different_types) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_parent_child_different_types)) - end - end + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "added" }) + test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "init" }) test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_parent_child_different_types.id, + clusters.LevelControl.attributes.Options:write(mock_device_parent_child_different_types, 10, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + test.socket.matter:__expect_send({ + mock_device_parent_child_different_types.id, + clusters.ColorControl.attributes.Options:write(mock_device_parent_child_different_types, 10, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + mock_device_parent_child_different_types:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_child_different_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_parent_child_different_types) - mock_device_parent_child_different_types:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", @@ -534,10 +619,16 @@ local function test_init_parent_child_different_types() end local function test_init_parent_child_unsupported_device_type() + test.mock_device.add_test_device(mock_device_parent_child_unsupported_device_type) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "doConfigure" }) mock_device_parent_child_unsupported_device_type:expect_metadata_update({ profile = "switch-binary" }) + test.socket.matter:__expect_send({ + mock_device_parent_child_unsupported_device_type.id, + clusters.LevelControl.attributes.Options:write(mock_device_parent_child_unsupported_device_type, 10, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) mock_device_parent_child_unsupported_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_parent_child_unsupported_device_type) mock_device_parent_child_unsupported_device_type:expect_device_create({ type = "EDGE_CHILD", @@ -549,6 +640,7 @@ local function test_init_parent_child_unsupported_device_type() end local function test_init_light_level_motion() + test.mock_device.add_test_device(mock_device_light_level_motion) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -562,8 +654,20 @@ local function test_init_light_level_motion() subscribe_request:merge(cluster:subscribe(mock_device_light_level_motion)) end end + + test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "added" }) test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_light_level_motion) + + test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "init" }) + test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_light_level_motion.id, + clusters.LevelControl.attributes.Options:write(mock_device_light_level_motion, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + mock_device_light_level_motion:expect_metadata_update({ profile = "light-level-motion" }) + mock_device_light_level_motion:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.register_coroutine_test( @@ -594,6 +698,13 @@ test.register_coroutine_test( { test_init = test_init_onoff_client } ) +test.register_coroutine_test( + "Test init for device with requiring the switch category as a vendor override", + function() + end, + { test_init = test_init_switch_vendor_override } +) + test.register_coroutine_test( "Test init for mounted onoff control parent cluster as server", function() @@ -650,4 +761,4 @@ test.register_coroutine_test( { test_init = test_init_light_level_motion } ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua index d4ead24458..da74443896 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua @@ -1,22 +1,16 @@ --- Copyright 2024 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. +-- Copyright © 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" +local version = require "version" + +if version.api < 11 then + clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" +end local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("water-valve-level.yml"), @@ -63,6 +57,10 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + -- the following subscribe is due to the init event sent by the test framework. test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua index 8680a57740..774e6bfa52 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -156,6 +145,10 @@ local function test_init_mock_3switch() } test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_3switch) + test.socket.device_lifecycle:__queue_receive({ mock_3switch.id, "added" }) + test.socket.matter:__expect_send({mock_3switch.id, subscribe_request}) + + -- the following subscribe is due to the init event sent by the test framework. test.socket.matter:__expect_send({mock_3switch.id, subscribe_request}) test.mock_device.add_test_device(mock_3switch) end @@ -168,6 +161,9 @@ local function test_init_mock_2switch() } test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_2switch) + test.socket.device_lifecycle:__queue_receive({ mock_2switch.id, "added" }) + test.socket.matter:__expect_send({mock_2switch.id, subscribe_request}) + test.socket.matter:__expect_send({mock_2switch.id, subscribe_request}) test.mock_device.add_test_device(mock_2switch) end @@ -180,6 +176,9 @@ local function test_init_mock_3switch_non_sequential() } test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_3switch_non_sequential) + test.socket.device_lifecycle:__queue_receive({ mock_3switch_non_sequential.id, "added" }) + test.socket.matter:__expect_send({mock_3switch_non_sequential.id, subscribe_request}) + test.socket.matter:__expect_send({mock_3switch_non_sequential.id, subscribe_request}) test.mock_device.add_test_device(mock_3switch_non_sequential) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index de62865597..be01a7bf32 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -1,25 +1,16 @@ --- Copyright 2024 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. +-- Copyright © 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" - local clusters = require "st.matter.clusters" + +test.disable_startup_messages() + local TRANSITION_TIME = 0 local OPTIONS_MASK = 0x01 -local OPTIONS_OVERRIDE = 0x01 +local HANDLE_COMMAND_IF_OFF = 0x01 local parent_ep = 10 local child1_ep = 20 @@ -32,6 +23,10 @@ local mock_device = test.mock_device.build_test_matter_device({ vendor_id = 0x0000, product_id = 0x0000, }, + matter_version = { + hardware = 1, + software = 1, + }, endpoints = { { endpoint_id = 0, @@ -159,6 +154,7 @@ for i, endpoint in ipairs(mock_device.endpoints) do end local function test_init() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -170,7 +166,8 @@ local function test_init() clusters.ColorControl.attributes.CurrentHue, clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do @@ -178,12 +175,20 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, child1_ep, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, child2_ep, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, child2_ep, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + mock_device:expect_metadata_update({ profile = "light-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device) for _, child in pairs(mock_children) do test.mock_device.add_test_device(child) end @@ -225,6 +230,9 @@ for i, endpoint in ipairs(mock_device_parent_child_endpoints_non_sequential.endp end local function test_init_parent_child_endpoints_non_sequential() + local unsup_mock_device = mock_device_parent_child_endpoints_non_sequential + + test.mock_device.add_test_device(unsup_mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -236,46 +244,56 @@ local function test_init_parent_child_endpoints_non_sequential() clusters.ColorControl.attributes.CurrentHue, clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_parent_child_endpoints_non_sequential) + local subscribe_request = cluster_subscribe_list[1]:subscribe(unsup_mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_parent_child_endpoints_non_sequential)) + subscribe_request:merge(cluster:subscribe(unsup_mock_device)) end end - test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "doConfigure" }) - mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "added" }) + test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "init" }) + test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({unsup_mock_device.id, clusters.LevelControl.attributes.Options:write(unsup_mock_device, child1_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({unsup_mock_device.id, clusters.LevelControl.attributes.Options:write(unsup_mock_device, child2_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({unsup_mock_device.id, clusters.ColorControl.attributes.Options:write(unsup_mock_device, child2_ep_non_sequential, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + + unsup_mock_device:expect_metadata_update({ profile = "switch-binary" }) + unsup_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_parent_child_endpoints_non_sequential) for _, child in pairs(mock_children_non_sequential) do test.mock_device.add_test_device(child) end - mock_device_parent_child_endpoints_non_sequential:expect_device_create({ + unsup_mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", profile = "light-color-level", - parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, + parent_device_id = unsup_mock_device.id, parent_assigned_child_key = string.format("%d", child2_ep_non_sequential) }) -- switch-binary will be selected as an overridden child device profile - mock_device_parent_child_endpoints_non_sequential:expect_device_create({ + unsup_mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 3", profile = "switch-binary", - parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, + parent_device_id = unsup_mock_device.id, parent_assigned_child_key = string.format("%d", child3_ep_non_sequential) }) - mock_device_parent_child_endpoints_non_sequential:expect_device_create({ + unsup_mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 4", profile = "light-level", - parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, + parent_device_id = unsup_mock_device.id, parent_assigned_child_key = string.format("%d", child1_ep_non_sequential) }) end @@ -478,7 +496,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child2_ep, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child2_ep, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -553,7 +571,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColor(mock_device, child2_ep, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColor(mock_device, child2_ep, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -675,4 +693,14 @@ test.register_coroutine_test( { test_init = test_init_parent_child_endpoints_non_sequential } ) -test.run_registered_tests() +test.register_coroutine_test( + "Test info changed event with matter_version update", + function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 } })) -- bump to 2 + mock_children[child1_ep]:expect_metadata_update({ profile = "light-level" }) + mock_children[child2_ep]:expect_metadata_update({ profile = "light-color-level" }) + mock_device:expect_metadata_update({ profile = "light-binary" }) + end +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua index cbf8fb0afe..2370984c97 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua @@ -1,32 +1,24 @@ --- Copyright 2023 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. +-- Copyright © 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" - local clusters = require "st.matter.clusters" -local child_profile = t_utils.get_profile_definition("plug-binary.yml") -local child_profile_override = t_utils.get_profile_definition("switch-binary.yml") +test.disable_startup_messages() + +local TRANSITION_TIME = 0 +local OPTIONS_MASK = 0x01 +local HANDLE_COMMAND_IF_OFF = 0x01 + local parent_ep = 10 local child1_ep = 20 local child2_ep = 30 local mock_device = test.mock_device.build_test_matter_device({ label = "Matter Switch", - profile = t_utils.get_profile_definition("plug-binary.yml"), + profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -47,36 +39,44 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, }, device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + {device_type_id = 0x0100, device_type_revision = 2} -- On/Off Light } }, { endpoint_id = child1_ep, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} }, device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light } }, { endpoint_id = child2_ep, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, }, device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light } }, } }) -local mock_device_child_profile_override = test.mock_device.build_test_matter_device({ +local child1_ep_non_sequential = 50 +local child2_ep_non_sequential = 30 +local child3_ep_non_sequential = 40 + +local mock_device_parent_child_endpoints_non_sequential = test.mock_device.build_test_matter_device({ label = "Matter Switch", - profile = t_utils.get_profile_definition("switch-binary.yml"), + profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), manufacturer_info = { vendor_id = 0x1321, - product_id = 0x000D, + product_id = 0x000C, }, endpoints = { { @@ -94,20 +94,33 @@ local mock_device_child_profile_override = test.mock_device.build_test_matter_de {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, }, device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + {device_type_id = 0x0100, device_type_revision = 2} -- On/Off Light } }, { - endpoint_id = child1_ep, + endpoint_id = child1_ep_non_sequential, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} }, device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light } }, { - endpoint_id = child2_ep, + endpoint_id = child2_ep_non_sequential, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, + }, + device_types = { + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light + } + }, + { + endpoint_id = child3_ep_non_sequential, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, }, @@ -118,11 +131,16 @@ local mock_device_child_profile_override = test.mock_device.build_test_matter_de } }) +local child_profiles = { + [child1_ep] = t_utils.get_profile_definition("light-level.yml"), + [child2_ep] = t_utils.get_profile_definition("light-color-level.yml"), +} + local mock_children = {} for i, endpoint in ipairs(mock_device.endpoints) do if endpoint.endpoint_id ~= parent_ep and endpoint.endpoint_id ~= 0 then local child_data = { - profile = child_profile, + profile = child_profiles[endpoint.endpoint_id], device_network_id = string.format("%s:%d", mock_device.id, endpoint.endpoint_id), parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) @@ -132,16 +150,41 @@ for i, endpoint in ipairs(mock_device.endpoints) do end local function test_init() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, child1_ep, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, child2_ep, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, child2_ep, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + mock_device:expect_metadata_update({ profile = "light-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device) for _, child in pairs(mock_children) do test.mock_device.add_test_device(child) end @@ -149,7 +192,7 @@ local function test_init() mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", - profile = "plug-binary", + profile = "light-level", parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", child1_ep) }) @@ -157,54 +200,94 @@ local function test_init() mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 3", - profile = "plug-binary", + profile = "light-color-level", parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", child2_ep) }) end -local mock_children_child_profile_override = {} -for i, endpoint in ipairs(mock_device_child_profile_override.endpoints) do +local child_profiles_non_sequential = { + [child1_ep_non_sequential] = t_utils.get_profile_definition("light-level.yml"), + [child2_ep_non_sequential] = t_utils.get_profile_definition("light-color-level.yml"), + [child3_ep_non_sequential] = t_utils.get_profile_definition("light-color-level.yml"), +} + +local mock_children_non_sequential = {} +for i, endpoint in ipairs(mock_device_parent_child_endpoints_non_sequential.endpoints) do if endpoint.endpoint_id ~= parent_ep and endpoint.endpoint_id ~= 0 then local child_data = { - profile = child_profile_override, - device_network_id = string.format("%s:%d", mock_device_child_profile_override.id, endpoint.endpoint_id), - parent_device_id = mock_device_child_profile_override.id, + profile = child_profiles_non_sequential[endpoint.endpoint_id], + device_network_id = string.format("%s:%d", mock_device_parent_child_endpoints_non_sequential.id, endpoint.endpoint_id), + parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) } - mock_children_child_profile_override[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) + mock_children_non_sequential[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) end end -local function test_init_child_profile_override() +local function test_init_parent_child_endpoints_non_sequential() + test.mock_device.add_test_device(mock_device_parent_child_endpoints_non_sequential) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_child_profile_override) - test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_parent_child_endpoints_non_sequential) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_parent_child_endpoints_non_sequential)) + end + end - test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "doConfigure" }) - mock_device_child_profile_override:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "added" }) + test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_child_profile_override) - for _, child in pairs(mock_children_child_profile_override) do + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "init" }) + test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, clusters.LevelControl.attributes.Options:write(mock_device_parent_child_endpoints_non_sequential, child1_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, clusters.LevelControl.attributes.Options:write(mock_device_parent_child_endpoints_non_sequential, child2_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, clusters.ColorControl.attributes.Options:write(mock_device_parent_child_endpoints_non_sequential, child2_ep_non_sequential, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "switch-binary" }) + mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + for _, child in pairs(mock_children_non_sequential) do test.mock_device.add_test_device(child) end - mock_device:expect_device_create({ + mock_device_parent_child_endpoints_non_sequential:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", - profile = "switch-binary", - parent_device_id = mock_device_child_profile_override.id, - parent_assigned_child_key = string.format("%d", child1_ep) + profile = "light-color-level", + parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, + parent_assigned_child_key = string.format("%d", child2_ep_non_sequential) }) - mock_device:expect_device_create({ + -- switch-binary will be selected as an overridden child device profile + mock_device_parent_child_endpoints_non_sequential:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 3", profile = "switch-binary", - parent_device_id = mock_device_child_profile_override.id, - parent_assigned_child_key = string.format("%d", child2_ep) + parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, + parent_assigned_child_key = string.format("%d", child3_ep_non_sequential) + }) + + mock_device_parent_child_endpoints_non_sequential:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 4", + profile = "light-level", + parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, + parent_assigned_child_key = string.format("%d", child1_ep_non_sequential) }) end @@ -363,19 +446,244 @@ test.register_message_test( } ) -test.register_coroutine_test( - "Added should call refresh for child devices", function() - test.socket.matter:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive({ mock_children[child1_ep].id, "added" }) - local req = clusters.OnOff.attributes.OnOff:read(mock_children[child1_ep]) - test.socket.matter:__expect_send({mock_device.id, req}) - end +test.register_message_test( + "Current level reports should generate appropriate events", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.attributes.CurrentLevel:build_test_report_data(mock_device, child1_ep, 50) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child1_ep]:generate_test_message("main", capabilities.switchLevel.level(math.floor((50 / 254.0 * 100) + 0.5))) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } + } + }, + } +) + +test.register_message_test( + "Set color temperature should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_children[child2_ep].id, + { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child2_ep, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, child2_ep) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, child2_ep, 556) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) + }, + } +) + +test.register_message_test( + "X and Y color values should report hue and saturation once both have been received", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, child2_ep, 15091) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, child2_ep, 21547) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.hue(50)) + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.saturation(72)) + } + } +) + +test.register_message_test( + "Set color command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_children[child2_ep].id, + { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 50, saturation = 72 } } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColor(mock_device, child2_ep, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColor:build_test_command_response(mock_device, child2_ep) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, child2_ep, 15091) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, child2_ep, 21547) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.hue(50)) + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.saturation(72)) + } + } +) + +test.register_message_test( + "Min and max level attributes set capability constraint for child devices", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, child1_ep, 1) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, child1_ep, 254) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child1_ep]:generate_test_message("main", capabilities.switchLevel.levelRange({minimum = 1, maximum = 100})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, child2_ep, 127) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, child2_ep, 203) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.switchLevel.levelRange({minimum = 50, maximum = 80})) + } + } +) + +test.register_message_test( + "Min and max color temp attributes set capability constraint for child devices", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, child2_ep, 153) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, child2_ep, 555) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 1800, maximum = 6500})) + } + } ) test.register_coroutine_test( - "Child device profiles should be overriden for specific devices", function() - end, - { test_init = test_init_child_profile_override } + "Test child devices are created in order of their endpoints", + function() + end, + { test_init = test_init_parent_child_endpoints_non_sequential } ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua b/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua new file mode 100644 index 0000000000..a8a8e83b4b --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua @@ -0,0 +1,163 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local clusters = require "st.matter.clusters" + +local mock_device_color_temp = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 1}, -- On/Off Light + {device_type_id = 0x010C, device_type_revision = 1} -- Color Temperature Light + } + } + } +}) + +local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, +} + +local function test_init() + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_color_temp) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_color_temp)) + end + end + test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) + test.mock_device.add_test_device(mock_device_color_temp) +end +test.set_test_init_function(test_init) + +local fields = require "switch_utils.fields" + +test.register_message_test( + "Color Temperature Step Command Test", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device_color_temp.id, + { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 20 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device_color_temp.id, + clusters.ColorControl.server.commands.StepColorTemperature(mock_device_color_temp, 1, clusters.ColorControl.types.StepModeEnum.DOWN, 187, fields.TRANSITION_TIME_FAST, fields.COLOR_TEMPERATURE_MIRED_MIN, fields.COLOR_TEMPERATURE_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + }, + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device_color_temp.id, + { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 90 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device_color_temp.id, + clusters.ColorControl.server.commands.StepColorTemperature(mock_device_color_temp, 1, clusters.ColorControl.types.StepModeEnum.DOWN, 840, fields.TRANSITION_TIME_FAST, fields.COLOR_TEMPERATURE_MIRED_MIN, fields.COLOR_TEMPERATURE_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + }, + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device_color_temp.id, + { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { -50 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device_color_temp.id, + clusters.ColorControl.server.commands.StepColorTemperature(mock_device_color_temp, 1, clusters.ColorControl.types.StepModeEnum.UP, 467, fields.TRANSITION_TIME_FAST, fields.COLOR_TEMPERATURE_MIRED_MIN, fields.COLOR_TEMPERATURE_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + }, + } + } +) + + +test.register_message_test( + "Level Step Command Test", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device_color_temp.id, + { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device_color_temp.id, + clusters.LevelControl.server.commands.Step(mock_device_color_temp, 1, clusters.LevelControl.types.StepModeEnum.UP, 64, fields.TRANSITION_TIME_FAST, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + }, + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device_color_temp.id, + { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { -50 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device_color_temp.id, + clusters.LevelControl.server.commands.Step(mock_device_color_temp, 1, clusters.LevelControl.types.StepModeEnum.DOWN, 127, fields.TRANSITION_TIME_FAST, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + }, + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device_color_temp.id, + { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 100 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device_color_temp.id, + clusters.LevelControl.server.commands.Step(mock_device_color_temp, 1, clusters.LevelControl.types.StepModeEnum.UP, 254, fields.TRANSITION_TIME_FAST, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + }, + } + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua b/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua index 398d3756c8..0a72a1316e 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -201,6 +190,8 @@ local function configure_buttons() end local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.Switch.events.InitialPress } @@ -208,13 +199,17 @@ local function test_init() for i, clus in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ profile = "12-button-keyboard" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) configure_buttons() - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) device_info_copy.profile.id = "12-buttons-keyboard" local device_info_json = dkjson.encode(device_info_copy) diff --git a/drivers/SmartThings/matter-switch/src/third-reality-mk1/init.lua b/drivers/SmartThings/matter-switch/src/third-reality-mk1/init.lua deleted file mode 100644 index a579929fe0..0000000000 --- a/drivers/SmartThings/matter-switch/src/third-reality-mk1/init.lua +++ /dev/null @@ -1,138 +0,0 @@ --- 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 capabilities = require "st.capabilities" -local clusters = require "st.matter.clusters" -local device_lib = require "st.device" -local im = require "st.matter.interaction_model" -local log = require "log" - -local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" - -------------------------------------------------------------------------------------- --- Third Reality MK1 specifics -------------------------------------------------------------------------------------- - -local THIRD_REALITY_MK1_FINGERPRINT = { vendor_id = 0x1407, product_id = 0x1388 } - -local function is_third_reality_mk1(opts, driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER and - device.manufacturer_info.vendor_id == THIRD_REALITY_MK1_FINGERPRINT.vendor_id and - device.manufacturer_info.product_id == THIRD_REALITY_MK1_FINGERPRINT.product_id then - log.info("Using Third Reality MK1 sub driver") - return true - end - return false -end - -local function endpoint_to_component(device, ep) - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} - for component, endpoint in pairs(map) do - if endpoint == ep then - return component - end - end - return "main" -end - --- override subscribe function to prevent subscribing to additional events from the main driver -local function subscribe(device) - local ib = im.InteractionInfoBlock(nil, clusters.Switch.ID, nil, clusters.Switch.events.InitialPress.ID) - local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) - subscribe_request:with_info_block(ib) - device:send(subscribe_request) -end - -local function configure_buttons(device) - local ms_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - for _, ep in ipairs(ms_eps) do - if device.profile.components[endpoint_to_component(device, ep)] then - device.log.info(string.format("Configuring Supported Values for generic switch endpoint %d", ep)) - local supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ep, supportedButtonValues_event) - device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) - else - device.log.info(string.format("Component not found for generic switch endpoint %d. Skipping Supported Value configuration", ep)) - end - end -end - -local function build_button_component_map(device) - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - table.sort(button_eps) - local component_map = {} - component_map["main"] = button_eps[1] - for component_num = 2, 12 do - component_map["F" .. component_num] = button_eps[component_num] - end - device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) -end - -local function device_init(driver, device) - device:set_endpoint_to_component_fn(endpoint_to_component) - device:extend_device("subscribe", subscribe) - device:subscribe() -end - --- override device_added to prevent it running in the main driver -local function device_added(driver, device) end - -local function info_changed(driver, device, event, args) - if device.profile.id ~= args.old_st_store.profile.id then - configure_buttons(device) - device:subscribe() - end -end - -local function match_profile(driver, device) - device:try_update_metadata({profile = "12-button-keyboard"}) - build_button_component_map(device) - configure_buttons(device) -end - -local function do_configure(driver, device) - match_profile(driver, device) -end - -local function driver_switched(driver, device) - match_profile(driver, device) -end - -local function initial_press_event_handler(driver, device, ib, response) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true})) -end - -local third_reality_mk1_handler = { - NAME = "ThirdReality MK1 Handler", - lifecycle_handlers = { - init = device_init, - added = device_added, - infoChanged = info_changed, - doConfigure = do_configure, - driverSwitched = driver_switched - }, - matter_handlers = { - event = { - [clusters.Switch.ID] = { - [clusters.Switch.events.InitialPress.ID] = initial_press_event_handler - } - } - }, - supported_capabilities = { - capabilities.button - }, - can_handle = is_third_reality_mk1 -} - -return third_reality_mk1_handler diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index e25443d49c..30e99caadb 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -16,6 +16,12 @@ matterManufacturer: vendorId: 0x1209 productId: 0x3012 deviceProfileName: thermostat-heating-only-nostate + #Cync + - id: "4921/121" + deviceLabel: Cync Fan Switch + vendorId: 0x1339 + productId: 0x0079 + deviceProfileName: fan-generic #Eve - id: "4874/79" deviceLabel: Eve Thermo @@ -27,6 +33,11 @@ matterManufacturer: vendorId: 0x130A productId: 0x007D deviceProfileName: thermostat-heating-only-nostate + - id: "4874/127" + deviceLabel: Eve Thermostat (Europe) + vendorId: 0x130A + productId: 0x007F + deviceProfileName: thermostat-heating-only-nostate-nobattery #Habi - id: "5415/1" deviceLabel: habi Matter Wireless Room Thermostat @@ -38,6 +49,23 @@ matterManufacturer: vendorId: 0x1527 productId: 0x0002 deviceProfileName: thermostat-heating-only-batteryLevel + #Lux + - id: "4614/1" + deviceLabel: LUX TQ1 Smart Thermostat + vendorId: 0x1206 + productId: 0x0001 + deviceProfileName: thermostat-nostate-nobattery + - id: "4614/17" + deviceLabel: LUX TQX Smart Thermostat + vendorId: 0x1206 + productId: 0x0011 + deviceProfileName: thermostat-humidity-nostate-nobattery + #Meross + - id: "4933/57345" + deviceLabel: Smart Wi-Fi Thermostat + vendorId: 0x1345 + productId: 0xE001 + deviceProfileName: thermostat-fan-nostate-nobattery #Siterwell - id: "4736/769" deviceLabel: Siterwell Radiator Thermostat @@ -60,61 +88,61 @@ matterManufacturer: deviceLabel: xCREAS Smart Air Purifier Cyclone400 vendorId: 0x156A productId: 0x0001 - deviceProfileName: air-purifier-modular + deviceProfileName: air-purifier-hepa-wind-aqs-pm25-meas-pm25-level matterGeneric: - id: "matter/hvac/heatcool" deviceLabel: Matter Thermostat deviceTypes: - id: 0x0300 # HVAC Heating/Cooling Unit - deviceProfileName: thermostat + deviceProfileName: thermostat-nostate-nobattery - id: "matter/hvac/thermostat" deviceLabel: Matter Thermostat deviceTypes: - id: 0x0301 # Thermostat - deviceProfileName: thermostat + deviceProfileName: thermostat-nostate-nobattery - id: "matter/hvac/heatcool/humidity" deviceLabel: Matter Thermostat deviceTypes: - id: 0x0300 # HVAC Heating/Cooling Unit - id: 0x0307 # Humidity Sensor - deviceProfileName: thermostat-humidity + deviceProfileName: thermostat-humidity-nostate-nobattery - id: "matter/hvac/thermostat/humidity" deviceLabel: Matter Thermostat deviceTypes: - id: 0x0301 # Thermostat - id: 0x0307 # Humidity Sensor - deviceProfileName: thermostat-humidity + deviceProfileName: thermostat-humidity-nostate-nobattery - id: "matter/hvac/heatcool/fan" deviceLabel: Matter Thermostat deviceTypes: - id: 0x0300 # HVAC Heating/Cooling Unit - id: 0x002B # Fan - deviceProfileName: thermostat-fan + deviceProfileName: thermostat-fan-nostate-nobattery - id: "matter/hvac/thermostat/fan" deviceLabel: Matter Thermostat deviceTypes: - id: 0x0301 # Thermostat - id: 0x002B # Fan - deviceProfileName: thermostat-fan + deviceProfileName: thermostat-fan-nostate-nobattery - id: "matter/hvac/heatcool/humidity/fan" deviceLabel: Matter Thermostat deviceTypes: - id: 0x0300 # HVAC Heating/Cooling Unit - id: 0x0307 # Humidity Sensor - id: 0x002B # Fan - deviceProfileName: thermostat-humidity-fan + deviceProfileName: thermostat-humidity-fan-nostate-nobattery - id: "matter/hvac/thermostat/humidity/fan" deviceLabel: Matter Thermostat deviceTypes: - id: 0x0301 # Thermostat - id: 0x0307 # Humidity Sensor - id: 0x002B # Fan - deviceProfileName: thermostat-humidity-fan + deviceProfileName: thermostat-humidity-fan-nostate-nobattery - id: "matter/room-air-conditioner" deviceLabel: Matter Room Air Conditioner deviceTypes: - id: 0x0072 - deviceProfileName: room-air-conditioner + deviceProfileName: room-air-conditioner-fan-heating-cooling-nostate - id: "matter/fan" deviceLabel: Matter Fan deviceTypes: @@ -124,7 +152,7 @@ matterGeneric: deviceLabel: Matter Air Purifier deviceTypes: - id: 0x002D # Air Purifier - deviceProfileName: air-purifier-hepa-ac-wind + deviceProfileName: air-purifier - id: "matter/air-purifier/quality-sensor" deviceLabel: Matter Air Purifier & Quality Sensor deviceTypes: diff --git a/drivers/SmartThings/matter-thermostat/profiles/fan.yml b/drivers/SmartThings/matter-thermostat/profiles/fan.yml index 5c9792309e..e86941d625 100644 --- a/drivers/SmartThings/matter-thermostat/profiles/fan.yml +++ b/drivers/SmartThings/matter-thermostat/profiles/fan.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: fan components: - id: main diff --git a/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml b/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml index 19dbc88e37..7d3dd309f2 100644 --- a/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml +++ b/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml @@ -9,6 +9,15 @@ components: - id: fanMode version: 1 optional: true + - id: fanSpeedPercent + version: 1 + optional: true + - id: fanOscillationMode + version: 1 + optional: true + - id: windMode + version: 1 + optional: true - id: thermostatFanMode version: 1 optional: true diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/init.lua b/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/init.lua deleted file mode 100644 index 3fa5f37100..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/init.lua +++ /dev/null @@ -1,102 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local ActivatedCarbonFilterMonitoringServerAttributes = require "ActivatedCarbonFilterMonitoring.server.attributes" -local ActivatedCarbonFilterMonitoringServerCommands = require "ActivatedCarbonFilterMonitoring.server.commands" -local ActivatedCarbonFilterMonitoringTypes = require "ActivatedCarbonFilterMonitoring.types" - -local ActivatedCarbonFilterMonitoring = {} - -ActivatedCarbonFilterMonitoring.ID = 0x0072 -ActivatedCarbonFilterMonitoring.NAME = "ActivatedCarbonFilterMonitoring" -ActivatedCarbonFilterMonitoring.server = {} -ActivatedCarbonFilterMonitoring.client = {} -ActivatedCarbonFilterMonitoring.server.attributes = ActivatedCarbonFilterMonitoringServerAttributes:set_parent_cluster(ActivatedCarbonFilterMonitoring) -ActivatedCarbonFilterMonitoring.server.commands = ActivatedCarbonFilterMonitoringServerCommands:set_parent_cluster(ActivatedCarbonFilterMonitoring) -ActivatedCarbonFilterMonitoring.types = ActivatedCarbonFilterMonitoringTypes - -function ActivatedCarbonFilterMonitoring:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "Condition", - [0x0001] = "DegradationDirection", - [0x0002] = "ChangeIndication", - [0x0003] = "InPlaceIndicator", - [0x0004] = "LastChangedTime", - [0x0005] = "ReplacementProductList", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - -function ActivatedCarbonFilterMonitoring:get_server_command_by_id(command_id) - local server_id_map = { - [0x0000] = "ResetCondition", - } - if server_id_map[command_id] ~= nil then - return self.server.commands[server_id_map[command_id]] - end - return nil -end - -ActivatedCarbonFilterMonitoring.attribute_direction_map = { - ["Condition"] = "server", - ["DegradationDirection"] = "server", - ["ChangeIndication"] = "server", - ["InPlaceIndicator"] = "server", - ["LastChangedTime"] = "server", - ["ReplacementProductList"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -ActivatedCarbonFilterMonitoring.command_direction_map = { - ["ResetCondition"] = "server", -} - -ActivatedCarbonFilterMonitoring.FeatureMap = ActivatedCarbonFilterMonitoring.types.Feature - -function ActivatedCarbonFilterMonitoring.are_features_supported(feature, feature_map) - if (ActivatedCarbonFilterMonitoring.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = ActivatedCarbonFilterMonitoring.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, ActivatedCarbonFilterMonitoring.NAME)) - end - return ActivatedCarbonFilterMonitoring[direction].attributes[key] -end -ActivatedCarbonFilterMonitoring.attributes = {} -setmetatable(ActivatedCarbonFilterMonitoring.attributes, attribute_helper_mt) - -local command_helper_mt = {} -command_helper_mt.__index = function(self, key) - local direction = ActivatedCarbonFilterMonitoring.command_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown command %s on cluster %s", key, ActivatedCarbonFilterMonitoring.NAME)) - end - return ActivatedCarbonFilterMonitoring[direction].commands[key] -end -ActivatedCarbonFilterMonitoring.commands = {} -setmetatable(ActivatedCarbonFilterMonitoring.commands, command_helper_mt) - -local event_helper_mt = {} -event_helper_mt.__index = function(self, key) - return ActivatedCarbonFilterMonitoring.server.events[key] -end -ActivatedCarbonFilterMonitoring.events = {} -setmetatable(ActivatedCarbonFilterMonitoring.events, event_helper_mt) - -setmetatable(ActivatedCarbonFilterMonitoring, {__index = cluster_base}) - -return ActivatedCarbonFilterMonitoring - diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua deleted file mode 100644 index 320826dacc..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua +++ /dev/null @@ -1,76 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local AttributeList = { - ID = 0xFFFB, - NAME = "AttributeList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - -function AttributeList:augment_type(data_type_obj) - for i, v in ipairs(data_type_obj.elements) do - data_type_obj.elements[i] = data_types.validate_or_build_type(v, AttributeList.element_type) - end -end - -function AttributeList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function AttributeList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - - -function AttributeList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function AttributeList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function AttributeList:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function AttributeList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) -return AttributeList - diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua b/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua deleted file mode 100644 index 4980356485..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local ChangeIndication = { - ID = 0x0002, - NAME = "ChangeIndication", - base_type = require "ActivatedCarbonFilterMonitoring.types.ChangeIndicationEnum", -} - -function ChangeIndication:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function ChangeIndication:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function ChangeIndication:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function ChangeIndication:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function ChangeIndication:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function ChangeIndication:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(ChangeIndication, {__call = ChangeIndication.new_value, __index = ChangeIndication.base_type}) -return ChangeIndication - diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua b/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua deleted file mode 100644 index e668aa4c48..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local Condition = { - ID = 0x0000, - NAME = "Condition", - base_type = require "st.matter.data_types.Uint8", -} - -function Condition:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function Condition:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function Condition:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function Condition:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function Condition:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function Condition:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(Condition, {__call = Condition.new_value, __index = Condition.base_type}) -return Condition - diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/init.lua deleted file mode 100644 index a02378a50d..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("ActivatedCarbonFilterMonitoring.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local ActivatedCarbonFilterMonitoringServerAttributes = {} - -function ActivatedCarbonFilterMonitoringServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(ActivatedCarbonFilterMonitoringServerAttributes, attr_mt) - -return ActivatedCarbonFilterMonitoringServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua b/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua deleted file mode 100644 index 040ce653b4..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua +++ /dev/null @@ -1,91 +0,0 @@ -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local ResetCondition = {} - -ResetCondition.NAME = "ResetCondition" -ResetCondition.ID = 0x0000 -ResetCondition.field_defs = { -} - -function ResetCondition:build_test_command_response(device, endpoint_id, status) - return self._cluster:build_test_command_response( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil, - status - ) -end - -function ResetCondition:init(device, endpoint_id) - local out = {} - local args = {} - if #args > #self.field_defs then - error(self.NAME .. " received too many arguments") - end - for i,v in ipairs(self.field_defs) do - if v.is_optional and args[i] == nil then - out[v.name] = nil - elseif v.is_nullable and args[i] == nil then - out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) - out[v.name].field_id = v.field_id - elseif not v.is_optional and args[i] == nil then - out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) - out[v.name].field_id = v.field_id - else - out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) - out[v.name].field_id = v.field_id - end - end - setmetatable(out, { - __index = ResetCondition, - __tostring = ResetCondition.pretty_print - }) - return self._cluster:build_cluster_command( - device, - out, - endpoint_id, - self._cluster.ID, - self.ID - ) -end - -function ResetCondition:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function ResetCondition:augment_type(base_type_obj) - local elems = {} - for _, v in ipairs(base_type_obj.elements) do - for _, field_def in ipairs(self.field_defs) do - if field_def.field_id == v.field_id and - field_def.is_nullable and - (v.value == nil and v.elements == nil) then - elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) - elseif field_def.field_id == v.field_id and not - (field_def.is_optional and v.value == nil) then - elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) - if field_def.element_type ~= nil then - for i, e in ipairs(elems[field_def.name].elements) do - elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) - end - end - end - end - end - base_type_obj.elements = elems -end - -function ResetCondition:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(ResetCondition, {__call = ResetCondition.init}) - -return ResetCondition - diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/init.lua b/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/init.lua deleted file mode 100644 index 7f641481d4..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/init.lua +++ /dev/null @@ -1,23 +0,0 @@ -local command_mt = {} -command_mt.__command_cache = {} -command_mt.__index = function(self, key) - if command_mt.__command_cache[key] == nil then - local req_loc = string.format("ActivatedCarbonFilterMonitoring.server.commands.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) - end - return command_mt.__command_cache[key] -end - -local ActivatedCarbonFilterMonitoringServerCommands = {} - -function ActivatedCarbonFilterMonitoringServerCommands:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(ActivatedCarbonFilterMonitoringServerCommands, command_mt) - -return ActivatedCarbonFilterMonitoringServerCommands - diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua b/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua deleted file mode 100644 index 438de24c94..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua +++ /dev/null @@ -1,33 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local ChangeIndicationEnum = {} --- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility --- with how types were handled in api < 10. -local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) -new_mt.__index.pretty_print = function(self) - local name_lookup = { - [self.OK] = "OK", - [self.WARNING] = "WARNING", - [self.CRITICAL] = "CRITICAL", - } - return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) -end -new_mt.__tostring = new_mt.__index.pretty_print - -new_mt.__index.OK = 0x00 -new_mt.__index.WARNING = 0x01 -new_mt.__index.CRITICAL = 0x02 - -ChangeIndicationEnum.OK = 0x00 -ChangeIndicationEnum.WARNING = 0x01 -ChangeIndicationEnum.CRITICAL = 0x02 - -ChangeIndicationEnum.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(ChangeIndicationEnum, new_mt) - -return ChangeIndicationEnum - diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/Feature.lua deleted file mode 100644 index 88474d1b0f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/Feature.lua +++ /dev/null @@ -1,98 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.CONDITION = 0x0001 -Feature.WARNING = 0x0002 -Feature.REPLACEMENT_PRODUCT_LIST = 0x0004 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - CONDITION = 0x0001, - WARNING = 0x0002, - REPLACEMENT_PRODUCT_LIST = 0x0004, -} - -Feature.is_condition_set = function(self) - return (self.value & self.CONDITION) ~= 0 -end - -Feature.set_condition = function(self) - if self.value ~= nil then - self.value = self.value | self.CONDITION - else - self.value = self.CONDITION - end -end - -Feature.unset_condition = function(self) - self.value = self.value & (~self.CONDITION & self.BASE_MASK) -end - -Feature.is_warning_set = function(self) - return (self.value & self.WARNING) ~= 0 -end - -Feature.set_warning = function(self) - if self.value ~= nil then - self.value = self.value | self.WARNING - else - self.value = self.WARNING - end -end - -Feature.unset_warning = function(self) - self.value = self.value & (~self.WARNING & self.BASE_MASK) -end - -Feature.is_replacement_product_list_set = function(self) - return (self.value & self.REPLACEMENT_PRODUCT_LIST) ~= 0 -end - -Feature.set_replacement_product_list = function(self) - if self.value ~= nil then - self.value = self.value | self.REPLACEMENT_PRODUCT_LIST - else - self.value = self.REPLACEMENT_PRODUCT_LIST - end -end - -Feature.unset_replacement_product_list = function(self) - self.value = self.value & (~self.REPLACEMENT_PRODUCT_LIST & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.CONDITION | - Feature.WARNING | - Feature.REPLACEMENT_PRODUCT_LIST - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_condition_set = Feature.is_condition_set, - set_condition = Feature.set_condition, - unset_condition = Feature.unset_condition, - is_warning_set = Feature.is_warning_set, - set_warning = Feature.set_warning, - unset_warning = Feature.unset_warning, - is_replacement_product_list_set = Feature.is_replacement_product_list_set, - set_replacement_product_list = Feature.set_replacement_product_list, - unset_replacement_product_list = Feature.unset_replacement_product_list, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/init.lua b/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/init.lua deleted file mode 100644 index 2ff8e6e89a..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/init.lua +++ /dev/null @@ -1,15 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("ActivatedCarbonFilterMonitoring.types." .. key) - end - return types_mt.__types_cache[key] -end - -local ActivatedCarbonFilterMonitoringTypes = {} - -setmetatable(ActivatedCarbonFilterMonitoringTypes, types_mt) - -return ActivatedCarbonFilterMonitoringTypes - diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/init.lua b/drivers/SmartThings/matter-thermostat/src/AirQuality/init.lua deleted file mode 100644 index 78e77fce51..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/init.lua +++ /dev/null @@ -1,59 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local AirQualityServerAttributes = require "AirQuality.server.attributes" -local AirQualityTypes = require "AirQuality.types" - -local AirQuality = {} - -AirQuality.ID = 0x005B -AirQuality.NAME = "AirQuality" -AirQuality.server = {} -AirQuality.client = {} -AirQuality.server.attributes = AirQualityServerAttributes:set_parent_cluster(AirQuality) -AirQuality.types = AirQualityTypes - -function AirQuality:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "AirQuality", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - --- Attribute Mapping -AirQuality.attribute_direction_map = { - ["AirQuality"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -AirQuality.FeatureMap = AirQuality.types.Feature - -function AirQuality.are_features_supported(feature, feature_map) - if (AirQuality.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = AirQuality.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, AirQuality.NAME)) - end - return AirQuality[direction].attributes[key] -end -AirQuality.attributes = {} -setmetatable(AirQuality.attributes, attribute_helper_mt) - -setmetatable(AirQuality, {__index = cluster_base}) - -return AirQuality - diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AcceptedCommandList.lua deleted file mode 100644 index 6a8d95df1d..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AcceptedCommandList.lua +++ /dev/null @@ -1,75 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local AcceptedCommandList = { - ID = 0xFFF9, - NAME = "AcceptedCommandList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - -function AcceptedCommandList:augment_type(data_type_obj) - for i, v in ipairs(data_type_obj.elements) do - data_type_obj.elements[i] = data_types.validate_or_build_type(v, AcceptedCommandList.element_type) - end -end - -function AcceptedCommandList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function AcceptedCommandList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AcceptedCommandList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AcceptedCommandList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function AcceptedCommandList:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function AcceptedCommandList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) -return AcceptedCommandList - diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AirQuality.lua b/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AirQuality.lua deleted file mode 100644 index f92f43543a..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AirQuality.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local AirQuality = { - ID = 0x0000, - NAME = "AirQuality", - base_type = require "AirQuality.types.AirQualityEnum", -} - -function AirQuality:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function AirQuality:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AirQuality:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AirQuality:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function AirQuality:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function AirQuality:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(AirQuality, {__call = AirQuality.new_value, __index = AirQuality.base_type}) -return AirQuality - diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AttributeList.lua deleted file mode 100644 index 93e96817e6..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AttributeList.lua +++ /dev/null @@ -1,75 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local AttributeList = { - ID = 0xFFFB, - NAME = "AttributeList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - -function AttributeList:augment_type(data_type_obj) - for i, v in ipairs(data_type_obj.elements) do - data_type_obj.elements[i] = data_types.validate_or_build_type(v, AttributeList.element_type) - end -end - -function AttributeList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function AttributeList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AttributeList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AttributeList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function AttributeList:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function AttributeList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) -return AttributeList - diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/EventList.lua b/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/EventList.lua deleted file mode 100644 index 69155cd7ca..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/EventList.lua +++ /dev/null @@ -1,75 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local EventList = { - ID = 0xFFFA, - NAME = "EventList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - -function EventList:augment_type(data_type_obj) - for i, v in ipairs(data_type_obj.elements) do - data_type_obj.elements[i] = data_types.validate_or_build_type(v, EventList.element_type) - end -end - -function EventList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function EventList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function EventList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function EventList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function EventList:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function EventList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(EventList, {__call = EventList.new_value, __index = EventList.base_type}) -return EventList - diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/init.lua deleted file mode 100644 index aef3b476a9..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("AirQuality.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local AirQualityServerAttributes = {} - -function AirQualityServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(AirQualityServerAttributes, attr_mt) - -return AirQualityServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/AirQualityEnum.lua b/drivers/SmartThings/matter-thermostat/src/AirQuality/types/AirQualityEnum.lua deleted file mode 100644 index 317a42dc9b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/AirQualityEnum.lua +++ /dev/null @@ -1,45 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local AirQualityEnum = {} --- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility --- with how types were handled in api < 10. -local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) -new_mt.__index.pretty_print = function(self) - local name_lookup = { - [self.UNKNOWN] = "UNKNOWN", - [self.GOOD] = "GOOD", - [self.FAIR] = "FAIR", - [self.MODERATE] = "MODERATE", - [self.POOR] = "POOR", - [self.VERY_POOR] = "VERY_POOR", - [self.EXTREMELY_POOR] = "EXTREMELY_POOR", - } - return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) -end -new_mt.__tostring = new_mt.__index.pretty_print - -new_mt.__index.UNKNOWN = 0x00 -new_mt.__index.GOOD = 0x01 -new_mt.__index.FAIR = 0x02 -new_mt.__index.MODERATE = 0x03 -new_mt.__index.POOR = 0x04 -new_mt.__index.VERY_POOR = 0x05 -new_mt.__index.EXTREMELY_POOR = 0x06 - -AirQualityEnum.UNKNOWN = 0x00 -AirQualityEnum.GOOD = 0x01 -AirQualityEnum.FAIR = 0x02 -AirQualityEnum.MODERATE = 0x03 -AirQualityEnum.POOR = 0x04 -AirQualityEnum.VERY_POOR = 0x05 -AirQualityEnum.EXTREMELY_POOR = 0x06 - -AirQualityEnum.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(AirQualityEnum, new_mt) - -return AirQualityEnum - diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/AirQuality/types/Feature.lua deleted file mode 100644 index 86b90ce627..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/Feature.lua +++ /dev/null @@ -1,120 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.FAIR = 0x0001 -Feature.MODERATE = 0x0002 -Feature.VERY_POOR = 0x0004 -Feature.EXTREMELY_POOR = 0x0008 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - FAIR = 0x0001, - MODERATE = 0x0002, - VERY_POOR = 0x0004, - EXTREMELY_POOR = 0x0008, -} - -Feature.is_fair_set = function(self) - return (self.value & self.FAIR) ~= 0 -end - -Feature.set_fair = function(self) - if self.value ~= nil then - self.value = self.value | self.FAIR - else - self.value = self.FAIR - end -end - -Feature.unset_fair = function(self) - self.value = self.value & (~self.FAIR & self.BASE_MASK) -end - -Feature.is_moderate_set = function(self) - return (self.value & self.MODERATE) ~= 0 -end - -Feature.set_moderate = function(self) - if self.value ~= nil then - self.value = self.value | self.MODERATE - else - self.value = self.MODERATE - end -end - -Feature.unset_moderate = function(self) - self.value = self.value & (~self.MODERATE & self.BASE_MASK) -end - -Feature.is_very_poor_set = function(self) - return (self.value & self.VERY_POOR) ~= 0 -end - -Feature.set_very_poor = function(self) - if self.value ~= nil then - self.value = self.value | self.VERY_POOR - else - self.value = self.VERY_POOR - end -end - -Feature.unset_very_poor = function(self) - self.value = self.value & (~self.VERY_POOR & self.BASE_MASK) -end - -Feature.is_extremely_poor_set = function(self) - return (self.value & self.EXTREMELY_POOR) ~= 0 -end - -Feature.set_extremely_poor = function(self) - if self.value ~= nil then - self.value = self.value | self.EXTREMELY_POOR - else - self.value = self.EXTREMELY_POOR - end -end - -Feature.unset_extremely_poor = function(self) - self.value = self.value & (~self.EXTREMELY_POOR & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.FAIR | - Feature.MODERATE | - Feature.VERY_POOR | - Feature.EXTREMELY_POOR - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_fair_set = Feature.is_fair_set, - set_fair = Feature.set_fair, - unset_fair = Feature.unset_fair, - is_moderate_set = Feature.is_moderate_set, - set_moderate = Feature.set_moderate, - unset_moderate = Feature.unset_moderate, - is_very_poor_set = Feature.is_very_poor_set, - set_very_poor = Feature.set_very_poor, - unset_very_poor = Feature.unset_very_poor, - is_extremely_poor_set = Feature.is_extremely_poor_set, - set_extremely_poor = Feature.set_extremely_poor, - unset_extremely_poor = Feature.unset_extremely_poor, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/init.lua b/drivers/SmartThings/matter-thermostat/src/AirQuality/types/init.lua deleted file mode 100644 index 88a2b861b7..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/init.lua +++ /dev/null @@ -1,15 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("AirQuality.types." .. key) - end - return types_mt.__types_cache[key] -end - -local AirQualityTypes = {} - -setmetatable(AirQualityTypes, types_mt) - -return AirQualityTypes - diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/init.lua deleted file mode 100644 index f5109f8943..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local CarbonDioxideConcentrationMeasurementServerAttributes = require "CarbonDioxideConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local CarbonDioxideConcentrationMeasurement = {} - -CarbonDioxideConcentrationMeasurement.ID = 0x040D -CarbonDioxideConcentrationMeasurement.NAME = "CarbonDioxideConcentrationMeasurement" -CarbonDioxideConcentrationMeasurement.server = {} -CarbonDioxideConcentrationMeasurement.client = {} -CarbonDioxideConcentrationMeasurement.server.attributes = CarbonDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(CarbonDioxideConcentrationMeasurement) -CarbonDioxideConcentrationMeasurement.types = ConcentrationMeasurement.types - -function CarbonDioxideConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function CarbonDioxideConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -CarbonDioxideConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -CarbonDioxideConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function CarbonDioxideConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = CarbonDioxideConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, CarbonDioxideConcentrationMeasurement.NAME)) - end - return CarbonDioxideConcentrationMeasurement[direction].attributes[key] -end -CarbonDioxideConcentrationMeasurement.attributes = {} -setmetatable(CarbonDioxideConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(CarbonDioxideConcentrationMeasurement, {__index = cluster_base}) - -return CarbonDioxideConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index 93583c2080..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("CarbonDioxideConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local CarbonDioxideConcentrationMeasurementServerAttributes = {} - -function CarbonDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(CarbonDioxideConcentrationMeasurementServerAttributes, attr_mt) - -return CarbonDioxideConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/init.lua deleted file mode 100644 index e8cdb487f5..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local CarbonMonoxideConcentrationMeasurementServerAttributes = require "CarbonMonoxideConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local CarbonMonoxideConcentrationMeasurement = {} - -CarbonMonoxideConcentrationMeasurement.ID = 0x040C -CarbonMonoxideConcentrationMeasurement.NAME = "CarbonMonoxideConcentrationMeasurement" -CarbonMonoxideConcentrationMeasurement.server = {} -CarbonMonoxideConcentrationMeasurement.client = {} -CarbonMonoxideConcentrationMeasurement.server.attributes = CarbonMonoxideConcentrationMeasurementServerAttributes:set_parent_cluster(CarbonMonoxideConcentrationMeasurement) -CarbonMonoxideConcentrationMeasurement.types = ConcentrationMeasurement.types - -function CarbonMonoxideConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function CarbonMonoxideConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -CarbonMonoxideConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -CarbonMonoxideConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function CarbonMonoxideConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = CarbonMonoxideConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, CarbonMonoxideConcentrationMeasurement.NAME)) - end - return CarbonMonoxideConcentrationMeasurement[direction].attributes[key] -end -CarbonMonoxideConcentrationMeasurement.attributes = {} -setmetatable(CarbonMonoxideConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(CarbonMonoxideConcentrationMeasurement, {__index = cluster_base}) - -return CarbonMonoxideConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index 2307e8977d..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("CarbonMonoxideConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local CarbonMonoxideConcentrationMeasurementServerAttributes = {} - -function CarbonMonoxideConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(CarbonMonoxideConcentrationMeasurementServerAttributes, attr_mt) - -return CarbonMonoxideConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/init.lua deleted file mode 100644 index 596d1bfe80..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/init.lua +++ /dev/null @@ -1,108 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local ConcentrationMeasurementServerAttributes = require "ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurementTypes = require "ConcentrationMeasurement.types" - -local ConcentrationMeasurement = {} - -ConcentrationMeasurement.ID = 0x040C -ConcentrationMeasurement.NAME = "CarbonMonoxideConcentrationMeasurement" -ConcentrationMeasurement.server = {} -ConcentrationMeasurement.client = {} -ConcentrationMeasurement.server.attributes = ConcentrationMeasurementServerAttributes:set_parent_cluster(ConcentrationMeasurement) -ConcentrationMeasurement.types = ConcentrationMeasurementTypes - -function ConcentrationMeasurement:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "MeasuredValue", - [0x0001] = "MinMeasuredValue", - [0x0002] = "MaxMeasuredValue", - [0x0003] = "PeakMeasuredValue", - [0x0004] = "PeakMeasuredValueWindow", - [0x0005] = "AverageMeasuredValue", - [0x0006] = "AverageMeasuredValueWindow", - [0x0007] = "Uncertainty", - [0x0008] = "MeasurementUnit", - [0x0009] = "MeasurementMedium", - [0x000A] = "LevelValue", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - -function ConcentrationMeasurement:get_server_command_by_id(command_id) - local server_id_map = { - } - if server_id_map[command_id] ~= nil then - return self.server.commands[server_id_map[command_id]] - end - return nil -end - -ConcentrationMeasurement.attribute_direction_map = { - ["MeasuredValue"] = "server", - ["MinMeasuredValue"] = "server", - ["MaxMeasuredValue"] = "server", - ["PeakMeasuredValue"] = "server", - ["PeakMeasuredValueWindow"] = "server", - ["AverageMeasuredValue"] = "server", - ["AverageMeasuredValueWindow"] = "server", - ["Uncertainty"] = "server", - ["MeasurementUnit"] = "server", - ["MeasurementMedium"] = "server", - ["LevelValue"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -ConcentrationMeasurement.command_direction_map = { -} - -ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function ConcentrationMeasurement.are_features_supported(feature, feature_map) - if (ConcentrationMeasurement.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = ConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, ConcentrationMeasurement.NAME)) - end - return ConcentrationMeasurement[direction].attributes[key] -end -ConcentrationMeasurement.attributes = {} -setmetatable(ConcentrationMeasurement.attributes, attribute_helper_mt) - -local command_helper_mt = {} -command_helper_mt.__index = function(self, key) - local direction = ConcentrationMeasurement.command_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown command %s on cluster %s", key, ConcentrationMeasurement.NAME)) - end - return ConcentrationMeasurement[direction].commands[key] -end -ConcentrationMeasurement.commands = {} -setmetatable(ConcentrationMeasurement.commands, command_helper_mt) - -local event_helper_mt = {} -event_helper_mt.__index = function(self, key) - return ConcentrationMeasurement.server.events[key] -end -ConcentrationMeasurement.events = {} -setmetatable(ConcentrationMeasurement.events, event_helper_mt) - -setmetatable(ConcentrationMeasurement, {__index = cluster_base}) - -return ConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index e7023a336c..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,70 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function LevelValue:read(device, endpoint_id, cluster_id) - return cluster_base.read( - device, - endpoint_id, - cluster_id, - self.ID, - nil --event_id - ) -end - - -function LevelValue:subscribe(device, endpoint_id, cluster_id) - return cluster_base.subscribe( - device, - endpoint_id, - cluster_id, - self.ID, - nil --event_id - ) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status, - cluster_id -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - cluster_id, - self.ID, - data, - status - ) -end - -function LevelValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index c658d2d3aa..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,70 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function MeasuredValue:read(device, endpoint_id, cluster_id) - return cluster_base.read( - device, - endpoint_id, - cluster_id, - self.ID, - nil --event_id - ) -end - - -function MeasuredValue:subscribe(device, endpoint_id, cluster_id) - return cluster_base.subscribe( - device, - endpoint_id, - cluster_id, - self.ID, - nil --event_id - ) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status, - cluster_id -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - cluster_id, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index 3d50e8b97b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,69 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function MeasurementUnit:read(device, endpoint_id, cluster_id) - return cluster_base.read( - device, - endpoint_id, - cluster_id, - self.ID, - nil --event_id - ) -end - -function MeasurementUnit:subscribe(device, endpoint_id, cluster_id) - return cluster_base.subscribe( - device, - endpoint_id, - cluster_id, - self.ID, - nil --event_id - ) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status, - cluster_id -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - cluster_id, - self.ID, - data, - status - ) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index a1a0092151..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("ConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local ConcentrationMeasurementServerAttributes = {} - -function ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(ConcentrationMeasurementServerAttributes, attr_mt) - -return ConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/Feature.lua deleted file mode 100644 index 9aa2413903..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/Feature.lua +++ /dev/null @@ -1,164 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.NUMERIC_MEASUREMENT = 0x0001 -Feature.LEVEL_INDICATION = 0x0002 -Feature.MEDIUM_LEVEL = 0x0004 -Feature.CRITICAL_LEVEL = 0x0008 -Feature.PEAK_MEASUREMENT = 0x0010 -Feature.AVERAGE_MEASUREMENT = 0x0020 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - NUMERIC_MEASUREMENT = 0x0001, - LEVEL_INDICATION = 0x0002, - MEDIUM_LEVEL = 0x0004, - CRITICAL_LEVEL = 0x0008, - PEAK_MEASUREMENT = 0x0010, - AVERAGE_MEASUREMENT = 0x0020, -} - -Feature.is_numeric_measurement_set = function(self) - return (self.value & self.NUMERIC_MEASUREMENT) ~= 0 -end - -Feature.set_numeric_measurement = function(self) - if self.value ~= nil then - self.value = self.value | self.NUMERIC_MEASUREMENT - else - self.value = self.NUMERIC_MEASUREMENT - end -end - -Feature.unset_numeric_measurement = function(self) - self.value = self.value & (~self.NUMERIC_MEASUREMENT & self.BASE_MASK) -end - -Feature.is_level_indication_set = function(self) - return (self.value & self.LEVEL_INDICATION) ~= 0 -end - -Feature.set_level_indication = function(self) - if self.value ~= nil then - self.value = self.value | self.LEVEL_INDICATION - else - self.value = self.LEVEL_INDICATION - end -end - -Feature.unset_level_indication = function(self) - self.value = self.value & (~self.LEVEL_INDICATION & self.BASE_MASK) -end - -Feature.is_medium_level_set = function(self) - return (self.value & self.MEDIUM_LEVEL) ~= 0 -end - -Feature.set_medium_level = function(self) - if self.value ~= nil then - self.value = self.value | self.MEDIUM_LEVEL - else - self.value = self.MEDIUM_LEVEL - end -end - -Feature.unset_medium_level = function(self) - self.value = self.value & (~self.MEDIUM_LEVEL & self.BASE_MASK) -end - -Feature.is_critical_level_set = function(self) - return (self.value & self.CRITICAL_LEVEL) ~= 0 -end - -Feature.set_critical_level = function(self) - if self.value ~= nil then - self.value = self.value | self.CRITICAL_LEVEL - else - self.value = self.CRITICAL_LEVEL - end -end - -Feature.unset_critical_level = function(self) - self.value = self.value & (~self.CRITICAL_LEVEL & self.BASE_MASK) -end - -Feature.is_peak_measurement_set = function(self) - return (self.value & self.PEAK_MEASUREMENT) ~= 0 -end - -Feature.set_peak_measurement = function(self) - if self.value ~= nil then - self.value = self.value | self.PEAK_MEASUREMENT - else - self.value = self.PEAK_MEASUREMENT - end -end - -Feature.unset_peak_measurement = function(self) - self.value = self.value & (~self.PEAK_MEASUREMENT & self.BASE_MASK) -end - -Feature.is_average_measurement_set = function(self) - return (self.value & self.AVERAGE_MEASUREMENT) ~= 0 -end - -Feature.set_average_measurement = function(self) - if self.value ~= nil then - self.value = self.value | self.AVERAGE_MEASUREMENT - else - self.value = self.AVERAGE_MEASUREMENT - end -end - -Feature.unset_average_measurement = function(self) - self.value = self.value & (~self.AVERAGE_MEASUREMENT & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.NUMERIC_MEASUREMENT | - Feature.LEVEL_INDICATION | - Feature.MEDIUM_LEVEL | - Feature.CRITICAL_LEVEL | - Feature.PEAK_MEASUREMENT | - Feature.AVERAGE_MEASUREMENT - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_numeric_measurement_set = Feature.is_numeric_measurement_set, - set_numeric_measurement = Feature.set_numeric_measurement, - unset_numeric_measurement = Feature.unset_numeric_measurement, - is_level_indication_set = Feature.is_level_indication_set, - set_level_indication = Feature.set_level_indication, - unset_level_indication = Feature.unset_level_indication, - is_medium_level_set = Feature.is_medium_level_set, - set_medium_level = Feature.set_medium_level, - unset_medium_level = Feature.unset_medium_level, - is_critical_level_set = Feature.is_critical_level_set, - set_critical_level = Feature.set_critical_level, - unset_critical_level = Feature.unset_critical_level, - is_peak_measurement_set = Feature.is_peak_measurement_set, - set_peak_measurement = Feature.set_peak_measurement, - unset_peak_measurement = Feature.unset_peak_measurement, - is_average_measurement_set = Feature.is_average_measurement_set, - set_average_measurement = Feature.set_average_measurement, - unset_average_measurement = Feature.unset_average_measurement, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/LevelValueEnum.lua b/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/LevelValueEnum.lua deleted file mode 100644 index b1264d72b8..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/LevelValueEnum.lua +++ /dev/null @@ -1,39 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local LevelValueEnum = {} --- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility --- with how types were handled in api < 10. -local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) -new_mt.__index.pretty_print = function(self) - local name_lookup = { - [self.UNKNOWN] = "UNKNOWN", - [self.LOW] = "LOW", - [self.MEDIUM] = "MEDIUM", - [self.HIGH] = "HIGH", - [self.CRITICAL] = "CRITICAL", - } - return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) -end -new_mt.__tostring = new_mt.__index.pretty_print - -new_mt.__index.UNKNOWN = 0x00 -new_mt.__index.LOW = 0x01 -new_mt.__index.MEDIUM = 0x02 -new_mt.__index.HIGH = 0x03 -new_mt.__index.CRITICAL = 0x04 - -LevelValueEnum.UNKNOWN = 0x00 -LevelValueEnum.LOW = 0x01 -LevelValueEnum.MEDIUM = 0x02 -LevelValueEnum.HIGH = 0x03 -LevelValueEnum.CRITICAL = 0x04 - -LevelValueEnum.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(LevelValueEnum, new_mt) - -return LevelValueEnum - diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/MeasurementUnitEnum.lua b/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/MeasurementUnitEnum.lua deleted file mode 100644 index c8302c5cc4..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/MeasurementUnitEnum.lua +++ /dev/null @@ -1,48 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local MeasurementUnitEnum = {} --- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility --- with how types were handled in api < 10. -local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) -new_mt.__index.pretty_print = function(self) - local name_lookup = { - [self.PPM] = "PPM", - [self.PPB] = "PPB", - [self.PPT] = "PPT", - [self.MGM3] = "MGM3", - [self.UGM3] = "UGM3", - [self.NGM3] = "NGM3", - [self.PM3] = "PM3", - [self.BQM3] = "BQM3", - } - return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) -end -new_mt.__tostring = new_mt.__index.pretty_print - -new_mt.__index.PPM = 0x00 -new_mt.__index.PPB = 0x01 -new_mt.__index.PPT = 0x02 -new_mt.__index.MGM3 = 0x03 -new_mt.__index.UGM3 = 0x04 -new_mt.__index.NGM3 = 0x05 -new_mt.__index.PM3 = 0x06 -new_mt.__index.BQM3 = 0x07 - -MeasurementUnitEnum.PPM = 0x00 -MeasurementUnitEnum.PPB = 0x01 -MeasurementUnitEnum.PPT = 0x02 -MeasurementUnitEnum.MGM3 = 0x03 -MeasurementUnitEnum.UGM3 = 0x04 -MeasurementUnitEnum.NGM3 = 0x05 -MeasurementUnitEnum.PM3 = 0x06 -MeasurementUnitEnum.BQM3 = 0x07 - -MeasurementUnitEnum.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(MeasurementUnitEnum, new_mt) - -return MeasurementUnitEnum - diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/init.lua b/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/init.lua deleted file mode 100644 index f6da1e6b62..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/init.lua +++ /dev/null @@ -1,15 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("ConcentrationMeasurement.types." .. key) - end - return types_mt.__types_cache[key] -end - -local ConcentrationMeasurementTypes = {} - -setmetatable(ConcentrationMeasurementTypes, types_mt) - -return ConcentrationMeasurementTypes - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/init.lua deleted file mode 100644 index bc5685ae75..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/init.lua +++ /dev/null @@ -1,76 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local ElectricalEnergyMeasurementServerAttributes = require "ElectricalEnergyMeasurement.server.attributes" -local ElectricalEnergyMeasurementEvents = require "ElectricalEnergyMeasurement.server.events" -local ElectricalEnergyMeasurementTypes = require "ElectricalEnergyMeasurement.types" -local ElectricalEnergyMeasurement = {} - -ElectricalEnergyMeasurement.ID = 0x0091 -ElectricalEnergyMeasurement.NAME = "ElectricalEnergyMeasurement" -ElectricalEnergyMeasurement.server = {} -ElectricalEnergyMeasurement.client = {} -ElectricalEnergyMeasurement.server.attributes = ElectricalEnergyMeasurementServerAttributes:set_parent_cluster(ElectricalEnergyMeasurement) -ElectricalEnergyMeasurement.server.events = ElectricalEnergyMeasurementEvents:set_parent_cluster(ElectricalEnergyMeasurement) -ElectricalEnergyMeasurement.types = ElectricalEnergyMeasurementTypes - -function ElectricalEnergyMeasurement:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "Accuracy", - [0x0001] = "CumulativeEnergyImported", - [0x0002] = "CumulativeEnergyExported", - [0x0003] = "PeriodicEnergyImported", - [0x0004] = "PeriodicEnergyExported", - [0x0005] = "CumulativeEnergyReset", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - -ElectricalEnergyMeasurement.attribute_direction_map = { - ["Accuracy"] = "server", - ["CumulativeEnergyImported"] = "server", - ["CumulativeEnergyExported"] = "server", - ["PeriodicEnergyImported"] = "server", - ["PeriodicEnergyExported"] = "server", - ["CumulativeEnergyReset"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -ElectricalEnergyMeasurement.FeatureMap = ElectricalEnergyMeasurement.types.Feature - -function ElectricalEnergyMeasurement.are_features_supported(feature, feature_map) - if (ElectricalEnergyMeasurement.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = ElectricalEnergyMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalEnergyMeasurement.NAME)) - end - return ElectricalEnergyMeasurement[direction].attributes[key] -end -ElectricalEnergyMeasurement.attributes = {} -setmetatable(ElectricalEnergyMeasurement.attributes, attribute_helper_mt) - -local event_helper_mt = {} -event_helper_mt.__index = function(self, key) - return ElectricalEnergyMeasurement.server.events[key] -end -ElectricalEnergyMeasurement.events = {} -setmetatable(ElectricalEnergyMeasurement.events, event_helper_mt) - -setmetatable(ElectricalEnergyMeasurement, {__index = cluster_base}) - -return ElectricalEnergyMeasurement - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua deleted file mode 100644 index 22befec642..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local CumulativeEnergyExported = { - ID = 0x0002, - NAME = "CumulativeEnergyExported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", -} - -function CumulativeEnergyExported:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function CumulativeEnergyExported:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function CumulativeEnergyExported:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function CumulativeEnergyExported:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function CumulativeEnergyExported:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function CumulativeEnergyExported:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(CumulativeEnergyExported, {__call = CumulativeEnergyExported.new_value, __index = CumulativeEnergyExported.base_type}) -return CumulativeEnergyExported - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua deleted file mode 100644 index 3dc58635e1..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local CumulativeEnergyImported = { - ID = 0x0001, - NAME = "CumulativeEnergyImported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", -} - -function CumulativeEnergyImported:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function CumulativeEnergyImported:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function CumulativeEnergyImported:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function CumulativeEnergyImported:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function CumulativeEnergyImported:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function CumulativeEnergyImported:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(CumulativeEnergyImported, {__call = CumulativeEnergyImported.new_value, __index = CumulativeEnergyImported.base_type}) -return CumulativeEnergyImported - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua deleted file mode 100644 index 4c1ee29274..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local PeriodicEnergyExported = { - ID = 0x0004, - NAME = "PeriodicEnergyExported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", -} - -function PeriodicEnergyExported:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function PeriodicEnergyExported:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function PeriodicEnergyExported:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function PeriodicEnergyExported:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function PeriodicEnergyExported:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function PeriodicEnergyExported:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(PeriodicEnergyExported, {__call = PeriodicEnergyExported.new_value, __index = PeriodicEnergyExported.base_type}) -return PeriodicEnergyExported - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua deleted file mode 100644 index 753b91ea2d..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local PeriodicEnergyImported = { - ID = 0x0003, - NAME = "PeriodicEnergyImported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", -} - -function PeriodicEnergyImported:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function PeriodicEnergyImported:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function PeriodicEnergyImported:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function PeriodicEnergyImported:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function PeriodicEnergyImported:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function PeriodicEnergyImported:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(PeriodicEnergyImported, {__call = PeriodicEnergyImported.new_value, __index = PeriodicEnergyImported.base_type}) -return PeriodicEnergyImported - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/init.lua deleted file mode 100644 index adfdf42bbf..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("ElectricalEnergyMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local ElectricalEnergyMeasurementServerAttributes = {} - -function ElectricalEnergyMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(ElectricalEnergyMeasurementServerAttributes, attr_mt) - -return ElectricalEnergyMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/CumulativeEnergyMeasured.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/CumulativeEnergyMeasured.lua deleted file mode 100644 index 13136bd791..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/CumulativeEnergyMeasured.lua +++ /dev/null @@ -1,108 +0,0 @@ -local data_types = require "st.matter.data_types" -local cluster_base = require "st.matter.cluster_base" -local TLVParser = require "st.matter.TLV.TLVParser" -local StructureABC = require "st.matter.data_types.base_defs.StructureABC" - -local CumulativeEnergyMeasured = { - ID = 0x0000, - NAME = "CumulativeEnergyMeasured", - base_type = data_types.Structure, -} - -CumulativeEnergyMeasured.field_defs = { - { - name = "energy_imported", - field_id = 0, - is_nullable = false, - is_optional = true, - data_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", - }, - { - name = "energy_exported", - field_id = 1, - is_nullable = false, - is_optional = true, - data_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", - }, -} - -function CumulativeEnergyMeasured:augment_type(base_type_obj) - local elems = {} - for _, v in ipairs(base_type_obj.elements) do - for _, field_def in ipairs(self.field_defs) do - if field_def.field_id == v.field_id and not - ((field_def.is_nullable or field_def.is_optional) and v.elements == nil) then - elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) - if field_def.element_type ~= nil then - for i, e in ipairs(elems[field_def.name].elements) do - elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) - end - end - end - end - end - base_type_obj.elements = elems -end - -function CumulativeEnergyMeasured:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - nil, --attribute_id - self.ID - ) -end - -function CumulativeEnergyMeasured:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - nil, --attribute_id - self.ID - ) -end - -function CumulativeEnergyMeasured:build_test_event_report( - device, - endpoint_id, - fields, - status -) - local data = {} - data.elements = {} - data.num_elements = 0 - setmetatable(data, StructureABC.new_mt({NAME = "CumulativeEnergyMeasuredEventData", ID = 0x15})) - for idx, field_def in ipairs(self.field_defs) do --Note: idx is 1 when field_id is 0 - if (not field_def.is_optional and not field_def.is_nullable) and not fields[field_def.name] then - error("Missing non optional or non_nullable field: " .. field_def.name) - elseif fields[field_def.name] then - data.elements[field_def.name] = data_types.validate_or_build_type(fields[field_def.name], field_def.data_type, field_def.name) - data.elements[field_def.name].field_id = field_def.field_id - data.num_elements = data.num_elements + 1 - end - end - return cluster_base.build_test_event_report( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function CumulativeEnergyMeasured:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -function CumulativeEnergyMeasured:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -return CumulativeEnergyMeasured - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/PeriodicEnergyMeasured.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/PeriodicEnergyMeasured.lua deleted file mode 100644 index e2c4b1b577..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/PeriodicEnergyMeasured.lua +++ /dev/null @@ -1,108 +0,0 @@ -local data_types = require "st.matter.data_types" -local cluster_base = require "st.matter.cluster_base" -local TLVParser = require "st.matter.TLV.TLVParser" -local StructureABC = require "st.matter.data_types.base_defs.StructureABC" - -local PeriodicEnergyMeasured = { - ID = 0x0001, - NAME = "PeriodicEnergyMeasured", - base_type = data_types.Structure, -} - -PeriodicEnergyMeasured.field_defs = { - { - name = "energy_imported", - field_id = 0, - is_nullable = false, - is_optional = true, - data_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", - }, - { - name = "energy_exported", - field_id = 1, - is_nullable = false, - is_optional = true, - data_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", - }, -} - -function PeriodicEnergyMeasured:augment_type(base_type_obj) - local elems = {} - for _, v in ipairs(base_type_obj.elements) do - for _, field_def in ipairs(self.field_defs) do - if field_def.field_id == v.field_id and not - ((field_def.is_nullable or field_def.is_optional) and v.elements == nil) then - elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) - if field_def.element_type ~= nil then - for i, e in ipairs(elems[field_def.name].elements) do - elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) - end - end - end - end - end - base_type_obj.elements = elems -end - -function PeriodicEnergyMeasured:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - nil, --attribute_id - self.ID - ) -end - -function PeriodicEnergyMeasured:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - nil, --attribute_id - self.ID - ) -end - -function PeriodicEnergyMeasured:build_test_event_report( - device, - endpoint_id, - fields, - status -) - local data = {} - data.elements = {} - data.num_elements = 0 - setmetatable(data, StructureABC.new_mt({NAME = "PeriodicEnergyMeasuredEventData", ID = 0x15})) - for idx, field_def in ipairs(self.field_defs) do --Note: idx is 1 when field_id is 0 - if (not field_def.is_optional and not field_def.is_nullable) and not fields[field_def.name] then - error("Missing non optional or non_nullable field: " .. field_def.name) - elseif fields[field_def.name] then - data.elements[field_def.name] = data_types.validate_or_build_type(fields[field_def.name], field_def.data_type, field_def.name) - data.elements[field_def.name].field_id = field_def.field_id - data.num_elements = data.num_elements + 1 - end - end - return cluster_base.build_test_event_report( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function PeriodicEnergyMeasured:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -function PeriodicEnergyMeasured:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -return PeriodicEnergyMeasured - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/init.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/init.lua deleted file mode 100644 index 02b085583e..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/init.lua +++ /dev/null @@ -1,25 +0,0 @@ -local event_mt = {} -event_mt.__event_cache = {} -event_mt.__index = function(self, key) - if event_mt.__event_cache[key] == nil then - local req_loc = string.format("ElectricalEnergyMeasurement.server.events.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - event_mt.__event_cache[key] = raw_def - end - return event_mt.__event_cache[key] -end - - -local ElectricalEnergyMeasurementEvents = {} - -function ElectricalEnergyMeasurementEvents:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(ElectricalEnergyMeasurementEvents, event_mt) - -return ElectricalEnergyMeasurementEvents - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua deleted file mode 100644 index 950b260227..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua +++ /dev/null @@ -1,98 +0,0 @@ -local data_types = require "st.matter.data_types" -local StructureABC = require "st.matter.data_types.base_defs.StructureABC" -local EnergyMeasurementStruct = {} -local new_mt = StructureABC.new_mt({NAME = "EnergyMeasurementStruct", ID = data_types.name_to_id_map["Structure"]}) - -EnergyMeasurementStruct.field_defs = { - { - name = "energy", - field_id = 0, - is_nullable = false, - is_optional = false, - data_type = require "st.matter.data_types.Int64", - }, - { - name = "start_timestamp", - field_id = 1, - is_nullable = false, - is_optional = true, - data_type = require "st.matter.data_types.Uint32", - }, - { - name = "end_timestamp", - field_id = 2, - is_nullable = false, - is_optional = true, - data_type = require "st.matter.data_types.Uint32", - }, - { - name = "start_systime", - field_id = 3, - is_nullable = false, - is_optional = true, - data_type = require "st.matter.data_types.Uint64", - }, - { - name = "end_systime", - field_id = 4, - is_nullable = false, - is_optional = true, - data_type = require "st.matter.data_types.Uint64", - }, -} - -EnergyMeasurementStruct.init = function(cls, tbl) - local o = {} - o.elements = {} - o.num_elements = 0 - setmetatable(o, new_mt) - for idx, field_def in ipairs(cls.field_defs) do - if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then - error("Missing non optional or non_nullable field: " .. field_def.name) - else - o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) - o.elements[field_def.name].field_id = field_def.field_id - o.num_elements = o.num_elements + 1 - end - end - return o -end - -EnergyMeasurementStruct.serialize = function(self, buf, include_control, tag) - return data_types['Structure'].serialize(self.elements, buf, include_control, tag) -end - -new_mt.__call = EnergyMeasurementStruct.init -new_mt.__index.serialize = EnergyMeasurementStruct.serialize - -EnergyMeasurementStruct.augment_type = function(self, val) - local elems = {} - local num_elements = 0 - for _, v in pairs(val.elements) do - for _, field_def in ipairs(self.field_defs) do - if field_def.field_id == v.field_id and - field_def.is_nullable and - (v.value == nil and v.elements == nil) then - elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) - num_elements = num_elements + 1 - elseif field_def.field_id == v.field_id and not - (field_def.is_optional and v.value == nil) then - elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) - num_elements = num_elements + 1 - if field_def.element_type ~= nil then - for i, e in ipairs(elems[field_def.name].elements) do - elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) - end - end - end - end - end - val.elements = elems - val.num_elements = num_elements - setmetatable(val, new_mt) -end - -setmetatable(EnergyMeasurementStruct, new_mt) - -return EnergyMeasurementStruct - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/Feature.lua deleted file mode 100644 index 717ba6a2f3..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/Feature.lua +++ /dev/null @@ -1,116 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.IMPORTED_ENERGY = 0x0001 -Feature.EXPORTED_ENERGY = 0x0002 -Feature.CUMULATIVE_ENERGY = 0x0004 -Feature.PERIODIC_ENERGY = 0x0008 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - IMPORTED_ENERGY = 0x0001, - EXPORTED_ENERGY = 0x0002, - CUMULATIVE_ENERGY = 0x0004, - PERIODIC_ENERGY = 0x0008, -} - -Feature.is_imported_energy_set = function(self) - return (self.value & self.IMPORTED_ENERGY) ~= 0 -end - -Feature.set_imported_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.IMPORTED_ENERGY - else - self.value = self.IMPORTED_ENERGY - end -end - -Feature.unset_imported_energy = function(self) - self.value = self.value & (~self.IMPORTED_ENERGY & self.BASE_MASK) -end -Feature.is_exported_energy_set = function(self) - return (self.value & self.EXPORTED_ENERGY) ~= 0 -end - -Feature.set_exported_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.EXPORTED_ENERGY - else - self.value = self.EXPORTED_ENERGY - end -end - -Feature.unset_exported_energy = function(self) - self.value = self.value & (~self.EXPORTED_ENERGY & self.BASE_MASK) -end -Feature.is_cumulative_energy_set = function(self) - return (self.value & self.CUMULATIVE_ENERGY) ~= 0 -end - -Feature.set_cumulative_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.CUMULATIVE_ENERGY - else - self.value = self.CUMULATIVE_ENERGY - end -end - -Feature.unset_cumulative_energy = function(self) - self.value = self.value & (~self.CUMULATIVE_ENERGY & self.BASE_MASK) -end -Feature.is_periodic_energy_set = function(self) - return (self.value & self.PERIODIC_ENERGY) ~= 0 -end - -Feature.set_periodic_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.PERIODIC_ENERGY - else - self.value = self.PERIODIC_ENERGY - end -end - -Feature.unset_periodic_energy = function(self) - self.value = self.value & (~self.PERIODIC_ENERGY & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.IMPORTED_ENERGY | - Feature.EXPORTED_ENERGY | - Feature.CUMULATIVE_ENERGY | - Feature.PERIODIC_ENERGY - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_imported_energy_set = Feature.is_imported_energy_set, - set_imported_energy = Feature.set_imported_energy, - unset_imported_energy = Feature.unset_imported_energy, - is_exported_energy_set = Feature.is_exported_energy_set, - set_exported_energy = Feature.set_exported_energy, - unset_exported_energy = Feature.unset_exported_energy, - is_cumulative_energy_set = Feature.is_cumulative_energy_set, - set_cumulative_energy = Feature.set_cumulative_energy, - unset_cumulative_energy = Feature.unset_cumulative_energy, - is_periodic_energy_set = Feature.is_periodic_energy_set, - set_periodic_energy = Feature.set_periodic_energy, - unset_periodic_energy = Feature.unset_periodic_energy, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/init.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/init.lua deleted file mode 100644 index bb0c39fe0e..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/init.lua +++ /dev/null @@ -1,15 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("ElectricalEnergyMeasurement.types." .. key) - end - return types_mt.__types_cache[key] -end - -local ElectricalEnergyMeasurementTypes = {} - -setmetatable(ElectricalEnergyMeasurementTypes, types_mt) - -return ElectricalEnergyMeasurementTypes - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/init.lua deleted file mode 100644 index 54785d16c6..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/init.lua +++ /dev/null @@ -1,94 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local ElectricalPowerMeasurementServerAttributes = require "ElectricalPowerMeasurement.server.attributes" -local ElectricalPowerMeasurementTypes = require "ElectricalPowerMeasurement.types" - -local ElectricalPowerMeasurement = {} - -ElectricalPowerMeasurement.ID = 0x0090 -ElectricalPowerMeasurement.NAME = "ElectricalPowerMeasurement" -ElectricalPowerMeasurement.server = {} -ElectricalPowerMeasurement.client = {} -ElectricalPowerMeasurement.server.attributes = ElectricalPowerMeasurementServerAttributes:set_parent_cluster(ElectricalPowerMeasurement) -ElectricalPowerMeasurement.types = ElectricalPowerMeasurementTypes - -function ElectricalPowerMeasurement:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "PowerMode", - [0x0001] = "NumberOfMeasurementTypes", - [0x0002] = "Accuracy", - [0x0003] = "Ranges", - [0x0004] = "Voltage", - [0x0005] = "ActiveCurrent", - [0x0006] = "ReactiveCurrent", - [0x0007] = "ApparentCurrent", - [0x0008] = "ActivePower", - [0x0009] = "ReactivePower", - [0x000A] = "ApparentPower", - [0x000B] = "RMSVoltage", - [0x000C] = "RMSCurrent", - [0x000D] = "RMSPower", - [0x000E] = "Frequency", - [0x000F] = "HarmonicCurrents", - [0x0010] = "HarmonicPhases", - [0x0011] = "PowerFactor", - [0x0012] = "NeutralCurrent", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - -ElectricalPowerMeasurement.attribute_direction_map = { - ["PowerMode"] = "server", - ["NumberOfMeasurementTypes"] = "server", - ["Accuracy"] = "server", - ["Ranges"] = "server", - ["Voltage"] = "server", - ["ActiveCurrent"] = "server", - ["ReactiveCurrent"] = "server", - ["ApparentCurrent"] = "server", - ["ActivePower"] = "server", - ["ReactivePower"] = "server", - ["ApparentPower"] = "server", - ["RMSVoltage"] = "server", - ["RMSCurrent"] = "server", - ["RMSPower"] = "server", - ["Frequency"] = "server", - ["HarmonicCurrents"] = "server", - ["HarmonicPhases"] = "server", - ["PowerFactor"] = "server", - ["NeutralCurrent"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -ElectricalPowerMeasurement.FeatureMap = ElectricalPowerMeasurement.types.Feature - -function ElectricalPowerMeasurement.are_features_supported(feature, feature_map) - if (ElectricalPowerMeasurement.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = ElectricalPowerMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalPowerMeasurement.NAME)) - end - return ElectricalPowerMeasurement[direction].attributes[key] -end -ElectricalPowerMeasurement.attributes = {} -setmetatable(ElectricalPowerMeasurement.attributes, attribute_helper_mt) - -setmetatable(ElectricalPowerMeasurement, {__index = cluster_base}) - -return ElectricalPowerMeasurement - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua deleted file mode 100644 index 6c34abd2f4..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local ActivePower = { - ID = 0x0008, - NAME = "ActivePower", - base_type = require "st.matter.data_types.Int64", -} - -function ActivePower:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function ActivePower:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function ActivePower:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function ActivePower:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function ActivePower:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function ActivePower:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(ActivePower, {__call = ActivePower.new_value, __index = ActivePower.base_type}) -return ActivePower - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/init.lua deleted file mode 100644 index 0c30fa8dd4..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("ElectricalPowerMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local ElectricalPowerMeasurementServerAttributes = {} - -function ElectricalPowerMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(ElectricalPowerMeasurementServerAttributes, attr_mt) - -return ElectricalPowerMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/Feature.lua deleted file mode 100644 index cbda4f3478..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/Feature.lua +++ /dev/null @@ -1,138 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.DIRECT_CURRENT = 0x0001 -Feature.ALTERNATING_CURRENT = 0x0002 -Feature.POLYPHASE_POWER = 0x0004 -Feature.HARMONICS = 0x0008 -Feature.POWER_QUALITY = 0x0010 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - DIRECT_CURRENT = 0x0001, - ALTERNATING_CURRENT = 0x0002, - POLYPHASE_POWER = 0x0004, - HARMONICS = 0x0008, - POWER_QUALITY = 0x0010, -} - -Feature.is_direct_current_set = function(self) - return (self.value & self.DIRECT_CURRENT) ~= 0 -end - -Feature.set_direct_current = function(self) - if self.value ~= nil then - self.value = self.value | self.DIRECT_CURRENT - else - self.value = self.DIRECT_CURRENT - end -end - -Feature.unset_direct_current = function(self) - self.value = self.value & (~self.DIRECT_CURRENT & self.BASE_MASK) -end -Feature.is_alternating_current_set = function(self) - return (self.value & self.ALTERNATING_CURRENT) ~= 0 -end - -Feature.set_alternating_current = function(self) - if self.value ~= nil then - self.value = self.value | self.ALTERNATING_CURRENT - else - self.value = self.ALTERNATING_CURRENT - end -end - -Feature.unset_alternating_current = function(self) - self.value = self.value & (~self.ALTERNATING_CURRENT & self.BASE_MASK) -end -Feature.is_polyphase_power_set = function(self) - return (self.value & self.POLYPHASE_POWER) ~= 0 -end - -Feature.set_polyphase_power = function(self) - if self.value ~= nil then - self.value = self.value | self.POLYPHASE_POWER - else - self.value = self.POLYPHASE_POWER - end -end - -Feature.unset_polyphase_power = function(self) - self.value = self.value & (~self.POLYPHASE_POWER & self.BASE_MASK) -end -Feature.is_harmonics_set = function(self) - return (self.value & self.HARMONICS) ~= 0 -end - -Feature.set_harmonics = function(self) - if self.value ~= nil then - self.value = self.value | self.HARMONICS - else - self.value = self.HARMONICS - end -end - -Feature.unset_harmonics = function(self) - self.value = self.value & (~self.HARMONICS & self.BASE_MASK) -end -Feature.is_power_quality_set = function(self) - return (self.value & self.POWER_QUALITY) ~= 0 -end - -Feature.set_power_quality = function(self) - if self.value ~= nil then - self.value = self.value | self.POWER_QUALITY - else - self.value = self.POWER_QUALITY - end -end - -Feature.unset_power_quality = function(self) - self.value = self.value & (~self.POWER_QUALITY & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.DIRECT_CURRENT | - Feature.ALTERNATING_CURRENT | - Feature.POLYPHASE_POWER | - Feature.HARMONICS | - Feature.POWER_QUALITY - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_direct_current_set = Feature.is_direct_current_set, - set_direct_current = Feature.set_direct_current, - unset_direct_current = Feature.unset_direct_current, - is_alternating_current_set = Feature.is_alternating_current_set, - set_alternating_current = Feature.set_alternating_current, - unset_alternating_current = Feature.unset_alternating_current, - is_polyphase_power_set = Feature.is_polyphase_power_set, - set_polyphase_power = Feature.set_polyphase_power, - unset_polyphase_power = Feature.unset_polyphase_power, - is_harmonics_set = Feature.is_harmonics_set, - set_harmonics = Feature.set_harmonics, - unset_harmonics = Feature.unset_harmonics, - is_power_quality_set = Feature.is_power_quality_set, - set_power_quality = Feature.set_power_quality, - unset_power_quality = Feature.unset_power_quality, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/init.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/init.lua deleted file mode 100644 index 16d13a0688..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/init.lua +++ /dev/null @@ -1,15 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("ElectricalPowerMeasurement.types." .. key) - end - return types_mt.__types_cache[key] -end - -local ElectricalPowerMeasurementTypes = {} - -setmetatable(ElectricalPowerMeasurementTypes, types_mt) - -return ElectricalPowerMeasurementTypes - diff --git a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/init.lua deleted file mode 100644 index 5920a9dc66..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local FormaldehydeConcentrationMeasurementServerAttributes = require "FormaldehydeConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local FormaldehydeConcentrationMeasurement = {} - -FormaldehydeConcentrationMeasurement.ID = 0x042B -FormaldehydeConcentrationMeasurement.NAME = "FormaldehydeConcentrationMeasurement" -FormaldehydeConcentrationMeasurement.server = {} -FormaldehydeConcentrationMeasurement.client = {} -FormaldehydeConcentrationMeasurement.server.attributes = FormaldehydeConcentrationMeasurementServerAttributes:set_parent_cluster(FormaldehydeConcentrationMeasurement) -FormaldehydeConcentrationMeasurement.types = ConcentrationMeasurement.types - -function FormaldehydeConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function FormaldehydeConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -FormaldehydeConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -FormaldehydeConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function FormaldehydeConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = FormaldehydeConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, FormaldehydeConcentrationMeasurement.NAME)) - end - return FormaldehydeConcentrationMeasurement[direction].attributes[key] -end -FormaldehydeConcentrationMeasurement.attributes = {} -setmetatable(FormaldehydeConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(FormaldehydeConcentrationMeasurement, {__index = cluster_base}) - -return FormaldehydeConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index 37900b0fb1..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("FormaldehydeConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local FormaldehydeConcentrationMeasurementServerAttributes = {} - -function FormaldehydeConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(FormaldehydeConcentrationMeasurementServerAttributes, attr_mt) - -return FormaldehydeConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/init.lua b/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/init.lua deleted file mode 100644 index 84400d7833..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/init.lua +++ /dev/null @@ -1,102 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local HepaFilterMonitoringServerAttributes = require "HepaFilterMonitoring.server.attributes" -local HepaFilterMonitoringServerCommands = require "HepaFilterMonitoring.server.commands" -local HepaFilterMonitoringTypes = require "HepaFilterMonitoring.types" - -local HepaFilterMonitoring = {} - -HepaFilterMonitoring.ID = 0x0071 -HepaFilterMonitoring.NAME = "HepaFilterMonitoring" -HepaFilterMonitoring.server = {} -HepaFilterMonitoring.client = {} -HepaFilterMonitoring.server.attributes = HepaFilterMonitoringServerAttributes:set_parent_cluster(HepaFilterMonitoring) -HepaFilterMonitoring.server.commands = HepaFilterMonitoringServerCommands:set_parent_cluster(HepaFilterMonitoring) -HepaFilterMonitoring.types = HepaFilterMonitoringTypes - -function HepaFilterMonitoring:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "Condition", - [0x0001] = "DegradationDirection", - [0x0002] = "ChangeIndication", - [0x0003] = "InPlaceIndicator", - [0x0004] = "LastChangedTime", - [0x0005] = "ReplacementProductList", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - -function HepaFilterMonitoring:get_server_command_by_id(command_id) - local server_id_map = { - [0x0000] = "ResetCondition", - } - if server_id_map[command_id] ~= nil then - return self.server.commands[server_id_map[command_id]] - end - return nil -end - -HepaFilterMonitoring.attribute_direction_map = { - ["Condition"] = "server", - ["DegradationDirection"] = "server", - ["ChangeIndication"] = "server", - ["InPlaceIndicator"] = "server", - ["LastChangedTime"] = "server", - ["ReplacementProductList"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -HepaFilterMonitoring.command_direction_map = { - ["ResetCondition"] = "server", -} - -HepaFilterMonitoring.FeatureMap = HepaFilterMonitoring.types.Feature - -function HepaFilterMonitoring.are_features_supported(feature, feature_map) - if (HepaFilterMonitoring.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = HepaFilterMonitoring.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, HepaFilterMonitoring.NAME)) - end - return HepaFilterMonitoring[direction].attributes[key] -end -HepaFilterMonitoring.attributes = {} -setmetatable(HepaFilterMonitoring.attributes, attribute_helper_mt) - -local command_helper_mt = {} -command_helper_mt.__index = function(self, key) - local direction = HepaFilterMonitoring.command_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown command %s on cluster %s", key, HepaFilterMonitoring.NAME)) - end - return HepaFilterMonitoring[direction].commands[key] -end -HepaFilterMonitoring.commands = {} -setmetatable(HepaFilterMonitoring.commands, command_helper_mt) - -local event_helper_mt = {} -event_helper_mt.__index = function(self, key) - return HepaFilterMonitoring.server.events[key] -end -HepaFilterMonitoring.events = {} -setmetatable(HepaFilterMonitoring.events, event_helper_mt) - -setmetatable(HepaFilterMonitoring, {__index = cluster_base}) - -return HepaFilterMonitoring - diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/AttributeList.lua deleted file mode 100644 index f2ca149f03..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/AttributeList.lua +++ /dev/null @@ -1,75 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local AttributeList = { - ID = 0xFFFB, - NAME = "AttributeList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - -function AttributeList:augment_type(data_type_obj) - for i, v in ipairs(data_type_obj.elements) do - data_type_obj.elements[i] = data_types.validate_or_build_type(v, AttributeList.element_type) - end -end - -function AttributeList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function AttributeList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function AttributeList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function AttributeList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function AttributeList:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function AttributeList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) -return AttributeList - diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua b/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua deleted file mode 100644 index 955b89eb88..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local ChangeIndication = { - ID = 0x0002, - NAME = "ChangeIndication", - base_type = require "HepaFilterMonitoring.types.ChangeIndicationEnum", -} - -function ChangeIndication:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function ChangeIndication:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function ChangeIndication:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function ChangeIndication:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function ChangeIndication:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function ChangeIndication:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(ChangeIndication, {__call = ChangeIndication.new_value, __index = ChangeIndication.base_type}) -return ChangeIndication - diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/Condition.lua b/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/Condition.lua deleted file mode 100644 index e668aa4c48..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/Condition.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local Condition = { - ID = 0x0000, - NAME = "Condition", - base_type = require "st.matter.data_types.Uint8", -} - -function Condition:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function Condition:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function Condition:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function Condition:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function Condition:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function Condition:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(Condition, {__call = Condition.new_value, __index = Condition.base_type}) -return Condition - diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/init.lua deleted file mode 100644 index 8d7ffe6c00..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("HepaFilterMonitoring.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local HepaFilterMonitoringServerAttributes = {} - -function HepaFilterMonitoringServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(HepaFilterMonitoringServerAttributes, attr_mt) - -return HepaFilterMonitoringServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/ResetCondition.lua b/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/ResetCondition.lua deleted file mode 100644 index 040ce653b4..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/ResetCondition.lua +++ /dev/null @@ -1,91 +0,0 @@ -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local ResetCondition = {} - -ResetCondition.NAME = "ResetCondition" -ResetCondition.ID = 0x0000 -ResetCondition.field_defs = { -} - -function ResetCondition:build_test_command_response(device, endpoint_id, status) - return self._cluster:build_test_command_response( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil, - status - ) -end - -function ResetCondition:init(device, endpoint_id) - local out = {} - local args = {} - if #args > #self.field_defs then - error(self.NAME .. " received too many arguments") - end - for i,v in ipairs(self.field_defs) do - if v.is_optional and args[i] == nil then - out[v.name] = nil - elseif v.is_nullable and args[i] == nil then - out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) - out[v.name].field_id = v.field_id - elseif not v.is_optional and args[i] == nil then - out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) - out[v.name].field_id = v.field_id - else - out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) - out[v.name].field_id = v.field_id - end - end - setmetatable(out, { - __index = ResetCondition, - __tostring = ResetCondition.pretty_print - }) - return self._cluster:build_cluster_command( - device, - out, - endpoint_id, - self._cluster.ID, - self.ID - ) -end - -function ResetCondition:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function ResetCondition:augment_type(base_type_obj) - local elems = {} - for _, v in ipairs(base_type_obj.elements) do - for _, field_def in ipairs(self.field_defs) do - if field_def.field_id == v.field_id and - field_def.is_nullable and - (v.value == nil and v.elements == nil) then - elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) - elseif field_def.field_id == v.field_id and not - (field_def.is_optional and v.value == nil) then - elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) - if field_def.element_type ~= nil then - for i, e in ipairs(elems[field_def.name].elements) do - elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) - end - end - end - end - end - base_type_obj.elements = elems -end - -function ResetCondition:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(ResetCondition, {__call = ResetCondition.init}) - -return ResetCondition - diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/init.lua b/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/init.lua deleted file mode 100644 index 55a4ea7a30..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/init.lua +++ /dev/null @@ -1,23 +0,0 @@ -local command_mt = {} -command_mt.__command_cache = {} -command_mt.__index = function(self, key) - if command_mt.__command_cache[key] == nil then - local req_loc = string.format("HepaFilterMonitoring.server.commands.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) - end - return command_mt.__command_cache[key] -end - -local HepaFilterMonitoringServerCommands = {} - -function HepaFilterMonitoringServerCommands:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(HepaFilterMonitoringServerCommands, command_mt) - -return HepaFilterMonitoringServerCommands - diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/ChangeIndicationEnum.lua b/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/ChangeIndicationEnum.lua deleted file mode 100644 index 438de24c94..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/ChangeIndicationEnum.lua +++ /dev/null @@ -1,33 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local ChangeIndicationEnum = {} --- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility --- with how types were handled in api < 10. -local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) -new_mt.__index.pretty_print = function(self) - local name_lookup = { - [self.OK] = "OK", - [self.WARNING] = "WARNING", - [self.CRITICAL] = "CRITICAL", - } - return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) -end -new_mt.__tostring = new_mt.__index.pretty_print - -new_mt.__index.OK = 0x00 -new_mt.__index.WARNING = 0x01 -new_mt.__index.CRITICAL = 0x02 - -ChangeIndicationEnum.OK = 0x00 -ChangeIndicationEnum.WARNING = 0x01 -ChangeIndicationEnum.CRITICAL = 0x02 - -ChangeIndicationEnum.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(ChangeIndicationEnum, new_mt) - -return ChangeIndicationEnum - diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/Feature.lua deleted file mode 100644 index 88474d1b0f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/Feature.lua +++ /dev/null @@ -1,98 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.CONDITION = 0x0001 -Feature.WARNING = 0x0002 -Feature.REPLACEMENT_PRODUCT_LIST = 0x0004 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - CONDITION = 0x0001, - WARNING = 0x0002, - REPLACEMENT_PRODUCT_LIST = 0x0004, -} - -Feature.is_condition_set = function(self) - return (self.value & self.CONDITION) ~= 0 -end - -Feature.set_condition = function(self) - if self.value ~= nil then - self.value = self.value | self.CONDITION - else - self.value = self.CONDITION - end -end - -Feature.unset_condition = function(self) - self.value = self.value & (~self.CONDITION & self.BASE_MASK) -end - -Feature.is_warning_set = function(self) - return (self.value & self.WARNING) ~= 0 -end - -Feature.set_warning = function(self) - if self.value ~= nil then - self.value = self.value | self.WARNING - else - self.value = self.WARNING - end -end - -Feature.unset_warning = function(self) - self.value = self.value & (~self.WARNING & self.BASE_MASK) -end - -Feature.is_replacement_product_list_set = function(self) - return (self.value & self.REPLACEMENT_PRODUCT_LIST) ~= 0 -end - -Feature.set_replacement_product_list = function(self) - if self.value ~= nil then - self.value = self.value | self.REPLACEMENT_PRODUCT_LIST - else - self.value = self.REPLACEMENT_PRODUCT_LIST - end -end - -Feature.unset_replacement_product_list = function(self) - self.value = self.value & (~self.REPLACEMENT_PRODUCT_LIST & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.CONDITION | - Feature.WARNING | - Feature.REPLACEMENT_PRODUCT_LIST - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_condition_set = Feature.is_condition_set, - set_condition = Feature.set_condition, - unset_condition = Feature.unset_condition, - is_warning_set = Feature.is_warning_set, - set_warning = Feature.set_warning, - unset_warning = Feature.unset_warning, - is_replacement_product_list_set = Feature.is_replacement_product_list_set, - set_replacement_product_list = Feature.set_replacement_product_list, - unset_replacement_product_list = Feature.unset_replacement_product_list, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/init.lua b/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/init.lua deleted file mode 100644 index 77aca088ff..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/init.lua +++ /dev/null @@ -1,15 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("HepaFilterMonitoring.types." .. key) - end - return types_mt.__types_cache[key] -end - -local HepaFilterMonitoringTypes = {} - -setmetatable(HepaFilterMonitoringTypes, types_mt) - -return HepaFilterMonitoringTypes - diff --git a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/init.lua deleted file mode 100644 index b60e71050a..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local NitrogenDioxideConcentrationMeasurementServerAttributes = require "NitrogenDioxideConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local NitrogenDioxideConcentrationMeasurement = {} - -NitrogenDioxideConcentrationMeasurement.ID = 0x0413 -NitrogenDioxideConcentrationMeasurement.NAME = "NitrogenDioxideConcentrationMeasurement" -NitrogenDioxideConcentrationMeasurement.server = {} -NitrogenDioxideConcentrationMeasurement.client = {} -NitrogenDioxideConcentrationMeasurement.server.attributes = NitrogenDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(NitrogenDioxideConcentrationMeasurement) -NitrogenDioxideConcentrationMeasurement.types = ConcentrationMeasurement.types - -function NitrogenDioxideConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function NitrogenDioxideConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -NitrogenDioxideConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -NitrogenDioxideConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function NitrogenDioxideConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = NitrogenDioxideConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, NitrogenDioxideConcentrationMeasurement.NAME)) - end - return NitrogenDioxideConcentrationMeasurement[direction].attributes[key] -end -NitrogenDioxideConcentrationMeasurement.attributes = {} -setmetatable(NitrogenDioxideConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(NitrogenDioxideConcentrationMeasurement, {__index = cluster_base}) - -return NitrogenDioxideConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index 06c3d6dd55..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("NitrogenDioxideConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local NitrogenDioxideConcentrationMeasurementServerAttributes = {} - -function NitrogenDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(NitrogenDioxideConcentrationMeasurementServerAttributes, attr_mt) - -return NitrogenDioxideConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/init.lua deleted file mode 100644 index 83fd04857e..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local OzoneConcentrationMeasurementServerAttributes = require "OzoneConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local OzoneConcentrationMeasurement = {} - -OzoneConcentrationMeasurement.ID = 0x0415 -OzoneConcentrationMeasurement.NAME = "OzoneConcentrationMeasurement" -OzoneConcentrationMeasurement.server = {} -OzoneConcentrationMeasurement.client = {} -OzoneConcentrationMeasurement.server.attributes = OzoneConcentrationMeasurementServerAttributes:set_parent_cluster(OzoneConcentrationMeasurement) -OzoneConcentrationMeasurement.types = ConcentrationMeasurement.types - -function OzoneConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function OzoneConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -OzoneConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -OzoneConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function OzoneConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = OzoneConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, OzoneConcentrationMeasurement.NAME)) - end - return OzoneConcentrationMeasurement[direction].attributes[key] -end -OzoneConcentrationMeasurement.attributes = {} -setmetatable(OzoneConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(OzoneConcentrationMeasurement, {__index = cluster_base}) - -return OzoneConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index fe0048cd99..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("OzoneConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local OzoneConcentrationMeasurementServerAttributes = {} - -function OzoneConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(OzoneConcentrationMeasurementServerAttributes, attr_mt) - -return OzoneConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/init.lua deleted file mode 100644 index 98eebd407e..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local Pm10ConcentrationMeasurementServerAttributes = require "Pm10ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local Pm10ConcentrationMeasurement = {} - -Pm10ConcentrationMeasurement.ID = 0x042D -Pm10ConcentrationMeasurement.NAME = "Pm10ConcentrationMeasurement" -Pm10ConcentrationMeasurement.server = {} -Pm10ConcentrationMeasurement.client = {} -Pm10ConcentrationMeasurement.server.attributes = Pm10ConcentrationMeasurementServerAttributes:set_parent_cluster(Pm10ConcentrationMeasurement) -Pm10ConcentrationMeasurement.types = ConcentrationMeasurement.types - -function Pm10ConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function Pm10ConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -Pm10ConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -Pm10ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function Pm10ConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = Pm10ConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, Pm10ConcentrationMeasurement.NAME)) - end - return Pm10ConcentrationMeasurement[direction].attributes[key] -end -Pm10ConcentrationMeasurement.attributes = {} -setmetatable(Pm10ConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(Pm10ConcentrationMeasurement, {__index = cluster_base}) - -return Pm10ConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index 55f08c7e43..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("Pm10ConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local Pm10ConcentrationMeasurementServerAttributes = {} - -function Pm10ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(Pm10ConcentrationMeasurementServerAttributes, attr_mt) - -return Pm10ConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/init.lua deleted file mode 100644 index 0b3caa3bd2..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local Pm1ConcentrationMeasurementServerAttributes = require "Pm1ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local Pm1ConcentrationMeasurement = {} - -Pm1ConcentrationMeasurement.ID = 0x042C -Pm1ConcentrationMeasurement.NAME = "Pm1ConcentrationMeasurement" -Pm1ConcentrationMeasurement.server = {} -Pm1ConcentrationMeasurement.client = {} -Pm1ConcentrationMeasurement.server.attributes = Pm1ConcentrationMeasurementServerAttributes:set_parent_cluster(Pm1ConcentrationMeasurement) -Pm1ConcentrationMeasurement.types = ConcentrationMeasurement.types - -function Pm1ConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function Pm1ConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -Pm1ConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -Pm1ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function Pm1ConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = Pm1ConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, Pm1ConcentrationMeasurement.NAME)) - end - return Pm1ConcentrationMeasurement[direction].attributes[key] -end -Pm1ConcentrationMeasurement.attributes = {} -setmetatable(Pm1ConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(Pm1ConcentrationMeasurement, {__index = cluster_base}) - -return Pm1ConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index f668e41a07..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("Pm1ConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local Pm1ConcentrationMeasurementServerAttributes = {} - -function Pm1ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(Pm1ConcentrationMeasurementServerAttributes, attr_mt) - -return Pm1ConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/init.lua deleted file mode 100644 index 5234346d60..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local Pm25ConcentrationMeasurementServerAttributes = require "Pm25ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local Pm25ConcentrationMeasurement = {} - -Pm25ConcentrationMeasurement.ID = 0x042A -Pm25ConcentrationMeasurement.NAME = "Pm25ConcentrationMeasurement" -Pm25ConcentrationMeasurement.server = {} -Pm25ConcentrationMeasurement.client = {} -Pm25ConcentrationMeasurement.server.attributes = Pm25ConcentrationMeasurementServerAttributes:set_parent_cluster(Pm25ConcentrationMeasurement) -Pm25ConcentrationMeasurement.types = ConcentrationMeasurement.types - -function Pm25ConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function Pm25ConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -Pm25ConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -Pm25ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function Pm25ConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = Pm25ConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, Pm25ConcentrationMeasurement.NAME)) - end - return Pm25ConcentrationMeasurement[direction].attributes[key] -end -Pm25ConcentrationMeasurement.attributes = {} -setmetatable(Pm25ConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(Pm25ConcentrationMeasurement, {__index = cluster_base}) - -return Pm25ConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index 2c7d5fce7b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("Pm25ConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local Pm25ConcentrationMeasurementServerAttributes = {} - -function Pm25ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(Pm25ConcentrationMeasurementServerAttributes, attr_mt) - -return Pm25ConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/init.lua deleted file mode 100644 index 2a4cc04a0d..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local RadonConcentrationMeasurementServerAttributes = require "RadonConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local RadonConcentrationMeasurement = {} - -RadonConcentrationMeasurement.ID = 0x042F -RadonConcentrationMeasurement.NAME = "RadonConcentrationMeasurement" -RadonConcentrationMeasurement.server = {} -RadonConcentrationMeasurement.client = {} -RadonConcentrationMeasurement.server.attributes = RadonConcentrationMeasurementServerAttributes:set_parent_cluster(RadonConcentrationMeasurement) -RadonConcentrationMeasurement.types = ConcentrationMeasurement.types - -function RadonConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function RadonConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -RadonConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -RadonConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function RadonConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = RadonConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, RadonConcentrationMeasurement.NAME)) - end - return RadonConcentrationMeasurement[direction].attributes[key] -end -RadonConcentrationMeasurement.attributes = {} -setmetatable(RadonConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(RadonConcentrationMeasurement, {__index = cluster_base}) - -return RadonConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index b83ef67bfc..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("RadonConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local RadonConcentrationMeasurementServerAttributes = {} - -function RadonConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(RadonConcentrationMeasurementServerAttributes, attr_mt) - -return RadonConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua deleted file mode 100644 index a99c1dea50..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes = require "TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" - -local TotalVolatileOrganicCompoundsConcentrationMeasurement = {} - -TotalVolatileOrganicCompoundsConcentrationMeasurement.ID = 0x042E -TotalVolatileOrganicCompoundsConcentrationMeasurement.NAME = "TotalVolatileOrganicCompoundsConcentrationMeasurement" -TotalVolatileOrganicCompoundsConcentrationMeasurement.server = {} -TotalVolatileOrganicCompoundsConcentrationMeasurement.client = {} -TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes = TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes:set_parent_cluster(TotalVolatileOrganicCompoundsConcentrationMeasurement) -TotalVolatileOrganicCompoundsConcentrationMeasurement.types = ConcentrationMeasurement.types - -function TotalVolatileOrganicCompoundsConcentrationMeasurement:get_attribute_by_id(attr_id) - return ConcentrationMeasurement:get_attribute_by_id(attr_id) -end - -function TotalVolatileOrganicCompoundsConcentrationMeasurement:get_server_command_by_id(command_id) - return ConcentrationMeasurement:get_server_command_by_id(command_id) -end - -TotalVolatileOrganicCompoundsConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map - -TotalVolatileOrganicCompoundsConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature - -function TotalVolatileOrganicCompoundsConcentrationMeasurement.are_features_supported(feature, feature_map) - return ConcentrationMeasurement.are_features_supported(feature, feature_map) -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = TotalVolatileOrganicCompoundsConcentrationMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, TotalVolatileOrganicCompoundsConcentrationMeasurement.NAME)) - end - return TotalVolatileOrganicCompoundsConcentrationMeasurement[direction].attributes[key] -end -TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes = {} -setmetatable(TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes, attribute_helper_mt) - -setmetatable(TotalVolatileOrganicCompoundsConcentrationMeasurement, {__index = cluster_base}) - -return TotalVolatileOrganicCompoundsConcentrationMeasurement - diff --git a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua deleted file mode 100644 index c0c1b65c37..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes = {} - -function TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes, attr_mt) - -return TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/init.lua b/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/init.lua deleted file mode 100644 index 1155cfd636..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/init.lua +++ /dev/null @@ -1,92 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local WaterHeaterModeServerAttributes = require "WaterHeaterMode.server.attributes" -local WaterHeaterModeServerCommands = require "WaterHeaterMode.server.commands" -local WaterHeaterModeTypes = require "WaterHeaterMode.types" - -local WaterHeaterMode = {} - -WaterHeaterMode.ID = 0x009E -WaterHeaterMode.NAME = "WaterHeaterMode" -WaterHeaterMode.server = {} -WaterHeaterMode.client = {} -WaterHeaterMode.server.attributes = WaterHeaterModeServerAttributes:set_parent_cluster(WaterHeaterMode) -WaterHeaterMode.server.commands = WaterHeaterModeServerCommands:set_parent_cluster(WaterHeaterMode) -WaterHeaterMode.types = WaterHeaterModeTypes - -function WaterHeaterMode:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "SupportedModes", - [0x0001] = "CurrentMode", - [0x0002] = "StartUpMode", - [0x0003] = "OnMode", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - -function WaterHeaterMode:get_server_command_by_id(command_id) - local server_id_map = { - [0x0000] = "ChangeToMode", - } - if server_id_map[command_id] ~= nil then - return self.server.commands[server_id_map[command_id]] - end - return nil -end - -WaterHeaterMode.attribute_direction_map = { - ["SupportedModes"] = "server", - ["CurrentMode"] = "server", - ["StartUpMode"] = "server", - ["OnMode"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -WaterHeaterMode.command_direction_map = { - ["ChangeToMode"] = "server", - ["ChangeToModeResponse"] = "client", -} - -WaterHeaterMode.FeatureMap = WaterHeaterMode.types.Feature - -function WaterHeaterMode.are_features_supported(feature, feature_map) - if (WaterHeaterMode.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = WaterHeaterMode.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, WaterHeaterMode.NAME)) - end - return WaterHeaterMode[direction].attributes[key] -end -WaterHeaterMode.attributes = {} -setmetatable(WaterHeaterMode.attributes, attribute_helper_mt) - -local command_helper_mt = {} -command_helper_mt.__index = function(self, key) - local direction = WaterHeaterMode.command_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown command %s on cluster %s", key, WaterHeaterMode.NAME)) - end - return WaterHeaterMode[direction].commands[key] -end -WaterHeaterMode.commands = {} -setmetatable(WaterHeaterMode.commands, command_helper_mt) - -setmetatable(WaterHeaterMode, {__index = cluster_base}) - -return WaterHeaterMode - diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/AcceptedCommandList.lua deleted file mode 100644 index 6a8d95df1d..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/AcceptedCommandList.lua +++ /dev/null @@ -1,75 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local AcceptedCommandList = { - ID = 0xFFF9, - NAME = "AcceptedCommandList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - -function AcceptedCommandList:augment_type(data_type_obj) - for i, v in ipairs(data_type_obj.elements) do - data_type_obj.elements[i] = data_types.validate_or_build_type(v, AcceptedCommandList.element_type) - end -end - -function AcceptedCommandList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function AcceptedCommandList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AcceptedCommandList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AcceptedCommandList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function AcceptedCommandList:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function AcceptedCommandList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) -return AcceptedCommandList - diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/init.lua deleted file mode 100644 index 020a4125ce..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/init.lua +++ /dev/null @@ -1,24 +0,0 @@ -local attr_mt = {} -attr_mt.__attr_cache = {} -attr_mt.__index = function(self, key) - if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("WaterHeaterMode.server.attributes.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - attr_mt.__attr_cache[key] = raw_def - end - return attr_mt.__attr_cache[key] -end - -local WaterHeaterModeServerAttributes = {} - -function WaterHeaterModeServerAttributes:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(WaterHeaterModeServerAttributes, attr_mt) - -return WaterHeaterModeServerAttributes - diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/init.lua b/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/init.lua deleted file mode 100644 index 9736c4577f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/init.lua +++ /dev/null @@ -1,23 +0,0 @@ -local command_mt = {} -command_mt.__command_cache = {} -command_mt.__index = function(self, key) - if command_mt.__command_cache[key] == nil then - local req_loc = string.format("WaterHeaterMode.server.commands.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) - end - return command_mt.__command_cache[key] -end - -local WaterHeaterModeServerCommands = {} - -function WaterHeaterModeServerCommands:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(WaterHeaterModeServerCommands, command_mt) - -return WaterHeaterModeServerCommands - diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/Feature.lua deleted file mode 100644 index da49bf4115..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/Feature.lua +++ /dev/null @@ -1,54 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.ON_OFF = 0x0001 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - ON_OFF = 0x0001, -} - -Feature.is_on_off_set = function(self) - return (self.value & self.ON_OFF) ~= 0 -end - -Feature.set_on_off = function(self) - if self.value ~= nil then - self.value = self.value | self.ON_OFF - else - self.value = self.ON_OFF - end -end - -Feature.unset_on_off = function(self) - self.value = self.value & (~self.ON_OFF & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.ON_OFF - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_on_off_set = Feature.is_on_off_set, - set_on_off = Feature.set_on_off, - unset_on_off = Feature.unset_on_off, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/init.lua b/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/init.lua deleted file mode 100644 index f0198ff8a0..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/init.lua +++ /dev/null @@ -1,15 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("WaterHeaterMode.types." .. key) - end - return types_mt.__types_cache[key] -end - -local WaterHeaterModeTypes = {} - -setmetatable(WaterHeaterModeTypes, types_mt) - -return WaterHeaterModeTypes - diff --git a/drivers/SmartThings/matter-thermostat/src/embedded-cluster-utils.lua b/drivers/SmartThings/matter-thermostat/src/embedded-cluster-utils.lua deleted file mode 100644 index 3c38ef55d0..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/embedded-cluster-utils.lua +++ /dev/null @@ -1,93 +0,0 @@ -local clusters = require "st.matter.clusters" -local utils = require "st.utils" - --- Include driver-side definitions when lua libs api version is < 10 -local version = require "version" -if version.api < 10 then - clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" - clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" -end - -if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" -end - -if version.api < 13 then - clusters.WaterHeaterMode = require "WaterHeaterMode" -end - -local embedded_cluster_utils = {} - -local embedded_clusters_api_10 = { - [clusters.HepaFilterMonitoring.ID] = clusters.HepaFilterMonitoring, - [clusters.ActivatedCarbonFilterMonitoring.ID] = clusters.ActivatedCarbonFilterMonitoring, - [clusters.AirQuality.ID] = clusters.AirQuality, - [clusters.CarbonMonoxideConcentrationMeasurement.ID] = clusters.CarbonMonoxideConcentrationMeasurement, - [clusters.CarbonDioxideConcentrationMeasurement.ID] = clusters.CarbonDioxideConcentrationMeasurement, - [clusters.FormaldehydeConcentrationMeasurement.ID] = clusters.FormaldehydeConcentrationMeasurement, - [clusters.NitrogenDioxideConcentrationMeasurement.ID] = clusters.NitrogenDioxideConcentrationMeasurement, - [clusters.OzoneConcentrationMeasurement.ID] = clusters.OzoneConcentrationMeasurement, - [clusters.Pm1ConcentrationMeasurement.ID] = clusters.Pm1ConcentrationMeasurement, - [clusters.Pm10ConcentrationMeasurement.ID] = clusters.Pm10ConcentrationMeasurement, - [clusters.Pm25ConcentrationMeasurement.ID] = clusters.Pm25ConcentrationMeasurement, - [clusters.RadonConcentrationMeasurement.ID] = clusters.RadonConcentrationMeasurement, - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.ID] = clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement, -} - -local embedded_clusters_api_11 = { - [clusters.ElectricalEnergyMeasurement.ID] = clusters.ElectricalEnergyMeasurement, - [clusters.ElectricalPowerMeasurement.ID] = clusters.ElectricalPowerMeasurement, -} - -local embedded_clusters_api_13 = { - [clusters.WaterHeaterMode.ID] = clusters.WaterHeaterMode -} - -function embedded_cluster_utils.get_endpoints(device, cluster_id, opts) - -- If using older lua libs and need to check for an embedded cluster feature, - -- we must use the embedded cluster definitions here - if version.api < 10 and embedded_clusters_api_10[cluster_id] ~= nil or - version.api < 11 and embedded_clusters_api_11[cluster_id] ~= nil or - version.api < 13 and embedded_clusters_api_13[cluster_id] ~= nil then - local embedded_cluster = embedded_clusters_api_10[cluster_id] or embedded_clusters_api_11[cluster_id] or embedded_clusters_api_13[cluster_id] - local opts = opts or {} - if utils.table_size(opts) > 1 then - device.log.warn_with({hub_logs = true}, "Invalid options for get_endpoints") - return - end - local clus_has_features = function(clus, feature_bitmap) - if not feature_bitmap or not clus then return false end - return embedded_cluster.are_features_supported(feature_bitmap, clus.feature_map) - end - local eps = {} - for _, ep in ipairs(device.endpoints) do - for _, clus in ipairs(ep.clusters) do - if ((clus.cluster_id == cluster_id) - and (opts.feature_bitmap == nil or clus_has_features(clus, opts.feature_bitmap)) - and ((opts.cluster_type == nil and clus.cluster_type == "SERVER" or clus.cluster_type == "BOTH") - or (opts.cluster_type == clus.cluster_type)) - or (cluster_id == nil)) then - table.insert(eps, ep.endpoint_id) - if cluster_id == nil then break end - end - end - end - return eps - else - return device:get_endpoints(cluster_id, opts) - end -end - -return embedded_cluster_utils \ No newline at end of file diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/init.lua new file mode 100644 index 0000000000..7bfdfd7248 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/init.lua @@ -0,0 +1,105 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local ActivatedCarbonFilterMonitoringServerAttributes = require "embedded_clusters.ActivatedCarbonFilterMonitoring.server.attributes" +local ActivatedCarbonFilterMonitoringServerCommands = require "embedded_clusters.ActivatedCarbonFilterMonitoring.server.commands" +local ActivatedCarbonFilterMonitoringTypes = require "embedded_clusters.ActivatedCarbonFilterMonitoring.types" + +local ActivatedCarbonFilterMonitoring = {} + +ActivatedCarbonFilterMonitoring.ID = 0x0072 +ActivatedCarbonFilterMonitoring.NAME = "ActivatedCarbonFilterMonitoring" +ActivatedCarbonFilterMonitoring.server = {} +ActivatedCarbonFilterMonitoring.client = {} +ActivatedCarbonFilterMonitoring.server.attributes = ActivatedCarbonFilterMonitoringServerAttributes:set_parent_cluster(ActivatedCarbonFilterMonitoring) +ActivatedCarbonFilterMonitoring.server.commands = ActivatedCarbonFilterMonitoringServerCommands:set_parent_cluster(ActivatedCarbonFilterMonitoring) +ActivatedCarbonFilterMonitoring.types = ActivatedCarbonFilterMonitoringTypes + +function ActivatedCarbonFilterMonitoring:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "Condition", + [0x0001] = "DegradationDirection", + [0x0002] = "ChangeIndication", + [0x0003] = "InPlaceIndicator", + [0x0004] = "LastChangedTime", + [0x0005] = "ReplacementProductList", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function ActivatedCarbonFilterMonitoring:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "ResetCondition", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +ActivatedCarbonFilterMonitoring.attribute_direction_map = { + ["Condition"] = "server", + ["DegradationDirection"] = "server", + ["ChangeIndication"] = "server", + ["InPlaceIndicator"] = "server", + ["LastChangedTime"] = "server", + ["ReplacementProductList"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +ActivatedCarbonFilterMonitoring.command_direction_map = { + ["ResetCondition"] = "server", +} + +ActivatedCarbonFilterMonitoring.FeatureMap = ActivatedCarbonFilterMonitoring.types.Feature + +function ActivatedCarbonFilterMonitoring.are_features_supported(feature, feature_map) + if (ActivatedCarbonFilterMonitoring.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ActivatedCarbonFilterMonitoring.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ActivatedCarbonFilterMonitoring.NAME)) + end + return ActivatedCarbonFilterMonitoring[direction].attributes[key] +end +ActivatedCarbonFilterMonitoring.attributes = {} +setmetatable(ActivatedCarbonFilterMonitoring.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = ActivatedCarbonFilterMonitoring.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, ActivatedCarbonFilterMonitoring.NAME)) + end + return ActivatedCarbonFilterMonitoring[direction].commands[key] +end +ActivatedCarbonFilterMonitoring.commands = {} +setmetatable(ActivatedCarbonFilterMonitoring.commands, command_helper_mt) + +local event_helper_mt = {} +event_helper_mt.__index = function(self, key) + return ActivatedCarbonFilterMonitoring.server.events[key] +end +ActivatedCarbonFilterMonitoring.events = {} +setmetatable(ActivatedCarbonFilterMonitoring.events, event_helper_mt) + +setmetatable(ActivatedCarbonFilterMonitoring, {__index = cluster_base}) + +return ActivatedCarbonFilterMonitoring + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua new file mode 100644 index 0000000000..df65143e66 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua @@ -0,0 +1,71 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ChangeIndication = { + ID = 0x0002, + NAME = "ChangeIndication", + base_type = require "embedded_clusters.ActivatedCarbonFilterMonitoring.types.ChangeIndicationEnum", +} + +function ChangeIndication:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function ChangeIndication:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function ChangeIndication:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function ChangeIndication:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ChangeIndication:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function ChangeIndication:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(ChangeIndication, {__call = ChangeIndication.new_value, __index = ChangeIndication.base_type}) +return ChangeIndication + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua new file mode 100644 index 0000000000..361e423a1c --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua @@ -0,0 +1,71 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local Condition = { + ID = 0x0000, + NAME = "Condition", + base_type = require "st.matter.data_types.Uint8", +} + +function Condition:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function Condition:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function Condition:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function Condition:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Condition:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function Condition:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(Condition, {__call = Condition.new_value, __index = Condition.base_type}) +return Condition + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/init.lua new file mode 100644 index 0000000000..1ecdf6a931 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.ActivatedCarbonFilterMonitoring.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local ActivatedCarbonFilterMonitoringServerAttributes = {} + +function ActivatedCarbonFilterMonitoringServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ActivatedCarbonFilterMonitoringServerAttributes, attr_mt) + +return ActivatedCarbonFilterMonitoringServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua new file mode 100644 index 0000000000..1ac942f780 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua @@ -0,0 +1,94 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ResetCondition = {} + +ResetCondition.NAME = "ResetCondition" +ResetCondition.ID = 0x0000 +ResetCondition.field_defs = { +} + +function ResetCondition:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, + status + ) +end + +function ResetCondition:init(device, endpoint_id) + local out = {} + local args = {} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = ResetCondition, + __tostring = ResetCondition.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function ResetCondition:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ResetCondition:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + base_type_obj.elements = elems +end + +function ResetCondition:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(ResetCondition, {__call = ResetCondition.init}) + +return ResetCondition + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/init.lua new file mode 100644 index 0000000000..ec343da578 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/init.lua @@ -0,0 +1,26 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("embedded_clusters.ActivatedCarbonFilterMonitoring.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local ActivatedCarbonFilterMonitoringServerCommands = {} + +function ActivatedCarbonFilterMonitoringServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ActivatedCarbonFilterMonitoringServerCommands, command_mt) + +return ActivatedCarbonFilterMonitoringServerCommands + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua new file mode 100644 index 0000000000..6b5ab62494 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua @@ -0,0 +1,36 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local ChangeIndicationEnum = {} +-- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility +-- with how types were handled in api < 10. +local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.OK] = "OK", + [self.WARNING] = "WARNING", + [self.CRITICAL] = "CRITICAL", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.OK = 0x00 +new_mt.__index.WARNING = 0x01 +new_mt.__index.CRITICAL = 0x02 + +ChangeIndicationEnum.OK = 0x00 +ChangeIndicationEnum.WARNING = 0x01 +ChangeIndicationEnum.CRITICAL = 0x02 + +ChangeIndicationEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(ChangeIndicationEnum, new_mt) + +return ChangeIndicationEnum + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/Feature.lua new file mode 100644 index 0000000000..906e769676 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/Feature.lua @@ -0,0 +1,101 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.CONDITION = 0x0001 +Feature.WARNING = 0x0002 +Feature.REPLACEMENT_PRODUCT_LIST = 0x0004 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + CONDITION = 0x0001, + WARNING = 0x0002, + REPLACEMENT_PRODUCT_LIST = 0x0004, +} + +Feature.is_condition_set = function(self) + return (self.value & self.CONDITION) ~= 0 +end + +Feature.set_condition = function(self) + if self.value ~= nil then + self.value = self.value | self.CONDITION + else + self.value = self.CONDITION + end +end + +Feature.unset_condition = function(self) + self.value = self.value & (~self.CONDITION & self.BASE_MASK) +end + +Feature.is_warning_set = function(self) + return (self.value & self.WARNING) ~= 0 +end + +Feature.set_warning = function(self) + if self.value ~= nil then + self.value = self.value | self.WARNING + else + self.value = self.WARNING + end +end + +Feature.unset_warning = function(self) + self.value = self.value & (~self.WARNING & self.BASE_MASK) +end + +Feature.is_replacement_product_list_set = function(self) + return (self.value & self.REPLACEMENT_PRODUCT_LIST) ~= 0 +end + +Feature.set_replacement_product_list = function(self) + if self.value ~= nil then + self.value = self.value | self.REPLACEMENT_PRODUCT_LIST + else + self.value = self.REPLACEMENT_PRODUCT_LIST + end +end + +Feature.unset_replacement_product_list = function(self) + self.value = self.value & (~self.REPLACEMENT_PRODUCT_LIST & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.CONDITION | + Feature.WARNING | + Feature.REPLACEMENT_PRODUCT_LIST + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_condition_set = Feature.is_condition_set, + set_condition = Feature.set_condition, + unset_condition = Feature.unset_condition, + is_warning_set = Feature.is_warning_set, + set_warning = Feature.set_warning, + unset_warning = Feature.unset_warning, + is_replacement_product_list_set = Feature.is_replacement_product_list_set, + set_replacement_product_list = Feature.set_replacement_product_list, + unset_replacement_product_list = Feature.unset_replacement_product_list, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/init.lua new file mode 100644 index 0000000000..7cdc2d1561 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/init.lua @@ -0,0 +1,18 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.ActivatedCarbonFilterMonitoring.types." .. key) + end + return types_mt.__types_cache[key] +end + +local ActivatedCarbonFilterMonitoringTypes = {} + +setmetatable(ActivatedCarbonFilterMonitoringTypes, types_mt) + +return ActivatedCarbonFilterMonitoringTypes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/init.lua new file mode 100644 index 0000000000..4f6d19cc9f --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/init.lua @@ -0,0 +1,62 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local AirQualityServerAttributes = require "embedded_clusters.AirQuality.server.attributes" +local AirQualityTypes = require "embedded_clusters.AirQuality.types" + +local AirQuality = {} + +AirQuality.ID = 0x005B +AirQuality.NAME = "AirQuality" +AirQuality.server = {} +AirQuality.client = {} +AirQuality.server.attributes = AirQualityServerAttributes:set_parent_cluster(AirQuality) +AirQuality.types = AirQualityTypes + +function AirQuality:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "AirQuality", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +-- Attribute Mapping +AirQuality.attribute_direction_map = { + ["AirQuality"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +AirQuality.FeatureMap = AirQuality.types.Feature + +function AirQuality.are_features_supported(feature, feature_map) + if (AirQuality.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = AirQuality.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, AirQuality.NAME)) + end + return AirQuality[direction].attributes[key] +end +AirQuality.attributes = {} +setmetatable(AirQuality.attributes, attribute_helper_mt) + +setmetatable(AirQuality, {__index = cluster_base}) + +return AirQuality + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AcceptedCommandList.lua new file mode 100644 index 0000000000..a1e56c6597 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AcceptedCommandList.lua @@ -0,0 +1,78 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AcceptedCommandList = { + ID = 0xFFF9, + NAME = "AcceptedCommandList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +function AcceptedCommandList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, AcceptedCommandList.element_type) + end +end + +function AcceptedCommandList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function AcceptedCommandList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AcceptedCommandList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AcceptedCommandList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AcceptedCommandList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AcceptedCommandList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) +return AcceptedCommandList + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AirQuality.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AirQuality.lua new file mode 100644 index 0000000000..1beb6218f9 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AirQuality.lua @@ -0,0 +1,71 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AirQuality = { + ID = 0x0000, + NAME = "AirQuality", + base_type = require "embedded_clusters.AirQuality.types.AirQualityEnum", +} + +function AirQuality:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function AirQuality:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AirQuality:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AirQuality:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AirQuality:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AirQuality:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(AirQuality, {__call = AirQuality.new_value, __index = AirQuality.base_type}) +return AirQuality + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AttributeList.lua new file mode 100644 index 0000000000..238b50ade3 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AttributeList.lua @@ -0,0 +1,78 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AttributeList = { + ID = 0xFFFB, + NAME = "AttributeList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +function AttributeList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, AttributeList.element_type) + end +end + +function AttributeList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function AttributeList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AttributeList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AttributeList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AttributeList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AttributeList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) +return AttributeList + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/EventList.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/EventList.lua new file mode 100644 index 0000000000..719f17a231 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/EventList.lua @@ -0,0 +1,78 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local EventList = { + ID = 0xFFFA, + NAME = "EventList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +function EventList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, EventList.element_type) + end +end + +function EventList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function EventList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function EventList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function EventList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function EventList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function EventList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(EventList, {__call = EventList.new_value, __index = EventList.base_type}) +return EventList + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/init.lua new file mode 100644 index 0000000000..50295b081a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.AirQuality.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local AirQualityServerAttributes = {} + +function AirQualityServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(AirQualityServerAttributes, attr_mt) + +return AirQualityServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/AirQualityEnum.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/AirQualityEnum.lua new file mode 100644 index 0000000000..c2c255614a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/AirQualityEnum.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local AirQualityEnum = {} +-- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility +-- with how types were handled in api < 10. +local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.UNKNOWN] = "UNKNOWN", + [self.GOOD] = "GOOD", + [self.FAIR] = "FAIR", + [self.MODERATE] = "MODERATE", + [self.POOR] = "POOR", + [self.VERY_POOR] = "VERY_POOR", + [self.EXTREMELY_POOR] = "EXTREMELY_POOR", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.UNKNOWN = 0x00 +new_mt.__index.GOOD = 0x01 +new_mt.__index.FAIR = 0x02 +new_mt.__index.MODERATE = 0x03 +new_mt.__index.POOR = 0x04 +new_mt.__index.VERY_POOR = 0x05 +new_mt.__index.EXTREMELY_POOR = 0x06 + +AirQualityEnum.UNKNOWN = 0x00 +AirQualityEnum.GOOD = 0x01 +AirQualityEnum.FAIR = 0x02 +AirQualityEnum.MODERATE = 0x03 +AirQualityEnum.POOR = 0x04 +AirQualityEnum.VERY_POOR = 0x05 +AirQualityEnum.EXTREMELY_POOR = 0x06 + +AirQualityEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(AirQualityEnum, new_mt) + +return AirQualityEnum + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/Feature.lua new file mode 100644 index 0000000000..906a09a2bb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/Feature.lua @@ -0,0 +1,123 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.FAIR = 0x0001 +Feature.MODERATE = 0x0002 +Feature.VERY_POOR = 0x0004 +Feature.EXTREMELY_POOR = 0x0008 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + FAIR = 0x0001, + MODERATE = 0x0002, + VERY_POOR = 0x0004, + EXTREMELY_POOR = 0x0008, +} + +Feature.is_fair_set = function(self) + return (self.value & self.FAIR) ~= 0 +end + +Feature.set_fair = function(self) + if self.value ~= nil then + self.value = self.value | self.FAIR + else + self.value = self.FAIR + end +end + +Feature.unset_fair = function(self) + self.value = self.value & (~self.FAIR & self.BASE_MASK) +end + +Feature.is_moderate_set = function(self) + return (self.value & self.MODERATE) ~= 0 +end + +Feature.set_moderate = function(self) + if self.value ~= nil then + self.value = self.value | self.MODERATE + else + self.value = self.MODERATE + end +end + +Feature.unset_moderate = function(self) + self.value = self.value & (~self.MODERATE & self.BASE_MASK) +end + +Feature.is_very_poor_set = function(self) + return (self.value & self.VERY_POOR) ~= 0 +end + +Feature.set_very_poor = function(self) + if self.value ~= nil then + self.value = self.value | self.VERY_POOR + else + self.value = self.VERY_POOR + end +end + +Feature.unset_very_poor = function(self) + self.value = self.value & (~self.VERY_POOR & self.BASE_MASK) +end + +Feature.is_extremely_poor_set = function(self) + return (self.value & self.EXTREMELY_POOR) ~= 0 +end + +Feature.set_extremely_poor = function(self) + if self.value ~= nil then + self.value = self.value | self.EXTREMELY_POOR + else + self.value = self.EXTREMELY_POOR + end +end + +Feature.unset_extremely_poor = function(self) + self.value = self.value & (~self.EXTREMELY_POOR & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.FAIR | + Feature.MODERATE | + Feature.VERY_POOR | + Feature.EXTREMELY_POOR + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_fair_set = Feature.is_fair_set, + set_fair = Feature.set_fair, + unset_fair = Feature.unset_fair, + is_moderate_set = Feature.is_moderate_set, + set_moderate = Feature.set_moderate, + unset_moderate = Feature.unset_moderate, + is_very_poor_set = Feature.is_very_poor_set, + set_very_poor = Feature.set_very_poor, + unset_very_poor = Feature.unset_very_poor, + is_extremely_poor_set = Feature.is_extremely_poor_set, + set_extremely_poor = Feature.set_extremely_poor, + unset_extremely_poor = Feature.unset_extremely_poor, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/init.lua new file mode 100644 index 0000000000..b77d67de82 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/init.lua @@ -0,0 +1,18 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.AirQuality.types." .. key) + end + return types_mt.__types_cache[key] +end + +local AirQualityTypes = {} + +setmetatable(AirQualityTypes, types_mt) + +return AirQualityTypes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..4de97147e4 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local CarbonDioxideConcentrationMeasurementServerAttributes = require "embedded_clusters.CarbonDioxideConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local CarbonDioxideConcentrationMeasurement = {} + +CarbonDioxideConcentrationMeasurement.ID = 0x040D +CarbonDioxideConcentrationMeasurement.NAME = "CarbonDioxideConcentrationMeasurement" +CarbonDioxideConcentrationMeasurement.server = {} +CarbonDioxideConcentrationMeasurement.client = {} +CarbonDioxideConcentrationMeasurement.server.attributes = CarbonDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(CarbonDioxideConcentrationMeasurement) +CarbonDioxideConcentrationMeasurement.types = ConcentrationMeasurement.types + +function CarbonDioxideConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function CarbonDioxideConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +CarbonDioxideConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +CarbonDioxideConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function CarbonDioxideConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = CarbonDioxideConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, CarbonDioxideConcentrationMeasurement.NAME)) + end + return CarbonDioxideConcentrationMeasurement[direction].attributes[key] +end +CarbonDioxideConcentrationMeasurement.attributes = {} +setmetatable(CarbonDioxideConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(CarbonDioxideConcentrationMeasurement, {__index = cluster_base}) + +return CarbonDioxideConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..0206213e6f --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.CarbonDioxideConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local CarbonDioxideConcentrationMeasurementServerAttributes = {} + +function CarbonDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(CarbonDioxideConcentrationMeasurementServerAttributes, attr_mt) + +return CarbonDioxideConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..a6e1f24d1d --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local CarbonMonoxideConcentrationMeasurementServerAttributes = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local CarbonMonoxideConcentrationMeasurement = {} + +CarbonMonoxideConcentrationMeasurement.ID = 0x040C +CarbonMonoxideConcentrationMeasurement.NAME = "CarbonMonoxideConcentrationMeasurement" +CarbonMonoxideConcentrationMeasurement.server = {} +CarbonMonoxideConcentrationMeasurement.client = {} +CarbonMonoxideConcentrationMeasurement.server.attributes = CarbonMonoxideConcentrationMeasurementServerAttributes:set_parent_cluster(CarbonMonoxideConcentrationMeasurement) +CarbonMonoxideConcentrationMeasurement.types = ConcentrationMeasurement.types + +function CarbonMonoxideConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function CarbonMonoxideConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +CarbonMonoxideConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +CarbonMonoxideConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function CarbonMonoxideConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = CarbonMonoxideConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, CarbonMonoxideConcentrationMeasurement.NAME)) + end + return CarbonMonoxideConcentrationMeasurement[direction].attributes[key] +end +CarbonMonoxideConcentrationMeasurement.attributes = {} +setmetatable(CarbonMonoxideConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(CarbonMonoxideConcentrationMeasurement, {__index = cluster_base}) + +return CarbonMonoxideConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..1a7e7b508c --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.CarbonMonoxideConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local CarbonMonoxideConcentrationMeasurementServerAttributes = {} + +function CarbonMonoxideConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(CarbonMonoxideConcentrationMeasurementServerAttributes, attr_mt) + +return CarbonMonoxideConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..cb4cffa2d2 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/init.lua @@ -0,0 +1,111 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local ConcentrationMeasurementServerAttributes = require "embedded_clusters.ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurementTypes = require "embedded_clusters.ConcentrationMeasurement.types" + +local ConcentrationMeasurement = {} + +ConcentrationMeasurement.ID = 0x040C +ConcentrationMeasurement.NAME = "CarbonMonoxideConcentrationMeasurement" +ConcentrationMeasurement.server = {} +ConcentrationMeasurement.client = {} +ConcentrationMeasurement.server.attributes = ConcentrationMeasurementServerAttributes:set_parent_cluster(ConcentrationMeasurement) +ConcentrationMeasurement.types = ConcentrationMeasurementTypes + +function ConcentrationMeasurement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "MeasuredValue", + [0x0001] = "MinMeasuredValue", + [0x0002] = "MaxMeasuredValue", + [0x0003] = "PeakMeasuredValue", + [0x0004] = "PeakMeasuredValueWindow", + [0x0005] = "AverageMeasuredValue", + [0x0006] = "AverageMeasuredValueWindow", + [0x0007] = "Uncertainty", + [0x0008] = "MeasurementUnit", + [0x0009] = "MeasurementMedium", + [0x000A] = "LevelValue", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function ConcentrationMeasurement:get_server_command_by_id(command_id) + local server_id_map = { + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +ConcentrationMeasurement.attribute_direction_map = { + ["MeasuredValue"] = "server", + ["MinMeasuredValue"] = "server", + ["MaxMeasuredValue"] = "server", + ["PeakMeasuredValue"] = "server", + ["PeakMeasuredValueWindow"] = "server", + ["AverageMeasuredValue"] = "server", + ["AverageMeasuredValueWindow"] = "server", + ["Uncertainty"] = "server", + ["MeasurementUnit"] = "server", + ["MeasurementMedium"] = "server", + ["LevelValue"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +ConcentrationMeasurement.command_direction_map = { +} + +ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function ConcentrationMeasurement.are_features_supported(feature, feature_map) + if (ConcentrationMeasurement.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ConcentrationMeasurement.NAME)) + end + return ConcentrationMeasurement[direction].attributes[key] +end +ConcentrationMeasurement.attributes = {} +setmetatable(ConcentrationMeasurement.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = ConcentrationMeasurement.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, ConcentrationMeasurement.NAME)) + end + return ConcentrationMeasurement[direction].commands[key] +end +ConcentrationMeasurement.commands = {} +setmetatable(ConcentrationMeasurement.commands, command_helper_mt) + +local event_helper_mt = {} +event_helper_mt.__index = function(self, key) + return ConcentrationMeasurement.server.events[key] +end +ConcentrationMeasurement.events = {} +setmetatable(ConcentrationMeasurement.events, event_helper_mt) + +setmetatable(ConcentrationMeasurement, {__index = cluster_base}) + +return ConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..e4f88c8491 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,73 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function LevelValue:read(device, endpoint_id, cluster_id) + return cluster_base.read( + device, + endpoint_id, + cluster_id, + self.ID, + nil --event_id + ) +end + + +function LevelValue:subscribe(device, endpoint_id, cluster_id) + return cluster_base.subscribe( + device, + endpoint_id, + cluster_id, + self.ID, + nil --event_id + ) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status, + cluster_id +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + cluster_id, + self.ID, + data, + status + ) +end + +function LevelValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..2ab739841f --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,73 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function MeasuredValue:read(device, endpoint_id, cluster_id) + return cluster_base.read( + device, + endpoint_id, + cluster_id, + self.ID, + nil --event_id + ) +end + + +function MeasuredValue:subscribe(device, endpoint_id, cluster_id) + return cluster_base.subscribe( + device, + endpoint_id, + cluster_id, + self.ID, + nil --event_id + ) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status, + cluster_id +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + cluster_id, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..0fa14745c0 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,72 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function MeasurementUnit:read(device, endpoint_id, cluster_id) + return cluster_base.read( + device, + endpoint_id, + cluster_id, + self.ID, + nil --event_id + ) +end + +function MeasurementUnit:subscribe(device, endpoint_id, cluster_id) + return cluster_base.subscribe( + device, + endpoint_id, + cluster_id, + self.ID, + nil --event_id + ) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status, + cluster_id +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + cluster_id, + self.ID, + data, + status + ) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..19cde9aa55 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.ConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local ConcentrationMeasurementServerAttributes = {} + +function ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ConcentrationMeasurementServerAttributes, attr_mt) + +return ConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/Feature.lua new file mode 100644 index 0000000000..0bb19bec62 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/Feature.lua @@ -0,0 +1,167 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.NUMERIC_MEASUREMENT = 0x0001 +Feature.LEVEL_INDICATION = 0x0002 +Feature.MEDIUM_LEVEL = 0x0004 +Feature.CRITICAL_LEVEL = 0x0008 +Feature.PEAK_MEASUREMENT = 0x0010 +Feature.AVERAGE_MEASUREMENT = 0x0020 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + NUMERIC_MEASUREMENT = 0x0001, + LEVEL_INDICATION = 0x0002, + MEDIUM_LEVEL = 0x0004, + CRITICAL_LEVEL = 0x0008, + PEAK_MEASUREMENT = 0x0010, + AVERAGE_MEASUREMENT = 0x0020, +} + +Feature.is_numeric_measurement_set = function(self) + return (self.value & self.NUMERIC_MEASUREMENT) ~= 0 +end + +Feature.set_numeric_measurement = function(self) + if self.value ~= nil then + self.value = self.value | self.NUMERIC_MEASUREMENT + else + self.value = self.NUMERIC_MEASUREMENT + end +end + +Feature.unset_numeric_measurement = function(self) + self.value = self.value & (~self.NUMERIC_MEASUREMENT & self.BASE_MASK) +end + +Feature.is_level_indication_set = function(self) + return (self.value & self.LEVEL_INDICATION) ~= 0 +end + +Feature.set_level_indication = function(self) + if self.value ~= nil then + self.value = self.value | self.LEVEL_INDICATION + else + self.value = self.LEVEL_INDICATION + end +end + +Feature.unset_level_indication = function(self) + self.value = self.value & (~self.LEVEL_INDICATION & self.BASE_MASK) +end + +Feature.is_medium_level_set = function(self) + return (self.value & self.MEDIUM_LEVEL) ~= 0 +end + +Feature.set_medium_level = function(self) + if self.value ~= nil then + self.value = self.value | self.MEDIUM_LEVEL + else + self.value = self.MEDIUM_LEVEL + end +end + +Feature.unset_medium_level = function(self) + self.value = self.value & (~self.MEDIUM_LEVEL & self.BASE_MASK) +end + +Feature.is_critical_level_set = function(self) + return (self.value & self.CRITICAL_LEVEL) ~= 0 +end + +Feature.set_critical_level = function(self) + if self.value ~= nil then + self.value = self.value | self.CRITICAL_LEVEL + else + self.value = self.CRITICAL_LEVEL + end +end + +Feature.unset_critical_level = function(self) + self.value = self.value & (~self.CRITICAL_LEVEL & self.BASE_MASK) +end + +Feature.is_peak_measurement_set = function(self) + return (self.value & self.PEAK_MEASUREMENT) ~= 0 +end + +Feature.set_peak_measurement = function(self) + if self.value ~= nil then + self.value = self.value | self.PEAK_MEASUREMENT + else + self.value = self.PEAK_MEASUREMENT + end +end + +Feature.unset_peak_measurement = function(self) + self.value = self.value & (~self.PEAK_MEASUREMENT & self.BASE_MASK) +end + +Feature.is_average_measurement_set = function(self) + return (self.value & self.AVERAGE_MEASUREMENT) ~= 0 +end + +Feature.set_average_measurement = function(self) + if self.value ~= nil then + self.value = self.value | self.AVERAGE_MEASUREMENT + else + self.value = self.AVERAGE_MEASUREMENT + end +end + +Feature.unset_average_measurement = function(self) + self.value = self.value & (~self.AVERAGE_MEASUREMENT & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.NUMERIC_MEASUREMENT | + Feature.LEVEL_INDICATION | + Feature.MEDIUM_LEVEL | + Feature.CRITICAL_LEVEL | + Feature.PEAK_MEASUREMENT | + Feature.AVERAGE_MEASUREMENT + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_numeric_measurement_set = Feature.is_numeric_measurement_set, + set_numeric_measurement = Feature.set_numeric_measurement, + unset_numeric_measurement = Feature.unset_numeric_measurement, + is_level_indication_set = Feature.is_level_indication_set, + set_level_indication = Feature.set_level_indication, + unset_level_indication = Feature.unset_level_indication, + is_medium_level_set = Feature.is_medium_level_set, + set_medium_level = Feature.set_medium_level, + unset_medium_level = Feature.unset_medium_level, + is_critical_level_set = Feature.is_critical_level_set, + set_critical_level = Feature.set_critical_level, + unset_critical_level = Feature.unset_critical_level, + is_peak_measurement_set = Feature.is_peak_measurement_set, + set_peak_measurement = Feature.set_peak_measurement, + unset_peak_measurement = Feature.unset_peak_measurement, + is_average_measurement_set = Feature.is_average_measurement_set, + set_average_measurement = Feature.set_average_measurement, + unset_average_measurement = Feature.unset_average_measurement, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/LevelValueEnum.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/LevelValueEnum.lua new file mode 100644 index 0000000000..02b4f727df --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/LevelValueEnum.lua @@ -0,0 +1,42 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local LevelValueEnum = {} +-- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility +-- with how types were handled in api < 10. +local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.UNKNOWN] = "UNKNOWN", + [self.LOW] = "LOW", + [self.MEDIUM] = "MEDIUM", + [self.HIGH] = "HIGH", + [self.CRITICAL] = "CRITICAL", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.UNKNOWN = 0x00 +new_mt.__index.LOW = 0x01 +new_mt.__index.MEDIUM = 0x02 +new_mt.__index.HIGH = 0x03 +new_mt.__index.CRITICAL = 0x04 + +LevelValueEnum.UNKNOWN = 0x00 +LevelValueEnum.LOW = 0x01 +LevelValueEnum.MEDIUM = 0x02 +LevelValueEnum.HIGH = 0x03 +LevelValueEnum.CRITICAL = 0x04 + +LevelValueEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(LevelValueEnum, new_mt) + +return LevelValueEnum + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/MeasurementUnitEnum.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/MeasurementUnitEnum.lua new file mode 100644 index 0000000000..6efd90901a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/MeasurementUnitEnum.lua @@ -0,0 +1,51 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local MeasurementUnitEnum = {} +-- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility +-- with how types were handled in api < 10. +local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.PPM] = "PPM", + [self.PPB] = "PPB", + [self.PPT] = "PPT", + [self.MGM3] = "MGM3", + [self.UGM3] = "UGM3", + [self.NGM3] = "NGM3", + [self.PM3] = "PM3", + [self.BQM3] = "BQM3", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.PPM = 0x00 +new_mt.__index.PPB = 0x01 +new_mt.__index.PPT = 0x02 +new_mt.__index.MGM3 = 0x03 +new_mt.__index.UGM3 = 0x04 +new_mt.__index.NGM3 = 0x05 +new_mt.__index.PM3 = 0x06 +new_mt.__index.BQM3 = 0x07 + +MeasurementUnitEnum.PPM = 0x00 +MeasurementUnitEnum.PPB = 0x01 +MeasurementUnitEnum.PPT = 0x02 +MeasurementUnitEnum.MGM3 = 0x03 +MeasurementUnitEnum.UGM3 = 0x04 +MeasurementUnitEnum.NGM3 = 0x05 +MeasurementUnitEnum.PM3 = 0x06 +MeasurementUnitEnum.BQM3 = 0x07 + +MeasurementUnitEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(MeasurementUnitEnum, new_mt) + +return MeasurementUnitEnum + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/init.lua new file mode 100644 index 0000000000..c339f17414 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/init.lua @@ -0,0 +1,18 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.ConcentrationMeasurement.types." .. key) + end + return types_mt.__types_cache[key] +end + +local ConcentrationMeasurementTypes = {} + +setmetatable(ConcentrationMeasurementTypes, types_mt) + +return ConcentrationMeasurementTypes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua new file mode 100644 index 0000000000..0f564673a9 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua @@ -0,0 +1,56 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local ElectricalEnergyMeasurementServerAttributes = require "embedded_clusters.ElectricalEnergyMeasurement.server.attributes" +local ElectricalEnergyMeasurementTypes = require "embedded_clusters.ElectricalEnergyMeasurement.types" +local ElectricalEnergyMeasurement = {} + +ElectricalEnergyMeasurement.ID = 0x0091 +ElectricalEnergyMeasurement.NAME = "ElectricalEnergyMeasurement" +ElectricalEnergyMeasurement.server = {} +ElectricalEnergyMeasurement.client = {} +ElectricalEnergyMeasurement.server.attributes = ElectricalEnergyMeasurementServerAttributes:set_parent_cluster(ElectricalEnergyMeasurement) +ElectricalEnergyMeasurement.types = ElectricalEnergyMeasurementTypes + +function ElectricalEnergyMeasurement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0001] = "CumulativeEnergyImported", + [0x0003] = "PeriodicEnergyImported", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +ElectricalEnergyMeasurement.attribute_direction_map = { + ["CumulativeEnergyImported"] = "server", + ["PeriodicEnergyImported"] = "server", +} + +ElectricalEnergyMeasurement.FeatureMap = ElectricalEnergyMeasurement.types.Feature + +function ElectricalEnergyMeasurement.are_features_supported(feature, feature_map) + if (ElectricalEnergyMeasurement.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ElectricalEnergyMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalEnergyMeasurement.NAME)) + end + return ElectricalEnergyMeasurement[direction].attributes[key] +end +ElectricalEnergyMeasurement.attributes = {} +setmetatable(ElectricalEnergyMeasurement.attributes, attribute_helper_mt) + +setmetatable(ElectricalEnergyMeasurement, {__index = cluster_base}) + +return ElectricalEnergyMeasurement + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua new file mode 100644 index 0000000000..2d41790440 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua @@ -0,0 +1,71 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CumulativeEnergyImported = { + ID = 0x0001, + NAME = "CumulativeEnergyImported", + base_type = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", +} + +function CumulativeEnergyImported:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function CumulativeEnergyImported:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CumulativeEnergyImported:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CumulativeEnergyImported:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CumulativeEnergyImported:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function CumulativeEnergyImported:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(CumulativeEnergyImported, {__call = CumulativeEnergyImported.new_value, __index = CumulativeEnergyImported.base_type}) +return CumulativeEnergyImported + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua new file mode 100644 index 0000000000..5daccf48ab --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua @@ -0,0 +1,71 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local PeriodicEnergyImported = { + ID = 0x0003, + NAME = "PeriodicEnergyImported", + base_type = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", +} + +function PeriodicEnergyImported:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function PeriodicEnergyImported:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function PeriodicEnergyImported:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function PeriodicEnergyImported:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function PeriodicEnergyImported:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function PeriodicEnergyImported:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(PeriodicEnergyImported, {__call = PeriodicEnergyImported.new_value, __index = PeriodicEnergyImported.base_type}) +return PeriodicEnergyImported + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..57bc0d1f72 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.ElectricalEnergyMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local ElectricalEnergyMeasurementServerAttributes = {} + +function ElectricalEnergyMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ElectricalEnergyMeasurementServerAttributes, attr_mt) + +return ElectricalEnergyMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua new file mode 100644 index 0000000000..a4c58a3646 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua @@ -0,0 +1,101 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +local EnergyMeasurementStruct = {} +local new_mt = StructureABC.new_mt({NAME = "EnergyMeasurementStruct", ID = data_types.name_to_id_map["Structure"]}) + +EnergyMeasurementStruct.field_defs = { + { + name = "energy", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Int64", + }, + { + name = "start_timestamp", + field_id = 1, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint32", + }, + { + name = "end_timestamp", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint32", + }, + { + name = "start_systime", + field_id = 3, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint64", + }, + { + name = "end_systime", + field_id = 4, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint64", + }, +} + +EnergyMeasurementStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +EnergyMeasurementStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = EnergyMeasurementStruct.init +new_mt.__index.serialize = EnergyMeasurementStruct.serialize + +EnergyMeasurementStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(EnergyMeasurementStruct, new_mt) + +return EnergyMeasurementStruct + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua new file mode 100644 index 0000000000..e3db76f49d --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua @@ -0,0 +1,31 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.IMPORTED_ENERGY = 0x0001 +Feature.EXPORTED_ENERGY = 0x0002 +Feature.CUMULATIVE_ENERGY = 0x0004 +Feature.PERIODIC_ENERGY = 0x0008 + +function Feature.bits_are_valid(feature) + local max = + Feature.IMPORTED_ENERGY | + Feature.EXPORTED_ENERGY | + Feature.CUMULATIVE_ENERGY | + Feature.PERIODIC_ENERGY + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua new file mode 100644 index 0000000000..ec29b53e05 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua @@ -0,0 +1,18 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.ElectricalEnergyMeasurement.types." .. key) + end + return types_mt.__types_cache[key] +end + +local ElectricalEnergyMeasurementTypes = {} + +setmetatable(ElectricalEnergyMeasurementTypes, types_mt) + +return ElectricalEnergyMeasurementTypes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/init.lua new file mode 100644 index 0000000000..c9061e3cc4 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/init.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local ElectricalPowerMeasurementServerAttributes = require "embedded_clusters.ElectricalPowerMeasurement.server.attributes" + +local ElectricalPowerMeasurement = {} + +ElectricalPowerMeasurement.ID = 0x0090 +ElectricalPowerMeasurement.NAME = "ElectricalPowerMeasurement" +ElectricalPowerMeasurement.server = {} +ElectricalPowerMeasurement.client = {} +ElectricalPowerMeasurement.server.attributes = ElectricalPowerMeasurementServerAttributes:set_parent_cluster(ElectricalPowerMeasurement) + +function ElectricalPowerMeasurement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0008] = "ActivePower", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +ElectricalPowerMeasurement.attribute_direction_map = { + ["ActivePower"] = "server", +} + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ElectricalPowerMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalPowerMeasurement.NAME)) + end + return ElectricalPowerMeasurement[direction].attributes[key] +end +ElectricalPowerMeasurement.attributes = {} +setmetatable(ElectricalPowerMeasurement.attributes, attribute_helper_mt) + +setmetatable(ElectricalPowerMeasurement, {__index = cluster_base}) + +return ElectricalPowerMeasurement + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua new file mode 100644 index 0000000000..f1696509f5 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua @@ -0,0 +1,70 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ActivePower = { + ID = 0x0008, + NAME = "ActivePower", + base_type = require "st.matter.data_types.Int64", +} + +function ActivePower:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function ActivePower:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ActivePower:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ActivePower:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ActivePower:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function ActivePower:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(ActivePower, {__call = ActivePower.new_value, __index = ActivePower.base_type}) +return ActivePower diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..6de69b94ff --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.ElectricalPowerMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local ElectricalPowerMeasurementServerAttributes = {} + +function ElectricalPowerMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ElectricalPowerMeasurementServerAttributes, attr_mt) + +return ElectricalPowerMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..cdfd1d597e --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local FormaldehydeConcentrationMeasurementServerAttributes = require "embedded_clusters.FormaldehydeConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local FormaldehydeConcentrationMeasurement = {} + +FormaldehydeConcentrationMeasurement.ID = 0x042B +FormaldehydeConcentrationMeasurement.NAME = "FormaldehydeConcentrationMeasurement" +FormaldehydeConcentrationMeasurement.server = {} +FormaldehydeConcentrationMeasurement.client = {} +FormaldehydeConcentrationMeasurement.server.attributes = FormaldehydeConcentrationMeasurementServerAttributes:set_parent_cluster(FormaldehydeConcentrationMeasurement) +FormaldehydeConcentrationMeasurement.types = ConcentrationMeasurement.types + +function FormaldehydeConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function FormaldehydeConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +FormaldehydeConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +FormaldehydeConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function FormaldehydeConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = FormaldehydeConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, FormaldehydeConcentrationMeasurement.NAME)) + end + return FormaldehydeConcentrationMeasurement[direction].attributes[key] +end +FormaldehydeConcentrationMeasurement.attributes = {} +setmetatable(FormaldehydeConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(FormaldehydeConcentrationMeasurement, {__index = cluster_base}) + +return FormaldehydeConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..3dee13abe3 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.FormaldehydeConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local FormaldehydeConcentrationMeasurementServerAttributes = {} + +function FormaldehydeConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(FormaldehydeConcentrationMeasurementServerAttributes, attr_mt) + +return FormaldehydeConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/init.lua new file mode 100644 index 0000000000..7a36d3aa92 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/init.lua @@ -0,0 +1,105 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local HepaFilterMonitoringServerAttributes = require "embedded_clusters.HepaFilterMonitoring.server.attributes" +local HepaFilterMonitoringServerCommands = require "embedded_clusters.HepaFilterMonitoring.server.commands" +local HepaFilterMonitoringTypes = require "embedded_clusters.HepaFilterMonitoring.types" + +local HepaFilterMonitoring = {} + +HepaFilterMonitoring.ID = 0x0071 +HepaFilterMonitoring.NAME = "HepaFilterMonitoring" +HepaFilterMonitoring.server = {} +HepaFilterMonitoring.client = {} +HepaFilterMonitoring.server.attributes = HepaFilterMonitoringServerAttributes:set_parent_cluster(HepaFilterMonitoring) +HepaFilterMonitoring.server.commands = HepaFilterMonitoringServerCommands:set_parent_cluster(HepaFilterMonitoring) +HepaFilterMonitoring.types = HepaFilterMonitoringTypes + +function HepaFilterMonitoring:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "Condition", + [0x0001] = "DegradationDirection", + [0x0002] = "ChangeIndication", + [0x0003] = "InPlaceIndicator", + [0x0004] = "LastChangedTime", + [0x0005] = "ReplacementProductList", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function HepaFilterMonitoring:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "ResetCondition", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +HepaFilterMonitoring.attribute_direction_map = { + ["Condition"] = "server", + ["DegradationDirection"] = "server", + ["ChangeIndication"] = "server", + ["InPlaceIndicator"] = "server", + ["LastChangedTime"] = "server", + ["ReplacementProductList"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +HepaFilterMonitoring.command_direction_map = { + ["ResetCondition"] = "server", +} + +HepaFilterMonitoring.FeatureMap = HepaFilterMonitoring.types.Feature + +function HepaFilterMonitoring.are_features_supported(feature, feature_map) + if (HepaFilterMonitoring.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = HepaFilterMonitoring.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, HepaFilterMonitoring.NAME)) + end + return HepaFilterMonitoring[direction].attributes[key] +end +HepaFilterMonitoring.attributes = {} +setmetatable(HepaFilterMonitoring.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = HepaFilterMonitoring.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, HepaFilterMonitoring.NAME)) + end + return HepaFilterMonitoring[direction].commands[key] +end +HepaFilterMonitoring.commands = {} +setmetatable(HepaFilterMonitoring.commands, command_helper_mt) + +local event_helper_mt = {} +event_helper_mt.__index = function(self, key) + return HepaFilterMonitoring.server.events[key] +end +HepaFilterMonitoring.events = {} +setmetatable(HepaFilterMonitoring.events, event_helper_mt) + +setmetatable(HepaFilterMonitoring, {__index = cluster_base}) + +return HepaFilterMonitoring + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/ChangeIndication.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/ChangeIndication.lua new file mode 100644 index 0000000000..9dca020204 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/ChangeIndication.lua @@ -0,0 +1,71 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ChangeIndication = { + ID = 0x0002, + NAME = "ChangeIndication", + base_type = require "embedded_clusters.HepaFilterMonitoring.types.ChangeIndicationEnum", +} + +function ChangeIndication:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function ChangeIndication:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function ChangeIndication:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function ChangeIndication:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ChangeIndication:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function ChangeIndication:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(ChangeIndication, {__call = ChangeIndication.new_value, __index = ChangeIndication.base_type}) +return ChangeIndication + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/Condition.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/Condition.lua new file mode 100644 index 0000000000..76a4fd9760 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/Condition.lua @@ -0,0 +1,71 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local Condition = { + ID = 0x0000, + NAME = "Condition", + base_type = require "st.matter.data_types.Uint8", +} + +function Condition:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function Condition:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function Condition:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function Condition:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Condition:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function Condition:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(Condition, {__call = Condition.new_value, __index = Condition.base_type}) +return Condition + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/init.lua new file mode 100644 index 0000000000..2590980846 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.HepaFilterMonitoring.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local HepaFilterMonitoringServerAttributes = {} + +function HepaFilterMonitoringServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(HepaFilterMonitoringServerAttributes, attr_mt) + +return HepaFilterMonitoringServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/ResetCondition.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/ResetCondition.lua new file mode 100644 index 0000000000..1ac942f780 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/ResetCondition.lua @@ -0,0 +1,94 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ResetCondition = {} + +ResetCondition.NAME = "ResetCondition" +ResetCondition.ID = 0x0000 +ResetCondition.field_defs = { +} + +function ResetCondition:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, + status + ) +end + +function ResetCondition:init(device, endpoint_id) + local out = {} + local args = {} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = ResetCondition, + __tostring = ResetCondition.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function ResetCondition:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ResetCondition:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + base_type_obj.elements = elems +end + +function ResetCondition:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(ResetCondition, {__call = ResetCondition.init}) + +return ResetCondition + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/init.lua new file mode 100644 index 0000000000..77ae141f77 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/init.lua @@ -0,0 +1,26 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("embedded_clusters.HepaFilterMonitoring.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local HepaFilterMonitoringServerCommands = {} + +function HepaFilterMonitoringServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(HepaFilterMonitoringServerCommands, command_mt) + +return HepaFilterMonitoringServerCommands + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/ChangeIndicationEnum.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/ChangeIndicationEnum.lua new file mode 100644 index 0000000000..6b5ab62494 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/ChangeIndicationEnum.lua @@ -0,0 +1,36 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local ChangeIndicationEnum = {} +-- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility +-- with how types were handled in api < 10. +local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.OK] = "OK", + [self.WARNING] = "WARNING", + [self.CRITICAL] = "CRITICAL", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.OK = 0x00 +new_mt.__index.WARNING = 0x01 +new_mt.__index.CRITICAL = 0x02 + +ChangeIndicationEnum.OK = 0x00 +ChangeIndicationEnum.WARNING = 0x01 +ChangeIndicationEnum.CRITICAL = 0x02 + +ChangeIndicationEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(ChangeIndicationEnum, new_mt) + +return ChangeIndicationEnum + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/Feature.lua new file mode 100644 index 0000000000..906e769676 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/Feature.lua @@ -0,0 +1,101 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.CONDITION = 0x0001 +Feature.WARNING = 0x0002 +Feature.REPLACEMENT_PRODUCT_LIST = 0x0004 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + CONDITION = 0x0001, + WARNING = 0x0002, + REPLACEMENT_PRODUCT_LIST = 0x0004, +} + +Feature.is_condition_set = function(self) + return (self.value & self.CONDITION) ~= 0 +end + +Feature.set_condition = function(self) + if self.value ~= nil then + self.value = self.value | self.CONDITION + else + self.value = self.CONDITION + end +end + +Feature.unset_condition = function(self) + self.value = self.value & (~self.CONDITION & self.BASE_MASK) +end + +Feature.is_warning_set = function(self) + return (self.value & self.WARNING) ~= 0 +end + +Feature.set_warning = function(self) + if self.value ~= nil then + self.value = self.value | self.WARNING + else + self.value = self.WARNING + end +end + +Feature.unset_warning = function(self) + self.value = self.value & (~self.WARNING & self.BASE_MASK) +end + +Feature.is_replacement_product_list_set = function(self) + return (self.value & self.REPLACEMENT_PRODUCT_LIST) ~= 0 +end + +Feature.set_replacement_product_list = function(self) + if self.value ~= nil then + self.value = self.value | self.REPLACEMENT_PRODUCT_LIST + else + self.value = self.REPLACEMENT_PRODUCT_LIST + end +end + +Feature.unset_replacement_product_list = function(self) + self.value = self.value & (~self.REPLACEMENT_PRODUCT_LIST & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.CONDITION | + Feature.WARNING | + Feature.REPLACEMENT_PRODUCT_LIST + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_condition_set = Feature.is_condition_set, + set_condition = Feature.set_condition, + unset_condition = Feature.unset_condition, + is_warning_set = Feature.is_warning_set, + set_warning = Feature.set_warning, + unset_warning = Feature.unset_warning, + is_replacement_product_list_set = Feature.is_replacement_product_list_set, + set_replacement_product_list = Feature.set_replacement_product_list, + unset_replacement_product_list = Feature.unset_replacement_product_list, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/init.lua new file mode 100644 index 0000000000..6c36af1aec --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/init.lua @@ -0,0 +1,18 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.HepaFilterMonitoring.types." .. key) + end + return types_mt.__types_cache[key] +end + +local HepaFilterMonitoringTypes = {} + +setmetatable(HepaFilterMonitoringTypes, types_mt) + +return HepaFilterMonitoringTypes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..eae9be65f0 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local NitrogenDioxideConcentrationMeasurementServerAttributes = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local NitrogenDioxideConcentrationMeasurement = {} + +NitrogenDioxideConcentrationMeasurement.ID = 0x0413 +NitrogenDioxideConcentrationMeasurement.NAME = "NitrogenDioxideConcentrationMeasurement" +NitrogenDioxideConcentrationMeasurement.server = {} +NitrogenDioxideConcentrationMeasurement.client = {} +NitrogenDioxideConcentrationMeasurement.server.attributes = NitrogenDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(NitrogenDioxideConcentrationMeasurement) +NitrogenDioxideConcentrationMeasurement.types = ConcentrationMeasurement.types + +function NitrogenDioxideConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function NitrogenDioxideConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +NitrogenDioxideConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +NitrogenDioxideConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function NitrogenDioxideConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = NitrogenDioxideConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, NitrogenDioxideConcentrationMeasurement.NAME)) + end + return NitrogenDioxideConcentrationMeasurement[direction].attributes[key] +end +NitrogenDioxideConcentrationMeasurement.attributes = {} +setmetatable(NitrogenDioxideConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(NitrogenDioxideConcentrationMeasurement, {__index = cluster_base}) + +return NitrogenDioxideConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..c82517d362 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.NitrogenDioxideConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local NitrogenDioxideConcentrationMeasurementServerAttributes = {} + +function NitrogenDioxideConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(NitrogenDioxideConcentrationMeasurementServerAttributes, attr_mt) + +return NitrogenDioxideConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..c49ea94b39 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local OzoneConcentrationMeasurementServerAttributes = require "embedded_clusters.OzoneConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local OzoneConcentrationMeasurement = {} + +OzoneConcentrationMeasurement.ID = 0x0415 +OzoneConcentrationMeasurement.NAME = "OzoneConcentrationMeasurement" +OzoneConcentrationMeasurement.server = {} +OzoneConcentrationMeasurement.client = {} +OzoneConcentrationMeasurement.server.attributes = OzoneConcentrationMeasurementServerAttributes:set_parent_cluster(OzoneConcentrationMeasurement) +OzoneConcentrationMeasurement.types = ConcentrationMeasurement.types + +function OzoneConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function OzoneConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +OzoneConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +OzoneConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function OzoneConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = OzoneConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, OzoneConcentrationMeasurement.NAME)) + end + return OzoneConcentrationMeasurement[direction].attributes[key] +end +OzoneConcentrationMeasurement.attributes = {} +setmetatable(OzoneConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(OzoneConcentrationMeasurement, {__index = cluster_base}) + +return OzoneConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..918b680495 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.OzoneConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local OzoneConcentrationMeasurementServerAttributes = {} + +function OzoneConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(OzoneConcentrationMeasurementServerAttributes, attr_mt) + +return OzoneConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..3b333b5417 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local Pm10ConcentrationMeasurementServerAttributes = require "embedded_clusters.Pm10ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local Pm10ConcentrationMeasurement = {} + +Pm10ConcentrationMeasurement.ID = 0x042D +Pm10ConcentrationMeasurement.NAME = "Pm10ConcentrationMeasurement" +Pm10ConcentrationMeasurement.server = {} +Pm10ConcentrationMeasurement.client = {} +Pm10ConcentrationMeasurement.server.attributes = Pm10ConcentrationMeasurementServerAttributes:set_parent_cluster(Pm10ConcentrationMeasurement) +Pm10ConcentrationMeasurement.types = ConcentrationMeasurement.types + +function Pm10ConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function Pm10ConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +Pm10ConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +Pm10ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function Pm10ConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = Pm10ConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, Pm10ConcentrationMeasurement.NAME)) + end + return Pm10ConcentrationMeasurement[direction].attributes[key] +end +Pm10ConcentrationMeasurement.attributes = {} +setmetatable(Pm10ConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(Pm10ConcentrationMeasurement, {__index = cluster_base}) + +return Pm10ConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..3b1e6617a4 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.Pm10ConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local Pm10ConcentrationMeasurementServerAttributes = {} + +function Pm10ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(Pm10ConcentrationMeasurementServerAttributes, attr_mt) + +return Pm10ConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..b2e6656a9a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local Pm1ConcentrationMeasurementServerAttributes = require "embedded_clusters.Pm1ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local Pm1ConcentrationMeasurement = {} + +Pm1ConcentrationMeasurement.ID = 0x042C +Pm1ConcentrationMeasurement.NAME = "Pm1ConcentrationMeasurement" +Pm1ConcentrationMeasurement.server = {} +Pm1ConcentrationMeasurement.client = {} +Pm1ConcentrationMeasurement.server.attributes = Pm1ConcentrationMeasurementServerAttributes:set_parent_cluster(Pm1ConcentrationMeasurement) +Pm1ConcentrationMeasurement.types = ConcentrationMeasurement.types + +function Pm1ConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function Pm1ConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +Pm1ConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +Pm1ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function Pm1ConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = Pm1ConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, Pm1ConcentrationMeasurement.NAME)) + end + return Pm1ConcentrationMeasurement[direction].attributes[key] +end +Pm1ConcentrationMeasurement.attributes = {} +setmetatable(Pm1ConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(Pm1ConcentrationMeasurement, {__index = cluster_base}) + +return Pm1ConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..2635da32a6 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.Pm1ConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local Pm1ConcentrationMeasurementServerAttributes = {} + +function Pm1ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(Pm1ConcentrationMeasurementServerAttributes, attr_mt) + +return Pm1ConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..e6e6144f94 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local Pm25ConcentrationMeasurementServerAttributes = require "embedded_clusters.Pm25ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local Pm25ConcentrationMeasurement = {} + +Pm25ConcentrationMeasurement.ID = 0x042A +Pm25ConcentrationMeasurement.NAME = "Pm25ConcentrationMeasurement" +Pm25ConcentrationMeasurement.server = {} +Pm25ConcentrationMeasurement.client = {} +Pm25ConcentrationMeasurement.server.attributes = Pm25ConcentrationMeasurementServerAttributes:set_parent_cluster(Pm25ConcentrationMeasurement) +Pm25ConcentrationMeasurement.types = ConcentrationMeasurement.types + +function Pm25ConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function Pm25ConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +Pm25ConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +Pm25ConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function Pm25ConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = Pm25ConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, Pm25ConcentrationMeasurement.NAME)) + end + return Pm25ConcentrationMeasurement[direction].attributes[key] +end +Pm25ConcentrationMeasurement.attributes = {} +setmetatable(Pm25ConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(Pm25ConcentrationMeasurement, {__index = cluster_base}) + +return Pm25ConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..5c432da0ec --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.Pm25ConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local Pm25ConcentrationMeasurementServerAttributes = {} + +function Pm25ConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(Pm25ConcentrationMeasurementServerAttributes, attr_mt) + +return Pm25ConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..3d4b21d602 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local RadonConcentrationMeasurementServerAttributes = require "embedded_clusters.RadonConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local RadonConcentrationMeasurement = {} + +RadonConcentrationMeasurement.ID = 0x042F +RadonConcentrationMeasurement.NAME = "RadonConcentrationMeasurement" +RadonConcentrationMeasurement.server = {} +RadonConcentrationMeasurement.client = {} +RadonConcentrationMeasurement.server.attributes = RadonConcentrationMeasurementServerAttributes:set_parent_cluster(RadonConcentrationMeasurement) +RadonConcentrationMeasurement.types = ConcentrationMeasurement.types + +function RadonConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function RadonConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +RadonConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +RadonConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function RadonConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = RadonConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, RadonConcentrationMeasurement.NAME)) + end + return RadonConcentrationMeasurement[direction].attributes[key] +end +RadonConcentrationMeasurement.attributes = {} +setmetatable(RadonConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(RadonConcentrationMeasurement, {__index = cluster_base}) + +return RadonConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..8aa225e510 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.RadonConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local RadonConcentrationMeasurementServerAttributes = {} + +function RadonConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(RadonConcentrationMeasurementServerAttributes, attr_mt) + +return RadonConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua new file mode 100644 index 0000000000..cad49a14e1 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua @@ -0,0 +1,47 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" + +local TotalVolatileOrganicCompoundsConcentrationMeasurement = {} + +TotalVolatileOrganicCompoundsConcentrationMeasurement.ID = 0x042E +TotalVolatileOrganicCompoundsConcentrationMeasurement.NAME = "TotalVolatileOrganicCompoundsConcentrationMeasurement" +TotalVolatileOrganicCompoundsConcentrationMeasurement.server = {} +TotalVolatileOrganicCompoundsConcentrationMeasurement.client = {} +TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes = TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes:set_parent_cluster(TotalVolatileOrganicCompoundsConcentrationMeasurement) +TotalVolatileOrganicCompoundsConcentrationMeasurement.types = ConcentrationMeasurement.types + +function TotalVolatileOrganicCompoundsConcentrationMeasurement:get_attribute_by_id(attr_id) + return ConcentrationMeasurement:get_attribute_by_id(attr_id) +end + +function TotalVolatileOrganicCompoundsConcentrationMeasurement:get_server_command_by_id(command_id) + return ConcentrationMeasurement:get_server_command_by_id(command_id) +end + +TotalVolatileOrganicCompoundsConcentrationMeasurement.attribute_direction_map = ConcentrationMeasurement.attribute_direction_map + +TotalVolatileOrganicCompoundsConcentrationMeasurement.FeatureMap = ConcentrationMeasurement.types.Feature + +function TotalVolatileOrganicCompoundsConcentrationMeasurement.are_features_supported(feature, feature_map) + return ConcentrationMeasurement.are_features_supported(feature, feature_map) +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = TotalVolatileOrganicCompoundsConcentrationMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, TotalVolatileOrganicCompoundsConcentrationMeasurement.NAME)) + end + return TotalVolatileOrganicCompoundsConcentrationMeasurement[direction].attributes[key] +end +TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes = {} +setmetatable(TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes, attribute_helper_mt) + +setmetatable(TotalVolatileOrganicCompoundsConcentrationMeasurement, {__index = cluster_base}) + +return TotalVolatileOrganicCompoundsConcentrationMeasurement + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..b67cbcd7b6 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes = {} + +function TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes, attr_mt) + +return TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/init.lua new file mode 100644 index 0000000000..3a1c2f1bfb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/init.lua @@ -0,0 +1,95 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local WaterHeaterModeServerAttributes = require "embedded_clusters.WaterHeaterMode.server.attributes" +local WaterHeaterModeServerCommands = require "embedded_clusters.WaterHeaterMode.server.commands" +local WaterHeaterModeTypes = require "embedded_clusters.WaterHeaterMode.types" + +local WaterHeaterMode = {} + +WaterHeaterMode.ID = 0x009E +WaterHeaterMode.NAME = "WaterHeaterMode" +WaterHeaterMode.server = {} +WaterHeaterMode.client = {} +WaterHeaterMode.server.attributes = WaterHeaterModeServerAttributes:set_parent_cluster(WaterHeaterMode) +WaterHeaterMode.server.commands = WaterHeaterModeServerCommands:set_parent_cluster(WaterHeaterMode) +WaterHeaterMode.types = WaterHeaterModeTypes + +function WaterHeaterMode:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "SupportedModes", + [0x0001] = "CurrentMode", + [0x0002] = "StartUpMode", + [0x0003] = "OnMode", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function WaterHeaterMode:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "ChangeToMode", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +WaterHeaterMode.attribute_direction_map = { + ["SupportedModes"] = "server", + ["CurrentMode"] = "server", + ["StartUpMode"] = "server", + ["OnMode"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +WaterHeaterMode.command_direction_map = { + ["ChangeToMode"] = "server", + ["ChangeToModeResponse"] = "client", +} + +WaterHeaterMode.FeatureMap = WaterHeaterMode.types.Feature + +function WaterHeaterMode.are_features_supported(feature, feature_map) + if (WaterHeaterMode.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = WaterHeaterMode.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, WaterHeaterMode.NAME)) + end + return WaterHeaterMode[direction].attributes[key] +end +WaterHeaterMode.attributes = {} +setmetatable(WaterHeaterMode.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = WaterHeaterMode.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, WaterHeaterMode.NAME)) + end + return WaterHeaterMode[direction].commands[key] +end +WaterHeaterMode.commands = {} +setmetatable(WaterHeaterMode.commands, command_helper_mt) + +setmetatable(WaterHeaterMode, {__index = cluster_base}) + +return WaterHeaterMode + diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/CurrentMode.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/CurrentMode.lua similarity index 93% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/CurrentMode.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/CurrentMode.lua index aa20156f74..165b906df5 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/CurrentMode.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/CurrentMode.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -24,7 +27,6 @@ function CurrentMode:read(device, endpoint_id) ) end - function CurrentMode:subscribe(device, endpoint_id) return cluster_base.subscribe( device, diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/SupportedModes.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/SupportedModes.lua similarity index 90% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/SupportedModes.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/SupportedModes.lua index 1f393a17d4..903a374d43 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/SupportedModes.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/SupportedModes.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -6,7 +9,7 @@ local SupportedModes = { ID = 0x0000, NAME = "SupportedModes", base_type = require "st.matter.data_types.Array", - element_type = require "WaterHeaterMode.types.ModeOptionStruct", + element_type = require "embedded_clusters.WaterHeaterMode.types.ModeOptionStruct", } function SupportedModes:augment_type(data_type_obj) diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/init.lua new file mode 100644 index 0000000000..fb7bed9828 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.WaterHeaterMode.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local WaterHeaterModeServerAttributes = {} + +function WaterHeaterModeServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(WaterHeaterModeServerAttributes, attr_mt) + +return WaterHeaterModeServerAttributes + diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/ChangeToMode.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/ChangeToMode.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/ChangeToMode.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/ChangeToMode.lua index 73ddbd1029..726352b448 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/ChangeToMode.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/ChangeToMode.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/init.lua new file mode 100644 index 0000000000..64660ed336 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/init.lua @@ -0,0 +1,26 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("embedded_clusters.WaterHeaterMode.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local WaterHeaterModeServerCommands = {} + +function WaterHeaterModeServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(WaterHeaterModeServerCommands, command_mt) + +return WaterHeaterModeServerCommands + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/Feature.lua new file mode 100644 index 0000000000..c7e987399d --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/Feature.lua @@ -0,0 +1,57 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.ON_OFF = 0x0001 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + ON_OFF = 0x0001, +} + +Feature.is_on_off_set = function(self) + return (self.value & self.ON_OFF) ~= 0 +end + +Feature.set_on_off = function(self) + if self.value ~= nil then + self.value = self.value | self.ON_OFF + else + self.value = self.ON_OFF + end +end + +Feature.unset_on_off = function(self) + self.value = self.value & (~self.ON_OFF & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.ON_OFF + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_on_off_set = Feature.is_on_off_set, + set_on_off = Feature.set_on_off, + unset_on_off = Feature.unset_on_off, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeOptionStruct.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeOptionStruct.lua similarity index 94% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeOptionStruct.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeOptionStruct.lua index f770a7916c..1db8c5b634 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeOptionStruct.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeOptionStruct.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local StructureABC = require "st.matter.data_types.base_defs.StructureABC" @@ -24,7 +27,7 @@ ModeOptionStruct.field_defs = { is_nullable = false, is_optional = false, data_type = require "st.matter.data_types.Array", - element_type = require "WaterHeaterMode.types.ModeTagStruct", + element_type = require "embedded_clusters.WaterHeaterMode.types.ModeTagStruct", }, } diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTag.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTag.lua similarity index 90% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTag.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTag.lua index 009e70a40e..7d37d4374d 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTag.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTag.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTagStruct.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTagStruct.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTagStruct.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTagStruct.lua index 1c41eb320e..17dfd269d6 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTagStruct.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTagStruct.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local StructureABC = require "st.matter.data_types.base_defs.StructureABC" diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/init.lua new file mode 100644 index 0000000000..bae583dfb4 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/init.lua @@ -0,0 +1,18 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.WaterHeaterMode.types." .. key) + end + return types_mt.__types_cache[key] +end + +local WaterHeaterModeTypes = {} + +setmetatable(WaterHeaterModeTypes, types_mt) + +return WaterHeaterModeTypes + diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index baa20bc31c..54cb3318f6 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -1,2313 +1,515 @@ --- Copyright 2022 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -local capabilities = require "st.capabilities" -local log = require "log" -local clusters = require "st.matter.clusters" -local embedded_cluster_utils = require "embedded-cluster-utils" -local im = require "st.matter.interaction_model" - -local MatterDriver = require "st.matter.driver" -local utils = require "st.utils" - -local SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities" --- declare match_profile function for use throughout file -local match_profile - --- Include driver-side definitions when lua libs api version is < 10 -local version = require "version" -if version.api < 10 then - clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" - clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" - -- new modes add in Matter 1.2 - clusters.Thermostat.types.ThermostatSystemMode.DRY = 0x8 - clusters.Thermostat.types.ThermostatSystemMode.SLEEP = 0x9 -end - -local SAVED_SYSTEM_MODE_IB = "__saved_system_mode_ib" -local DISALLOWED_THERMOSTAT_MODES = "__DISALLOWED_CONTROL_OPERATIONS" -local OPTIONAL_THERMOSTAT_MODES_SEEN = "__OPTIONAL_THERMOSTAT_MODES_SEEN" - -if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" -end - -if version.api < 13 then - clusters.WaterHeaterMode = require "WaterHeaterMode" -end - -local THERMOSTAT_MODE_MAP = { - [clusters.Thermostat.types.ThermostatSystemMode.OFF] = capabilities.thermostatMode.thermostatMode.off, - [clusters.Thermostat.types.ThermostatSystemMode.AUTO] = capabilities.thermostatMode.thermostatMode.auto, - [clusters.Thermostat.types.ThermostatSystemMode.COOL] = capabilities.thermostatMode.thermostatMode.cool, - [clusters.Thermostat.types.ThermostatSystemMode.HEAT] = capabilities.thermostatMode.thermostatMode.heat, - [clusters.Thermostat.types.ThermostatSystemMode.EMERGENCY_HEATING] = capabilities.thermostatMode.thermostatMode.emergency_heat, - [clusters.Thermostat.types.ThermostatSystemMode.PRECOOLING] = capabilities.thermostatMode.thermostatMode.precooling, - [clusters.Thermostat.types.ThermostatSystemMode.FAN_ONLY] = capabilities.thermostatMode.thermostatMode.fanonly, - [clusters.Thermostat.types.ThermostatSystemMode.DRY] = capabilities.thermostatMode.thermostatMode.dryair, - [clusters.Thermostat.types.ThermostatSystemMode.SLEEP] = capabilities.thermostatMode.thermostatMode.asleep, -} - -local THERMOSTAT_OPERATING_MODE_MAP = { - [0] = capabilities.thermostatOperatingState.thermostatOperatingState.heating, - [1] = capabilities.thermostatOperatingState.thermostatOperatingState.cooling, - [2] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, - [3] = capabilities.thermostatOperatingState.thermostatOperatingState.heating, - [4] = capabilities.thermostatOperatingState.thermostatOperatingState.cooling, - [5] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, - [6] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, -} - -local WIND_MODE_MAP = { - [0] = capabilities.windMode.windMode.sleepWind, - [1] = capabilities.windMode.windMode.naturalWind -} - -local ROCK_MODE_MAP = { - [0] = capabilities.fanOscillationMode.fanOscillationMode.horizontal, - [1] = capabilities.fanOscillationMode.fanOscillationMode.vertical, - [2] = capabilities.fanOscillationMode.fanOscillationMode.swing -} - -local RAC_DEVICE_TYPE_ID = 0x0072 -local AP_DEVICE_TYPE_ID = 0x002D -local FAN_DEVICE_TYPE_ID = 0x002B -local WATER_HEATER_DEVICE_TYPE_ID = 0x050F -local HEAT_PUMP_DEVICE_TYPE_ID = 0x0309 -local THERMOSTAT_DEVICE_TYPE_ID = 0x0301 -local ELECTRICAL_SENSOR_DEVICE_TYPE_ID = 0x0510 - -local MIN_ALLOWED_PERCENT_VALUE = 0 -local MAX_ALLOWED_PERCENT_VALUE = 100 -local DEFAULT_REPORT_TIME_INTERVAL = 15 * 60 -- Report cumulative energy every 15 minutes -local MAX_REPORT_TIMEOUT = 30 * 60 -local POLL_INTERVAL = 60 -- To read CumulativeEnergyImported every 60 seconds. - -local RECURRING_POLL_TIMER = "__recurring_poll_timer" -local RECURRING_REPORT_TIMER = "__recurring_report_poll_timer" -local DEVICE_POWER_CONSUMPTION_REPORT_TIME_INTERVAL = "__pcr_time_interval" -local DEVICE_REPORTING_TIME_INTERVAL_CONSIDERED = "__timer_interval_considered" -local TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP = "__total_cumulative_energy_imported_map" -local LAST_REPORTED_TIME = "__last_reported_time" -local SUPPORTED_WATER_HEATER_MODES_WITH_IDX = "__supported_water_heater_modes_with_idx" -local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -local MGM3_PPM_CONVERSION_FACTOR = 24.45 - --- For RPC version >= 6, we can always assume that the values received from temperatureSetpoint --- are in Celsius, but we still limit the setpoint range to somewhat reasonable values. --- For RPC <= 5, this is a work around to handle when units for temperatureSetpoint is changed for the App. --- When units are switched, we will never know the units of the received command value as the arguments don't contain the unit. --- So to handle this we assume the following ranges considering usual thermostat/water-heater temperatures: --- Thermostat: --- 1. if the received setpoint command value is in range 5 ~ 40, it is inferred as *C --- 2. if the received setpoint command value is in range 41 ~ 104, it is inferred as *F -local THERMOSTAT_MAX_TEMP_IN_C = version.rpc >= 6 and 100.0 or 40.0 -local THERMOSTAT_MIN_TEMP_IN_C = version.rpc >= 6 and 0.0 or 5.0 --- Water Heater: --- 1. if the received setpoint command value is in range 30 ~ 80, it is inferred as *C --- 2. if the received setpoint command value is in range 86 ~ 176, it is inferred as *F -local WATER_HEATER_MAX_TEMP_IN_C = version.rpc >= 6 and 100.0 or 80.0 -local WATER_HEATER_MIN_TEMP_IN_C = version.rpc >= 6 and 0.0 or 30.0 - -local setpoint_limit_device_field = { - MIN_SETPOINT_DEADBAND_CHECKED = "MIN_SETPOINT_DEADBAND_CHECKED", - MIN_HEAT = "MIN_HEAT", - MAX_HEAT = "MAX_HEAT", - MIN_COOL = "MIN_COOL", - MAX_COOL = "MAX_COOL", - MIN_DEADBAND = "MIN_DEADBAND", - MIN_TEMP = "MIN_TEMP", - MAX_TEMP = "MAX_TEMP" -} - -local battery_support = { - NO_BATTERY = "NO_BATTERY", - BATTERY_LEVEL = "BATTERY_LEVEL", - BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" -} - -local profiling_data = { - BATTERY_SUPPORT = "__BATTERY_SUPPORT", - THERMOSTAT_RUNNING_STATE_SUPPORT = "__THERMOSTAT_RUNNING_STATE_SUPPORT" -} - -local subscribed_attributes = { - [capabilities.switch.ID] = { - clusters.OnOff.attributes.OnOff - }, - [capabilities.temperatureMeasurement.ID] = { - clusters.Thermostat.attributes.LocalTemperature, - clusters.TemperatureMeasurement.attributes.MeasuredValue, - clusters.TemperatureMeasurement.attributes.MinMeasuredValue, - clusters.TemperatureMeasurement.attributes.MaxMeasuredValue - }, - [capabilities.relativeHumidityMeasurement.ID] = { - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue - }, - [capabilities.thermostatMode.ID] = { - clusters.Thermostat.attributes.SystemMode, - clusters.Thermostat.attributes.ControlSequenceOfOperation - }, - [capabilities.thermostatOperatingState.ID] = { - clusters.Thermostat.attributes.ThermostatRunningState - }, - [capabilities.thermostatFanMode.ID] = { - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode - }, - [capabilities.thermostatCoolingSetpoint.ID] = { - clusters.Thermostat.attributes.OccupiedCoolingSetpoint, - clusters.Thermostat.attributes.AbsMinCoolSetpointLimit, - clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit - }, - [capabilities.thermostatHeatingSetpoint.ID] = { - clusters.Thermostat.attributes.OccupiedHeatingSetpoint, - clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, - clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit - }, - [capabilities.airConditionerFanMode.ID] = { - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode - }, - [capabilities.airPurifierFanMode.ID] = { - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode - }, - [capabilities.fanMode.ID] = { - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode - }, - [capabilities.fanSpeedPercent.ID] = { - clusters.FanControl.attributes.PercentCurrent - }, - [capabilities.windMode.ID] = { - clusters.FanControl.attributes.WindSupport, - clusters.FanControl.attributes.WindSetting - }, - [capabilities.fanOscillationMode.ID] = { - clusters.FanControl.attributes.RockSupport, - clusters.FanControl.attributes.RockSetting - }, - [capabilities.battery.ID] = { - clusters.PowerSource.attributes.BatPercentRemaining - }, - [capabilities.batteryLevel.ID] = { - clusters.PowerSource.attributes.BatChargeLevel - }, - [capabilities.filterState.ID] = { - clusters.HepaFilterMonitoring.attributes.Condition, - clusters.ActivatedCarbonFilterMonitoring.attributes.Condition - }, - [capabilities.filterStatus.ID] = { - clusters.HepaFilterMonitoring.attributes.ChangeIndication, - clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication - }, - [capabilities.airQualityHealthConcern.ID] = { - clusters.AirQuality.attributes.AirQuality - }, - [capabilities.carbonMonoxideMeasurement.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.carbonMonoxideHealthConcern.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.carbonDioxideMeasurement.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.carbonDioxideHealthConcern.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.nitrogenDioxideMeasurement.ID] = { - clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit - }, - [capabilities.nitrogenDioxideHealthConcern.ID] = { - clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.ozoneMeasurement.ID] = { - clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue, - clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit - }, - [capabilities.ozoneHealthConcern.ID] = { - clusters.OzoneConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.formaldehydeMeasurement.ID] = { - clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue, - clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.formaldehydeHealthConcern.ID] = { - clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.veryFineDustSensor.ID] = { - clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.veryFineDustHealthConcern.ID] = { - clusters.Pm1ConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.fineDustHealthConcern.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.fineDustSensor.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.dustSensor.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, - clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.dustHealthConcern.ID] = { - clusters.Pm10ConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.radonMeasurement.ID] = { - clusters.RadonConcentrationMeasurement.attributes.MeasuredValue, - clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.radonHealthConcern.ID] = { - clusters.RadonConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.tvocMeasurement.ID] = { - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue, - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.tvocHealthConcern.ID] = { - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue - }, - [capabilities.powerMeter.ID] = { - clusters.ElectricalPowerMeasurement.attributes.ActivePower - }, - [capabilities.mode.ID] = { - clusters.WaterHeaterMode.attributes.CurrentMode, - clusters.WaterHeaterMode.attributes.SupportedModes - }, - [capabilities.powerConsumptionReport.ID] = { - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported - }, - [capabilities.energyMeter.ID] = { - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported - }, -} - -local function supports_capability_by_id_modular(device, capability, component) - if not device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - device.log.warn_with({hub_logs = true}, "Device has overriden supports_capability_by_id, but does not have supported capabilities set.") - return false - end - for _, component_capabilities in ipairs(device:get_field(SUPPORTED_COMPONENT_CAPABILITIES)) do - local comp_id = component_capabilities[1] - local capability_ids = component_capabilities[2] - if (component == nil) or (component == comp_id) then - for _, cap in ipairs(capability_ids) do - if cap == capability then - return true - end - end - end - end - return false -end - -local function epoch_to_iso8601(time) - return os.date("!%Y-%m-%dT%H:%M:%SZ", time) -end - -local get_total_cumulative_energy_imported = function(device) - local total_cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} - local total_energy = 0 - for _, energyWh in pairs(total_cumulative_energy_imported) do - total_energy = total_energy + energyWh - end - return total_energy -end - -local function schedule_energy_report_timer(device) - if not device:supports_capability(capabilities.powerConsumptionReport) then - return - end - - local polling_schedule_timer = device:get_field(RECURRING_REPORT_TIMER) - if polling_schedule_timer ~= nil then - return - end - - -- The powerConsumption report needs to be updated at least every 15 minutes in order to be included in SmartThings Energy - local pcr_interval = device:get_field(DEVICE_POWER_CONSUMPTION_REPORT_TIME_INTERVAL) or DEFAULT_REPORT_TIME_INTERVAL - pcr_interval = utils.clamp_value(pcr_interval, DEFAULT_REPORT_TIME_INTERVAL, MAX_REPORT_TIMEOUT) - local timer = device.thread:call_on_schedule(pcr_interval, function() - local last_time = device:get_field(LAST_REPORTED_TIME) or 0 - local current_time = os.time() - local total_energy = get_total_cumulative_energy_imported(device) - device:set_field(LAST_REPORTED_TIME, current_time, { persist = true }) - - -- Calculate the energy consumed between the start and the end time - local previousTotalConsumptionWh = device:get_latest_state( - "main", capabilities.powerConsumptionReport.ID, capabilities.powerConsumptionReport.powerConsumption.NAME - ) or { energy = 0 } - local deltaEnergyWh = math.max(total_energy - previousTotalConsumptionWh.energy, 0.0) - local startTime = epoch_to_iso8601(last_time) - local endTime = epoch_to_iso8601(current_time - 1) - - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ - start = startTime, - ["end"] = endTime, - deltaEnergy = deltaEnergyWh, - energy = total_energy - })) - end, "polling_report_schedule_timer") - - device:set_field(RECURRING_REPORT_TIMER, timer) -end - -local function delete_reporting_timer(device) - local reporting_poll_timer = device:get_field(RECURRING_REPORT_TIMER) - if reporting_poll_timer ~= nil then - device.thread:cancel_timer(reporting_poll_timer) - device:set_field(RECURRING_REPORT_TIMER, nil) - end -end - -local function device_removed(driver, device) - delete_reporting_timer(device) - local poll_timer = device:get_field(RECURRING_POLL_TIMER) - if poll_timer ~= nil then - device.thread:cancel_timer(poll_timer) - device:set_field(RECURRING_POLL_TIMER, nil) - end -end - -local function tbl_contains(array, value) - for idx, element in ipairs(array) do - if element == value then - return true, idx - end - end - return false, nil -end - -local function get_field_for_endpoint(device, field, endpoint) - return device:get_field(string.format("%s_%d", field, endpoint)) -end - -local function set_field_for_endpoint(device, field, endpoint, value, additional_params) - device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) -end - -local function find_default_endpoint(device, cluster) - local res = device.MATTER_DEFAULT_ENDPOINT - local eps = embedded_cluster_utils.get_endpoints(device, cluster) - table.sort(eps) - for _, v in ipairs(eps) do - if v ~= 0 then --0 is the matter RootNode endpoint - return v - end - end - device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) - return res -end - -local function component_to_endpoint(device, component_name, cluster_id) - -- Use the find_default_endpoint function to return the first endpoint that - -- supports a given cluster. - local component_to_endpoint_map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) - if component_to_endpoint_map ~= nil and component_to_endpoint_map[component_name] ~= nil then - return component_to_endpoint_map[component_name] - end - if not cluster_id then return device.MATTER_DEFAULT_ENDPOINT end - return find_default_endpoint(device, cluster_id) -end - -local endpoint_to_component = function (device, endpoint_id) - local component_to_endpoint_map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) - if component_to_endpoint_map ~= nil then - for comp, ep in pairs(component_to_endpoint_map) do - if ep == endpoint_id then - return comp - end - end - end - return "main" -end - -local function create_poll_schedule(device) - local poll_timer = device:get_field(RECURRING_POLL_TIMER) - if poll_timer ~= nil then - return - end - - local cumul_imp_eps = embedded_cluster_utils.get_endpoints( - device, clusters.ElectricalEnergyMeasurement.ID, - { - feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY | - clusters.ElectricalEnergyMeasurement.types.Feature.IMPORTED_ENERGY - } - ) or {} - if #cumul_imp_eps == 0 then - return - end - - device:send(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(device)) - -- Setup a timer to read cumulative energy imported attribute every minute. - local timer = device.thread:call_on_schedule(POLL_INTERVAL, function() - device:send(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(device)) - end, "polling_schedule_timer") - - device:set_field(RECURRING_POLL_TIMER, timer) -end - -local function schedule_polls_for_cumulative_energy_imported(device) - if not device:supports_capability(capabilities.powerConsumptionReport) then - return - end - create_poll_schedule(device) - schedule_energy_report_timer(device) -end - -local function device_init(driver, device) - if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - -- assume that device is using a modular profile, override supports_capability_by_id - -- library function to utilize optional capabilities - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) - end - device:subscribe() - device:set_component_to_endpoint_fn(component_to_endpoint) - device:set_endpoint_to_component_fn(endpoint_to_component) - if not device:get_field(setpoint_limit_device_field.MIN_SETPOINT_DEADBAND_CHECKED) then - local auto_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}) - --Query min setpoint deadband if needed - if #auto_eps ~= 0 and device:get_field(setpoint_limit_device_field.MIN_DEADBAND) == nil then - local deadband_read = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - deadband_read:merge(clusters.Thermostat.attributes.MinSetpointDeadBand:read()) - device:send(deadband_read) - end - end - schedule_polls_for_cumulative_energy_imported(device) -end - -local function info_changed(driver, device, event, args) - if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - -- This indicates the device should be using a modular profile, so - -- re-up subscription with new capabilities using the modular supports_capability override - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) - end - - if device.profile.id ~= args.old_st_store.profile.id then - device:subscribe() - end - schedule_polls_for_cumulative_energy_imported(device) -end - -local function get_endpoints_for_dt(device, device_type) - local endpoints = {} - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == device_type then - table.insert(endpoints, ep.endpoint_id) - break - end - end - end - table.sort(endpoints) - return endpoints -end - -local function get_device_type(device) - -- For cases where a device has multiple device types, this list indicates which - -- device type will be the "main" device type for purposes of selecting a profile - -- with an appropriate category. This is done to promote consistency between - -- devices with similar device type compositions that may report their device types - -- listed in different orders - local device_type_priority = { - [RAC_DEVICE_TYPE_ID] = 1, - [AP_DEVICE_TYPE_ID] = 2, - [THERMOSTAT_DEVICE_TYPE_ID] = 3, - [FAN_DEVICE_TYPE_ID] = 4, - [WATER_HEATER_DEVICE_TYPE_ID] = 5, - [HEAT_PUMP_DEVICE_TYPE_ID] = 6 - } - - local main_device_type = false - - for _, ep in ipairs(device.endpoints) do - if ep.device_types ~= nil then - for _, dt in ipairs(ep.device_types) do - if not device_type_priority[main_device_type] or (device_type_priority[dt.device_type_id] and - device_type_priority[dt.device_type_id] < device_type_priority[main_device_type]) then - main_device_type = dt.device_type_id - end - end - end - end - - return main_device_type -end - -local AIR_QUALITY_MAP = { - {capabilities.carbonDioxideMeasurement.ID, "-co2", clusters.CarbonDioxideConcentrationMeasurement}, - {capabilities.carbonDioxideHealthConcern.ID, "-co2", clusters.CarbonDioxideConcentrationMeasurement}, - {capabilities.carbonMonoxideMeasurement.ID, "-co", clusters.CarbonMonoxideConcentrationMeasurement}, - {capabilities.carbonMonoxideHealthConcern.ID, "-co", clusters.CarbonMonoxideConcentrationMeasurement}, - {capabilities.dustSensor.ID, "-pm10", clusters.Pm10ConcentrationMeasurement}, - {capabilities.dustHealthConcern.ID, "-pm10", clusters.Pm10ConcentrationMeasurement}, - {capabilities.fineDustSensor.ID, "-pm25", clusters.Pm25ConcentrationMeasurement}, - {capabilities.fineDustHealthConcern.ID, "-pm25", clusters.Pm25ConcentrationMeasurement}, - {capabilities.formaldehydeMeasurement.ID, "-ch2o", clusters.FormaldehydeConcentrationMeasurement}, - {capabilities.formaldehydeHealthConcern.ID, "-ch2o", clusters.FormaldehydeConcentrationMeasurement}, - {capabilities.nitrogenDioxideHealthConcern.ID, "-no2", clusters.NitrogenDioxideConcentrationMeasurement}, - {capabilities.nitrogenDioxideMeasurement.ID, "-no2", clusters.NitrogenDioxideConcentrationMeasurement}, - {capabilities.ozoneHealthConcern.ID, "-ozone", clusters.OzoneConcentrationMeasurement}, - {capabilities.ozoneMeasurement.ID, "-ozone", clusters.OzoneConcentrationMeasurement}, - {capabilities.radonHealthConcern.ID, "-radon", clusters.RadonConcentrationMeasurement}, - {capabilities.radonMeasurement.ID, "-radon", clusters.RadonConcentrationMeasurement}, - {capabilities.tvocHealthConcern.ID, "-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement}, - {capabilities.tvocMeasurement.ID, "-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement}, - {capabilities.veryFineDustHealthConcern.ID, "-pm1", clusters.Pm1ConcentrationMeasurement}, - {capabilities.veryFineDustSensor.ID, "-pm1", clusters.Pm1ConcentrationMeasurement}, -} - -local function create_level_measurement_profile(device) - local meas_name, level_name = "", "" - for _, details in ipairs(AIR_QUALITY_MAP) do - local cap_id = details[1] - local cluster = details[3] - -- capability describes either a HealthConcern or Measurement/Sensor - if (cap_id:match("HealthConcern$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) - if #attr_eps > 0 then - level_name = level_name .. details[2] - end - elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) - if #attr_eps > 0 then - meas_name = meas_name .. details[2] - end - end - end - return meas_name, level_name -end - -local function supported_level_measurements(device) - local measurement_caps, level_caps = {}, {} - for _, details in ipairs(AIR_QUALITY_MAP) do - local cap_id = details[1] - local cluster = details[3] - -- capability describes either a HealthConcern or Measurement/Sensor - if (cap_id:match("HealthConcern$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) - if #attr_eps > 0 then - table.insert(level_caps, cap_id) - end - elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) - if #attr_eps > 0 then - table.insert(measurement_caps, cap_id) - end - end - end - return measurement_caps, level_caps -end - -local function create_air_quality_sensor_profile(device) - local aqs_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) - local profile_name = "" - if #aqs_eps > 0 then - profile_name = profile_name .. "-aqs" - end - local meas_name, level_name = create_level_measurement_profile(device) - if meas_name ~= "" then - profile_name = profile_name .. meas_name .. "-meas" - end - if level_name ~= "" then - profile_name = profile_name .. level_name .. "-level" - end - return profile_name -end - -local function create_fan_profile(device) - local fan_eps = device:get_endpoints(clusters.FanControl.ID) - local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) - local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) - local profile_name = "" - if #fan_eps > 0 then - profile_name = profile_name .. "-fan" - end - if #rock_eps > 0 then - profile_name = profile_name .. "-rock" - end - if #wind_eps > 0 then - profile_name = profile_name .. "-wind" - end - return profile_name -end - -local function create_air_purifier_profile(device) - local hepa_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID) - local ac_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID) - local fan_eps_seen = false - local profile_name = "air-purifier" - if #hepa_filter_eps > 0 then - profile_name = profile_name .. "-hepa" - end - if #ac_filter_eps > 0 then - profile_name = profile_name .. "-ac" - end - - -- air purifier profiles include -fan later in the name for historical reasons. - -- save this information for use at that point. - local fan_profile = create_fan_profile(device) - if fan_profile ~= "" then - fan_eps_seen = true - end - fan_profile = string.gsub(fan_profile, "-fan", "") - profile_name = profile_name .. fan_profile - - return profile_name, fan_eps_seen -end - -local function create_thermostat_modes_profile(device) - local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) - local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) - - local thermostat_modes = "" - if #heat_eps == 0 and #cool_eps == 0 then - return "No Heating nor Cooling Support" - elseif #heat_eps > 0 and #cool_eps == 0 then - thermostat_modes = thermostat_modes .. "-heating-only" - elseif #cool_eps > 0 and #heat_eps == 0 then - thermostat_modes = thermostat_modes .. "-cooling-only" - end - return thermostat_modes -end - -local function profiling_data_still_required(device) - for _, field in pairs(profiling_data) do - if device:get_field(field) == nil then - return true -- data still required if a field is nil - end - end - return false -end - -local function match_profile_switch(driver, device) - if profiling_data_still_required(device) then return end - - local running_state_supported = device:get_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) - local battery_supported = device:get_field(profiling_data.BATTERY_SUPPORT) - - local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) - local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) - local device_type = get_device_type(device) - local profile_name - if device_type == RAC_DEVICE_TYPE_ID then - profile_name = "room-air-conditioner" - - if #humidity_eps > 0 then - profile_name = profile_name .. "-humidity" - end - - -- Room AC does not support the rocking feature of FanControl. - local fan_name = create_fan_profile(device) - fan_name = string.gsub(fan_name, "-rock", "") - profile_name = profile_name .. fan_name - - local thermostat_modes = create_thermostat_modes_profile(device) - if thermostat_modes == "" then - profile_name = profile_name .. "-heating-cooling" - else - device.log.warn_with({hub_logs=true}, "Device does not support both heating and cooling. No matching profile") - return - end - - if profile_name == "room-air-conditioner-humidity-fan-wind-heating-cooling" then - profile_name = "room-air-conditioner" - end - - if not running_state_supported and profile_name == "room-air-conditioner-fan-heating-cooling" then - profile_name = profile_name .. "-nostate" - end - - elseif device_type == FAN_DEVICE_TYPE_ID then - profile_name = create_fan_profile(device) - -- remove leading "-" - profile_name = string.sub(profile_name, 2) - if profile_name == "fan" then - profile_name = "fan-generic" - end - - elseif device_type == AP_DEVICE_TYPE_ID then - local fan_eps_found - profile_name, fan_eps_found = create_air_purifier_profile(device) - if #thermostat_eps > 0 then - profile_name = profile_name .. "-thermostat" - - if #humidity_eps > 0 then - profile_name = profile_name .. "-humidity" - end - - if fan_eps_found then - profile_name = profile_name .. "-fan" - end - - local thermostat_modes = create_thermostat_modes_profile(device) - if thermostat_modes ~= "No Heating nor Cooling Support" then - profile_name = profile_name .. thermostat_modes - end - - if not running_state_supported then - profile_name = profile_name .. "-nostate" - end - - if battery_supported == battery_support.BATTERY_LEVEL then - profile_name = profile_name .. "-batteryLevel" - elseif battery_supported == battery_support.NO_BATTERY then - profile_name = profile_name .. "-nobattery" - end - elseif #device:get_endpoints(clusters.TemperatureMeasurement.ID) > 0 then - profile_name = profile_name .. "-temperature" - - if #humidity_eps > 0 then - profile_name = profile_name .. "-humidity" - end - - if fan_eps_found then - profile_name = profile_name .. "-fan" - end - end - profile_name = profile_name .. create_air_quality_sensor_profile(device) - elseif device_type == WATER_HEATER_DEVICE_TYPE_ID then - -- If a Water Heater is composed of Electrical Sensor device type, it must support both ElectricalEnergyMeasurement and - -- ElectricalPowerMeasurement clusters. - local electrical_sensor_eps = get_endpoints_for_dt(device, ELECTRICAL_SENSOR_DEVICE_TYPE_ID) or {} - if #electrical_sensor_eps > 0 then - profile_name = "water-heater-power-energy-powerConsumption" - end - elseif device_type == HEAT_PUMP_DEVICE_TYPE_ID then - profile_name = "heat-pump" - local MAX_HEAT_PUMP_THERMOSTAT_COMPONENTS = 2 - for i = 1, math.min(MAX_HEAT_PUMP_THERMOSTAT_COMPONENTS, #thermostat_eps) do - profile_name = profile_name .. "-thermostat" - if tbl_contains(humidity_eps, thermostat_eps[i]) then - profile_name = profile_name .. "-humidity" - end - end - elseif #thermostat_eps > 0 then - profile_name = "thermostat" - - if #humidity_eps > 0 then - profile_name = profile_name .. "-humidity" - end - - -- thermostat profiles support neither wind nor rocking FanControl attributes - local fan_name = create_fan_profile(device) - if fan_name ~= "" then - profile_name = profile_name .. "-fan" - end - - local thermostat_modes = create_thermostat_modes_profile(device) - if thermostat_modes == "No Heating nor Cooling Support" then - device.log.warn_with({hub_logs=true}, "Device does not support either heating or cooling. No matching profile") - return - else - profile_name = profile_name .. thermostat_modes - end - - if not running_state_supported then - profile_name = profile_name .. "-nostate" - end - - if battery_supported == battery_support.BATTERY_LEVEL then - profile_name = profile_name .. "-batteryLevel" - elseif battery_supported == battery_support.NO_BATTERY then - profile_name = profile_name .. "-nobattery" - end - else - device.log.warn_with({hub_logs=true}, "Device type is not supported in thermostat driver") - return - end - - if profile_name then - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) - end - -- clear all profiling data fields after profiling is complete. - for _, field in pairs(profiling_data) do - device:set_field(field, nil) - end -end - -local function get_thermostat_optional_capabilities(device) - local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) - local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) - local running_state_supported = device:get_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) - - local supported_thermostat_capabilities = {} - - if #heat_eps > 0 then - table.insert(supported_thermostat_capabilities, capabilities.thermostatHeatingSetpoint.ID) - end - if #cool_eps > 0 then - table.insert(supported_thermostat_capabilities, capabilities.thermostatCoolingSetpoint.ID) - end - - if running_state_supported then - table.insert(supported_thermostat_capabilities, capabilities.thermostatOperatingState.ID) - end - - return supported_thermostat_capabilities -end - -local function get_air_quality_optional_capabilities(device) - local supported_air_quality_capabilities = {} - - local measurement_caps, level_caps = supported_level_measurements(device) - - for _, cap_id in ipairs(measurement_caps) do - table.insert(supported_air_quality_capabilities, cap_id) - end - - for _, cap_id in ipairs(level_caps) do - table.insert(supported_air_quality_capabilities, cap_id) - end - - return supported_air_quality_capabilities -end - -local function match_modular_profile_air_purifer(driver, device) - local optional_supported_component_capabilities = {} - local main_component_capabilities = {} - local hepa_filter_component_capabilities = {} - local ac_filter_component_capabilties = {} - local profile_name = "air-purifier-modular" - - local MAIN_COMPONENT_IDX = 1 - local CAPABILITIES_LIST_IDX = 2 - - local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) - local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) - if #humidity_eps > 0 then - table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) - end - if #temp_eps > 0 then - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - end - - local hepa_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID) - local ac_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID) - - if #hepa_filter_eps > 0 then - local filter_state_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID, {feature_bitmap = clusters.HepaFilterMonitoring.types.Feature.CONDITION}) - if #filter_state_eps > 0 then - table.insert(hepa_filter_component_capabilities, capabilities.filterState.ID) - end - - table.insert(hepa_filter_component_capabilities, capabilities.filterStatus.ID) - end - if #ac_filter_eps > 0 then - local filter_state_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID, {feature_bitmap = clusters.ActivatedCarbonFilterMonitoring.types.Feature.CONDITION}) - if #filter_state_eps > 0 then - table.insert(ac_filter_component_capabilties, capabilities.filterState.ID) - end - - table.insert(ac_filter_component_capabilties, capabilities.filterStatus.ID) - end - - -- determine fan capabilities, note that airPurifierFanMode is already mandatory - local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) - local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) - - if #rock_eps > 0 then - table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID) - end - if #wind_eps > 0 then - table.insert(main_component_capabilities, capabilities.windMode.ID) - end - - local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) - - if #thermostat_eps > 0 then - -- thermostatMode and temperatureMeasurement should be expected if thermostat is present - table.insert(main_component_capabilities, capabilities.thermostatMode.ID) - - -- only add temperatureMeasurement if it is not already added via TemperatureMeasurement cluster support - if #temp_eps == 0 then - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - end - local thermostat_capabilities = get_thermostat_optional_capabilities(device) - for _, capability_id in pairs(thermostat_capabilities) do - table.insert(main_component_capabilities, capability_id) - end - end - - local aqs_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) - if #aqs_eps > 0 then - table.insert(main_component_capabilities, capabilities.airQualityHealthConcern.ID) - end - - local supported_air_quality_capabilities = get_air_quality_optional_capabilities(device) - for _, capability_id in pairs(supported_air_quality_capabilities) do - table.insert(main_component_capabilities, capability_id) - end - - table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) - if #ac_filter_component_capabilties > 0 then - table.insert(optional_supported_component_capabilities, {"activatedCarbonFilter", ac_filter_component_capabilties}) - end - if #hepa_filter_component_capabilities > 0 then - table.insert(optional_supported_component_capabilities, {"hepaFilter", hepa_filter_component_capabilities}) - end - - device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) - - -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities - table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airPurifierFanMode.ID) - table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.fanSpeedPercent.ID) - table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.refresh.ID) - table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.firmwareUpdate.ID) - - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) -end - -local function match_modular_profile_thermostat(driver, device) - local optional_supported_component_capabilities = {} - local main_component_capabilities = {} - local profile_name = "thermostat-modular" - - local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) - if #humidity_eps > 0 then - table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) - end - - -- determine fan capabilities - local fan_eps = device:get_endpoints(clusters.FanControl.ID) - local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) - local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) - - if #fan_eps > 0 then - table.insert(main_component_capabilities, capabilities.fanMode.ID) - end - if #rock_eps > 0 then - table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID) - end - if #wind_eps > 0 then - table.insert(main_component_capabilities, capabilities.windMode.ID) - end - - local thermostat_capabilities = get_thermostat_optional_capabilities(device) - for _, capability_id in pairs(thermostat_capabilities) do - table.insert(main_component_capabilities, capability_id) - end - - local battery_supported = device:get_field(profiling_data.BATTERY_SUPPORT) - if battery_supported == battery_support.BATTERY_LEVEL then - table.insert(main_component_capabilities, capabilities.batteryLevel.ID) - elseif battery_supported == battery_support.BATTERY_PERCENTAGE then - table.insert(main_component_capabilities, capabilities.battery.ID) - end - - table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) - device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) - - -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities - table.insert(main_component_capabilities, capabilities.thermostatMode.ID) - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - table.insert(main_component_capabilities, capabilities.refresh.ID) - table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) - - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) -end - -local function match_modular_profile_room_ac(driver, device) - local running_state_supported = device:get_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) - local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) - local optional_supported_component_capabilities = {} - local main_component_capabilities = {} - local profile_name = "room-air-conditioner-modular" - - if #humidity_eps > 0 then - table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) - end - - -- determine fan capabilities - local fan_eps = device:get_endpoints(clusters.FanControl.ID) - local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) - -- Note: Room AC does not support the rocking feature of FanControl. - - if #fan_eps > 0 then - table.insert(main_component_capabilities, capabilities.airConditionerFanMode.ID) - table.insert(main_component_capabilities, capabilities.fanSpeedPercent.ID) - end - if #wind_eps > 0 then - table.insert(main_component_capabilities, capabilities.windMode.ID) - end - - local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) - local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) - - if #heat_eps > 0 then - table.insert(main_component_capabilities, capabilities.thermostatHeatingSetpoint.ID) - end - if #cool_eps > 0 then - table.insert(main_component_capabilities, capabilities.thermostatCoolingSetpoint.ID) - end - - if running_state_supported then - table.insert(main_component_capabilities, capabilities.thermostatOperatingState.ID) - end - - table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) - device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) - - -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities - table.insert(main_component_capabilities, capabilities.switch.ID) - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - table.insert(main_component_capabilities, capabilities.thermostatMode.ID) - table.insert(main_component_capabilities, capabilities.refresh.ID) - table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) - - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) -end - -local function match_modular_profile(driver, device, device_type) - if profiling_data_still_required(device) then return end - - if device_type == AP_DEVICE_TYPE_ID then - match_modular_profile_air_purifer(driver, device) - elseif device_type == RAC_DEVICE_TYPE_ID then - match_modular_profile_room_ac(driver, device) - elseif device_type == THERMOSTAT_DEVICE_TYPE_ID then - match_modular_profile_thermostat(driver, device) - else - device.log.warn_with({hub_logs=true}, "Device type is not supported by modular profile in thermostat driver, trying profile switch instead") - match_profile_switch(driver, device) - return - end - - -- clear all profiling data fields after profiling is complete. - for _, field in pairs(profiling_data) do - device:set_field(field, nil) - end -end - -local function supports_modular_profile(device) - local supported_modular_device_types = { - AP_DEVICE_TYPE_ID, - RAC_DEVICE_TYPE_ID, - THERMOSTAT_DEVICE_TYPE_ID, - } - local device_type = get_device_type(device) - if not tbl_contains(supported_modular_device_types, device_type) then - device_type = false - end - return version.api >= 14 and version.rpc >= 8 and device_type -end - -function match_profile(driver, device) - local modular_device_type = supports_modular_profile(device) - if modular_device_type then - match_modular_profile(driver, device, modular_device_type) - else - match_profile_switch(driver, device) - end -end - -local function do_configure(driver, device) - match_profile(driver, device) -end - -local function driver_switched(driver, device) - match_profile(driver, device) -end - -local function device_added(driver, device) - local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - req:merge(clusters.Thermostat.attributes.ControlSequenceOfOperation:read(device)) - req:merge(clusters.FanControl.attributes.FanModeSequence:read(device)) - req:merge(clusters.FanControl.attributes.WindSupport:read(device)) - req:merge(clusters.FanControl.attributes.RockSupport:read(device)) - - local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) - if #thermostat_eps > 0 then - req:merge(clusters.Thermostat.attributes.AttributeList:read(device)) - else - device:set_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, false) - end - local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) - if #battery_feature_eps > 0 then - req:merge(clusters.PowerSource.attributes.AttributeList:read(device)) - else - device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.NO_BATTERY) - end - device:send(req) - local heat_pump_eps = get_endpoints_for_dt(device, HEAT_PUMP_DEVICE_TYPE_ID) or {} - if #heat_pump_eps > 0 then - local thermostat_eps = get_endpoints_for_dt(device, THERMOSTAT_DEVICE_TYPE_ID) or {} - local component_to_endpoint_map = { - ["thermostatOne"] = thermostat_eps[1], - ["thermostatTwo"] = thermostat_eps[2], - } - device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, {persist = true}) - end -end - -local function store_unit_factory(capability_name) - return function(driver, device, ib, response) - device:set_field(capability_name.."_unit", ib.data.value, {persist = true}) - end -end - -local units = { - PPM = 0, - PPB = 1, - PPT = 2, - MGM3 = 3, - UGM3 = 4, - NGM3 = 5, - PM3 = 6, - BQM3 = 7, - PCIL = 0xFF -- not in matter spec -} - -local unit_strings = { - [units.PPM] = "ppm", - [units.PPB] = "ppb", - [units.PPT] = "ppt", - [units.MGM3] = "mg/m^3", - [units.NGM3] = "ng/m^3", - [units.UGM3] = "μg/m^3", - [units.BQM3] = "Bq/m^3", - [units.PCIL] = "pCi/L" -} - -local unit_default = { - [capabilities.carbonMonoxideMeasurement.NAME] = units.PPM, - [capabilities.carbonDioxideMeasurement.NAME] = units.PPM, - [capabilities.nitrogenDioxideMeasurement.NAME] = units.PPM, - [capabilities.ozoneMeasurement.NAME] = units.PPM, - [capabilities.formaldehydeMeasurement.NAME] = units.PPM, - [capabilities.veryFineDustSensor.NAME] = units.UGM3, - [capabilities.fineDustSensor.NAME] = units.UGM3, - [capabilities.dustSensor.NAME] = units.UGM3, - [capabilities.radonMeasurement.NAME] = units.BQM3, - [capabilities.tvocMeasurement.NAME] = units.PPB -- TVOC is typically within the range of 0-5500 ppb, with good to moderate values being < 660 ppb -} - --- All ConcentrationMesurement clusters inherit from the same base cluster definitions, --- so CarbonMonoxideConcentratinMeasurement is used below but the same enum types exist --- in all ConcentrationMeasurement clusters -local level_strings = { - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.UNKNOWN] = "unknown", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.LOW] = "good", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.MEDIUM] = "moderate", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.HIGH] = "unhealthy", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.CRITICAL] = "hazardous", -} - --- measured in g/mol -local molecular_weights = { - [capabilities.carbonDioxideMeasurement.NAME] = 44.010, - [capabilities.nitrogenDioxideMeasurement.NAME] = 28.014, - [capabilities.ozoneMeasurement.NAME] = 48.0, - [capabilities.formaldehydeMeasurement.NAME] = 30.031, - [capabilities.veryFineDustSensor.NAME] = "N/A", - [capabilities.fineDustSensor.NAME] = "N/A", - [capabilities.dustSensor.NAME] = "N/A", - [capabilities.radonMeasurement.NAME] = 222.018, - [capabilities.tvocMeasurement.NAME] = "N/A", -} - -local conversion_tables = { - [units.PPM] = { - [units.PPM] = function(value) return utils.round(value) end, - [units.PPB] = function(value) return utils.round(value * (10^3)) end, - [units.UGM3] = function(value, molecular_weight) return utils.round((value * molecular_weight * 10^3) / MGM3_PPM_CONVERSION_FACTOR) end, - [units.MGM3] = function(value, molecular_weight) return utils.round((value * molecular_weight) / MGM3_PPM_CONVERSION_FACTOR) end, - }, - [units.PPB] = { - [units.PPM] = function(value) return utils.round(value/(10^3)) end, - [units.PPB] = function(value) return utils.round(value) end, - }, - [units.PPT] = { - [units.PPM] = function(value) return utils.round(value/(10^6)) end - }, - [units.MGM3] = { - [units.UGM3] = function(value) return utils.round(value * (10^3)) end, - [units.PPM] = function(value, molecular_weight) return utils.round((value * MGM3_PPM_CONVERSION_FACTOR) / molecular_weight) end, - }, - [units.UGM3] = { - [units.UGM3] = function(value) return utils.round(value) end, - [units.PPM] = function(value, molecular_weight) return utils.round((value * MGM3_PPM_CONVERSION_FACTOR) / (molecular_weight * 10^3)) end, - }, - [units.NGM3] = { - [units.UGM3] = function(value) return utils.round(value/(10^3)) end - }, - [units.BQM3] = { - [units.PCIL] = function(value) return utils.round(value/37) end - }, -} - -local function unit_conversion(value, from_unit, to_unit, capability_name) - local conversion_function = conversion_tables[from_unit][to_unit] - if not conversion_function then - log.info_with( {hub_logs = true} , string.format("Unsupported unit conversion from %s to %s", unit_strings[from_unit], unit_strings[to_unit])) - return - end - - if not value then - log.info_with( {hub_logs = true} , "unit conversion value is nil") - return - end - - return conversion_function(value, molecular_weights[capability_name]) -end - -local function measurementHandlerFactory(capability_name, attribute, target_unit) - return function(driver, device, ib, response) - local reporting_unit = device:get_field(capability_name.."_unit") - - if not reporting_unit then - reporting_unit = unit_default[capability_name] - device:set_field(capability_name.."_unit", reporting_unit, {persist = true}) - end - - local value = nil - if reporting_unit then - value = unit_conversion(ib.data.value, reporting_unit, target_unit, capability_name) - end - - if value then - device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = value, unit = unit_strings[target_unit]})) - -- handle case where device profile supports both fineDustLevel and dustLevel - if capability_name == capabilities.fineDustSensor.NAME and device:supports_capability(capabilities.dustSensor) then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.dustSensor.fineDustLevel({value = value, unit = unit_strings[target_unit]})) - end - end - end -end - -local function levelHandlerFactory(attribute) - return function(driver, device, ib, response) - device:emit_event_for_endpoint(ib.endpoint_id, attribute(level_strings[ib.data.value])) - end -end - --- handlers -local function air_quality_attr_handler(driver, device, ib, response) - local state = ib.data.value - if state == 0 then -- Unknown - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unknown()) - elseif state == 1 then -- Good - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.good()) - elseif state == 2 then -- Fair - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.moderate()) - elseif state == 3 then -- Moderate - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.slightlyUnhealthy()) - elseif state == 4 then -- Poor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unhealthy()) - elseif state == 5 then -- VeryPoor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.veryUnhealthy()) - elseif state == 6 then -- ExtremelyPoor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.hazardous()) - end -end - -local function on_off_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.on()) - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) - end -end - -local function temp_event_handler(attribute) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local unit = "C" - - -- Only emit the capability for RPC version >= 5, since unit conversion for - -- range capabilities is only supported in that case. - if version.rpc >= 5 then - local event - if attribute == capabilities.thermostatCoolingSetpoint.coolingSetpoint then - local range = { - minimum = device:get_field(setpoint_limit_device_field.MIN_COOL) or THERMOSTAT_MIN_TEMP_IN_C, - maximum = device:get_field(setpoint_limit_device_field.MAX_COOL) or THERMOSTAT_MAX_TEMP_IN_C, - step = 0.1 - } - event = capabilities.thermostatCoolingSetpoint.coolingSetpointRange({value = range, unit = unit}) - device:emit_event_for_endpoint(ib.endpoint_id, event) - elseif attribute == capabilities.thermostatHeatingSetpoint.heatingSetpoint then - local MAX_TEMP_IN_C = THERMOSTAT_MAX_TEMP_IN_C - local MIN_TEMP_IN_C = THERMOSTAT_MIN_TEMP_IN_C - local is_water_heater_device = get_device_type(device) == WATER_HEATER_DEVICE_TYPE_ID - if is_water_heater_device then - MAX_TEMP_IN_C = WATER_HEATER_MAX_TEMP_IN_C - MIN_TEMP_IN_C = WATER_HEATER_MIN_TEMP_IN_C - end - - local range = { - minimum = device:get_field(setpoint_limit_device_field.MIN_HEAT) or MIN_TEMP_IN_C, - maximum = device:get_field(setpoint_limit_device_field.MAX_HEAT) or MAX_TEMP_IN_C, - step = 0.1 - } - event = capabilities.thermostatHeatingSetpoint.heatingSetpointRange({value = range, unit = unit}) - device:emit_event_for_endpoint(ib.endpoint_id, event) - end - end - - local temp = ib.data.value / 100.0 - device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = temp, unit = unit})) - end -end - -local temp_attr_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local temp = ib.data.value / 100.0 - local unit = "C" - temp = utils.clamp_value(temp, THERMOSTAT_MIN_TEMP_IN_C, THERMOSTAT_MAX_TEMP_IN_C) - set_field_for_endpoint(device, minOrMax, ib.endpoint_id, temp) - local min = get_field_for_endpoint(device, setpoint_limit_device_field.MIN_TEMP, ib.endpoint_id) - local max = get_field_for_endpoint(device, setpoint_limit_device_field.MAX_TEMP, ib.endpoint_id) - if min ~= nil and max ~= nil then - if min < max then - -- Only emit the capability for RPC version >= 5 (unit conversion for - -- temperature range capability is only supported for RPC >= 5) - if version.rpc >= 5 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) - end - set_field_for_endpoint(device, setpoint_limit_device_field.MIN_TEMP, ib.endpoint_id, nil) - set_field_for_endpoint(device, setpoint_limit_device_field.MAX_TEMP, ib.endpoint_id, nil) - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) - end - end - end -end - -local function humidity_attr_handler(driver, device, ib, response) - local humidity = math.floor(ib.data.value / 100.0) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) -end - -local function system_mode_handler(driver, device, ib, response) - if device:get_field(OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then -- this being nil means the sequence_of_operation_handler hasn't run. - device.log.info_with({hub_logs = true}, "In the SystemMode handler: ControlSequenceOfOperation has not run yet. Exiting early.") - device:set_field(SAVED_SYSTEM_MODE_IB, ib) - return - end - - local supported_modes = device:get_latest_state(device:endpoint_to_component(ib.endpoint_id), capabilities.thermostatMode.ID, capabilities.thermostatMode.supportedThermostatModes.NAME) or {} - -- check that the given mode was in the supported modes list - if tbl_contains(supported_modes, THERMOSTAT_MODE_MAP[ib.data.value].NAME) then - device:emit_event_for_endpoint(ib.endpoint_id, THERMOSTAT_MODE_MAP[ib.data.value]()) - return - end - -- if the value is not found in the supported modes list, check if it's disallowed and early return if so. - local disallowed_thermostat_modes = device:get_field(DISALLOWED_THERMOSTAT_MODES) or {} - if tbl_contains(disallowed_thermostat_modes, THERMOSTAT_MODE_MAP[ib.data.value].NAME) then - return - end - -- if we get here, then the reported mode is allowed and not in our mode map - -- add the mode to the OPTIONAL_THERMOSTAT_MODES_SEEN and supportedThermostatModes tables - local optional_modes_seen = utils.deep_copy(device:get_field(OPTIONAL_THERMOSTAT_MODES_SEEN)) or {} - table.insert(optional_modes_seen, THERMOSTAT_MODE_MAP[ib.data.value].NAME) - device:set_field(OPTIONAL_THERMOSTAT_MODES_SEEN, optional_modes_seen, {persist=true}) - local sm_copy = utils.deep_copy(supported_modes) - table.insert(sm_copy, THERMOSTAT_MODE_MAP[ib.data.value].NAME) - local supported_modes_event = capabilities.thermostatMode.supportedThermostatModes(sm_copy, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, supported_modes_event) - device:emit_event_for_endpoint(ib.endpoint_id, THERMOSTAT_MODE_MAP[ib.data.value]()) -end - -local function running_state_handler(driver, device, ib, response) - for mode, operating_state in pairs(THERMOSTAT_OPERATING_MODE_MAP) do - if ((ib.data.value >> mode) & 1) > 0 then - device:emit_event_for_endpoint(ib.endpoint_id, operating_state()) - return - end - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatOperatingState.thermostatOperatingState.idle()) -end - -local function sequence_of_operation_handler(driver, device, ib, response) - -- The ControlSequenceOfOperation attribute only directly specifies what can't be operated by the operating environment, not what can. - -- However, we assert here that a Cooling enum value implies that SystemMode supports cooling, and the same for a Heating enum. - -- We also assert that Off is supported, though per spec this is optional. - if device:get_field(OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then - device:set_field(OPTIONAL_THERMOSTAT_MODES_SEEN, {capabilities.thermostatMode.thermostatMode.off.NAME}, {persist=true}) - end - local supported_modes = utils.deep_copy(device:get_field(OPTIONAL_THERMOSTAT_MODES_SEEN)) - local disallowed_mode_operations = {} - - local modes_for_inclusion = {} - if ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.COOLING_WITH_REHEAT then - local _, found_idx = tbl_contains(supported_modes, capabilities.thermostatMode.thermostatMode.emergency_heat.NAME) - if found_idx then - table.remove(supported_modes, found_idx) -- if seen before, remove now - end - table.insert(supported_modes, capabilities.thermostatMode.thermostatMode.cool.NAME) - table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.heat.NAME) - table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.emergency_heat.NAME) - elseif ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.HEATING_WITH_REHEAT then - local _, found_idx = tbl_contains(supported_modes, capabilities.thermostatMode.thermostatMode.precooling.NAME) - if found_idx then - table.remove(supported_modes, found_idx) -- if seen before, remove now - end - table.insert(supported_modes, capabilities.thermostatMode.thermostatMode.heat.NAME) - table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.cool.NAME) - table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.precooling.NAME) - elseif ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.COOLING_AND_HEATING_WITH_REHEAT then - table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.cool.NAME) - table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.heat.NAME) - end - - -- check whether the Auto Mode should be supported in SystemMode, though this is unrelated to ControlSequenceOfOperation - local auto = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}) - if #auto > 0 then - table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.auto.NAME) - else - table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.auto.NAME) - end - - -- if a disallowed value was once allowed and added, it should be removed now. - for index, mode in pairs(supported_modes) do - if tbl_contains(disallowed_mode_operations, mode) then - table.remove(supported_modes, index) - end - end - -- do not include any values twice - for _, mode in pairs(modes_for_inclusion) do - if not tbl_contains(supported_modes, mode) then - table.insert(supported_modes, mode) - end - end - device:set_field(DISALLOWED_THERMOSTAT_MODES, disallowed_mode_operations) - local event = capabilities.thermostatMode.supportedThermostatModes(supported_modes, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, event) - - -- will be set by the SystemMode handler if this handler hasn't run yet. - if device:get_field(SAVED_SYSTEM_MODE_IB) then - system_mode_handler(driver, device, device:get_field(SAVED_SYSTEM_MODE_IB), response) - device:set_field(SAVED_SYSTEM_MODE_IB, nil) - end -end - -local function min_deadband_limit_handler(driver, device, ib, response) - local val = ib.data.value / 10.0 - log.info("Setting " .. setpoint_limit_device_field.MIN_DEADBAND .. " to " .. string.format("%s", val)) - device:set_field(setpoint_limit_device_field.MIN_DEADBAND, val, { persist = true }) - device:set_field(setpoint_limit_device_field.MIN_SETPOINT_DEADBAND_CHECKED, true, {persist = true}) -end - -local function fan_mode_handler(driver, device, ib, response) - local fan_mode_event = { - [clusters.FanControl.attributes.FanMode.OFF] = { capabilities.fanMode.fanMode.off(), - capabilities.airConditionerFanMode.fanMode("off"), - capabilities.airPurifierFanMode.airPurifierFanMode.off(), - nil }, -- 'OFF' is not supported by thermostatFanMode - [clusters.FanControl.attributes.FanMode.LOW] = { capabilities.fanMode.fanMode.low(), - capabilities.airConditionerFanMode.fanMode("low"), - capabilities.airPurifierFanMode.airPurifierFanMode.low(), - capabilities.thermostatFanMode.thermostatFanMode.on() }, - [clusters.FanControl.attributes.FanMode.MEDIUM] = { capabilities.fanMode.fanMode.medium(), - capabilities.airConditionerFanMode.fanMode("medium"), - capabilities.airPurifierFanMode.airPurifierFanMode.medium(), - capabilities.thermostatFanMode.thermostatFanMode.on() }, - [clusters.FanControl.attributes.FanMode.HIGH] = { capabilities.fanMode.fanMode.high(), - capabilities.airConditionerFanMode.fanMode("high"), - capabilities.airPurifierFanMode.airPurifierFanMode.high(), - capabilities.thermostatFanMode.thermostatFanMode.on() }, - [clusters.FanControl.attributes.FanMode.ON] = { capabilities.fanMode.fanMode.auto(), - capabilities.airConditionerFanMode.fanMode("auto"), - capabilities.airPurifierFanMode.airPurifierFanMode.auto(), - capabilities.thermostatFanMode.thermostatFanMode.on() }, - [clusters.FanControl.attributes.FanMode.AUTO] = { capabilities.fanMode.fanMode.auto(), - capabilities.airConditionerFanMode.fanMode("auto"), - capabilities.airPurifierFanMode.airPurifierFanMode.auto(), - capabilities.thermostatFanMode.thermostatFanMode.auto() }, - [clusters.FanControl.attributes.FanMode.SMART] = { capabilities.fanMode.fanMode.auto(), - capabilities.airConditionerFanMode.fanMode("auto"), - capabilities.airPurifierFanMode.airPurifierFanMode.auto(), - capabilities.thermostatFanMode.thermostatFanMode.auto() } - } - local fan_mode_idx = device:supports_capability_by_id(capabilities.fanMode.ID) and 1 or - device:supports_capability_by_id(capabilities.airConditionerFanMode.ID) and 2 or - device:supports_capability_by_id(capabilities.airPurifierFanMode.ID) and 3 or - device:supports_capability_by_id(capabilities.thermostatFanMode.ID) and 4 - if fan_mode_idx ~= false and fan_mode_event[ib.data.value][fan_mode_idx] then - device:emit_event_for_endpoint(ib.endpoint_id, fan_mode_event[ib.data.value][fan_mode_idx]) - else - log.warn(string.format("Invalid Fan Mode (%s)", ib.data.value)) - end -end - -local function fan_mode_sequence_handler(driver, device, ib, response) - local supportedFanModes, supported_fan_modes_attribute - if ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH then - supportedFanModes = { "off", "low", "medium", "high" } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH then - supportedFanModes = { "off", "low", "high" } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO then - supportedFanModes = { "off", "low", "medium", "high", "auto" } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH_AUTO then - supportedFanModes = { "off", "low", "high", "auto" } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_HIGH_AUTO then - supportedFanModes = { "off", "high", "auto" } - else - supportedFanModes = { "off", "high" } - end - - if device:supports_capability_by_id(capabilities.airPurifierFanMode.ID) then - supported_fan_modes_attribute = capabilities.airPurifierFanMode.supportedAirPurifierFanModes - elseif device:supports_capability_by_id(capabilities.airConditionerFanMode.ID) then - supported_fan_modes_attribute = capabilities.airConditionerFanMode.supportedAcFanModes - elseif device:supports_capability_by_id(capabilities.thermostatFanMode.ID) then - supported_fan_modes_attribute = capabilities.thermostatFanMode.supportedThermostatFanModes - -- Our thermostat fan mode control is not granular enough to handle all of the supported modes - if ib.data.value >= clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO and - ib.data.value <= clusters.FanControl.attributes.FanModeSequence.OFF_ON_AUTO then - supportedFanModes = { "auto", "on" } - else - supportedFanModes = { "on" } - end - else - supported_fan_modes_attribute = capabilities.fanMode.supportedFanModes - end - - local event = supported_fan_modes_attribute(supportedFanModes, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, event) -end - -local function fan_speed_percent_attr_handler(driver, device, ib, response) - local speed = 0 - if ib.data.value ~= nil then - speed = utils.clamp_value(ib.data.value, MIN_ALLOWED_PERCENT_VALUE, MAX_ALLOWED_PERCENT_VALUE) - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(speed)) -end - -local function wind_support_handler(driver, device, ib, response) - local supported_wind_modes = {capabilities.windMode.windMode.noWind.NAME} - for mode, wind_mode in pairs(WIND_MODE_MAP) do - if ((ib.data.value >> mode) & 1) > 0 then - table.insert(supported_wind_modes, wind_mode.NAME) - end - end - local event = capabilities.windMode.supportedWindModes(supported_wind_modes, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, event) -end - -local function wind_setting_handler(driver, device, ib, response) - for index, wind_mode in pairs(WIND_MODE_MAP) do - if ((ib.data.value >> index) & 1) > 0 then - device:emit_event_for_endpoint(ib.endpoint_id, wind_mode()) - return - end - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windMode.windMode.noWind()) -end - -local function rock_support_handler(driver, device, ib, response) - local supported_rock_modes = {capabilities.fanOscillationMode.fanOscillationMode.off.NAME} - for mode, rock_mode in pairs(ROCK_MODE_MAP) do - if ((ib.data.value >> mode) & 1) > 0 then - table.insert(supported_rock_modes, rock_mode.NAME) - end - end - local event = capabilities.fanOscillationMode.supportedFanOscillationModes(supported_rock_modes, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, event) -end - -local function rock_setting_handler(driver, device, ib, response) - for index, rock_mode in pairs(ROCK_MODE_MAP) do - if ((ib.data.value >> index) & 1) > 0 then - device:emit_event_for_endpoint(ib.endpoint_id, rock_mode()) - return - end - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanOscillationMode.fanOscillationMode.off()) -end - -local function hepa_filter_condition_handler(driver, device, ib, response) - local component = device.profile.components["hepaFilter"] - local condition = ib.data.value - device:emit_component_event(component, capabilities.filterState.filterLifeRemaining(condition)) -end - -local function hepa_filter_change_indication_handler(driver, device, ib, response) - local component = device.profile.components["hepaFilter"] - if ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.OK then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) - elseif ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.WARNING then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) - elseif ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.CRITICAL then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.replace()) - end -end - -local function activated_carbon_filter_condition_handler(driver, device, ib, response) - local component = device.profile.components["activatedCarbonFilter"] - local condition = ib.data.value - device:emit_component_event(component, capabilities.filterState.filterLifeRemaining(condition)) -end - -local function activated_carbon_filter_change_indication_handler(driver, device, ib, response) - local component = device.profile.components["activatedCarbonFilter"] - if ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.OK then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) - elseif ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.WARNING then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) - elseif ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.CRITICAL then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.replace()) - end -end - -local function handle_switch_on(driver, device, cmd) - local endpoint_id = component_to_endpoint(device, cmd.component, clusters.OnOff.ID) - local req = clusters.OnOff.server.commands.On(device, endpoint_id) - device:send(req) -end - -local function handle_switch_off(driver, device, cmd) - local endpoint_id = component_to_endpoint(device, cmd.component, clusters.OnOff.ID) - local req = clusters.OnOff.server.commands.Off(device, endpoint_id) - device:send(req) -end +local log = require "log" +local version = require "version" +local MatterDriver = require "st.matter.driver" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local im = require "st.matter.interaction_model" +local attribute_handlers = require "thermostat_handlers.attribute_handlers" +local capability_handlers = require "thermostat_handlers.capability_handlers" +local fields = require "thermostat_utils.fields" +local thermostat_utils = require "thermostat_utils.utils" +local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils" -local function set_thermostat_mode(driver, device, cmd) - local mode_id = nil - for value, mode in pairs(THERMOSTAT_MODE_MAP) do - if mode.NAME == cmd.args.mode then - mode_id = value - break - end - end - if mode_id then - device:send(clusters.Thermostat.attributes.SystemMode:write(device, component_to_endpoint(device, cmd.component, clusters.Thermostat.ID), mode_id)) - end +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" end -local thermostat_mode_setter = function(mode_name) - return function(driver, device, cmd) - return set_thermostat_mode(driver, device, {component = cmd.component, args = {mode = mode_name}}) - end +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" end -local function set_setpoint(setpoint) - return function(driver, device, cmd) - local endpoint_id = component_to_endpoint(device, cmd.component, clusters.Thermostat.ID) - local MAX_TEMP_IN_C = THERMOSTAT_MAX_TEMP_IN_C - local MIN_TEMP_IN_C = THERMOSTAT_MIN_TEMP_IN_C - local is_water_heater_device = get_device_type(device) == WATER_HEATER_DEVICE_TYPE_ID - if is_water_heater_device then - MAX_TEMP_IN_C = WATER_HEATER_MAX_TEMP_IN_C - MIN_TEMP_IN_C = WATER_HEATER_MIN_TEMP_IN_C - end - local value = cmd.args.setpoint - if version.rpc <= 5 and value > MAX_TEMP_IN_C then - value = utils.f_to_c(value) - end - - -- Gather cached setpoint values when considering setpoint limits - -- Note: cached values should always exist, but defaults are chosen just in case to prevent - -- nil operation errors, and deadband logic from triggering. - local cached_cooling_val, cooling_setpoint = device:get_latest_state( - cmd.component, capabilities.thermostatCoolingSetpoint.ID, - capabilities.thermostatCoolingSetpoint.coolingSetpoint.NAME, - MAX_TEMP_IN_C, { value = MAX_TEMP_IN_C, unit = "C" } - ) - if cooling_setpoint and cooling_setpoint.unit == "F" then - cached_cooling_val = utils.f_to_c(cached_cooling_val) - end - local cached_heating_val, heating_setpoint = device:get_latest_state( - cmd.component, capabilities.thermostatHeatingSetpoint.ID, - capabilities.thermostatHeatingSetpoint.heatingSetpoint.NAME, - MIN_TEMP_IN_C, { value = MIN_TEMP_IN_C, unit = "C" } - ) - if heating_setpoint and heating_setpoint.unit == "F" then - cached_heating_val = utils.f_to_c(cached_heating_val) - end - local is_auto_capable = #device:get_endpoints( - clusters.Thermostat.ID, - {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE} - ) > 0 - - --Check setpoint limits for the device - local setpoint_type = string.match(setpoint.NAME, "Heat") or "Cool" - local deadband = device:get_field(setpoint_limit_device_field.MIN_DEADBAND) or 2.5 --spec default - if setpoint_type == "Heat" then - local min = device:get_field(setpoint_limit_device_field.MIN_HEAT) or MIN_TEMP_IN_C - local max = device:get_field(setpoint_limit_device_field.MAX_HEAT) or MAX_TEMP_IN_C - if value < min or value > max then - log.warn(string.format( - "Invalid setpoint (%s) outside the min (%s) and the max (%s)", - value, min, max - )) - device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpoint(heating_setpoint, {state_change = true})) - return - end - if is_auto_capable and value > (cached_cooling_val - deadband) then - log.warn(string.format( - "Invalid setpoint (%s) is greater than the cooling setpoint (%s) with the deadband (%s)", - value, cooling_setpoint, deadband - )) - device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpoint(heating_setpoint, {state_change = true})) - return - end - else - local min = device:get_field(setpoint_limit_device_field.MIN_COOL) or MIN_TEMP_IN_C - local max = device:get_field(setpoint_limit_device_field.MAX_COOL) or MAX_TEMP_IN_C - if value < min or value > max then - log.warn(string.format( - "Invalid setpoint (%s) outside the min (%s) and the max (%s)", - value, min, max - )) - device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpoint(cooling_setpoint, {state_change = true})) - return - end - if is_auto_capable and value < (cached_heating_val + deadband) then - log.warn(string.format( - "Invalid setpoint (%s) is less than the heating setpoint (%s) with the deadband (%s)", - value, heating_setpoint, deadband - )) - device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpoint(cooling_setpoint, {state_change = true})) - return - end - end - device:send(setpoint:write(device, component_to_endpoint(device, cmd.component, clusters.Thermostat.ID), utils.round(value * 100.0))) - end +if version.api < 13 then + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" end -local heating_setpoint_limit_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local MAX_TEMP_IN_C = THERMOSTAT_MAX_TEMP_IN_C - local MIN_TEMP_IN_C = THERMOSTAT_MIN_TEMP_IN_C - local is_water_heater_device = (get_device_type(device) == WATER_HEATER_DEVICE_TYPE_ID) - if is_water_heater_device then - MAX_TEMP_IN_C = WATER_HEATER_MAX_TEMP_IN_C - MIN_TEMP_IN_C = WATER_HEATER_MIN_TEMP_IN_C - end - local val = ib.data.value / 100.0 - val = utils.clamp_value(val, MIN_TEMP_IN_C, MAX_TEMP_IN_C) - device:set_field(minOrMax, val) - local min = device:get_field(setpoint_limit_device_field.MIN_HEAT) - local max = device:get_field(setpoint_limit_device_field.MAX_HEAT) - if min ~= nil and max ~= nil then - if min < max then - -- Only emit the capability for RPC version >= 5 (unit conversion for - -- heating setpoint range capability is only supported for RPC >= 5) - if version.rpc >= 5 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = min, maximum = max, step = 0.1 }, unit = "C" })) - end - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min heating setpoint %d that is not lower than the reported max %d", min, max)) - end - end - end -end +local ThermostatLifecycleHandlers = {} -local cooling_setpoint_limit_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local val = ib.data.value / 100.0 - val = utils.clamp_value(val, THERMOSTAT_MIN_TEMP_IN_C, THERMOSTAT_MAX_TEMP_IN_C) - device:set_field(minOrMax, val) - local min = device:get_field(setpoint_limit_device_field.MIN_COOL) - local max = device:get_field(setpoint_limit_device_field.MAX_COOL) - if min ~= nil and max ~= nil then - if min < max then - -- Only emit the capability for RPC version >= 5 (unit conversion for - -- cooling setpoint range capability is only supported for RPC >= 5) - if version.rpc >= 5 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { minimum = min, maximum = max, step = 0.1 }, unit = "C" })) - end - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min cooling setpoint %d that is not lower than the reported max %d", min, max)) - end - end - end -end +function ThermostatLifecycleHandlers.device_added(driver, device) + local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + req:merge(clusters.Thermostat.attributes.ControlSequenceOfOperation:read(device)) + req:merge(clusters.FanControl.attributes.FanModeSequence:read(device)) + req:merge(clusters.FanControl.attributes.WindSupport:read(device)) + req:merge(clusters.FanControl.attributes.RockSupport:read(device)) -local function set_fan_mode(device, cmd, fan_mode_capability) - local command_argument = cmd.args.fanMode - if fan_mode_capability == capabilities.airPurifierFanMode then - command_argument = cmd.args.airPurifierFanMode - elseif fan_mode_capability == capabilities.thermostatFanMode then - command_argument = cmd.args.mode - end - local fan_mode_id - if command_argument == "off" then - fan_mode_id = clusters.FanControl.attributes.FanMode.OFF - elseif command_argument == "on" then - fan_mode_id = clusters.FanControl.attributes.FanMode.ON - elseif command_argument == "auto" then - fan_mode_id = clusters.FanControl.attributes.FanMode.AUTO - elseif command_argument == "high" then - fan_mode_id = clusters.FanControl.attributes.FanMode.HIGH - elseif command_argument == "medium" then - fan_mode_id = clusters.FanControl.attributes.FanMode.MEDIUM - elseif tbl_contains({ "low", "sleep", "quiet", "windFree" }, command_argument) then - fan_mode_id = clusters.FanControl.attributes.FanMode.LOW + local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) + if #thermostat_eps > 0 then + req:merge(clusters.Thermostat.attributes.AttributeList:read(device)) else - device.log.warn(string.format("Invalid Fan Mode (%s) received from capability command", command_argument)) - return - end - device:send(clusters.FanControl.attributes.FanMode:write(device, component_to_endpoint(device, cmd.component, clusters.FanControl.ID), fan_mode_id)) -end - -local set_fan_mode_factory = function(fan_mode_capability) - return function(driver, device, cmd) - set_fan_mode(device, cmd, fan_mode_capability) - end -end - -local function thermostat_fan_mode_setter(mode_name) - return function(driver, device, cmd) - set_fan_mode(device, {component = cmd.component, args = {mode = mode_name}}, capabilities.thermostatFanMode) + device:set_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, false) end -end - -local function set_fan_speed_percent(driver, device, cmd) - local speed = math.floor(cmd.args.percent) - device:send(clusters.FanControl.attributes.PercentSetting:write(device, component_to_endpoint(device, cmd.component, clusters.FanControl.ID), speed)) -end - -local function set_wind_mode(driver, device, cmd) - local wind_mode = 0 - if cmd.args.windMode == capabilities.windMode.windMode.sleepWind.NAME then - wind_mode = clusters.FanControl.types.WindSupportMask.SLEEP_WIND - elseif cmd.args.windMode == capabilities.windMode.windMode.naturalWind.NAME then - wind_mode = clusters.FanControl.types.WindSupportMask.NATURAL_WIND - end - device:send(clusters.FanControl.attributes.WindSetting:write(device, component_to_endpoint(device, cmd.component, clusters.FanControl.ID), wind_mode)) -end - -local function set_rock_mode(driver, device, cmd) - local rock_mode = 0 - if cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.horizontal.NAME then - rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_LEFT_RIGHT - elseif cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.vertical.NAME then - rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_UP_DOWN - elseif cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.swing.NAME then - rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_ROUND - end - device:send(clusters.FanControl.attributes.RockSetting:write(device, component_to_endpoint(device, cmd.component, clusters.FanControl.ID), rock_mode)) -end - -local function set_water_heater_mode(driver, device, cmd) - device.log.info(string.format("set_water_heater_mode mode: %s", cmd.args.mode)) - local endpoint_id = component_to_endpoint(device, cmd.component, clusters.Thermostat.ID) - local supportedWaterHeaterModesWithIdx = device:get_field(SUPPORTED_WATER_HEATER_MODES_WITH_IDX) or {} - for i, mode in ipairs(supportedWaterHeaterModesWithIdx) do - if cmd.args.mode == mode[2] then - device:send(clusters.WaterHeaterMode.commands.ChangeToMode(device, endpoint_id, mode[1])) - return - end - end -end - -local function reset_filter_state(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - if cmd.component == "hepaFilter" then - device:send(clusters.HepaFilterMonitoring.server.commands.ResetCondition(device, endpoint_id)) + local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) + if #battery_feature_eps > 0 then + req:merge(clusters.PowerSource.attributes.AttributeList:read(device)) else - device:send(clusters.ActivatedCarbonFilterMonitoring.server.commands.ResetCondition(device, endpoint_id)) + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY) end -end - -local function battery_percent_remaining_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) + device:send(req) + local heat_pump_eps = thermostat_utils.get_endpoints_by_device_type(device, fields.HEAT_PUMP_DEVICE_TYPE_ID) or {} + if #heat_pump_eps > 0 then + local thermostat_eps = thermostat_utils.get_endpoints_by_device_type(device, fields.THERMOSTAT_DEVICE_TYPE_ID) or {} + local component_to_endpoint_map = { + ["thermostatOne"] = thermostat_eps[1], + ["thermostatTwo"] = thermostat_eps[2], + } + device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, {persist = true}) end end -local function active_power_handler(driver, device, ib, response) - if ib.data.value then - local watt_value = ib.data.value / 1000 - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W" })) - if type(device.register_native_capability_attr_handler) == "function" then - device:register_native_capability_attr_handler("powerMeter","power") - end - end +function ThermostatLifecycleHandlers.do_configure(driver, device) + local device_cfg = require "thermostat_utils.device_configuration" + device_cfg.match_profile(device) end -local function periodic_energy_imported_handler(driver, device, ib, response) - local endpoint_id = ib.endpoint_id - local cumul_eps = embedded_cluster_utils.get_endpoints(device, - clusters.ElectricalEnergyMeasurement.ID, - { feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY }) - - if ib.data then - if version.api < 11 then - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:augment_type(ib.data) - end - - local start_timestamp = ib.data.elements.start_timestamp.value or 0 - local end_timestamp = ib.data.elements.end_timestamp.value or 0 - - local device_reporting_time_interval = end_timestamp - start_timestamp - if not device:get_field(DEVICE_REPORTING_TIME_INTERVAL_CONSIDERED) and device_reporting_time_interval > DEFAULT_REPORT_TIME_INTERVAL then - -- This is a one time setup in order to consider a larger time interval if the interval the device chooses to report is greater than 15 minutes. - device:set_field(DEVICE_REPORTING_TIME_INTERVAL_CONSIDERED, true, { persist = true }) - device:set_field(DEVICE_POWER_CONSUMPTION_REPORT_TIME_INTERVAL, device_reporting_time_interval, { persist = true }) - delete_reporting_timer(device) - schedule_energy_report_timer(device) - end - - if tbl_contains(cumul_eps, endpoint_id) then - -- Since cluster at this endpoint supports both CUME & PERE features, we will prefer - -- cumulative_energy_imported_handler to handle the energy report for this endpoint. - return - end - - endpoint_id = string.format(ib.endpoint_id) - local energy_imported_Wh = utils.round(ib.data.elements.energy.value / 1000) --convert mWh to Wh - local cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} - cumulative_energy_imported[endpoint_id] = cumulative_energy_imported[endpoint_id] or 0 - cumulative_energy_imported[endpoint_id] = cumulative_energy_imported[endpoint_id] + energy_imported_Wh - device:set_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP, cumulative_energy_imported, { persist = true }) - local total_cumulative_energy_imported = get_total_cumulative_energy_imported(device) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({value = total_cumulative_energy_imported, unit = "Wh"})) - end +function ThermostatLifecycleHandlers.driver_switched(driver, device) + local device_cfg = require "thermostat_utils.device_configuration" + device_cfg.match_profile(device) end -local function cumulative_energy_imported_handler(driver, device, ib, response) - if ib.data then - if version.api < 11 then - clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:augment_type(ib.data) - end - local endpoint_id = string.format(ib.endpoint_id) - local cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} - local cumulative_energy_imported_Wh = utils.round( ib.data.elements.energy.value / 1000) -- convert mWh to Wh - cumulative_energy_imported[endpoint_id] = cumulative_energy_imported_Wh - device:set_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP, cumulative_energy_imported, { persist = true }) - local total_cumulative_energy_imported = get_total_cumulative_energy_imported(device) - device:emit_event(capabilities.energyMeter.energy({ value = total_cumulative_energy_imported, unit = "Wh" })) +function ThermostatLifecycleHandlers.device_init(driver, device) + if device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) and (version.api < 15 or version.rpc < 9) then + -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id + -- library function to utilize optional capabilities + device:extend_device("supports_capability_by_id", thermostat_utils.supports_capability_by_id_modular) end -end - -local function water_heater_supported_modes_attr_handler(driver, device, ib, response) - local supportWaterHeaterModes = {} - local supportWaterHeaterModesWithIdx = {} - for _, mode in ipairs(ib.data.elements) do - if version.api < 13 then - clusters.WaterHeaterMode.types.ModeOptionStruct:augment_type(mode) + device:subscribe() + device:set_component_to_endpoint_fn(thermostat_utils.component_to_endpoint) + device:set_endpoint_to_component_fn(thermostat_utils.endpoint_to_component) + thermostat_utils.handle_thermostat_operating_state_info(device) + if not device:get_field(fields.setpoint_limit_device_field.MIN_SETPOINT_DEADBAND_CHECKED) then + local auto_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}) + --Query min setpoint deadband if needed + if #auto_eps ~= 0 and device:get_field(fields.setpoint_limit_device_field.MIN_DEADBAND) == nil then + device:send(clusters.Thermostat.attributes.MinSetpointDeadBand:read()) end - table.insert(supportWaterHeaterModes, mode.elements.label.value) - table.insert(supportWaterHeaterModesWithIdx, {mode.elements.mode.value, mode.elements.label.value}) end - device:set_field(SUPPORTED_WATER_HEATER_MODES_WITH_IDX, supportWaterHeaterModesWithIdx, { persist = true }) - local event = capabilities.mode.supportedModes(supportWaterHeaterModes, { visibility = { displayed = false } }) - device:emit_event_for_endpoint(ib.endpoint_id, event) - event = capabilities.mode.supportedArguments(supportWaterHeaterModes, { visibility = { displayed = false } }) - device:emit_event_for_endpoint(ib.endpoint_id, event) -end -local function water_heater_mode_handler(driver, device, ib, response) - device.log.info(string.format("water_heater_mode_handler mode: %s", ib.data.value)) - local supportWaterHeaterModesWithIdx = device:get_field(SUPPORTED_WATER_HEATER_MODES_WITH_IDX) or {} - local currentMode = ib.data.value - for i, mode in ipairs(supportWaterHeaterModesWithIdx) do - if mode[1] == currentMode then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.mode.mode(mode[2])) - break - end + -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. + -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. + if #embedded_cluster_utils.get_endpoints( + device, + clusters.ElectricalEnergyMeasurement.ID, + {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY} + ) == 0 then device:set_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = false}) end end -local function battery_charge_level_attr_handler(driver, device, ib, response) - if ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.OK then - device:emit_event(capabilities.batteryLevel.battery.normal()) - elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.WARNING then - device:emit_event(capabilities.batteryLevel.battery.warning()) - elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.CRITICAL then - device:emit_event(capabilities.batteryLevel.battery.critical()) +function ThermostatLifecycleHandlers.info_changed(driver, device, event, args) + if device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then + -- This indicates the device should be using a modular profile, so + -- re-up subscription with new capabilities using the modular supports_capability override + device:extend_device("supports_capability_by_id", thermostat_utils.supports_capability_by_id_modular) end -end -local function power_source_attribute_list_handler(driver, device, ib, response) - for _, attr in ipairs(ib.data.elements) do - -- mark if the device if BatPercentRemaining (Attribute ID 0x0C) or - -- BatChargeLevel (Attribute ID 0x0E) is present and try profiling. - if attr.value == 0x0C then - device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_PERCENTAGE) - match_profile(driver, device) - return - elseif attr.value == 0x0E then - device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_LEVEL) - match_profile(driver, device) - return - end + if device.profile.id ~= args.old_st_store.profile.id then + thermostat_utils.handle_thermostat_operating_state_info(device) + device:subscribe() end end -local function thermostat_attribute_list_handler(driver, device, ib, response) - for _, attr in ipairs(ib.data.elements) do - -- mark whether the optional attribute ThermostatRunningState (0x029) is present and try profiling - if attr.value == 0x029 then - device:set_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, true) - match_profile(driver, device) - return - end - end - device:set_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, false) - match_profile(driver, device) +function ThermostatLifecycleHandlers.device_removed(driver, device) + device.log.info("device removed") end local matter_driver_template = { lifecycle_handlers = { - init = device_init, - added = device_added, - doConfigure = do_configure, - infoChanged = info_changed, - removed = device_removed, - driverSwitched = driver_switched + added = ThermostatLifecycleHandlers.device_added, + doConfigure = ThermostatLifecycleHandlers.do_configure, + driverSwitched = ThermostatLifecycleHandlers.driver_switched, + infoChanged = ThermostatLifecycleHandlers.info_changed, + init = ThermostatLifecycleHandlers.device_init, + removed = ThermostatLifecycleHandlers.device_removed, }, matter_handlers = { attr = { - [clusters.OnOff.ID] = { - [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, + [clusters.ActivatedCarbonFilterMonitoring.ID] = { + [clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.ID] = attribute_handlers.activated_carbon_filter_change_indication_handler, + [clusters.ActivatedCarbonFilterMonitoring.attributes.Condition.ID] = attribute_handlers.activated_carbon_filter_condition_handler, }, - [clusters.Thermostat.ID] = { - [clusters.Thermostat.attributes.LocalTemperature.ID] = temp_event_handler(capabilities.temperatureMeasurement.temperature), - [clusters.Thermostat.attributes.OccupiedCoolingSetpoint.ID] = temp_event_handler(capabilities.thermostatCoolingSetpoint.coolingSetpoint), - [clusters.Thermostat.attributes.OccupiedHeatingSetpoint.ID] = temp_event_handler(capabilities.thermostatHeatingSetpoint.heatingSetpoint), - [clusters.Thermostat.attributes.SystemMode.ID] = system_mode_handler, - [clusters.Thermostat.attributes.ThermostatRunningState.ID] = running_state_handler, - [clusters.Thermostat.attributes.ControlSequenceOfOperation.ID] = sequence_of_operation_handler, - [clusters.Thermostat.attributes.AbsMinHeatSetpointLimit.ID] = heating_setpoint_limit_handler_factory(setpoint_limit_device_field.MIN_HEAT), - [clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit.ID] = heating_setpoint_limit_handler_factory(setpoint_limit_device_field.MAX_HEAT), - [clusters.Thermostat.attributes.AbsMinCoolSetpointLimit.ID] = cooling_setpoint_limit_handler_factory(setpoint_limit_device_field.MIN_COOL), - [clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit.ID] = cooling_setpoint_limit_handler_factory(setpoint_limit_device_field.MAX_COOL), - [clusters.Thermostat.attributes.MinSetpointDeadBand.ID] = min_deadband_limit_handler, - [clusters.Thermostat.attributes.AttributeList.ID] = thermostat_attribute_list_handler, + [clusters.AirQuality.ID] = { + [clusters.AirQuality.attributes.AirQuality.ID] = attribute_handlers.air_quality_handler, + }, + [clusters.ElectricalEnergyMeasurement.ID] = { + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = attribute_handlers.energy_imported_factory(true), + [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = attribute_handlers.energy_imported_factory(false), + }, + [clusters.ElectricalPowerMeasurement.ID] = { + [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = attribute_handlers.active_power_handler }, [clusters.FanControl.ID] = { - [clusters.FanControl.attributes.FanModeSequence.ID] = fan_mode_sequence_handler, - [clusters.FanControl.attributes.FanMode.ID] = fan_mode_handler, - [clusters.FanControl.attributes.PercentCurrent.ID] = fan_speed_percent_attr_handler, - [clusters.FanControl.attributes.WindSupport.ID] = wind_support_handler, - [clusters.FanControl.attributes.WindSetting.ID] = wind_setting_handler, - [clusters.FanControl.attributes.RockSupport.ID] = rock_support_handler, - [clusters.FanControl.attributes.RockSetting.ID] = rock_setting_handler, + [clusters.FanControl.attributes.FanMode.ID] = attribute_handlers.fan_mode_handler, + [clusters.FanControl.attributes.FanModeSequence.ID] = attribute_handlers.fan_mode_sequence_handler, + [clusters.FanControl.attributes.PercentCurrent.ID] = attribute_handlers.percent_current_handler, + [clusters.FanControl.attributes.RockSetting.ID] = attribute_handlers.rock_setting_handler, + [clusters.FanControl.attributes.RockSupport.ID] = attribute_handlers.rock_support_handler, + [clusters.FanControl.attributes.WindSetting.ID] = attribute_handlers.wind_setting_handler, + [clusters.FanControl.attributes.WindSupport.ID] = attribute_handlers.wind_support_handler, }, - [clusters.TemperatureMeasurement.ID] = { - [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = temp_event_handler(capabilities.temperatureMeasurement.temperature), - [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = temp_attr_handler_factory(setpoint_limit_device_field.MIN_TEMP), - [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = temp_attr_handler_factory(setpoint_limit_device_field.MAX_TEMP), + [clusters.HepaFilterMonitoring.ID] = { + [clusters.HepaFilterMonitoring.attributes.ChangeIndication.ID] = attribute_handlers.hepa_filter_change_indication_handler, + [clusters.HepaFilterMonitoring.attributes.Condition.ID] = attribute_handlers.hepa_filter_condition_handler, }, - [clusters.RelativeHumidityMeasurement.ID] = { - [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = humidity_attr_handler + [clusters.OnOff.ID] = { + [clusters.OnOff.attributes.OnOff.ID] = attribute_handlers.on_off_handler, }, [clusters.PowerSource.ID] = { - [clusters.PowerSource.attributes.AttributeList.ID] = power_source_attribute_list_handler, - [clusters.PowerSource.attributes.BatChargeLevel.ID] = battery_charge_level_attr_handler, - [clusters.PowerSource.attributes.BatPercentRemaining.ID] = battery_percent_remaining_attr_handler, + [clusters.PowerSource.attributes.AttributeList.ID] = attribute_handlers.power_source_attribute_list_handler, + [clusters.PowerSource.attributes.BatChargeLevel.ID] = attribute_handlers.bat_charge_level_handler, + [clusters.PowerSource.attributes.BatPercentRemaining.ID] = attribute_handlers.bat_percent_remaining_handler, }, - [clusters.HepaFilterMonitoring.ID] = { - [clusters.HepaFilterMonitoring.attributes.Condition.ID] = hepa_filter_condition_handler, - [clusters.HepaFilterMonitoring.attributes.ChangeIndication.ID] = hepa_filter_change_indication_handler + [clusters.RelativeHumidityMeasurement.ID] = { + [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.relative_humidity_measured_value_handler }, - [clusters.ActivatedCarbonFilterMonitoring.ID] = { - [clusters.ActivatedCarbonFilterMonitoring.attributes.Condition.ID] = activated_carbon_filter_condition_handler, - [clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.ID] = activated_carbon_filter_change_indication_handler + [clusters.TemperatureMeasurement.ID] = { + [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.setpoint_limit_device_field.MAX_TEMP), + [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.temperature_handler_factory(capabilities.temperatureMeasurement.temperature), + [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.setpoint_limit_device_field.MIN_TEMP), }, - [clusters.AirQuality.ID] = { - [clusters.AirQuality.attributes.AirQuality.ID] = air_quality_attr_handler, + [clusters.Thermostat.ID] = { + [clusters.Thermostat.attributes.AttributeList.ID] = attribute_handlers.thermostat_attribute_list_handler, + [clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit.ID] = attribute_handlers.abs_cool_setpoint_limit_factory(fields.setpoint_limit_device_field.MAX_COOL), + [clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit.ID] = attribute_handlers.abs_heat_setpoint_limit_factory(fields.setpoint_limit_device_field.MAX_HEAT), + [clusters.Thermostat.attributes.AbsMinCoolSetpointLimit.ID] = attribute_handlers.abs_cool_setpoint_limit_factory(fields.setpoint_limit_device_field.MIN_COOL), + [clusters.Thermostat.attributes.AbsMinHeatSetpointLimit.ID] = attribute_handlers.abs_heat_setpoint_limit_factory(fields.setpoint_limit_device_field.MIN_HEAT), + [clusters.Thermostat.attributes.ControlSequenceOfOperation.ID] = attribute_handlers.control_sequence_of_operation_handler, + [clusters.Thermostat.attributes.LocalTemperature.ID] = attribute_handlers.temperature_handler_factory(capabilities.temperatureMeasurement.temperature), + [clusters.Thermostat.attributes.MinSetpointDeadBand.ID] = attribute_handlers.min_setpoint_deadband_handler, + [clusters.Thermostat.attributes.OccupiedCoolingSetpoint.ID] = attribute_handlers.temperature_handler_factory(capabilities.thermostatCoolingSetpoint.coolingSetpoint), + [clusters.Thermostat.attributes.OccupiedHeatingSetpoint.ID] = attribute_handlers.temperature_handler_factory(capabilities.thermostatHeatingSetpoint.heatingSetpoint), + [clusters.Thermostat.attributes.SystemMode.ID] = attribute_handlers.system_mode_handler, + [clusters.Thermostat.attributes.ThermostatRunningState.ID] = attribute_handlers.thermostat_running_state_handler, }, - [clusters.CarbonMonoxideConcentrationMeasurement.ID] = { - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.carbonMonoxideMeasurement.NAME, capabilities.carbonMonoxideMeasurement.carbonMonoxideLevel, units.PPM), - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.carbonMonoxideMeasurement.NAME), - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.carbonMonoxideHealthConcern.carbonMonoxideHealthConcern), + [clusters.WaterHeaterMode.ID] = { + [clusters.WaterHeaterMode.attributes.CurrentMode.ID] = attribute_handlers.water_heater_current_mode_handler, + [clusters.WaterHeaterMode.attributes.SupportedModes.ID] = attribute_handlers.water_heater_supported_modes_handler }, + -- CONCENTRATION MEASUREMENT CLUSTERS -- [clusters.CarbonDioxideConcentrationMeasurement.ID] = { - [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.carbonDioxideMeasurement.NAME, capabilities.carbonDioxideMeasurement.carbonDioxide, units.PPM), - [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.carbonDioxideMeasurement.NAME), - [clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.carbonDioxideMeasurement.NAME, capabilities.carbonDioxideMeasurement.carbonDioxide, fields.units.PPM), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.carbonDioxideMeasurement.NAME), + }, + [clusters.CarbonMonoxideConcentrationMeasurement.ID] = { + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.carbonMonoxideHealthConcern.carbonMonoxideHealthConcern), + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.carbonMonoxideMeasurement.NAME, capabilities.carbonMonoxideMeasurement.carbonMonoxideLevel, fields.units.PPM), + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.carbonMonoxideMeasurement.NAME), + }, + [clusters.FormaldehydeConcentrationMeasurement.ID] = { + [clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.formaldehydeHealthConcern.formaldehydeHealthConcern), + [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.formaldehydeMeasurement.NAME, capabilities.formaldehydeMeasurement.formaldehydeLevel, fields.units.PPM), + [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.formaldehydeMeasurement.NAME), }, [clusters.NitrogenDioxideConcentrationMeasurement.ID] = { - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.nitrogenDioxideMeasurement.NAME, capabilities.nitrogenDioxideMeasurement.nitrogenDioxide, units.PPM), - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.nitrogenDioxideMeasurement.NAME), - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.nitrogenDioxideHealthConcern.nitrogenDioxideHealthConcern) + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.nitrogenDioxideHealthConcern.nitrogenDioxideHealthConcern), + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.nitrogenDioxideMeasurement.NAME, capabilities.nitrogenDioxideMeasurement.nitrogenDioxide, fields.units.PPM), + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.nitrogenDioxideMeasurement.NAME), }, [clusters.OzoneConcentrationMeasurement.ID] = { - [clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.ozoneMeasurement.NAME, capabilities.ozoneMeasurement.ozone, units.PPM), - [clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.ozoneMeasurement.NAME), - [clusters.OzoneConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.ozoneHealthConcern.ozoneHealthConcern) - }, - [clusters.FormaldehydeConcentrationMeasurement.ID] = { - [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.formaldehydeMeasurement.NAME, capabilities.formaldehydeMeasurement.formaldehydeLevel, units.PPM), - [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.formaldehydeMeasurement.NAME), - [clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.formaldehydeHealthConcern.formaldehydeHealthConcern), + [clusters.OzoneConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.ozoneHealthConcern.ozoneHealthConcern), + [clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.ozoneMeasurement.NAME, capabilities.ozoneMeasurement.ozone, fields.units.PPM), + [clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.ozoneMeasurement.NAME), }, [clusters.Pm1ConcentrationMeasurement.ID] = { - [clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.veryFineDustSensor.NAME, capabilities.veryFineDustSensor.veryFineDustLevel, units.UGM3), - [clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.veryFineDustSensor.NAME), - [clusters.Pm1ConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern), - }, - [clusters.Pm25ConcentrationMeasurement.ID] = { - [clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.fineDustSensor.NAME, capabilities.fineDustSensor.fineDustLevel, units.UGM3), - [clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.fineDustSensor.NAME), - [clusters.Pm25ConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.fineDustHealthConcern.fineDustHealthConcern), + [clusters.Pm1ConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern), + [clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.veryFineDustSensor.NAME, capabilities.veryFineDustSensor.veryFineDustLevel, fields.units.UGM3), + [clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.veryFineDustSensor.NAME), }, [clusters.Pm10ConcentrationMeasurement.ID] = { - [clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.dustSensor.NAME, capabilities.dustSensor.dustLevel, units.UGM3), - [clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.dustSensor.NAME), - [clusters.Pm10ConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.dustHealthConcern.dustHealthConcern), + [clusters.Pm10ConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.dustHealthConcern.dustHealthConcern), + [clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.dustSensor.NAME, capabilities.dustSensor.dustLevel, fields.units.UGM3), + [clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.dustSensor.NAME), + }, + [clusters.Pm25ConcentrationMeasurement.ID] = { + [clusters.Pm25ConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.fineDustHealthConcern.fineDustHealthConcern), + [clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.fineDustSensor.NAME, capabilities.fineDustSensor.fineDustLevel, fields.units.UGM3), + [clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.fineDustSensor.NAME), }, [clusters.RadonConcentrationMeasurement.ID] = { - [clusters.RadonConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.radonMeasurement.NAME, capabilities.radonMeasurement.radonLevel, units.PCIL), - [clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.radonMeasurement.NAME), - [clusters.RadonConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.radonHealthConcern.radonHealthConcern) + [clusters.RadonConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.radonHealthConcern.radonHealthConcern), + [clusters.RadonConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.radonMeasurement.NAME, capabilities.radonMeasurement.radonLevel, fields.units.PCIL), + [clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.radonMeasurement.NAME), }, [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.ID] = { - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.tvocMeasurement.NAME, capabilities.tvocMeasurement.tvocLevel, units.PPB), - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.tvocMeasurement.NAME), - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.tvocHealthConcern.tvocHealthConcern) - }, - [clusters.ElectricalPowerMeasurement.ID] = { - [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = active_power_handler - }, - [clusters.ElectricalEnergyMeasurement.ID] = { - [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = cumulative_energy_imported_handler, - [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = periodic_energy_imported_handler - }, - [clusters.WaterHeaterMode.ID] = { - [clusters.WaterHeaterMode.attributes.CurrentMode.ID] = water_heater_mode_handler, - [clusters.WaterHeaterMode.attributes.SupportedModes.ID] = water_heater_supported_modes_attr_handler + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.tvocHealthConcern.tvocHealthConcern), + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.tvocMeasurement.NAME, capabilities.tvocMeasurement.tvocLevel, fields.units.PPB), + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.tvocMeasurement.NAME), }, }, }, - subscribed_attributes = subscribed_attributes, capability_handlers = { - [capabilities.switch.ID] = { - [capabilities.switch.commands.on.NAME] = handle_switch_on, - [capabilities.switch.commands.off.NAME] = handle_switch_off, + [capabilities.airConditionerFanMode.ID] = { + [capabilities.airConditionerFanMode.commands.setFanMode.NAME] = capability_handlers.fan_mode_command_factory(capabilities.airConditionerFanMode) }, - [capabilities.thermostatMode.ID] = { - [capabilities.thermostatMode.commands.setThermostatMode.NAME] = set_thermostat_mode, - [capabilities.thermostatMode.commands.auto.NAME] = thermostat_mode_setter(capabilities.thermostatMode.thermostatMode.auto.NAME), - [capabilities.thermostatMode.commands.off.NAME] = thermostat_mode_setter(capabilities.thermostatMode.thermostatMode.off.NAME), - [capabilities.thermostatMode.commands.cool.NAME] = thermostat_mode_setter(capabilities.thermostatMode.thermostatMode.cool.NAME), - [capabilities.thermostatMode.commands.heat.NAME] = thermostat_mode_setter(capabilities.thermostatMode.thermostatMode.heat.NAME), - [capabilities.thermostatMode.commands.emergencyHeat.NAME] = thermostat_mode_setter(capabilities.thermostatMode.thermostatMode.emergency_heat.NAME) + [capabilities.airPurifierFanMode.ID] = { + [capabilities.airPurifierFanMode.commands.setAirPurifierFanMode.NAME] = capability_handlers.fan_mode_command_factory(capabilities.airPurifierFanMode) }, - [capabilities.thermostatFanMode.ID] = { - [capabilities.thermostatFanMode.commands.setThermostatFanMode.NAME] = set_fan_mode_factory(capabilities.thermostatFanMode), - [capabilities.thermostatFanMode.commands.fanAuto.NAME] = thermostat_fan_mode_setter(capabilities.thermostatFanMode.thermostatFanMode.auto.NAME), - [capabilities.thermostatFanMode.commands.fanOn.NAME] = thermostat_fan_mode_setter(capabilities.thermostatFanMode.thermostatFanMode.on.NAME) + [capabilities.fanMode.ID] = { + [capabilities.fanMode.commands.setFanMode.NAME] = capability_handlers.fan_mode_command_factory(capabilities.fanMode) + }, + [capabilities.fanOscillationMode.ID] = { + [capabilities.fanOscillationMode.commands.setFanOscillationMode.NAME] = capability_handlers.handle_set_fan_oscillation_mode, + }, + [capabilities.fanSpeedPercent.ID] = { + [capabilities.fanSpeedPercent.commands.setPercent.NAME] = capability_handlers.handle_fan_speed_set_percent, + }, + [capabilities.filterState.ID] = { + [capabilities.filterState.commands.resetFilter.NAME] = capability_handlers.handle_filter_state_reset_filter, + }, + [capabilities.mode.ID] = { + [capabilities.mode.commands.setMode.NAME] = capability_handlers.handle_set_mode, + }, + [capabilities.switch.ID] = { + [capabilities.switch.commands.off.NAME] = capability_handlers.handle_switch_off, + [capabilities.switch.commands.on.NAME] = capability_handlers.handle_switch_on, }, [capabilities.thermostatCoolingSetpoint.ID] = { - [capabilities.thermostatCoolingSetpoint.commands.setCoolingSetpoint.NAME] = set_setpoint(clusters.Thermostat.attributes.OccupiedCoolingSetpoint) + [capabilities.thermostatCoolingSetpoint.commands.setCoolingSetpoint.NAME] = capability_handlers.thermostat_set_setpoint_factory(clusters.Thermostat.attributes.OccupiedCoolingSetpoint) }, [capabilities.thermostatHeatingSetpoint.ID] = { - [capabilities.thermostatHeatingSetpoint.commands.setHeatingSetpoint.NAME] = set_setpoint(clusters.Thermostat.attributes.OccupiedHeatingSetpoint) + [capabilities.thermostatHeatingSetpoint.commands.setHeatingSetpoint.NAME] = capability_handlers.thermostat_set_setpoint_factory(clusters.Thermostat.attributes.OccupiedHeatingSetpoint) + }, + [capabilities.thermostatFanMode.ID] = { + [capabilities.thermostatFanMode.commands.fanAuto.NAME] = capability_handlers.thermostat_fan_mode_command_factory(capabilities.thermostatFanMode.thermostatFanMode.auto.NAME), + [capabilities.thermostatFanMode.commands.fanOn.NAME] = capability_handlers.thermostat_fan_mode_command_factory(capabilities.thermostatFanMode.thermostatFanMode.on.NAME), + [capabilities.thermostatFanMode.commands.setThermostatFanMode.NAME] = capability_handlers.fan_mode_command_factory(capabilities.thermostatFanMode), + }, + [capabilities.thermostatMode.ID] = { + [capabilities.thermostatMode.commands.auto.NAME] = capability_handlers.thermostat_mode_command_factory(capabilities.thermostatMode.thermostatMode.auto.NAME), + [capabilities.thermostatMode.commands.cool.NAME] = capability_handlers.thermostat_mode_command_factory(capabilities.thermostatMode.thermostatMode.cool.NAME), + [capabilities.thermostatMode.commands.emergencyHeat.NAME] = capability_handlers.thermostat_mode_command_factory(capabilities.thermostatMode.thermostatMode.emergency_heat.NAME), + [capabilities.thermostatMode.commands.heat.NAME] = capability_handlers.thermostat_mode_command_factory(capabilities.thermostatMode.thermostatMode.heat.NAME), + [capabilities.thermostatMode.commands.off.NAME] = capability_handlers.thermostat_mode_command_factory(capabilities.thermostatMode.thermostatMode.off.NAME), + [capabilities.thermostatMode.commands.setThermostatMode.NAME] = capability_handlers.handle_set_thermostat_mode, + }, + [capabilities.windMode.ID] = { + [capabilities.windMode.commands.setWindMode.NAME] = capability_handlers.handle_set_wind_mode, }, + }, + subscribed_attributes = { [capabilities.airConditionerFanMode.ID] = { - [capabilities.airConditionerFanMode.commands.setFanMode.NAME] = set_fan_mode_factory(capabilities.airConditionerFanMode) + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode }, [capabilities.airPurifierFanMode.ID] = { - [capabilities.airPurifierFanMode.commands.setAirPurifierFanMode.NAME] = set_fan_mode_factory(capabilities.airPurifierFanMode) + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, + [capabilities.battery.ID] = { + clusters.PowerSource.attributes.BatPercentRemaining + }, + [capabilities.batteryLevel.ID] = { + clusters.PowerSource.attributes.BatChargeLevel + }, + [capabilities.energyMeter.ID] = { + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported }, [capabilities.fanMode.ID] = { - [capabilities.fanMode.commands.setFanMode.NAME] = set_fan_mode_factory(capabilities.fanMode) + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, + [capabilities.fanOscillationMode.ID] = { + clusters.FanControl.attributes.RockSupport, + clusters.FanControl.attributes.RockSetting }, [capabilities.fanSpeedPercent.ID] = { - [capabilities.fanSpeedPercent.commands.setPercent.NAME] = set_fan_speed_percent, + clusters.FanControl.attributes.PercentCurrent }, - [capabilities.windMode.ID] = { - [capabilities.windMode.commands.setWindMode.NAME] = set_wind_mode, + [capabilities.filterState.ID] = { + clusters.HepaFilterMonitoring.attributes.Condition, + clusters.ActivatedCarbonFilterMonitoring.attributes.Condition }, - [capabilities.fanOscillationMode.ID] = { - [capabilities.fanOscillationMode.commands.setFanOscillationMode.NAME] = set_rock_mode, + [capabilities.filterStatus.ID] = { + clusters.HepaFilterMonitoring.attributes.ChangeIndication, + clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication }, [capabilities.mode.ID] = { - [capabilities.mode.commands.setMode.NAME] = set_water_heater_mode, + clusters.WaterHeaterMode.attributes.CurrentMode, + clusters.WaterHeaterMode.attributes.SupportedModes + }, + [capabilities.powerConsumptionReport.ID] = { + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported + }, + [capabilities.powerMeter.ID] = { + clusters.ElectricalPowerMeasurement.attributes.ActivePower + }, + [capabilities.relativeHumidityMeasurement.ID] = { + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue + }, + [capabilities.switch.ID] = { + clusters.OnOff.attributes.OnOff + }, + [capabilities.temperatureMeasurement.ID] = { + clusters.Thermostat.attributes.LocalTemperature, + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue + }, + [capabilities.thermostatCoolingSetpoint.ID] = { + clusters.Thermostat.attributes.OccupiedCoolingSetpoint, + clusters.Thermostat.attributes.AbsMinCoolSetpointLimit, + clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit + }, + [capabilities.thermostatFanMode.ID] = { + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, + [capabilities.thermostatHeatingSetpoint.ID] = { + clusters.Thermostat.attributes.OccupiedHeatingSetpoint, + clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, + clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit + }, + [capabilities.thermostatMode.ID] = { + clusters.Thermostat.attributes.SystemMode, + clusters.Thermostat.attributes.ControlSequenceOfOperation + }, + [capabilities.thermostatOperatingState.ID] = { + clusters.Thermostat.attributes.ThermostatRunningState + }, + [capabilities.windMode.ID] = { + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.WindSetting + }, + -- AIR QUALITY SENSOR DEVICE TYPE SPECIFIC CAPABILITIES -- + [capabilities.airQualityHealthConcern.ID] = { + clusters.AirQuality.attributes.AirQuality + }, + [capabilities.atmosphericPressureMeasurement.ID] = { + clusters.PressureMeasurement.attributes.MeasuredValue + }, + [capabilities.carbonDioxideHealthConcern.ID] = { + clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.carbonDioxideMeasurement.ID] = { + clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue, + clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.carbonMonoxideHealthConcern.ID] = { + clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.carbonMonoxideMeasurement.ID] = { + clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, + clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.dustHealthConcern.ID] = { + clusters.Pm10ConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.dustSensor.ID] = { + clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, + clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.fineDustHealthConcern.ID] = { + clusters.Pm25ConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.fineDustSensor.ID] = { + clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.formaldehydeHealthConcern.ID] = { + clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.formaldehydeMeasurement.ID] = { + clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue, + clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.nitrogenDioxideHealthConcern.ID] = { + clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.nitrogenDioxideMeasurement.ID] = { + clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue, + clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit + }, + [capabilities.ozoneHealthConcern.ID] = { + clusters.OzoneConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.ozoneMeasurement.ID] = { + clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue, + clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit + }, + [capabilities.radonHealthConcern.ID] = { + clusters.RadonConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.radonMeasurement.ID] = { + clusters.RadonConcentrationMeasurement.attributes.MeasuredValue, + clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.relativeHumidityMeasurement.ID] = { + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue + }, + [capabilities.tvocHealthConcern.ID] = { + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue + }, + [capabilities.tvocMeasurement.ID] = { + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue, + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.veryFineDustHealthConcern.ID] = { + clusters.Pm1ConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.veryFineDustSensor.ID] = { + clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.filterState.ID] = { - [capabilities.filterState.commands.resetFilter.NAME] = reset_filter_state, - } }, supported_capabilities = { - capabilities.thermostatMode, - capabilities.thermostatHeatingSetpoint, - capabilities.thermostatCoolingSetpoint, - capabilities.thermostatFanMode, - capabilities.thermostatOperatingState, capabilities.airConditionerFanMode, - capabilities.fanMode, - capabilities.fanSpeedPercent, + capabilities.airQualityHealthConcern, capabilities.airPurifierFanMode, - capabilities.windMode, - capabilities.fanOscillationMode, capabilities.battery, capabilities.batteryLevel, - capabilities.filterState, - capabilities.filterStatus, - capabilities.airQualityHealthConcern, capabilities.carbonDioxideHealthConcern, capabilities.carbonDioxideMeasurement, capabilities.carbonMonoxideHealthConcern, capabilities.carbonMonoxideMeasurement, + capabilities.dustHealthConcern, + capabilities.dustSensor, + capabilities.energyMeter, + capabilities.fanMode, + capabilities.fanOscillationMode, + capabilities.fanSpeedPercent, + capabilities.filterState, + capabilities.filterStatus, + capabilities.fineDustHealthConcern, + capabilities.fineDustSensor, + capabilities.formaldehydeHealthConcern, + capabilities.formaldehydeMeasurement, + capabilities.mode, capabilities.nitrogenDioxideHealthConcern, capabilities.nitrogenDioxideMeasurement, capabilities.ozoneHealthConcern, capabilities.ozoneMeasurement, - capabilities.formaldehydeHealthConcern, - capabilities.formaldehydeMeasurement, - capabilities.veryFineDustHealthConcern, - capabilities.veryFineDustSensor, - capabilities.fineDustHealthConcern, - capabilities.fineDustSensor, - capabilities.dustSensor, - capabilities.dustHealthConcern, + capabilities.powerConsumptionReport, + capabilities.powerMeter, capabilities.radonHealthConcern, capabilities.radonMeasurement, + capabilities.thermostatCoolingSetpoint, + capabilities.thermostatFanMode, + capabilities.thermostatHeatingSetpoint, + capabilities.thermostatMode, + capabilities.thermostatOperatingState, capabilities.tvocHealthConcern, capabilities.tvocMeasurement, - capabilities.powerMeter, - capabilities.energyMeter, - capabilities.powerConsumptionReport, - capabilities.mode + capabilities.veryFineDustHealthConcern, + capabilities.veryFineDustSensor, + capabilities.windMode, }, } diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua index f05c5ee175..ca7fddcdd9 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua @@ -1,17 +1,8 @@ --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" +test.set_rpc_version(0) local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local SinglePrecisionFloat = require "st.matter.data_types.SinglePrecisionFloat" @@ -19,19 +10,19 @@ local clusters = require "st.matter.clusters" local version = require "version" if version.api < 10 then - clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" - clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" end local mock_device = test.mock_device.build_test_matter_device({ diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua index 871693d5a6..fd68c958d9 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -21,19 +11,19 @@ local version = require "version" version.api = 9 -- include driver-side cluster definitions to test embedded clusters on lower api versions -clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" -clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" -clusters.AirQuality = require "AirQuality" -clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" -clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" -clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" -clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" -clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" -clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" -clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" -clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" -clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" -clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" +clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" +clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" +clusters.AirQuality = require "embedded_clusters.AirQuality" +clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" +clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" +clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" +clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" +clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" +clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" +clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" +clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" +clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" +clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("air-purifier-hepa-ac-wind.yml"), diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua index c21de741e2..745076cd04 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua @@ -1,39 +1,30 @@ --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" -test.set_rpc_version(8) local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" -local dkjson = require "dkjson" local clusters = require "st.matter.clusters" +local im = require "st.matter.interaction_model" +local uint32 = require "st.matter.data_types.Uint32" local version = require "version" +test.disable_startup_messages() + if version.api < 10 then - clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" - clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" end local mock_device_basic = test.mock_device.build_test_matter_device({ @@ -224,6 +215,21 @@ local cluster_subscribe_list_configured = { } local function test_init_basic() + test.mock_device.add_test_device(mock_device_basic) + test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "added" }) + local read_attributes = { + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.RockSupport, + } + local read_request = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, clus in ipairs(read_attributes) do + read_request:merge(clus:read(mock_device_basic)) + end + test.socket.matter:__expect_send({ mock_device_basic.id, read_request }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "init" }) local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_basic) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -231,10 +237,25 @@ local function test_init_basic() end end test.socket.matter:__expect_send({mock_device_basic.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_basic) end local function test_init_ap_thermo_aqs_preconfigured() + test.mock_device.add_test_device(mock_device_ap_thermo_aqs) + test.socket.device_lifecycle:__queue_receive({ mock_device_ap_thermo_aqs.id, "added" }) + local read_attributes = { + clusters.Thermostat.attributes.AttributeList, + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.RockSupport, + } + local read_request = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, clus in ipairs(read_attributes) do + read_request:merge(clus:read(mock_device_ap_thermo_aqs)) + end + test.socket.matter:__expect_send({ mock_device_ap_thermo_aqs.id, read_request }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_ap_thermo_aqs.id, "init" }) local subscribe_request = nil for _, attributes in pairs(cluster_subscribe_list_configured) do for _, attribute in ipairs(attributes) do @@ -246,7 +267,6 @@ local function test_init_ap_thermo_aqs_preconfigured() end end test.socket.matter:__expect_send({mock_device_ap_thermo_aqs.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_ap_thermo_aqs) end local expected_update_metadata= { @@ -283,15 +303,16 @@ local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_basic) test.register_coroutine_test( "Test profile change on init for basic Air Purifier device", function() - mock_device_basic:set_field("__BATTERY_SUPPORT", "NO_BATTERY") -- since we're assuming this would have happened during device_added in this case. - mock_device_basic:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "doConfigure" }) mock_device_basic:expect_metadata_update(expected_update_metadata) mock_device_basic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - local device_info_copy = utils.deep_copy(mock_device_basic.raw_st_data) - device_info_copy.profile.id = "air-purifier-modular" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "infoChanged", device_info_json }) + + test.wait_for_events() + + local updated_device_profile = t_utils.get_profile_definition("air-purifier-modular.yml", + {enabled_optional_capabilities = expected_update_metadata.optional_component_capabilities} + ) + test.socket.device_lifecycle:__queue_receive(mock_device_basic:generate_info_changed({ profile = updated_device_profile })) test.socket.matter:__expect_send({mock_device_basic.id, subscribe_request}) end, { test_init = test_init_basic } @@ -339,7 +360,6 @@ local expected_update_metadata= { local subscribe_request = nil for _, attributes in pairs(cluster_subscribe_list_configured) do - print("Adding attribute to subscribe", attributes) for _, attribute in ipairs(attributes) do if subscribe_request == nil then subscribe_request = attribute:subscribe(mock_device_ap_thermo_aqs) @@ -352,15 +372,21 @@ end test.register_coroutine_test( "Test profile change on init for AP and Thermo and AQS combined device type", function() - mock_device_ap_thermo_aqs:set_field("__BATTERY_SUPPORT", "NO_BATTERY") -- since we're assuming this would have happened during device_added in this case. - mock_device_ap_thermo_aqs:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_ap_thermo_aqs.id, "doConfigure" }) - mock_device_ap_thermo_aqs:expect_metadata_update(expected_update_metadata) mock_device_ap_thermo_aqs:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - local device_info_copy = utils.deep_copy(mock_device_ap_thermo_aqs.raw_st_data) - device_info_copy.profile.id = "air-purifier-modular" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device_ap_thermo_aqs.id, "infoChanged", device_info_json }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device_ap_thermo_aqs.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(mock_device_ap_thermo_aqs, 1, {uint32(0)}) + }) + mock_device_ap_thermo_aqs:expect_metadata_update(expected_update_metadata) + + test.wait_for_events() + + local updated_device_profile = t_utils.get_profile_definition("air-purifier-modular.yml", + {enabled_optional_capabilities = expected_update_metadata.optional_component_capabilities} + ) + test.socket.device_lifecycle:__queue_receive(mock_device_ap_thermo_aqs:generate_info_changed({ profile = updated_device_profile })) test.socket.matter:__expect_send({mock_device_ap_thermo_aqs.id, subscribe_request}) end, { test_init = test_init_ap_thermo_aqs_preconfigured } diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua index 4a94a9a7e9..8eefceb23b 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua index 3a33e94251..e96bfb69fc 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.matter.clusters" @@ -27,8 +16,8 @@ local HEAT_PUMP_DEVICE_TYPE_ID = 0x0309 local THERMOSTAT_DEVICE_TYPE_ID = 0x0301 if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" end local device_desc = { @@ -83,6 +72,8 @@ local device_desc = { } local test_init_common = function(device) + test.disable_startup_messages() + test.mock_device.add_test_device(device) local cluster_subscribe_list = { clusters.Thermostat.attributes.SystemMode, clusters.Thermostat.attributes.ControlSequenceOfOperation, @@ -99,6 +90,7 @@ local test_init_common = function(device) clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, clusters.ElectricalPowerMeasurement.attributes.ActivePower, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, } test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(device) @@ -107,7 +99,6 @@ local test_init_common = function(device) subscribe_request:merge(cluster:subscribe(device)) end end - test.socket.matter:__expect_send({ device.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ device.id, "added" }) local read_request_on_added = { clusters.Thermostat.attributes.ControlSequenceOfOperation, @@ -124,15 +115,13 @@ local test_init_common = function(device) device.id, read_request }) - test.mock_device.add_test_device(device) + test.socket.device_lifecycle:__queue_receive({ device.id, "init" }) + test.socket.matter:__expect_send({ device.id, subscribe_request }) end local mock_device = test.mock_device.build_test_matter_device(device_desc) local function test_init() test_init_common(mock_device) - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) end -- Create device with Thermostat clusters having features AUTO, HEAT & COOL @@ -141,9 +130,6 @@ device_desc.endpoints[4].clusters[1].feature_map = 35 local mock_device_with_auto = test.mock_device.build_test_matter_device(device_desc) local test_init_auto = function() test_init_common(mock_device_with_auto) - test.socket.matter:__expect_send({ - mock_device_with_auto.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device_with_auto) - }) test.socket.matter:__expect_send({ mock_device_with_auto.id, clusters.Thermostat.attributes.MinSetpointDeadBand:read(mock_device_with_auto) }) @@ -151,13 +137,6 @@ end -- Set feature map of ElectricalEnergyMeasurement Cluster to only PERE and IMPE device_desc.endpoints[2].clusters[1].feature_map = 9 -local mock_device_with_pere_impe = test.mock_device.build_test_matter_device(device_desc) -local test_init_pere_impe = function() - test_init_common(mock_device_with_pere_impe) - test.socket.matter:__expect_send({ - mock_device_with_pere_impe.id, clusters.Thermostat.attributes.MinSetpointDeadBand:read(mock_device_with_pere_impe) - }) -end test.set_test_init_function(test_init) @@ -181,6 +160,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedHeatingSetpoint:build_test_report_data(mock_device, THERMOSTAT_ONE_EP, 40*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("thermostatOne", capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -194,6 +178,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedHeatingSetpoint:build_test_report_data(mock_device, THERMOSTAT_TWO_EP, 23*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("thermostatTwo", capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -213,6 +202,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedCoolingSetpoint:build_test_report_data(mock_device, THERMOSTAT_ONE_EP, 39*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("thermostatOne", capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -226,6 +220,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedCoolingSetpoint:build_test_report_data(mock_device, THERMOSTAT_TWO_EP, 19*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("thermostatTwo", capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -623,7 +622,7 @@ test.register_message_test( clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 15000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 15000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) } }, { @@ -635,16 +634,13 @@ test.register_message_test( ) test.register_coroutine_test( - "The total energy consumption of the device must be reported every 15 minutes", + "The total energy consumption of the device must be reported a maximum of every 15 minutes", function() - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 20Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 20Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -653,28 +649,22 @@ test.register_coroutine_test( })) ) - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ energy = 20, - deltaEnergy = 20, + deltaEnergy = 0.0, start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" + ["end"] = "1970-01-01T00:15:00Z" })) - ) + ) test.wait_for_events() - - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) + test.mock_time.advance_time(60 * 10) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 30000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 30Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 30000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 30Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -682,60 +672,10 @@ test.register_coroutine_test( value = 30, unit = "Wh" })) ) - - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.powerConsumptionReport.powerConsumption({ - energy = 30, - deltaEnergy = 10, - start = "1970-01-01T00:15:00Z", - ["end"] = "1970-01-01T00:29:59Z" - })) - ) - test.wait_for_events() end, { test_init = function() test_init() - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "polling_report_schedule_timer") - test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") - end - } -) - -test.register_coroutine_test( - "Ensure the driver does not send read request to devices without CUME & IMPE features", - function() - local timer = mock_device_with_pere_impe:get_field("__recurring_poll_timer") - assert(timer == nil, "Polling timer must not be created if the device does not support CUME & IMPE features") - end, - { - test_init = function() - test_init_pere_impe() - end - } -) - -test.register_coroutine_test( - "PeriodicEnergyImported should report the energyMeter values", - function() - test.socket.matter:__queue_receive({ mock_device_with_pere_impe.id, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data(mock_device_with_pere_impe, - HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 30000, start_timestamp = 0, end_timestamp = 100, start_systime = 0, end_systime = 0 })) }) -- 30Wh - - test.socket.capability:__expect_send( - mock_device_with_pere_impe:generate_test_message("main", - capabilities.energyMeter.energy({ - value = 30, unit = "Wh" - })) - ) - end, - { - test_init = function() - test_init_pere_impe() end } ) @@ -743,14 +683,11 @@ test.register_coroutine_test( test.register_coroutine_test( "Ensure only the cumulative energy reports are considered if the device supports both PERE and CUME features.", function() - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 20Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 20Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -759,85 +696,38 @@ test.register_coroutine_test( })) ) - test.wait_for_events() - - -- do not expect energyMeter event for this report. - test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data(mock_device, - HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 800, start_systime = 0, end_systime = 0 })) }) -- 20Wh - - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.powerConsumptionReport.powerConsumption({ - energy = 20, - deltaEnergy = 20, - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" - })) + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:18:00Z", + deltaEnergy = 0.0, + energy = 20 + })) ) + test.mock_time.advance_time(180) -- move time 180s, which is less than 15m (obviously). test.wait_for_events() - end, - { - test_init = function() - test_init() - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "polling_report_schedule_timer") - test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") - end - } -) -test.register_coroutine_test( - "Consider the device reported time interval in case it is greater than 15 minutes for powerConsumptionReport capability reports", - function() - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) + -- do not expect energyMeter event for this report. + test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data(mock_device, + HEAT_PUMP_EP, + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 800, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 20Wh + -- do not expect a powerConsumptionReport to be emitted test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 20Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 50000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 50Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.energyMeter.energy({ - value = 20, unit = "Wh" + value = 50, unit = "Wh" })) ) - - test.wait_for_events() - - -- do not expect energyMeter event for this report. Only consider the time interval as it is greater than 15 minutes. - test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data(mock_device, - HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 1080, start_systime = 0, end_systime = 0 })) }) -- 20Wh 18 minutes - - - test.wait_for_events() - test.mock_time.advance_time(60 * 18) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.powerConsumptionReport.powerConsumption({ - energy = 20, - deltaEnergy = 20, - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:17:59Z" - })) - ) - - test.wait_for_events() end, { test_init = function() test_init() - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "polling_report_schedule_timer") - test.timer.__create_and_queue_test_time_advance_timer(60 * 18, "interval", "polling_report_schedule_timer") - test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") end } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua index 6c30fae787..bb7b9e6bde 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua @@ -1,21 +1,11 @@ --- Copyright 2024 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" +test.set_rpc_version(0) local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -31,19 +21,22 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, }, device_types = { - device_type_id = 0x0016, device_type_revision = 1, -- RootNode + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode } }, { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, - } + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, + }, + device_types = { + { device_type_id = 0x0072, device_type_revision = 1 } -- Room Air Conditioner } + } } }) @@ -64,18 +57,18 @@ local mock_device_configure = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner - } + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner } + } } }) @@ -163,6 +156,9 @@ local function test_init() end end end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) end @@ -221,6 +217,9 @@ local function test_init_configure() end end end + test.socket.capability:__expect_send( + mock_device_configure:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({mock_device_configure.id, subscribe_request}) local read_setpoint_deadband = clusters.Thermostat.attributes.MinSetpointDeadBand:read() @@ -482,4 +481,88 @@ test.register_message_test( } ) +local ControlSequenceOfOperation = clusters.Thermostat.attributes.ControlSequenceOfOperation +test.register_message_test( + "Room AC control sequence reports should generate correct messages", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + ControlSequenceOfOperation:build_test_report_data(mock_device, 1, ControlSequenceOfOperation.COOLING_AND_HEATING_WITH_REHEAT) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"cool", "heat"}, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + ControlSequenceOfOperation:build_test_report_data(mock_device, 1, ControlSequenceOfOperation.HEATING_WITH_REHEAT) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"heat"}, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + ControlSequenceOfOperation:build_test_report_data(mock_device, 1, ControlSequenceOfOperation.COOLING_WITH_REHEAT) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"cool"}, {visibility={displayed=false}})) + }, + } +) + +test.register_message_test( + "Additional mode reports should extend the supported modes", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Thermostat.server.attributes.ControlSequenceOfOperation:build_test_report_data(mock_device, 1, 5) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"cool", "heat"}, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Thermostat.server.attributes.SystemMode:build_test_report_data(mock_device, 1, 5) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"cool", "heat", "emergency heat"}, {visibility={displayed=false}})) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.emergency_heat()) + } + } +) + + test.run_registered_tests() diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua index 096ce71221..be240c95d0 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua @@ -1,24 +1,14 @@ --- Copyright 2024 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" -local dkjson = require "dkjson" - local clusters = require "st.matter.clusters" +local im = require "st.matter.interaction_model" +local uint32 = require "st.matter.data_types.Uint32" +test.disable_startup_messages() test.set_rpc_version(8) local mock_device_basic = test.mock_device.build_test_matter_device({ @@ -38,18 +28,18 @@ local mock_device_basic = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner - } + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner } + } } }) @@ -70,22 +60,23 @@ local mock_device_no_state = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner - } + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner } + } } }) local function initialize_mock_device(generic_mock_device, generic_subscribed_attributes) + test.mock_device.add_test_device(generic_mock_device) local subscribe_request = nil for _, attributes in pairs(generic_subscribed_attributes) do for _, attribute in ipairs(attributes) do @@ -96,13 +87,31 @@ local function initialize_mock_device(generic_mock_device, generic_subscribed_at end end end + test.socket.capability:__expect_send( + generic_mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) - test.mock_device.add_test_device(generic_mock_device) return subscribe_request end +local function read_req_on_added(device) + local attributes = { + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.RockSupport, + clusters.Thermostat.attributes.AttributeList, + } + local read_request = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, clus in ipairs(attributes) do + read_request:merge(clus:read(device)) + end + test.socket.matter:__expect_send({ device.id, read_request }) +end + local subscribe_request_basic local function test_init_basic() + test.socket.matter:__set_channel_ordering("relaxed") local subscribed_attributes = { [capabilities.switch.ID] = { clusters.OnOff.attributes.OnOff @@ -145,6 +154,8 @@ local function test_init_basic() clusters.FanControl.attributes.WindSetting }, } + test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "added" }) + read_req_on_added(mock_device_basic) subscribe_request_basic = initialize_mock_device(mock_device_basic, subscribed_attributes) local read_setpoint_deadband = clusters.Thermostat.attributes.MinSetpointDeadBand:read() test.socket.matter:__expect_send({mock_device_basic.id, read_setpoint_deadband}) @@ -204,8 +215,8 @@ for _, attributes in pairs(subscribed_attributes_no_state) do end end - local function test_init_no_state() + test.socket.matter:__set_channel_ordering("relaxed") local subscribed_attributes = { [capabilities.switch.ID] = { clusters.OnOff.attributes.OnOff @@ -249,6 +260,8 @@ local function test_init_no_state() }, } + test.socket.device_lifecycle:__queue_receive({ mock_device_no_state.id, "added" }) + read_req_on_added(mock_device_no_state) -- initially, device onboards WITH thermostatOperatingState, the test below will -- check if it is removed correctly when switching to modular profile. This is done -- to test that cases where the modular profile is different from the static profile @@ -260,14 +273,19 @@ local function test_init_no_state() end -- run the profile configuration tests -local function test_room_ac_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request) +local function test_room_ac_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request, thermostat_attr_list_value) test.socket.device_lifecycle:__queue_receive({generic_mock_device.id, "doConfigure"}) - generic_mock_device:expect_metadata_update(expected_metadata) generic_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - local device_info_copy = utils.deep_copy(generic_mock_device.raw_st_data) - device_info_copy.profile.id = "room-air-conditioner-modular" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ generic_mock_device.id, "infoChanged", device_info_json }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + generic_mock_device.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(generic_mock_device, 1, {thermostat_attr_list_value}) + }) + generic_mock_device:expect_metadata_update(expected_metadata) + local updated_device_profile = t_utils.get_profile_definition("air-purifier-modular.yml", + {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} + ) + test.socket.device_lifecycle:__queue_receive(generic_mock_device:generate_info_changed({ profile = updated_device_profile })) test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) end @@ -292,9 +310,7 @@ local expected_metadata_basic= { test.register_coroutine_test( "Device with modular profile should enable correct optional capabilities - basic", function() - mock_device_basic:set_field("__BATTERY_SUPPORT", "NO_BATTERY") -- since we're assuming this would have happened during device_added in this case. - mock_device_basic:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", true) -- since we're assuming this would have happened during device_added in this case. - test_room_ac_device_type_update_modular_profile(mock_device_basic, expected_metadata_basic, subscribe_request_basic) + test_room_ac_device_type_update_modular_profile(mock_device_basic, expected_metadata_basic, subscribe_request_basic, uint32(0x29)) end, { test_init = test_init_basic } ) @@ -319,9 +335,7 @@ local expected_metadata_no_state = { test.register_coroutine_test( "Device with modular profile should enable correct optional capabilities - no thermo state", function() - mock_device_no_state:set_field("__BATTERY_SUPPORT", "NO_BATTERY") -- since we're assuming this would have happened during device_added in this case. - mock_device_no_state:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) -- since we're assuming this would have happened during device_added in this case. - test_room_ac_device_type_update_modular_profile(mock_device_no_state, expected_metadata_no_state, subscribe_request_no_state) + test_room_ac_device_type_update_modular_profile(mock_device_no_state, expected_metadata_no_state, subscribe_request_no_state, uint32(0)) end, { test_init = test_init_no_state } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_battery.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_battery.lua index 96fdf02f3d..a28882ca46 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_battery.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_battery.lua @@ -1,22 +1,14 @@ --- Copyright 2023 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" local uint32 = require "st.matter.data_types.Uint32" +test.set_rpc_version(7) + local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("thermostat-batteryLevel.yml"), manufacturer_info = { @@ -78,6 +70,9 @@ local function test_init() end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "cooling"}, {visibility = {displayed = false}})) + ) test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua index 59ae9a433f..2db6e20e59 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua @@ -1,25 +1,15 @@ --- Copyright 2022 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" - local clusters = require "st.matter.clusters" +test.set_rpc_version(7) + local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"), + profile = t_utils.get_profile_definition("thermostat-humidity-fan-nostate.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -56,7 +46,7 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local mock_device_simple = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("thermostat.yml"), + profile = t_utils.get_profile_definition("thermostat-nostate.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -91,7 +81,7 @@ local mock_device_simple = test.mock_device.build_test_matter_device({ }) local mock_device_no_battery = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("thermostat.yml"), + profile = t_utils.get_profile_definition("thermostat-nostate.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -133,7 +123,6 @@ local cluster_subscribe_list = { clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit, clusters.Thermostat.attributes.SystemMode, - clusters.Thermostat.attributes.ThermostatRunningState, clusters.Thermostat.attributes.ControlSequenceOfOperation, clusters.TemperatureMeasurement.attributes.MeasuredValue, clusters.TemperatureMeasurement.attributes.MinMeasuredValue, @@ -152,7 +141,6 @@ local cluster_subscribe_list_simple = { clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit, clusters.Thermostat.attributes.SystemMode, - clusters.Thermostat.attributes.ThermostatRunningState, clusters.Thermostat.attributes.ControlSequenceOfOperation, clusters.TemperatureMeasurement.attributes.MeasuredValue, clusters.TemperatureMeasurement.attributes.MinMeasuredValue, @@ -168,7 +156,6 @@ local cluster_subscribe_list_no_battery = { clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit, clusters.Thermostat.attributes.SystemMode, - clusters.Thermostat.attributes.ThermostatRunningState, clusters.Thermostat.attributes.ControlSequenceOfOperation, clusters.TemperatureMeasurement.attributes.MeasuredValue, clusters.TemperatureMeasurement.attributes.MinMeasuredValue, diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua index d1a70c3f06..90e529beb3 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua @@ -1,21 +1,11 @@ --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" - +local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" +local uint32 = require "st.matter.data_types.Uint32" local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"), @@ -135,32 +125,18 @@ local cluster_subscribe_list = { clusters.FanControl.attributes.FanModeSequence, } -local cluster_subscribe_list_disorder_endpoints = { - clusters.Thermostat.attributes.LocalTemperature, - clusters.Thermostat.attributes.OccupiedCoolingSetpoint, - clusters.Thermostat.attributes.OccupiedHeatingSetpoint, - clusters.Thermostat.attributes.AbsMinCoolSetpointLimit, - clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit, - clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, - clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit, - clusters.Thermostat.attributes.SystemMode, - clusters.Thermostat.attributes.ThermostatRunningState, - clusters.Thermostat.attributes.ControlSequenceOfOperation, - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, - clusters.FanControl.attributes.FanMode, - clusters.FanControl.attributes.FanModeSequence, -} - -local function test_init() - mock_device:set_field("MIN_SETPOINT_DEADBAND_CHECKED", 1, {persist = true}) - test.socket.matter:__set_channel_ordering("relaxed") - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do +local function get_subscribe_request(device, attribute_list) + local subscribe_request = attribute_list[1]:subscribe(device) + for i, cluster in ipairs(attribute_list) do if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) + subscribe_request:merge(cluster:subscribe(device)) end end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + return subscribe_request +end + +local function test_init() + test.disable_startup_messages() test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) @@ -171,19 +147,17 @@ local function test_init() read_req:merge(clusters.FanControl.attributes.RockSupport:read()) read_req:merge(clusters.Thermostat.attributes.AttributeList:read()) test.socket.matter:__expect_send({mock_device.id, read_req}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) + test.socket.matter:__expect_send({mock_device.id, get_subscribe_request(mock_device, cluster_subscribe_list)}) end test.set_test_init_function(test_init) local function test_init_disorder_endpoints() - mock_device_disorder_endpoints:set_field("MIN_SETPOINT_DEADBAND_CHECKED", 1, {persist = true}) - test.socket.matter:__set_channel_ordering("relaxed") - local subscribe_request_disorder_endpoints = cluster_subscribe_list_disorder_endpoints[1]:subscribe(mock_device_disorder_endpoints) - for i, cluster in ipairs(cluster_subscribe_list_disorder_endpoints) do - if i > 1 then - subscribe_request_disorder_endpoints:merge(cluster:subscribe(mock_device_disorder_endpoints)) - end - end - test.socket.matter:__expect_send({mock_device_disorder_endpoints.id, subscribe_request_disorder_endpoints}) + test.disable_startup_messages() test.mock_device.add_test_device(mock_device_disorder_endpoints) test.socket.device_lifecycle:__queue_receive({ mock_device_disorder_endpoints.id, "added" }) @@ -194,27 +168,117 @@ local function test_init_disorder_endpoints() read_req:merge(clusters.FanControl.attributes.RockSupport:read()) read_req:merge(clusters.Thermostat.attributes.AttributeList:read()) test.socket.matter:__expect_send({mock_device_disorder_endpoints.id, read_req}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_disorder_endpoints.id, "init" }) + test.socket.capability:__expect_send( + mock_device_disorder_endpoints:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) + test.socket.matter:__expect_send({mock_device_disorder_endpoints.id, get_subscribe_request( + mock_device_disorder_endpoints, cluster_subscribe_list)}) end +-- run the profile configuration tests +local function test_thermostat_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request) + test.socket.device_lifecycle:__queue_receive({generic_mock_device.id, "doConfigure"}) + generic_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + generic_mock_device.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(generic_mock_device, 1, {uint32(0)}) + }) + generic_mock_device:expect_metadata_update(expected_metadata) + + test.wait_for_events() + + local updated_device_profile = t_utils.get_profile_definition("thermostat-modular.yml", + {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} + ) + test.socket.device_lifecycle:__queue_receive(generic_mock_device:generate_info_changed({ profile = updated_device_profile })) + test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) +end + +local expected_metadata = { + optional_component_capabilities={ + { + "main", + { + "relativeHumidityMeasurement", + "fanSpeedPercent", + "fanMode", + "fanOscillationMode", + "thermostatHeatingSetpoint", + "thermostatCoolingSetpoint" + }, + }, + }, + profile="thermostat-modular", +} + +local new_cluster_subscribe_list = { + clusters.Thermostat.attributes.LocalTemperature, + clusters.Thermostat.attributes.OccupiedCoolingSetpoint, + clusters.Thermostat.attributes.OccupiedHeatingSetpoint, + clusters.Thermostat.attributes.AbsMinCoolSetpointLimit, + clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit, + clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, + clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit, + clusters.Thermostat.attributes.SystemMode, + clusters.Thermostat.attributes.ThermostatRunningState, + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.PercentCurrent, + clusters.FanControl.attributes.RockSupport, -- These two attributes will be subscribed to following the profile + clusters.FanControl.attributes.RockSetting, -- change since the fanOscillationMode capability will be enabled. +} + test.register_coroutine_test( "Profile change on doConfigure lifecycle event no battery & state support", function() - mock_device:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) - test.socket.device_lifecycle:__queue_receive({mock_device.id, "doConfigure"}) - mock_device:expect_metadata_update({ profile = "thermostat-humidity-fan-nostate-nobattery" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test_thermostat_device_type_update_modular_profile(mock_device, expected_metadata, + get_subscribe_request(mock_device, new_cluster_subscribe_list)) end ) test.register_coroutine_test( "Profile change on doConfigure lifecycle event no battery & state support with disorder endpoints", function() - mock_device_disorder_endpoints:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) - test.socket.device_lifecycle:__queue_receive({mock_device_disorder_endpoints.id, "doConfigure"}) - mock_device_disorder_endpoints:expect_metadata_update({ profile = "thermostat-humidity-fan-nostate-nobattery" }) - mock_device_disorder_endpoints:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test_thermostat_device_type_update_modular_profile(mock_device_disorder_endpoints, expected_metadata, + get_subscribe_request(mock_device_disorder_endpoints, new_cluster_subscribe_list)) end, { test_init = test_init_disorder_endpoints } ) +test.register_coroutine_test( + "PercentCurrent reports and setPercent commands should be handled correctly after profile change", + function() + test_thermostat_device_type_update_modular_profile(mock_device, expected_metadata, + get_subscribe_request(mock_device, new_cluster_subscribe_list)) + + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.FanControl.attributes.PercentCurrent:build_test_report_data(mock_device, 2, 10) + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.fanSpeedPercent.percent(10)) + ) + + test.wait_for_events() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "fanSpeedPercent", component = "main", command = "setPercent", args = { 50 } } + }) + + test.socket.matter:__expect_send({ + mock_device.id, + clusters.FanControl.attributes.PercentSetting:write(mock_device, 2, 50) + }) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua index b28d11dcc7..74e550b344 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -76,6 +65,9 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) local read_setpoint_deadband = clusters.Thermostat.attributes.MinSetpointDeadBand:read() diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits_rpc.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits_rpc.lua index 72b46af4f3..13eaa47bfe 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits_rpc.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits_rpc.lua @@ -1,19 +1,10 @@ --- --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -43,6 +34,9 @@ local mock_device = test.mock_device.build_test_matter_device({ }, {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY}, {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "BOTH"}, + }, + device_types = { + { device_type_id = 0x0301, device_type_revision = 1 } -- Thermostat } } } @@ -72,6 +66,9 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) local read_setpoint_deadband = clusters.Thermostat.attributes.MinSetpointDeadBand:read() diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua index 767b21a36e..3eed9709a2 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua @@ -1,21 +1,9 @@ --- Copyright 2022 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" local clusters = require "st.matter.clusters" @@ -32,7 +20,7 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, }, device_types = { - device_type_id = 0x0016, device_type_revision = 1, -- RootNode + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode } }, { @@ -48,6 +36,9 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + }, + device_types = { + { device_type_id = 0x0301, device_type_revision = 1 } -- Thermostat } } } @@ -82,6 +73,9 @@ local mock_device_auto = test.mock_device.build_test_matter_device({ {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + }, + device_types = { + { device_type_id = 0x0301, device_type_revision = 1 } -- Thermostat } } } @@ -115,7 +109,10 @@ local function test_init() end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) + test.mock_device.add_test_device(mock_device) end test.set_test_init_function(test_init) @@ -147,6 +144,9 @@ local function test_init_auto() end end test.socket.matter:__expect_send({mock_device_auto.id, subscribe_request}) + test.socket.capability:__expect_send( + mock_device_auto:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({mock_device_auto.id, clusters.Thermostat.attributes.MinSetpointDeadBand:read(mock_device_auto)}) test.mock_device.add_test_device(mock_device_auto) end @@ -219,6 +219,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedHeatingSetpoint:build_test_report_data(mock_device, 1, 40*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -238,6 +243,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedCoolingSetpoint:build_test_report_data(mock_device, 1, 40*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -703,28 +713,6 @@ test.register_message_test( } ) -test.register_message_test( - "Setting the heating setpoint to a Fahrenheit value should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 64 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, utils.round((64 - 32) * (5 / 9.0) * 100)) - } - } - } -) - test.register_message_test( "Setting the mode to cool should send the appropriate commands", { diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua index d8146257ef..ef85b147af 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua @@ -1,22 +1,9 @@ --- Copyright 2023 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -48,6 +35,9 @@ local mock_device = test.mock_device.build_test_matter_device({ { cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER" }, { cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER" }, { cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0301, device_type_revision = 1 } -- Thermostat } } } @@ -80,6 +70,9 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) test.mock_device.add_test_device(mock_device) end @@ -157,6 +150,12 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedHeatingSetpoint:build_test_report_data(mock_device, 3, 40 * 100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = 0.00, maximum = 100.00, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -177,6 +176,12 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedCoolingSetpoint:build_test_report_data(mock_device, 3, 40 * 100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { minimum = 0.00, maximum = 100.00, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -513,29 +518,6 @@ test.register_message_test( } ) -test.register_message_test( - "Setting the heating setpoint to a Fahrenheit value should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 64 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 3, - utils.round((64 - 32) * (5 / 9.0) * 100)) - } - } - } -) - test.register_message_test( "Setting the mode to cool should send the appropriate commands", { diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua index 5e44701d97..bf985671cf 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua @@ -1,25 +1,14 @@ --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" +local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" -local dkjson = require "dkjson" - local clusters = require "st.matter.clusters" +local im = require "st.matter.interaction_model" +local uint32 = require "st.matter.data_types.Uint32" -test.set_rpc_version(8) +test.disable_startup_messages() local mock_device_basic = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"), @@ -49,10 +38,10 @@ local mock_device_basic = test.mock_device.build_test_matter_device({ }, {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0}, }, device_types = { - {device_type_id = 0x0301, device_type_revision = 1}, -- Thermostat + {device_type_id = 0x0301, device_type_revision = 1} -- Thermostat } } } @@ -67,12 +56,12 @@ local function initialize_mock_device(generic_mock_device, generic_subscribed_at end end test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) - test.mock_device.add_test_device(generic_mock_device) return subscribe_request end local subscribe_request_basic local function test_init() + test.mock_device.add_test_device(mock_device_basic) local subscribed_attributes = { clusters.Thermostat.attributes.LocalTemperature, clusters.Thermostat.attributes.OccupiedCoolingSetpoint, @@ -92,18 +81,42 @@ local function test_init() clusters.FanControl.attributes.FanModeSequence, clusters.PowerSource.attributes.BatPercentRemaining, } + test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "added" }) + local read_attributes = { + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.RockSupport, + clusters.Thermostat.attributes.AttributeList, + } + local read_request = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, clus in ipairs(read_attributes) do + read_request:merge(clus:read(mock_device_basic)) + end + test.socket.matter:__expect_send({ mock_device_basic.id, read_request }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "init" }) + test.socket.capability:__expect_send( + mock_device_basic:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) subscribe_request_basic = initialize_mock_device(mock_device_basic, subscribed_attributes) end -- run the profile configuration tests local function test_thermostat_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request) test.socket.device_lifecycle:__queue_receive({generic_mock_device.id, "doConfigure"}) - generic_mock_device:expect_metadata_update(expected_metadata) generic_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - local device_info_copy = utils.deep_copy(generic_mock_device.raw_st_data) - device_info_copy.profile.id = "thermostat-modular" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ generic_mock_device.id, "infoChanged", device_info_json }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + generic_mock_device.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(generic_mock_device, 1, {uint32(0)}) + }) + generic_mock_device:expect_metadata_update(expected_metadata) + + local updated_device_profile = t_utils.get_profile_definition("thermostat-modular.yml", + {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} + ) + test.socket.device_lifecycle:__queue_receive(mock_device_basic:generate_info_changed({ profile = updated_device_profile })) test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) end @@ -125,8 +138,6 @@ local expected_metadata = { test.register_coroutine_test( "Device with modular profile should enable correct optional capabilities", function() - mock_device_basic:set_field("__BATTERY_SUPPORT", "NO_BATTERY") -- since we're assuming this would have happened during device_added in this case. - mock_device_basic:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) -- since we're assuming this would have happened during device_added in this case. test_thermostat_device_type_update_modular_profile(mock_device_basic, expected_metadata, subscribe_request_basic) end, { test_init = test_init } diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_rpc5.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_rpc5.lua new file mode 100644 index 0000000000..a9bbf59930 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_rpc5.lua @@ -0,0 +1,141 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local utils = require "st.utils" + +-- Temperature values are converted to Celsius by the hub before reaching the driver for rpc > 5. +-- This test file is meant to verify that the driver converts Fahrenheit values > 40 degrees from F to C for rpc <= 5. +test.set_rpc_version(5) + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + device_type_id = 0x0016, device_type_revision = 1, -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER"}, + { + cluster_id = clusters.Thermostat.ID, + cluster_revision=5, + cluster_type="SERVER", + feature_map=3, -- Heat and Cool features + }, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + }, + device_types = { + { device_type_id = 0x0301, device_type_revision = 1 } -- Thermostat + } + } + } +}) + +local function test_init() + local cluster_subscribe_list = { + clusters.Thermostat.attributes.LocalTemperature, + clusters.Thermostat.attributes.OccupiedCoolingSetpoint, + clusters.Thermostat.attributes.OccupiedHeatingSetpoint, + clusters.Thermostat.attributes.AbsMinCoolSetpointLimit, + clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit, + clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, + clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit, + clusters.Thermostat.attributes.SystemMode, + clusters.Thermostat.attributes.ThermostatRunningState, + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.FanModeSequence, + clusters.PowerSource.attributes.BatPercentRemaining, + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +test.register_message_test( + "Setting the heating setpoint to a Fahrenheit value should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 90 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, + utils.round((90 - 32) * (5 / 9.0) * 100)) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 41 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, + utils.round((41 - 32) * (5 / 9.0) * 100)) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 35 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, 35 * 100) + } + } + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_water_heater.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_water_heater.lua index ee8365c30c..b39db4136b 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_water_heater.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_water_heater.lua @@ -1,30 +1,19 @@ --- Copyright 2022 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" local version = require "version" local clusters = require "st.matter.clusters" if version.api < 13 then - clusters.WaterHeaterMode = require "WaterHeaterMode" + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" end if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" end local WATER_HEATER_EP = 10 @@ -89,7 +78,8 @@ local function test_init() clusters.WaterHeaterMode.attributes.CurrentMode, clusters.WaterHeaterMode.attributes.SupportedModes, clusters.ElectricalPowerMeasurement.attributes.ActivePower, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, } test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) @@ -100,35 +90,9 @@ local function test_init() end test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) test.mock_device.add_test_device(mock_device) - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) end test.set_test_init_function(test_init) -test.register_message_test( - "Setting the heating setpoint to a Fahrenheit value should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 90 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, WATER_HEATER_EP, - utils.round((90 - 32) * (5 / 9.0) * 100)) - } - } - } -) - test.register_message_test( "Heating setpoint reports should generate correct messages", { @@ -140,6 +104,12 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedHeatingSetpoint:build_test_report_data(mock_device, 1, 70*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = 0.00, maximum = 100.00, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -170,28 +140,6 @@ test.register_message_test( } ) -test.register_message_test( - "Setting the heating setpoint to a Fahrenheit value should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 100 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, WATER_HEATER_EP, utils.round((100 - 32) * (5 / 9.0) * 100)) - } - } - } -) - test.register_message_test( "Ensure WaterHeaderMode supportedModes are registered and setting Oven mode should send appropriate commands", { @@ -291,7 +239,7 @@ test.register_message_test( clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, ELECTRICAL_SENSOR_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 15000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 15000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) } }, { @@ -303,16 +251,13 @@ test.register_message_test( ) test.register_coroutine_test( - "The total energy consumption of the device must be reported every 15 minutes", + "The total energy consumption of the device must be reported, but in 15+ minute intervals", function() - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) + test.mock_time.advance_time(901) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, ELECTRICAL_SENSOR_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 20Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 20Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -321,55 +266,55 @@ test.register_coroutine_test( })) ) - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ energy = 20, - deltaEnergy = 20, - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" + deltaEnergy = 0.0, + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z" + })) + ) + + test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, + ELECTRICAL_SENSOR_EP, + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 30000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 30Wh + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.energyMeter.energy({ + value = 30, unit = "Wh" })) - ) + ) test.wait_for_events() + test.mock_time.advance_time(1001) - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, ELECTRICAL_SENSOR_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 30000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 30Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 50000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 30Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.energyMeter.energy({ - value = 30, unit = "Wh" + value = 50, unit = "Wh" })) ) - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ - energy = 30, - deltaEnergy = 10, - start = "1970-01-01T00:15:00Z", - ["end"] = "1970-01-01T00:29:59Z" + energy = 50, + deltaEnergy = 30, + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:31:41Z" })) ) - test.wait_for_events() end, { test_init = function() test_init() - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "polling_report_schedule_timer") - test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") end } ) diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua new file mode 100644 index 0000000000..f0a210bc16 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua @@ -0,0 +1,665 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local log = require "log" +local version = require "version" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local st_utils = require "st.utils" +local fields = require "thermostat_utils.fields" +local thermostat_utils = require "thermostat_utils.utils" + +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" +end + +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" +end + +if version.api < 13 then + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" +end + +local AttributeHandlers = {} + + +-- [[ THERMOSTAT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.thermostat_attribute_list_handler(driver, device, ib, response) + local device_cfg = require "thermostat_utils.device_configuration" + for _, attr in ipairs(ib.data.elements) do + -- mark whether the optional attribute ThermostatRunningState (0x029) is present and try profiling + if attr.value == 0x029 then + device:set_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, true) + device_cfg.match_profile(device) + return + end + end + device:set_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, false) + device_cfg.match_profile(device) +end + +function AttributeHandlers.system_mode_handler(driver, device, ib, response) + if device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then -- this being nil means the control_sequence_of_operation_handler hasn't run. + device.log.info_with({hub_logs = true}, "In the SystemMode handler: ControlSequenceOfOperation has not run yet. Exiting early.") + device:set_field(fields.SAVED_SYSTEM_MODE_IB, ib) + return + end + + local supported_modes = device:get_latest_state(device:endpoint_to_component(ib.endpoint_id), capabilities.thermostatMode.ID, capabilities.thermostatMode.supportedThermostatModes.NAME) or {} + -- check that the given mode was in the supported modes list + if thermostat_utils.tbl_contains(supported_modes, fields.THERMOSTAT_MODE_MAP[ib.data.value].NAME) then + device:emit_event_for_endpoint(ib.endpoint_id, fields.THERMOSTAT_MODE_MAP[ib.data.value]()) + return + end + -- if the value is not found in the supported modes list, check if it's disallowed and early return if so. + local disallowed_thermostat_modes = device:get_field(fields.DISALLOWED_THERMOSTAT_MODES) or {} + if thermostat_utils.tbl_contains(disallowed_thermostat_modes, fields.THERMOSTAT_MODE_MAP[ib.data.value].NAME) then + return + end + -- if we get here, then the reported mode is allowed and not in our mode map + -- add the mode to the OPTIONAL_THERMOSTAT_MODES_SEEN and supportedThermostatModes tables + local optional_modes_seen = st_utils.deep_copy(device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN)) or {} + table.insert(optional_modes_seen, fields.THERMOSTAT_MODE_MAP[ib.data.value].NAME) + device:set_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN, optional_modes_seen, {persist=true}) + local sm_copy = st_utils.deep_copy(supported_modes) + table.insert(sm_copy, fields.THERMOSTAT_MODE_MAP[ib.data.value].NAME) + local supported_modes_event = capabilities.thermostatMode.supportedThermostatModes(sm_copy, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, supported_modes_event) + device:emit_event_for_endpoint(ib.endpoint_id, fields.THERMOSTAT_MODE_MAP[ib.data.value]()) +end + +function AttributeHandlers.thermostat_running_state_handler(driver, device, ib, response) + for mode, operating_state in pairs(fields.THERMOSTAT_OPERATING_MODE_MAP) do + if ((ib.data.value >> mode) & 1) > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, operating_state()) + return + end + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatOperatingState.thermostatOperatingState.idle()) +end + +function AttributeHandlers.control_sequence_of_operation_handler(driver, device, ib, response) + -- The ControlSequenceOfOperation attribute only directly specifies what can't be operated by the operating environment, not what can. + -- However, we assert here that a Cooling enum value implies that SystemMode supports cooling, and the same for a Heating enum. + -- We also assert that Off is supported if the switch capability is not supported, though per spec this is optional. + if device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then + if device:supports_capability(capabilities.switch) == false then + device:set_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN, {capabilities.thermostatMode.thermostatMode.off.NAME}, {persist=true}) + else + device:set_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN, {}, {persist=true}) + end + end + local supported_modes = st_utils.deep_copy(device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN)) + local disallowed_mode_operations = {} + + local modes_for_inclusion = {} + if ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.COOLING_WITH_REHEAT then + local _, found_idx = thermostat_utils.tbl_contains(supported_modes, capabilities.thermostatMode.thermostatMode.emergency_heat.NAME) + if found_idx then + table.remove(supported_modes, found_idx) -- if seen before, remove now + end + table.insert(supported_modes, capabilities.thermostatMode.thermostatMode.cool.NAME) + table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.heat.NAME) + table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.emergency_heat.NAME) + elseif ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.HEATING_WITH_REHEAT then + local _, found_idx = thermostat_utils.tbl_contains(supported_modes, capabilities.thermostatMode.thermostatMode.precooling.NAME) + if found_idx then + table.remove(supported_modes, found_idx) -- if seen before, remove now + end + table.insert(supported_modes, capabilities.thermostatMode.thermostatMode.heat.NAME) + table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.cool.NAME) + table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.precooling.NAME) + elseif ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.COOLING_AND_HEATING_WITH_REHEAT then + table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.cool.NAME) + table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.heat.NAME) + end + + -- check whether the Auto Mode should be supported in SystemMode, though this is unrelated to ControlSequenceOfOperation + local auto = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}) + if #auto > 0 then + table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.auto.NAME) + else + table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.auto.NAME) + end + + -- if a disallowed value was once allowed and added, it should be removed now. + for index, mode in pairs(supported_modes) do + if thermostat_utils.tbl_contains(disallowed_mode_operations, mode) then + table.remove(supported_modes, index) + end + end + -- do not include any values twice + for _, mode in pairs(modes_for_inclusion) do + if not thermostat_utils.tbl_contains(supported_modes, mode) then + table.insert(supported_modes, mode) + end + end + device:set_field(fields.DISALLOWED_THERMOSTAT_MODES, disallowed_mode_operations) + local event = capabilities.thermostatMode.supportedThermostatModes(supported_modes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) + + -- will be set by the SystemMode handler if this handler hasn't run yet. + if device:get_field(fields.SAVED_SYSTEM_MODE_IB) then + AttributeHandlers.system_mode_handler(driver, device, device:get_field(fields.SAVED_SYSTEM_MODE_IB), response) + device:set_field(fields.SAVED_SYSTEM_MODE_IB, nil) + end +end + +function AttributeHandlers.min_setpoint_deadband_handler(driver, device, ib, response) + local val = ib.data.value / 10.0 + log.info("Setting " .. fields.setpoint_limit_device_field.MIN_DEADBAND .. " to " .. string.format("%s", val)) + device:set_field(fields.setpoint_limit_device_field.MIN_DEADBAND, val, { persist = true }) + device:set_field(fields.setpoint_limit_device_field.MIN_SETPOINT_DEADBAND_CHECKED, true, {persist = true}) +end + +function AttributeHandlers.abs_heat_setpoint_limit_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local MAX_TEMP_IN_C = fields.THERMOSTAT_MAX_TEMP_IN_C + local MIN_TEMP_IN_C = fields.THERMOSTAT_MIN_TEMP_IN_C + local is_water_heater_device = (thermostat_utils.get_device_type(device) == fields.WATER_HEATER_DEVICE_TYPE_ID) + if is_water_heater_device then + MAX_TEMP_IN_C = fields.WATER_HEATER_MAX_TEMP_IN_C + MIN_TEMP_IN_C = fields.WATER_HEATER_MIN_TEMP_IN_C + end + local val = ib.data.value / 100.0 + val = st_utils.clamp_value(val, MIN_TEMP_IN_C, MAX_TEMP_IN_C) + device:set_field(minOrMax, val) + local min = device:get_field(fields.setpoint_limit_device_field.MIN_HEAT) + local max = device:get_field(fields.setpoint_limit_device_field.MAX_HEAT) + if min ~= nil and max ~= nil then + if min < max then + -- Only emit the capability for RPC version >= 5 (unit conversion for + -- heating setpoint range capability is only supported for RPC >= 5) + if version.rpc >= 5 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = min, maximum = max, step = 0.1 }, unit = "C" })) + end + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min heating setpoint %d that is not lower than the reported max %d", min, max)) + end + end + end +end + +function AttributeHandlers.abs_cool_setpoint_limit_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local val = ib.data.value / 100.0 + val = st_utils.clamp_value(val, fields.THERMOSTAT_MIN_TEMP_IN_C, fields.THERMOSTAT_MAX_TEMP_IN_C) + device:set_field(minOrMax, val) + local min = device:get_field(fields.setpoint_limit_device_field.MIN_COOL) + local max = device:get_field(fields.setpoint_limit_device_field.MAX_COOL) + if min ~= nil and max ~= nil then + if min < max then + -- Only emit the capability for RPC version >= 5 (unit conversion for + -- cooling setpoint range capability is only supported for RPC >= 5) + if version.rpc >= 5 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { minimum = min, maximum = max, step = 0.1 }, unit = "C" })) + end + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min cooling setpoint %d that is not lower than the reported max %d", min, max)) + end + end + end +end + + +-- [[ TEMPERATURE MEASUREMENT CLUSER ATTRIBUTES ]] -- + +function AttributeHandlers.temperature_handler_factory(attribute) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local unit = "C" + + -- Only emit the capability for RPC version >= 5, since unit conversion for + -- range capabilities is only supported in that case. + if version.rpc >= 5 then + local event + if attribute == capabilities.thermostatCoolingSetpoint.coolingSetpoint then + local range = { + minimum = device:get_field(fields.setpoint_limit_device_field.MIN_COOL) or fields.THERMOSTAT_MIN_TEMP_IN_C, + maximum = device:get_field(fields.setpoint_limit_device_field.MAX_COOL) or fields.THERMOSTAT_MAX_TEMP_IN_C, + step = 0.1 + } + event = capabilities.thermostatCoolingSetpoint.coolingSetpointRange({value = range, unit = unit}) + device:emit_event_for_endpoint(ib.endpoint_id, event) + elseif attribute == capabilities.thermostatHeatingSetpoint.heatingSetpoint then + local MAX_TEMP_IN_C = fields.THERMOSTAT_MAX_TEMP_IN_C + local MIN_TEMP_IN_C = fields.THERMOSTAT_MIN_TEMP_IN_C + local is_water_heater_device = thermostat_utils.get_device_type(device) == fields.WATER_HEATER_DEVICE_TYPE_ID + if is_water_heater_device then + MAX_TEMP_IN_C = fields.WATER_HEATER_MAX_TEMP_IN_C + MIN_TEMP_IN_C = fields.WATER_HEATER_MIN_TEMP_IN_C + end + + local range = { + minimum = device:get_field(fields.setpoint_limit_device_field.MIN_HEAT) or MIN_TEMP_IN_C, + maximum = device:get_field(fields.setpoint_limit_device_field.MAX_HEAT) or MAX_TEMP_IN_C, + step = 0.1 + } + event = capabilities.thermostatHeatingSetpoint.heatingSetpointRange({value = range, unit = unit}) + device:emit_event_for_endpoint(ib.endpoint_id, event) + end + end + + local temp = ib.data.value / 100.0 + device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = temp, unit = unit})) + end +end + +function AttributeHandlers.temperature_measured_value_bounds_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local temp = ib.data.value / 100.0 + local unit = "C" + temp = st_utils.clamp_value(temp, fields.THERMOSTAT_MIN_TEMP_IN_C, fields.THERMOSTAT_MAX_TEMP_IN_C) + thermostat_utils.set_field_for_endpoint(device, minOrMax, ib.endpoint_id, temp) + local min = thermostat_utils.get_field_for_endpoint(device, fields.setpoint_limit_device_field.MIN_TEMP, ib.endpoint_id) + local max = thermostat_utils.get_field_for_endpoint(device, fields.setpoint_limit_device_field.MAX_TEMP, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + -- Only emit the capability for RPC version >= 5 (unit conversion for + -- temperature range capability is only supported for RPC >= 5) + if version.rpc >= 5 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) + end + thermostat_utils.set_field_for_endpoint(device, fields.setpoint_limit_device_field.MIN_TEMP, ib.endpoint_id, nil) + thermostat_utils.set_field_for_endpoint(device, fields.setpoint_limit_device_field.MAX_TEMP, ib.endpoint_id, nil) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) + end + end + end +end + + +--[[ FAN CONTROL CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.fan_mode_handler(driver, device, ib, response) + local fan_mode_event = { + [clusters.FanControl.attributes.FanMode.OFF] = { capabilities.fanMode.fanMode.off(), + capabilities.airConditionerFanMode.fanMode("off"), + capabilities.airPurifierFanMode.airPurifierFanMode.off(), + nil }, -- 'OFF' is not supported by thermostatFanMode + [clusters.FanControl.attributes.FanMode.LOW] = { capabilities.fanMode.fanMode.low(), + capabilities.airConditionerFanMode.fanMode("low"), + capabilities.airPurifierFanMode.airPurifierFanMode.low(), + capabilities.thermostatFanMode.thermostatFanMode.on() }, + [clusters.FanControl.attributes.FanMode.MEDIUM] = { capabilities.fanMode.fanMode.medium(), + capabilities.airConditionerFanMode.fanMode("medium"), + capabilities.airPurifierFanMode.airPurifierFanMode.medium(), + capabilities.thermostatFanMode.thermostatFanMode.on() }, + [clusters.FanControl.attributes.FanMode.HIGH] = { capabilities.fanMode.fanMode.high(), + capabilities.airConditionerFanMode.fanMode("high"), + capabilities.airPurifierFanMode.airPurifierFanMode.high(), + capabilities.thermostatFanMode.thermostatFanMode.on() }, + [clusters.FanControl.attributes.FanMode.ON] = { capabilities.fanMode.fanMode.auto(), + capabilities.airConditionerFanMode.fanMode("auto"), + capabilities.airPurifierFanMode.airPurifierFanMode.auto(), + capabilities.thermostatFanMode.thermostatFanMode.on() }, + [clusters.FanControl.attributes.FanMode.AUTO] = { capabilities.fanMode.fanMode.auto(), + capabilities.airConditionerFanMode.fanMode("auto"), + capabilities.airPurifierFanMode.airPurifierFanMode.auto(), + capabilities.thermostatFanMode.thermostatFanMode.auto() }, + [clusters.FanControl.attributes.FanMode.SMART] = { capabilities.fanMode.fanMode.auto(), + capabilities.airConditionerFanMode.fanMode("auto"), + capabilities.airPurifierFanMode.airPurifierFanMode.auto(), + capabilities.thermostatFanMode.thermostatFanMode.auto() } + } + local fan_mode_idx = device:supports_capability_by_id(capabilities.fanMode.ID) and 1 or + device:supports_capability_by_id(capabilities.airConditionerFanMode.ID) and 2 or + device:supports_capability_by_id(capabilities.airPurifierFanMode.ID) and 3 or + device:supports_capability_by_id(capabilities.thermostatFanMode.ID) and 4 + if fan_mode_idx ~= false and fan_mode_event[ib.data.value][fan_mode_idx] then + device:emit_event_for_endpoint(ib.endpoint_id, fan_mode_event[ib.data.value][fan_mode_idx]) + else + log.warn(string.format("Invalid Fan Mode (%s)", ib.data.value)) + end +end + +function AttributeHandlers.fan_mode_sequence_handler(driver, device, ib, response) + local supportedFanModes, supported_fan_modes_attribute + if ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH then + supportedFanModes = { "off", "low", "medium", "high" } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH then + supportedFanModes = { "off", "low", "high" } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO then + supportedFanModes = { "off", "low", "medium", "high", "auto" } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH_AUTO then + supportedFanModes = { "off", "low", "high", "auto" } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_HIGH_AUTO then + supportedFanModes = { "off", "high", "auto" } + else + supportedFanModes = { "off", "high" } + end + + if device:supports_capability_by_id(capabilities.airPurifierFanMode.ID) then + supported_fan_modes_attribute = capabilities.airPurifierFanMode.supportedAirPurifierFanModes + elseif device:supports_capability_by_id(capabilities.airConditionerFanMode.ID) then + supported_fan_modes_attribute = capabilities.airConditionerFanMode.supportedAcFanModes + elseif device:supports_capability_by_id(capabilities.thermostatFanMode.ID) then + supported_fan_modes_attribute = capabilities.thermostatFanMode.supportedThermostatFanModes + -- Our thermostat fan mode control is not granular enough to handle all of the supported modes + if ib.data.value >= clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO and + ib.data.value <= clusters.FanControl.attributes.FanModeSequence.OFF_ON_AUTO then + supportedFanModes = { "auto", "on" } + else + supportedFanModes = { "on" } + end + else + supported_fan_modes_attribute = capabilities.fanMode.supportedFanModes + end + + local event = supported_fan_modes_attribute(supportedFanModes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.percent_current_handler(driver, device, ib, response) + local speed = 0 + if ib.data.value ~= nil then + speed = st_utils.clamp_value(ib.data.value, fields.MIN_ALLOWED_PERCENT_VALUE, fields.MAX_ALLOWED_PERCENT_VALUE) + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(speed)) +end + +function AttributeHandlers.wind_support_handler(driver, device, ib, response) + local supported_wind_modes = {capabilities.windMode.windMode.noWind.NAME} + for mode, wind_mode in pairs(fields.WIND_MODE_MAP) do + if ((ib.data.value >> mode) & 1) > 0 then + table.insert(supported_wind_modes, wind_mode.NAME) + end + end + local event = capabilities.windMode.supportedWindModes(supported_wind_modes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.wind_setting_handler(driver, device, ib, response) + for index, wind_mode in pairs(fields.WIND_MODE_MAP) do + if ((ib.data.value >> index) & 1) > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, wind_mode()) + return + end + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windMode.windMode.noWind()) +end + +function AttributeHandlers.rock_support_handler(driver, device, ib, response) + local supported_rock_modes = {capabilities.fanOscillationMode.fanOscillationMode.off.NAME} + for mode, rock_mode in pairs(fields.ROCK_MODE_MAP) do + if ((ib.data.value >> mode) & 1) > 0 then + table.insert(supported_rock_modes, rock_mode.NAME) + end + end + local event = capabilities.fanOscillationMode.supportedFanOscillationModes(supported_rock_modes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.rock_setting_handler(driver, device, ib, response) + for index, rock_mode in pairs(fields.ROCK_MODE_MAP) do + if ((ib.data.value >> index) & 1) > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, rock_mode()) + return + end + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanOscillationMode.fanOscillationMode.off()) +end + + +-- [[ HEPA FILTER MONITORING CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.hepa_filter_condition_handler(driver, device, ib, response) + local component = device.profile.components["hepaFilter"] + local condition = ib.data.value + device:emit_component_event(component, capabilities.filterState.filterLifeRemaining(condition)) +end + +function AttributeHandlers.hepa_filter_change_indication_handler(driver, device, ib, response) + local component = device.profile.components["hepaFilter"] + if ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.OK then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.WARNING then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.CRITICAL then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.replace()) + end +end + + +-- [[ ACTIVATED CARBON FILTER MONITORING CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.activated_carbon_filter_condition_handler(driver, device, ib, response) + local component = device.profile.components["activatedCarbonFilter"] + local condition = ib.data.value + device:emit_component_event(component, capabilities.filterState.filterLifeRemaining(condition)) +end + +function AttributeHandlers.activated_carbon_filter_change_indication_handler(driver, device, ib, response) + local component = device.profile.components["activatedCarbonFilter"] + if ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.OK then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.WARNING then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.CRITICAL then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.replace()) + end +end + + +--[[ AIR QUALITY SENSOR CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.air_quality_handler(driver, device, ib, response) + local state = ib.data.value + if state == 0 then -- Unknown + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unknown()) + elseif state == 1 then -- Good + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.good()) + elseif state == 2 then -- Fair + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.moderate()) + elseif state == 3 then -- Moderate + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.slightlyUnhealthy()) + elseif state == 4 then -- Poor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unhealthy()) + elseif state == 5 then -- VeryPoor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.veryUnhealthy()) + elseif state == 6 then -- ExtremelyPoor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.hazardous()) + end +end + + +-- [[ CONCENTRATION CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.concentration_measurement_unit_factory(capability_name) + return function(driver, device, ib, response) + device:set_field(capability_name.."_unit", ib.data.value, {persist = true}) + end +end + +function AttributeHandlers.concentration_level_value_factory(attribute) + return function(driver, device, ib, response) + device:emit_event_for_endpoint(ib.endpoint_id, attribute(fields.level_strings[ib.data.value])) + end +end + +function AttributeHandlers.concentration_measured_value_factory(capability_name, attribute, target_unit) + return function(driver, device, ib, response) + local reporting_unit = device:get_field(capability_name.."_unit") + + if not reporting_unit then + reporting_unit = fields.unit_default[capability_name] + device:set_field(capability_name.."_unit", reporting_unit, {persist = true}) + end + + local value = nil + if reporting_unit then + value = thermostat_utils.unit_conversion(ib.data.value, reporting_unit, target_unit, capability_name) + end + + if value then + device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = value, unit = fields.unit_strings[target_unit]})) + -- handle case where device profile supports both fineDustLevel and dustLevel + if capability_name == capabilities.fineDustSensor.NAME and device:supports_capability(capabilities.dustSensor) then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.dustSensor.fineDustLevel({value = value, unit = fields.unit_strings[target_unit]})) + end + end + end +end + + +-- [[ POWER SOURCE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.bat_percent_remaining_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) + end +end + +function AttributeHandlers.bat_charge_level_handler(driver, device, ib, response) + if ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.OK then + device:emit_event(capabilities.batteryLevel.battery.normal()) + elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.WARNING then + device:emit_event(capabilities.batteryLevel.battery.warning()) + elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.CRITICAL then + device:emit_event(capabilities.batteryLevel.battery.critical()) + end +end + +function AttributeHandlers.power_source_attribute_list_handler(driver, device, ib, response) + local device_cfg = require "thermostat_utils.device_configuration" + for _, attr in ipairs(ib.data.elements) do + -- mark if the device if BatPercentRemaining (Attribute ID 0x0C) or + -- BatChargeLevel (Attribute ID 0x0E) is present and try profiling. + if attr.value == 0x0C then + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_PERCENTAGE) + device_cfg.match_profile(device) + return + elseif attr.value == 0x0E then + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_LEVEL) + device_cfg.match_profile(device) + return + end + end +end + + +-- [[ ELECTRICAL POWER MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.active_power_handler(driver, device, ib, response) + if ib.data.value then + local watt_value = ib.data.value / 1000 + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W" })) + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("powerMeter","power") + end + end +end + + +-- [[ ELECTRICAL ENERGY MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +local function periodic_energy_imported_handler(driver, device, ib, response) + if ib.data then + if version.api < 11 then + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:augment_type(ib.data) + end + local endpoint_id = string.format(ib.endpoint_id) + local energy_imported_Wh = st_utils.round(ib.data.elements.energy.value / 1000) --convert mWh to Wh + local cumulative_energy_imported = device:get_field(fields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} + cumulative_energy_imported[endpoint_id] = cumulative_energy_imported[endpoint_id] or 0 + cumulative_energy_imported[endpoint_id] = cumulative_energy_imported[endpoint_id] + energy_imported_Wh + device:set_field(fields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP, cumulative_energy_imported, { persist = true }) + local total_cumulative_energy_imported = thermostat_utils.get_total_cumulative_energy_imported(device) + device:emit_component_event(device.profile.components["main"], ib.endpoint_id, capabilities.energyMeter.energy({value = total_cumulative_energy_imported, unit = "Wh"})) + thermostat_utils.report_power_consumption_to_st_energy(device, total_cumulative_energy_imported) + end +end + +local function cumulative_energy_imported_handler(driver, device, ib, response) + if ib.data then + if version.api < 11 then + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:augment_type(ib.data) + end + local endpoint_id = string.format(ib.endpoint_id) + local cumulative_energy_imported = device:get_field(fields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} + local cumulative_energy_imported_Wh = st_utils.round( ib.data.elements.energy.value / 1000) -- convert mWh to Wh + cumulative_energy_imported[endpoint_id] = cumulative_energy_imported_Wh + device:set_field(fields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP, cumulative_energy_imported, { persist = true }) + local total_cumulative_energy_imported = thermostat_utils.get_total_cumulative_energy_imported(device) + device:emit_component_event(device.profile.components["main"], capabilities.energyMeter.energy({ value = total_cumulative_energy_imported, unit = "Wh" })) + thermostat_utils.report_power_consumption_to_st_energy(device, total_cumulative_energy_imported) + end +end + +function AttributeHandlers.energy_imported_factory(is_cumulative_report) + return function(driver, device, ib, response) + if is_cumulative_report then + cumulative_energy_imported_handler(driver, device, ib, response) + elseif device:get_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED) then + periodic_energy_imported_handler(driver, device, ib, response) + end + end +end + + +-- [[ WATER HEATER MODE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.water_heater_supported_modes_handler(driver, device, ib, response) + local supportWaterHeaterModes = {} + local supportWaterHeaterModesWithIdx = {} + for _, mode in ipairs(ib.data.elements) do + if version.api < 13 then + clusters.WaterHeaterMode.types.ModeOptionStruct:augment_type(mode) + end + table.insert(supportWaterHeaterModes, mode.elements.label.value) + table.insert(supportWaterHeaterModesWithIdx, {mode.elements.mode.value, mode.elements.label.value}) + end + device:set_field(fields.SUPPORTED_WATER_HEATER_MODES_WITH_IDX, supportWaterHeaterModesWithIdx, { persist = true }) + local event = capabilities.mode.supportedModes(supportWaterHeaterModes, { visibility = { displayed = false } }) + device:emit_event_for_endpoint(ib.endpoint_id, event) + event = capabilities.mode.supportedArguments(supportWaterHeaterModes, { visibility = { displayed = false } }) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.water_heater_current_mode_handler(driver, device, ib, response) + device.log.info(string.format("water_heater_current_mode_handler mode: %s", ib.data.value)) + local supportWaterHeaterModesWithIdx = device:get_field(fields.SUPPORTED_WATER_HEATER_MODES_WITH_IDX) or {} + local currentMode = ib.data.value + for i, mode in ipairs(supportWaterHeaterModesWithIdx) do + if mode[1] == currentMode then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.mode.mode(mode[2])) + break + end + end +end + + +-- [[ RELATIVE HUMIDITY MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.relative_humidity_measured_value_handler(driver, device, ib, response) + local humidity = math.floor(ib.data.value / 100.0) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) +end + + +-- [[ ON OFF CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.on_off_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.on()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) + end +end + +return AttributeHandlers diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/capability_handlers.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/capability_handlers.lua new file mode 100644 index 0000000000..4a32b7a700 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/capability_handlers.lua @@ -0,0 +1,256 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local log = require "log" +local version = require "version" +local st_utils = require "st.utils" +local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" +local fields = require "thermostat_utils.fields" +local thermostat_utils = require "thermostat_utils.utils" + +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" +end + +if version.api < 13 then + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" +end + +local CapabilityHandlers = {} + + +-- [[ FAN SPEED PERCENT CAPABILITY HANDLERS ]] -- + +function CapabilityHandlers.handle_fan_speed_set_percent(driver, device, cmd) + local speed = math.floor(cmd.args.percent) + device:send(clusters.FanControl.attributes.PercentSetting:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.FanControl.ID), speed)) +end + + +-- [[ WIND MODE CAPABILITY HANDLERS ]] -- + +function CapabilityHandlers.handle_set_wind_mode(driver, device, cmd) + local wind_mode = 0 + if cmd.args.windMode == capabilities.windMode.windMode.sleepWind.NAME then + wind_mode = clusters.FanControl.types.WindSupportMask.SLEEP_WIND + elseif cmd.args.windMode == capabilities.windMode.windMode.naturalWind.NAME then + wind_mode = clusters.FanControl.types.WindSupportMask.NATURAL_WIND + end + device:send(clusters.FanControl.attributes.WindSetting:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.FanControl.ID), wind_mode)) +end + + +-- [[ FAN OSCILLATION MODE HANDLERS ]] -- + +function CapabilityHandlers.handle_set_fan_oscillation_mode(driver, device, cmd) + local rock_mode = 0 + if cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.horizontal.NAME then + rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_LEFT_RIGHT + elseif cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.vertical.NAME then + rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_UP_DOWN + elseif cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.swing.NAME then + rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_ROUND + end + device:send(clusters.FanControl.attributes.RockSetting:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.FanControl.ID), rock_mode)) +end + + +-- [[ MODE CAPABILITY HANDLERS ]] -- + +function CapabilityHandlers.handle_set_mode(driver, device, cmd) + device.log.info(string.format("set_water_heater_mode mode: %s", cmd.args.mode)) + local endpoint_id = thermostat_utils.component_to_endpoint(device, cmd.component, clusters.Thermostat.ID) + local supportedWaterHeaterModesWithIdx = device:get_field(fields.SUPPORTED_WATER_HEATER_MODES_WITH_IDX) or {} + for i, mode in ipairs(supportedWaterHeaterModesWithIdx) do + if cmd.args.mode == mode[2] then + device:send(clusters.WaterHeaterMode.commands.ChangeToMode(device, endpoint_id, mode[1])) + return + end + end +end + + +-- [[ FILTER STATE CAPABLITY HANDLERS ]] -- + +function CapabilityHandlers.handle_filter_state_reset_filter(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + if cmd.component == "hepaFilter" then + device:send(clusters.HepaFilterMonitoring.server.commands.ResetCondition(device, endpoint_id)) + else + device:send(clusters.ActivatedCarbonFilterMonitoring.server.commands.ResetCondition(device, endpoint_id)) + end +end + + +-- [[ SWITCH CAPABLITY HANDLERS ]] -- + +function CapabilityHandlers.handle_switch_on(driver, device, cmd) + local endpoint_id = thermostat_utils.component_to_endpoint(device, cmd.component, clusters.OnOff.ID) + local req = clusters.OnOff.server.commands.On(device, endpoint_id) + device:send(req) +end + +function CapabilityHandlers.handle_switch_off(driver, device, cmd) + local endpoint_id = thermostat_utils.component_to_endpoint(device, cmd.component, clusters.OnOff.ID) + local req = clusters.OnOff.server.commands.Off(device, endpoint_id) + device:send(req) +end + + +-- [[ THERMOSTAT MODE CAPABLITY HANDLERS ]] -- + +function CapabilityHandlers.handle_set_thermostat_mode(driver, device, cmd) + local mode_id = nil + for value, mode in pairs(fields.THERMOSTAT_MODE_MAP) do + if mode.NAME == cmd.args.mode then + mode_id = value + break + end + end + if mode_id then + device:send(clusters.Thermostat.attributes.SystemMode:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.Thermostat.ID), mode_id)) + end +end + +function CapabilityHandlers.thermostat_mode_command_factory(mode_name) + return function(driver, device, cmd) + return CapabilityHandlers.handle_set_thermostat_mode(driver, device, {component = cmd.component, args = {mode = mode_name}}) + end +end + + +-- [[ FAN MODE CAPABILITY HANDLERS ]] -- + +local function set_fan_mode(device, cmd, fan_mode_capability) + local command_argument = cmd.args.fanMode + if fan_mode_capability == capabilities.airPurifierFanMode then + command_argument = cmd.args.airPurifierFanMode + elseif fan_mode_capability == capabilities.thermostatFanMode then + command_argument = cmd.args.mode + end + local fan_mode_id + if command_argument == "off" then + fan_mode_id = clusters.FanControl.attributes.FanMode.OFF + elseif command_argument == "on" then + fan_mode_id = clusters.FanControl.attributes.FanMode.ON + elseif command_argument == "auto" then + fan_mode_id = clusters.FanControl.attributes.FanMode.AUTO + elseif command_argument == "high" then + fan_mode_id = clusters.FanControl.attributes.FanMode.HIGH + elseif command_argument == "medium" then + fan_mode_id = clusters.FanControl.attributes.FanMode.MEDIUM + elseif thermostat_utils.tbl_contains({ "low", "sleep", "quiet", "windFree" }, command_argument) then + fan_mode_id = clusters.FanControl.attributes.FanMode.LOW + else + device.log.warn(string.format("Invalid Fan Mode (%s) received from capability command", command_argument)) + return + end + device:send(clusters.FanControl.attributes.FanMode:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.FanControl.ID), fan_mode_id)) +end + +function CapabilityHandlers.fan_mode_command_factory(fan_mode_capability) + return function(driver, device, cmd) + set_fan_mode(device, cmd, fan_mode_capability) + end +end + + +-- [[ THERMOSTAT FAN MODE CAPABILITY HANDLERS ]] -- + +function CapabilityHandlers.thermostat_fan_mode_command_factory(mode_name) + return function(driver, device, cmd) + set_fan_mode(device, {component = cmd.component, args = {mode = mode_name}}, capabilities.thermostatFanMode) + end +end + + +-- [[ THERMOSTAT HEATING/COOLING CAPABILITY HANDLERS ]] -- + +function CapabilityHandlers.thermostat_set_setpoint_factory(setpoint) + return function(driver, device, cmd) + local endpoint_id = thermostat_utils.component_to_endpoint(device, cmd.component, clusters.Thermostat.ID) + local MAX_TEMP_IN_C = fields.THERMOSTAT_MAX_TEMP_IN_C + local MIN_TEMP_IN_C = fields.THERMOSTAT_MIN_TEMP_IN_C + local is_water_heater_device = thermostat_utils.get_device_type(device) == fields.WATER_HEATER_DEVICE_TYPE_ID + if is_water_heater_device then + MAX_TEMP_IN_C = fields.WATER_HEATER_MAX_TEMP_IN_C + MIN_TEMP_IN_C = fields.WATER_HEATER_MIN_TEMP_IN_C + end + local value = cmd.args.setpoint + if version.rpc <= 5 and value > MAX_TEMP_IN_C then + value = st_utils.f_to_c(value) + end + + -- Gather cached setpoint values when considering setpoint limits + -- Note: cached values should always exist, but defaults are chosen just in case to prevent + -- nil operation errors, and deadband logic from triggering. + local cached_cooling_val, cooling_setpoint = device:get_latest_state( + cmd.component, capabilities.thermostatCoolingSetpoint.ID, + capabilities.thermostatCoolingSetpoint.coolingSetpoint.NAME, + MAX_TEMP_IN_C, { value = MAX_TEMP_IN_C, unit = "C" } + ) + if cooling_setpoint and cooling_setpoint.unit == "F" then + cached_cooling_val = st_utils.f_to_c(cached_cooling_val) + end + local cached_heating_val, heating_setpoint = device:get_latest_state( + cmd.component, capabilities.thermostatHeatingSetpoint.ID, + capabilities.thermostatHeatingSetpoint.heatingSetpoint.NAME, + MIN_TEMP_IN_C, { value = MIN_TEMP_IN_C, unit = "C" } + ) + if heating_setpoint and heating_setpoint.unit == "F" then + cached_heating_val = st_utils.f_to_c(cached_heating_val) + end + local is_auto_capable = #device:get_endpoints( + clusters.Thermostat.ID, + {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE} + ) > 0 + + --Check setpoint limits for the device + local setpoint_type = string.match(setpoint.NAME, "Heat") or "Cool" + local deadband = device:get_field(fields.setpoint_limit_device_field.MIN_DEADBAND) or 2.5 --spec default + if setpoint_type == "Heat" then + local min = device:get_field(fields.setpoint_limit_device_field.MIN_HEAT) or MIN_TEMP_IN_C + local max = device:get_field(fields.setpoint_limit_device_field.MAX_HEAT) or MAX_TEMP_IN_C + if value < min or value > max then + log.warn(string.format( + "Invalid setpoint (%s) outside the min (%s) and the max (%s)", + value, min, max + )) + device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpoint(heating_setpoint, {state_change = true})) + return + end + if is_auto_capable and value > (cached_cooling_val - deadband) then + log.warn(string.format( + "Invalid setpoint (%s) is greater than the cooling setpoint (%s) with the deadband (%s)", + value, cooling_setpoint, deadband + )) + device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpoint(heating_setpoint, {state_change = true})) + return + end + else + local min = device:get_field(fields.setpoint_limit_device_field.MIN_COOL) or MIN_TEMP_IN_C + local max = device:get_field(fields.setpoint_limit_device_field.MAX_COOL) or MAX_TEMP_IN_C + if value < min or value > max then + log.warn(string.format( + "Invalid setpoint (%s) outside the min (%s) and the max (%s)", + value, min, max + )) + device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpoint(cooling_setpoint, {state_change = true})) + return + end + if is_auto_capable and value < (cached_heating_val + deadband) then + log.warn(string.format( + "Invalid setpoint (%s) is less than the heating setpoint (%s) with the deadband (%s)", + value, heating_setpoint, deadband + )) + device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpoint(cooling_setpoint, {state_change = true})) + return + end + end + device:send(setpoint:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.Thermostat.ID), st_utils.round(value * 100.0))) + end +end + +return CapabilityHandlers diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua new file mode 100644 index 0000000000..3ae5c76c7e --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua @@ -0,0 +1,331 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils" +local version = require "version" +local fields = require "thermostat_utils.fields" +local thermostat_utils = require "thermostat_utils.utils" + +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" +end + +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" +end + +if version.api < 13 then + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" +end + +local DeviceConfigurationHelpers = {} + +function DeviceConfigurationHelpers.supported_level_measurements(device) + local measurement_caps, level_caps = {}, {} + for _, details in ipairs(fields.AIR_QUALITY_MAP) do + local cap_id = details[1] + local cluster = details[3] + -- capability describes either a HealthConcern or Measurement/Sensor + if (cap_id:match("HealthConcern$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) + if #attr_eps > 0 then + table.insert(level_caps, cap_id) + end + elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) + if #attr_eps > 0 then + table.insert(measurement_caps, cap_id) + end + end + end + return measurement_caps, level_caps +end + +function DeviceConfigurationHelpers.get_thermostat_optional_capabilities(device) + local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) + local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) + local running_state_supported = device:get_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) + + local supported_thermostat_capabilities = {} + + if #heat_eps > 0 then + table.insert(supported_thermostat_capabilities, capabilities.thermostatHeatingSetpoint.ID) + end + if #cool_eps > 0 then + table.insert(supported_thermostat_capabilities, capabilities.thermostatCoolingSetpoint.ID) + end + + if running_state_supported then + table.insert(supported_thermostat_capabilities, capabilities.thermostatOperatingState.ID) + end + + return supported_thermostat_capabilities +end + +function DeviceConfigurationHelpers.get_air_quality_optional_capabilities(device) + local supported_air_quality_capabilities = {} + + local measurement_caps, level_caps = DeviceConfigurationHelpers.supported_level_measurements(device) + + for _, cap_id in ipairs(measurement_caps) do + table.insert(supported_air_quality_capabilities, cap_id) + end + + for _, cap_id in ipairs(level_caps) do + table.insert(supported_air_quality_capabilities, cap_id) + end + + return supported_air_quality_capabilities +end + + +local DeviceConfiguration = {} + +function DeviceConfiguration.match_modular_profile_air_purifer(device) + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + local hepa_filter_component_capabilities = {} + local ac_filter_component_capabilties = {} + local profile_name = "air-purifier-modular" + + local MAIN_COMPONENT_IDX = 1 + local CAPABILITIES_LIST_IDX = 2 + + local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) + local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) + if #humidity_eps > 0 then + table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) + end + if #temp_eps > 0 then + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + end + + local hepa_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID) + local ac_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID) + + if #hepa_filter_eps > 0 then + local filter_state_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID, {feature_bitmap = clusters.HepaFilterMonitoring.types.Feature.CONDITION}) + if #filter_state_eps > 0 then + table.insert(hepa_filter_component_capabilities, capabilities.filterState.ID) + end + + table.insert(hepa_filter_component_capabilities, capabilities.filterStatus.ID) + end + if #ac_filter_eps > 0 then + local filter_state_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID, {feature_bitmap = clusters.ActivatedCarbonFilterMonitoring.types.Feature.CONDITION}) + if #filter_state_eps > 0 then + table.insert(ac_filter_component_capabilties, capabilities.filterState.ID) + end + + table.insert(ac_filter_component_capabilties, capabilities.filterStatus.ID) + end + + -- determine fan capabilities, note that airPurifierFanMode is already mandatory + local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) + local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) + + if #rock_eps > 0 then + table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID) + end + if #wind_eps > 0 then + table.insert(main_component_capabilities, capabilities.windMode.ID) + end + + local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) + + if #thermostat_eps > 0 then + -- thermostatMode and temperatureMeasurement should be expected if thermostat is present + table.insert(main_component_capabilities, capabilities.thermostatMode.ID) + + -- only add temperatureMeasurement if it is not already added via TemperatureMeasurement cluster support + if #temp_eps == 0 then + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + end + local thermostat_capabilities = DeviceConfigurationHelpers.get_thermostat_optional_capabilities(device) + for _, capability_id in pairs(thermostat_capabilities) do + table.insert(main_component_capabilities, capability_id) + end + end + + local aqs_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) + if #aqs_eps > 0 then + table.insert(main_component_capabilities, capabilities.airQualityHealthConcern.ID) + end + + local supported_air_quality_capabilities = DeviceConfigurationHelpers.get_air_quality_optional_capabilities(device) + for _, capability_id in pairs(supported_air_quality_capabilities) do + table.insert(main_component_capabilities, capability_id) + end + + table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + if #ac_filter_component_capabilties > 0 then + table.insert(optional_supported_component_capabilities, {"activatedCarbonFilter", ac_filter_component_capabilties}) + end + if #hepa_filter_component_capabilities > 0 then + table.insert(optional_supported_component_capabilities, {"hepaFilter", hepa_filter_component_capabilities}) + end + + device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) + + -- earlier modular profile gating (min api v14, rpc 8) ensures we are running >= 0.57 FW. + -- This gating specifies a workaround required only for 0.57 FW, which is not needed for 0.58 and higher. + if version.api < 15 or version.rpc < 9 then + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airPurifierFanMode.ID) + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.fanSpeedPercent.ID) + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.refresh.ID) + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.firmwareUpdate.ID) + + device:set_field(fields.SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end +end + +function DeviceConfiguration.match_modular_profile_thermostat(device) + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + local profile_name = "thermostat-modular" + + local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) + if #humidity_eps > 0 then + table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) + end + + -- determine fan capabilities + local fan_eps = device:get_endpoints(clusters.FanControl.ID) + local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) + local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) + + if #fan_eps > 0 then + if #device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.MULTI_SPEED}) > 0 then + table.insert(main_component_capabilities, capabilities.fanSpeedPercent.ID) + if #device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.AUTO}) > 0 then + table.insert(main_component_capabilities, capabilities.fanMode.ID) + end + else + table.insert(main_component_capabilities, capabilities.fanMode.ID) + end + end + if #rock_eps > 0 then + table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID) + end + if #wind_eps > 0 then + table.insert(main_component_capabilities, capabilities.windMode.ID) + end + + local thermostat_capabilities = DeviceConfigurationHelpers.get_thermostat_optional_capabilities(device) + for _, capability_id in pairs(thermostat_capabilities) do + table.insert(main_component_capabilities, capability_id) + end + + local battery_supported = device:get_field(fields.profiling_data.BATTERY_SUPPORT) + if battery_supported == fields.battery_support.BATTERY_LEVEL then + table.insert(main_component_capabilities, capabilities.batteryLevel.ID) + elseif battery_supported == fields.battery_support.BATTERY_PERCENTAGE then + table.insert(main_component_capabilities, capabilities.battery.ID) + end + + table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) + + -- earlier modular profile gating (min api v14, rpc 8) ensures we are running >= 0.57 FW. + -- This gating specifies a workaround required only for 0.57 FW, which is not needed for 0.58 and higher. + if version.api < 15 or version.rpc < 9 then + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + table.insert(main_component_capabilities, capabilities.thermostatMode.ID) + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + table.insert(main_component_capabilities, capabilities.refresh.ID) + table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) + + device:set_field(fields.SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end +end + +function DeviceConfiguration.match_modular_profile_room_ac(device) + local running_state_supported = device:get_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) + local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + local profile_name = "room-air-conditioner-modular" + + if #humidity_eps > 0 then + table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) + end + + -- determine fan capabilities + local fan_eps = device:get_endpoints(clusters.FanControl.ID) + local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) + -- Note: Room AC does not support the rocking feature of FanControl. + + if #fan_eps > 0 then + table.insert(main_component_capabilities, capabilities.airConditionerFanMode.ID) + table.insert(main_component_capabilities, capabilities.fanSpeedPercent.ID) + end + if #wind_eps > 0 then + table.insert(main_component_capabilities, capabilities.windMode.ID) + end + + local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) + local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) + + if #heat_eps > 0 then + table.insert(main_component_capabilities, capabilities.thermostatHeatingSetpoint.ID) + end + if #cool_eps > 0 then + table.insert(main_component_capabilities, capabilities.thermostatCoolingSetpoint.ID) + end + + if running_state_supported then + table.insert(main_component_capabilities, capabilities.thermostatOperatingState.ID) + end + + table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) + + -- earlier modular profile gating (min api v14, rpc 8) ensures we are running >= 0.57 FW. + -- This gating specifies a workaround required only for 0.57 FW, which is not needed for 0.58 and higher. + if version.api < 15 or version.rpc < 9 then + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + table.insert(main_component_capabilities, capabilities.switch.ID) + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + table.insert(main_component_capabilities, capabilities.thermostatMode.ID) + table.insert(main_component_capabilities, capabilities.refresh.ID) + table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) + + device:set_field(fields.SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end +end + +local match_modular_device_type = { + [fields.AP_DEVICE_TYPE_ID] = DeviceConfiguration.match_modular_profile_air_purifer, + [fields.RAC_DEVICE_TYPE_ID] = DeviceConfiguration.match_modular_profile_room_ac, + [fields.THERMOSTAT_DEVICE_TYPE_ID] = DeviceConfiguration.match_modular_profile_thermostat, +} + +local function profiling_data_still_required(device) + for _, field in pairs(fields.profiling_data) do + if device:get_field(field) == nil then + return true -- data still required if a field is nil + end + end + return false +end + +function DeviceConfiguration.match_profile(device) + if profiling_data_still_required(device) then return end + local primary_device_type = thermostat_utils.get_device_type(device) + if version.api >= 14 and version.rpc >= 8 and match_modular_device_type[primary_device_type] then + match_modular_device_type[primary_device_type](device) + return + else + local legacy_device_cfg = require "thermostat_utils.legacy_device_configuration" + legacy_device_cfg.match_profile(device) + end +end + +return DeviceConfiguration diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/embedded_cluster_utils.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/embedded_cluster_utils.lua new file mode 100644 index 0000000000..3368fe2fa4 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/embedded_cluster_utils.lua @@ -0,0 +1,94 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +local version = require "version" +local clusters = require "st.matter.clusters" +local utils = require "st.utils" + +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" +end + +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" +end + +if version.api < 13 then + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" +end + +local embedded_cluster_utils = {} + +local embedded_clusters_api_10 = { + [clusters.HepaFilterMonitoring.ID] = clusters.HepaFilterMonitoring, + [clusters.ActivatedCarbonFilterMonitoring.ID] = clusters.ActivatedCarbonFilterMonitoring, + [clusters.AirQuality.ID] = clusters.AirQuality, + [clusters.CarbonMonoxideConcentrationMeasurement.ID] = clusters.CarbonMonoxideConcentrationMeasurement, + [clusters.CarbonDioxideConcentrationMeasurement.ID] = clusters.CarbonDioxideConcentrationMeasurement, + [clusters.FormaldehydeConcentrationMeasurement.ID] = clusters.FormaldehydeConcentrationMeasurement, + [clusters.NitrogenDioxideConcentrationMeasurement.ID] = clusters.NitrogenDioxideConcentrationMeasurement, + [clusters.OzoneConcentrationMeasurement.ID] = clusters.OzoneConcentrationMeasurement, + [clusters.Pm1ConcentrationMeasurement.ID] = clusters.Pm1ConcentrationMeasurement, + [clusters.Pm10ConcentrationMeasurement.ID] = clusters.Pm10ConcentrationMeasurement, + [clusters.Pm25ConcentrationMeasurement.ID] = clusters.Pm25ConcentrationMeasurement, + [clusters.RadonConcentrationMeasurement.ID] = clusters.RadonConcentrationMeasurement, + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.ID] = clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement, +} + +local embedded_clusters_api_11 = { + [clusters.ElectricalEnergyMeasurement.ID] = clusters.ElectricalEnergyMeasurement, + [clusters.ElectricalPowerMeasurement.ID] = clusters.ElectricalPowerMeasurement, +} + +local embedded_clusters_api_13 = { + [clusters.WaterHeaterMode.ID] = clusters.WaterHeaterMode +} + +function embedded_cluster_utils.get_endpoints(device, cluster_id, opts) + -- If using older lua libs and need to check for an embedded cluster feature, + -- we must use the embedded cluster definitions here + if version.api < 10 and embedded_clusters_api_10[cluster_id] ~= nil or + version.api < 11 and embedded_clusters_api_11[cluster_id] ~= nil or + version.api < 13 and embedded_clusters_api_13[cluster_id] ~= nil then + local embedded_cluster = embedded_clusters_api_10[cluster_id] or embedded_clusters_api_11[cluster_id] or embedded_clusters_api_13[cluster_id] + local opts = opts or {} + if utils.table_size(opts) > 1 then + device.log.warn_with({hub_logs = true}, "Invalid options for get_endpoints") + return + end + local clus_has_features = function(clus, feature_bitmap) + if not feature_bitmap or not clus then return false end + return embedded_cluster.are_features_supported(feature_bitmap, clus.feature_map) + end + local eps = {} + for _, ep in ipairs(device.endpoints) do + for _, clus in ipairs(ep.clusters) do + if ((clus.cluster_id == cluster_id) + and (opts.feature_bitmap == nil or clus_has_features(clus, opts.feature_bitmap)) + and ((opts.cluster_type == nil and clus.cluster_type == "SERVER" or clus.cluster_type == "BOTH") + or (opts.cluster_type == clus.cluster_type)) + or (cluster_id == nil)) then + table.insert(eps, ep.endpoint_id) + if cluster_id == nil then break end + end + end + end + return eps + else + return device:get_endpoints(cluster_id, opts) + end +end + +return embedded_cluster_utils \ No newline at end of file diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/fields.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/fields.lua new file mode 100644 index 0000000000..770fd7d65e --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/fields.lua @@ -0,0 +1,238 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local version = require "version" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local st_utils = require "st.utils" + +if version.api < 10 then + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Thermostat.types.ThermostatSystemMode.DRY = 0x8 -- ThermostatSystemMode added in Matter 1.2 + clusters.Thermostat.types.ThermostatSystemMode.SLEEP = 0x9 -- ThermostatSystemMode added in Matter 1.2 +end + +local ThermostatFields = {} + +ThermostatFields.SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities" + +ThermostatFields.SAVED_SYSTEM_MODE_IB = "__saved_system_mode_ib" +ThermostatFields.DISALLOWED_THERMOSTAT_MODES = "__DISALLOWED_CONTROL_OPERATIONS" +ThermostatFields.OPTIONAL_THERMOSTAT_MODES_SEEN = "__OPTIONAL_THERMOSTAT_MODES_SEEN" + +ThermostatFields.RAC_DEVICE_TYPE_ID = 0x0072 +ThermostatFields.AP_DEVICE_TYPE_ID = 0x002D +ThermostatFields.FAN_DEVICE_TYPE_ID = 0x002B +ThermostatFields.WATER_HEATER_DEVICE_TYPE_ID = 0x050F +ThermostatFields.HEAT_PUMP_DEVICE_TYPE_ID = 0x0309 +ThermostatFields.THERMOSTAT_DEVICE_TYPE_ID = 0x0301 +ThermostatFields.ELECTRICAL_SENSOR_DEVICE_TYPE_ID = 0x0510 + +ThermostatFields.MIN_ALLOWED_PERCENT_VALUE = 0 +ThermostatFields.MAX_ALLOWED_PERCENT_VALUE = 100 +ThermostatFields.CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" +ThermostatFields.LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" +ThermostatFields.MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds +ThermostatFields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP = "__total_cumulative_energy_imported_map" +ThermostatFields.SUPPORTED_WATER_HEATER_MODES_WITH_IDX = "__supported_water_heater_modes_with_idx" +ThermostatFields.COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" +ThermostatFields.MGM3_PPM_CONVERSION_FACTOR = 24.45 + +-- For RPC version < 6: +-- issue context: driver cannot know a setpoint capability's unit (whether Celsius or Farenheit) +-- when a command is received, as the received arguments do not contain the unit. +-- workaround: map the following temperature ranges to either Celsius or Farenheit: +-- For Thermostats: +-- 1. if the received setpoint command value is in the range 5 ~ 40, it is inferred as *C +-- 2. if the received setpoint command value is in the range 41 ~ 104, it is inferred as *F +-- For Water Heaters: +-- 1. if the received setpoint command value is in the range 30 ~ 80, it is inferred as *C +-- 2. if the received setpoint command value is in the range 86 ~ 176, it is inferred as *F +-- For RPC version >= 6: +-- temperatureSetpoint always reports in Celsius, removing the need for the above workaround. +-- In this case, we use these fields simply to limit the setpoint's range to "reasonable" values on the platform. +ThermostatFields.THERMOSTAT_MAX_TEMP_IN_C = version.rpc >= 6 and 100.0 or 40.0 +ThermostatFields.THERMOSTAT_MIN_TEMP_IN_C = version.rpc >= 6 and 0.0 or 5.0 +ThermostatFields.WATER_HEATER_MAX_TEMP_IN_C = version.rpc >= 6 and 100.0 or 80.0 +ThermostatFields.WATER_HEATER_MIN_TEMP_IN_C = version.rpc >= 6 and 0.0 or 30.0 + +ThermostatFields.setpoint_limit_device_field = { + MIN_SETPOINT_DEADBAND_CHECKED = "MIN_SETPOINT_DEADBAND_CHECKED", + MIN_HEAT = "MIN_HEAT", + MAX_HEAT = "MAX_HEAT", + MIN_COOL = "MIN_COOL", + MAX_COOL = "MAX_COOL", + MIN_DEADBAND = "MIN_DEADBAND", + MIN_TEMP = "MIN_TEMP", + MAX_TEMP = "MAX_TEMP" +} + +ThermostatFields.battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_LEVEL = "BATTERY_LEVEL", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" +} + +ThermostatFields.profiling_data = { + BATTERY_SUPPORT = "__BATTERY_SUPPORT", + THERMOSTAT_RUNNING_STATE_SUPPORT = "__THERMOSTAT_RUNNING_STATE_SUPPORT" +} + +ThermostatFields.THERMOSTAT_MODE_MAP = { + [clusters.Thermostat.types.ThermostatSystemMode.OFF] = capabilities.thermostatMode.thermostatMode.off, + [clusters.Thermostat.types.ThermostatSystemMode.AUTO] = capabilities.thermostatMode.thermostatMode.auto, + [clusters.Thermostat.types.ThermostatSystemMode.COOL] = capabilities.thermostatMode.thermostatMode.cool, + [clusters.Thermostat.types.ThermostatSystemMode.HEAT] = capabilities.thermostatMode.thermostatMode.heat, + [clusters.Thermostat.types.ThermostatSystemMode.EMERGENCY_HEATING] = capabilities.thermostatMode.thermostatMode.emergency_heat, + [clusters.Thermostat.types.ThermostatSystemMode.PRECOOLING] = capabilities.thermostatMode.thermostatMode.precooling, + [clusters.Thermostat.types.ThermostatSystemMode.FAN_ONLY] = capabilities.thermostatMode.thermostatMode.fanonly, + [clusters.Thermostat.types.ThermostatSystemMode.DRY] = capabilities.thermostatMode.thermostatMode.dryair, + [clusters.Thermostat.types.ThermostatSystemMode.SLEEP] = capabilities.thermostatMode.thermostatMode.asleep, +} + +ThermostatFields.THERMOSTAT_OPERATING_MODE_MAP = { + [0] = capabilities.thermostatOperatingState.thermostatOperatingState.heating, + [1] = capabilities.thermostatOperatingState.thermostatOperatingState.cooling, + [2] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, + [3] = capabilities.thermostatOperatingState.thermostatOperatingState.heating, + [4] = capabilities.thermostatOperatingState.thermostatOperatingState.cooling, + [5] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, + [6] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, +} + +ThermostatFields.WIND_MODE_MAP = { + [0] = capabilities.windMode.windMode.sleepWind, + [1] = capabilities.windMode.windMode.naturalWind +} + +ThermostatFields.ROCK_MODE_MAP = { + [0] = capabilities.fanOscillationMode.fanOscillationMode.horizontal, + [1] = capabilities.fanOscillationMode.fanOscillationMode.vertical, + [2] = capabilities.fanOscillationMode.fanOscillationMode.swing +} + +ThermostatFields.AIR_QUALITY_MAP = { + {capabilities.carbonDioxideMeasurement.ID, "-co2", clusters.CarbonDioxideConcentrationMeasurement}, + {capabilities.carbonDioxideHealthConcern.ID, "-co2", clusters.CarbonDioxideConcentrationMeasurement}, + {capabilities.carbonMonoxideMeasurement.ID, "-co", clusters.CarbonMonoxideConcentrationMeasurement}, + {capabilities.carbonMonoxideHealthConcern.ID, "-co", clusters.CarbonMonoxideConcentrationMeasurement}, + {capabilities.dustSensor.ID, "-pm10", clusters.Pm10ConcentrationMeasurement}, + {capabilities.dustHealthConcern.ID, "-pm10", clusters.Pm10ConcentrationMeasurement}, + {capabilities.fineDustSensor.ID, "-pm25", clusters.Pm25ConcentrationMeasurement}, + {capabilities.fineDustHealthConcern.ID, "-pm25", clusters.Pm25ConcentrationMeasurement}, + {capabilities.formaldehydeMeasurement.ID, "-ch2o", clusters.FormaldehydeConcentrationMeasurement}, + {capabilities.formaldehydeHealthConcern.ID, "-ch2o", clusters.FormaldehydeConcentrationMeasurement}, + {capabilities.nitrogenDioxideHealthConcern.ID, "-no2", clusters.NitrogenDioxideConcentrationMeasurement}, + {capabilities.nitrogenDioxideMeasurement.ID, "-no2", clusters.NitrogenDioxideConcentrationMeasurement}, + {capabilities.ozoneHealthConcern.ID, "-ozone", clusters.OzoneConcentrationMeasurement}, + {capabilities.ozoneMeasurement.ID, "-ozone", clusters.OzoneConcentrationMeasurement}, + {capabilities.radonHealthConcern.ID, "-radon", clusters.RadonConcentrationMeasurement}, + {capabilities.radonMeasurement.ID, "-radon", clusters.RadonConcentrationMeasurement}, + {capabilities.tvocHealthConcern.ID, "-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement}, + {capabilities.tvocMeasurement.ID, "-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement}, + {capabilities.veryFineDustHealthConcern.ID, "-pm1", clusters.Pm1ConcentrationMeasurement}, + {capabilities.veryFineDustSensor.ID, "-pm1", clusters.Pm1ConcentrationMeasurement}, +} + +ThermostatFields.units = { + PPM = 0, + PPB = 1, + PPT = 2, + MGM3 = 3, + UGM3 = 4, + NGM3 = 5, + PM3 = 6, + BQM3 = 7, + PCIL = 0xFF -- not in matter spec +} + +local units = ThermostatFields.units -- copy units to avoid references below + +ThermostatFields.unit_strings = { + [units.PPM] = "ppm", + [units.PPB] = "ppb", + [units.PPT] = "ppt", + [units.MGM3] = "mg/m^3", + [units.NGM3] = "ng/m^3", + [units.UGM3] = "μg/m^3", + [units.BQM3] = "Bq/m^3", + [units.PCIL] = "pCi/L" +} + +ThermostatFields.unit_default = { + [capabilities.carbonMonoxideMeasurement.NAME] = units.PPM, + [capabilities.carbonDioxideMeasurement.NAME] = units.PPM, + [capabilities.nitrogenDioxideMeasurement.NAME] = units.PPM, + [capabilities.ozoneMeasurement.NAME] = units.PPM, + [capabilities.formaldehydeMeasurement.NAME] = units.PPM, + [capabilities.veryFineDustSensor.NAME] = units.UGM3, + [capabilities.fineDustSensor.NAME] = units.UGM3, + [capabilities.dustSensor.NAME] = units.UGM3, + [capabilities.radonMeasurement.NAME] = units.BQM3, + [capabilities.tvocMeasurement.NAME] = units.PPB -- TVOC is typically within the range of 0-5500 ppb, with good to moderate values being < 660 ppb +} + +-- All ConcentrationMesurement clusters inherit from the same base cluster definitions, +-- so CarbonMonoxideConcentratinMeasurement is used below but the same enum types exist +-- in all ConcentrationMeasurement clusters +ThermostatFields.level_strings = { + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.UNKNOWN] = "unknown", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.LOW] = "good", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.MEDIUM] = "moderate", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.HIGH] = "unhealthy", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.CRITICAL] = "hazardous", +} + +-- measured in g/mol +ThermostatFields.molecular_weights = { + [capabilities.carbonDioxideMeasurement.NAME] = 44.010, + [capabilities.nitrogenDioxideMeasurement.NAME] = 28.014, + [capabilities.ozoneMeasurement.NAME] = 48.0, + [capabilities.formaldehydeMeasurement.NAME] = 30.031, + [capabilities.veryFineDustSensor.NAME] = "N/A", + [capabilities.fineDustSensor.NAME] = "N/A", + [capabilities.dustSensor.NAME] = "N/A", + [capabilities.radonMeasurement.NAME] = 222.018, + [capabilities.tvocMeasurement.NAME] = "N/A", +} + +ThermostatFields.conversion_tables = { + [units.PPM] = { + [units.PPM] = function(value) return st_utils.round(value) end, + [units.PPB] = function(value) return st_utils.round(value * (10^3)) end, + [units.UGM3] = function(value, molecular_weight) return st_utils.round((value * molecular_weight * 10^3) / ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) end, + [units.MGM3] = function(value, molecular_weight) return st_utils.round((value * molecular_weight) / ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) end, + }, + [units.PPB] = { + [units.PPM] = function(value) return st_utils.round(value/(10^3)) end, + [units.PPB] = function(value) return st_utils.round(value) end, + }, + [units.PPT] = { + [units.PPM] = function(value) return st_utils.round(value/(10^6)) end + }, + [units.MGM3] = { + [units.UGM3] = function(value) return st_utils.round(value * (10^3)) end, + [units.PPM] = function(value, molecular_weight) return st_utils.round((value * ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) / molecular_weight) end, + }, + [units.UGM3] = { + [units.UGM3] = function(value) return st_utils.round(value) end, + [units.PPM] = function(value, molecular_weight) return st_utils.round((value * ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) / (molecular_weight * 10^3)) end, + }, + [units.NGM3] = { + [units.UGM3] = function(value) return st_utils.round(value/(10^3)) end + }, + [units.BQM3] = { + [units.PCIL] = function(value) return st_utils.round(value/37) end + }, +} + +return ThermostatFields \ No newline at end of file diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/legacy_device_configuration.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/legacy_device_configuration.lua new file mode 100644 index 0000000000..6cdda2a53a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/legacy_device_configuration.lua @@ -0,0 +1,259 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local version = require "version" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils" +local fields = require "thermostat_utils.fields" +local thermostat_utils = require "thermostat_utils.utils" + +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" + clusters.AirQuality = require "embedded_clusters.AirQuality" +end + +local LegacyConfigurationHelpers = {} + +function LegacyConfigurationHelpers.create_level_measurement_profile(device) + local meas_name, level_name = "", "" + for _, details in ipairs(fields.AIR_QUALITY_MAP) do + local cap_id = details[1] + local cluster = details[3] + -- capability describes either a HealthConcern or Measurement/Sensor + if (cap_id:match("HealthConcern$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) + if #attr_eps > 0 then + level_name = level_name .. details[2] + end + elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) + if #attr_eps > 0 then + meas_name = meas_name .. details[2] + end + end + end + return meas_name, level_name +end + +function LegacyConfigurationHelpers.create_air_quality_sensor_profile(device) + local aqs_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) + local profile_name = "" + if #aqs_eps > 0 then + profile_name = profile_name .. "-aqs" + end + local meas_name, level_name = LegacyConfigurationHelpers.create_level_measurement_profile(device) + if meas_name ~= "" then + profile_name = profile_name .. meas_name .. "-meas" + end + if level_name ~= "" then + profile_name = profile_name .. level_name .. "-level" + end + return profile_name +end + +function LegacyConfigurationHelpers.create_fan_profile(device) + local fan_eps = device:get_endpoints(clusters.FanControl.ID) + local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) + local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) + local profile_name = "" + if #fan_eps > 0 then + profile_name = profile_name .. "-fan" + end + if #rock_eps > 0 then + profile_name = profile_name .. "-rock" + end + if #wind_eps > 0 then + profile_name = profile_name .. "-wind" + end + return profile_name +end + +function LegacyConfigurationHelpers.create_air_purifier_profile(device) + local hepa_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID) + local ac_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID) + local fan_eps_seen = false + local profile_name = "air-purifier" + if #hepa_filter_eps > 0 then + profile_name = profile_name .. "-hepa" + end + if #ac_filter_eps > 0 then + profile_name = profile_name .. "-ac" + end + + -- air purifier profiles include -fan later in the name for historical reasons. + -- save this information for use at that point. + local fan_profile = LegacyConfigurationHelpers.create_fan_profile(device) + if fan_profile ~= "" then + fan_eps_seen = true + end + fan_profile = string.gsub(fan_profile, "-fan", "") + profile_name = profile_name .. fan_profile + + return profile_name, fan_eps_seen +end + +function LegacyConfigurationHelpers.create_thermostat_modes_profile(device) + local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) + local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) + + local thermostat_modes = "" + if #heat_eps == 0 and #cool_eps == 0 then + return "No Heating nor Cooling Support" + elseif #heat_eps > 0 and #cool_eps == 0 then + thermostat_modes = thermostat_modes .. "-heating-only" + elseif #cool_eps > 0 and #heat_eps == 0 then + thermostat_modes = thermostat_modes .. "-cooling-only" + end + return thermostat_modes +end + + +local LegacyDeviceConfiguration = {} + +function LegacyDeviceConfiguration.match_profile(device) + local running_state_supported = device:get_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) + local battery_supported = device:get_field(fields.profiling_data.BATTERY_SUPPORT) + + local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) + local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) + local device_type = thermostat_utils.get_device_type(device) + local profile_name + if device_type == fields.RAC_DEVICE_TYPE_ID then + profile_name = "room-air-conditioner" + + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + + -- Room AC does not support the rocking feature of FanControl. + local fan_name = LegacyConfigurationHelpers.create_fan_profile(device) + fan_name = string.gsub(fan_name, "-rock", "") + profile_name = profile_name .. fan_name + + local thermostat_modes = LegacyConfigurationHelpers.create_thermostat_modes_profile(device) + if thermostat_modes == "" then + profile_name = profile_name .. "-heating-cooling" + else + device.log.warn_with({hub_logs=true}, "Device does not support both heating and cooling. No matching profile") + return + end + + if profile_name == "room-air-conditioner-humidity-fan-wind-heating-cooling" then + profile_name = "room-air-conditioner" + end + + if not running_state_supported and profile_name == "room-air-conditioner-fan-heating-cooling" then + profile_name = profile_name .. "-nostate" + end + + elseif device_type == fields.FAN_DEVICE_TYPE_ID then + profile_name = LegacyConfigurationHelpers.create_fan_profile(device) + -- remove leading "-" + profile_name = string.sub(profile_name, 2) + if profile_name == "fan" then + profile_name = "fan-generic" + end + + elseif device_type == fields.AP_DEVICE_TYPE_ID then + local fan_eps_found + profile_name, fan_eps_found = LegacyConfigurationHelpers.create_air_purifier_profile(device) + if #thermostat_eps > 0 then + profile_name = profile_name .. "-thermostat" + + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + + if fan_eps_found then + profile_name = profile_name .. "-fan" + end + + local thermostat_modes = LegacyConfigurationHelpers.create_thermostat_modes_profile(device) + if thermostat_modes ~= "No Heating nor Cooling Support" then + profile_name = profile_name .. thermostat_modes + end + + if not running_state_supported then + profile_name = profile_name .. "-nostate" + end + + if battery_supported == fields.battery_support.BATTERY_LEVEL then + profile_name = profile_name .. "-batteryLevel" + elseif battery_supported == fields.battery_support.NO_BATTERY then + profile_name = profile_name .. "-nobattery" + end + elseif #device:get_endpoints(clusters.TemperatureMeasurement.ID) > 0 then + profile_name = profile_name .. "-temperature" + + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + + if fan_eps_found then + profile_name = profile_name .. "-fan" + end + end + profile_name = profile_name .. LegacyConfigurationHelpers.create_air_quality_sensor_profile(device) + elseif device_type == fields.WATER_HEATER_DEVICE_TYPE_ID then + -- If a Water Heater is composed of Electrical Sensor device type, it must support both ElectricalEnergyMeasurement and + -- ElectricalPowerMeasurement clusters. + local electrical_sensor_eps = thermostat_utils.get_endpoints_by_device_type(device, fields.ELECTRICAL_SENSOR_DEVICE_TYPE_ID) or {} + if #electrical_sensor_eps > 0 then + profile_name = "water-heater-power-energy-powerConsumption" + end + elseif device_type == fields.HEAT_PUMP_DEVICE_TYPE_ID then + profile_name = "heat-pump" + local MAX_HEAT_PUMP_THERMOSTAT_COMPONENTS = 2 + for i = 1, math.min(MAX_HEAT_PUMP_THERMOSTAT_COMPONENTS, #thermostat_eps) do + profile_name = profile_name .. "-thermostat" + if thermostat_utils.tbl_contains(humidity_eps, thermostat_eps[i]) then + profile_name = profile_name .. "-humidity" + end + end + elseif #thermostat_eps > 0 then + profile_name = "thermostat" + + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + + -- thermostat profiles support neither wind nor rocking FanControl attributes + local fan_name = LegacyConfigurationHelpers.create_fan_profile(device) + if fan_name ~= "" then + profile_name = profile_name .. "-fan" + end + + local thermostat_modes = LegacyConfigurationHelpers.create_thermostat_modes_profile(device) + if thermostat_modes == "No Heating nor Cooling Support" then + device.log.warn_with({hub_logs=true}, "Device does not support either heating or cooling. No matching profile") + return + else + profile_name = profile_name .. thermostat_modes + end + + if not running_state_supported then + profile_name = profile_name .. "-nostate" + end + + if battery_supported == fields.battery_support.BATTERY_LEVEL then + profile_name = profile_name .. "-batteryLevel" + elseif battery_supported == fields.battery_support.NO_BATTERY then + profile_name = profile_name .. "-nobattery" + end + else + device.log.warn_with({hub_logs=true}, "Device type is not supported in thermostat driver") + return + end + + if profile_name then + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) + end + -- clear all profiling data fields after profiling is complete. + for _, field in pairs(fields.profiling_data) do + device:set_field(field, nil) + end +end + +return LegacyDeviceConfiguration diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua new file mode 100644 index 0000000000..7773c9ba02 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua @@ -0,0 +1,203 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local log = require "log" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local fields = require "thermostat_utils.fields" +local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils" + +local ThermostatUtils = {} + +function ThermostatUtils.tbl_contains(array, value) + if value == nil then return false end + for _, element in pairs(array or {}) do + if element == value then + return true + end + end + return false +end + +function ThermostatUtils.get_field_for_endpoint(device, field, endpoint) + return device:get_field(string.format("%s_%d", field, endpoint)) +end + +function ThermostatUtils.set_field_for_endpoint(device, field, endpoint, value, additional_params) + device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) +end + +function ThermostatUtils.find_default_endpoint(device, cluster) + local res = device.MATTER_DEFAULT_ENDPOINT + local eps = embedded_cluster_utils.get_endpoints(device, cluster) + table.sort(eps) + for _, v in ipairs(eps) do + if v ~= 0 then --0 is the matter RootNode endpoint + return v + end + end + device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) + return res +end + +function ThermostatUtils.component_to_endpoint(device, component_name, cluster_id) + -- Use the find_default_endpoint function to return the first endpoint that + -- supports a given cluster. + local component_to_endpoint_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) + if component_to_endpoint_map ~= nil and component_to_endpoint_map[component_name] ~= nil then + return component_to_endpoint_map[component_name] + end + if not cluster_id then return device.MATTER_DEFAULT_ENDPOINT end + return ThermostatUtils.find_default_endpoint(device, cluster_id) +end + +function ThermostatUtils.endpoint_to_component(device, endpoint_id) + local component_to_endpoint_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) + if component_to_endpoint_map ~= nil then + for comp, ep in pairs(component_to_endpoint_map) do + if ep == endpoint_id then + return comp + end + end + end + return "main" +end + +function ThermostatUtils.get_total_cumulative_energy_imported(device) + local total_cumulative_energy_imported = device:get_field(fields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} + local total_energy = 0 + for _, energyWh in pairs(total_cumulative_energy_imported) do + total_energy = total_energy + energyWh + end + return total_energy +end + +function ThermostatUtils.get_endpoints_by_device_type(device, device_type) + local endpoints = {} + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == device_type then + table.insert(endpoints, ep.endpoint_id) + break + end + end + end + table.sort(endpoints) + return endpoints +end + + -- set the supportedThermostatOperatingStates attribute if the thermostatOperatingState capability is supported and it has not been set before +function ThermostatUtils.handle_thermostat_operating_state_info(device) + local thermostat_operating_state_supported = device:supports_capability(capabilities.thermostatOperatingState) + local latest_supported_operating_states = thermostat_operating_state_supported and device:get_latest_state( + "main", capabilities.thermostatOperatingState.ID, capabilities.thermostatOperatingState.supportedThermostatOperatingStates.NAME + ) + if thermostat_operating_state_supported and latest_supported_operating_states == nil then + local supported_operating_modes = { "idle" } + if #device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) > 0 then + table.insert(supported_operating_modes, "heating") + end + if #device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) > 0 then + table.insert(supported_operating_modes, "cooling") + end + local thermostat_ep_id = device:get_endpoints(clusters.Thermostat.ID)[1] + device:emit_event_for_endpoint(thermostat_ep_id, capabilities.thermostatOperatingState.supportedThermostatOperatingStates(supported_operating_modes, {visibility = {displayed = false}})) + end +end + +function ThermostatUtils.get_device_type(device) + -- For cases where a device has multiple device types, this list indicates which + -- device type will be the "main" device type for purposes of selecting a profile + -- with an appropriate category. This is done to promote consistency between + -- devices with similar device type compositions that may report their device types + -- listed in different orders + local device_type_priority = { + [fields.HEAT_PUMP_DEVICE_TYPE_ID] = 1, + [fields.RAC_DEVICE_TYPE_ID] = 2, + [fields.AP_DEVICE_TYPE_ID] = 3, + [fields.THERMOSTAT_DEVICE_TYPE_ID] = 4, + [fields.FAN_DEVICE_TYPE_ID] = 5, + [fields.WATER_HEATER_DEVICE_TYPE_ID] = 6, + } + + local main_device_type = false + + for _, ep in ipairs(device.endpoints) do + if ep.device_types ~= nil then + for _, dt in ipairs(ep.device_types) do + if not device_type_priority[main_device_type] or (device_type_priority[dt.device_type_id] and + device_type_priority[dt.device_type_id] < device_type_priority[main_device_type]) then + main_device_type = dt.device_type_id + end + end + end + end + + return main_device_type +end + +function ThermostatUtils.unit_conversion(value, from_unit, to_unit, capability_name) + local conversion_function = fields.conversion_tables[from_unit] and fields.conversion_tables[from_unit][to_unit] or nil + if not conversion_function then + log.info_with( {hub_logs = true} , string.format("Unsupported unit conversion from %s to %s", fields.unit_strings[from_unit], fields.unit_strings[to_unit])) + return + end + + if not value then + log.info_with( {hub_logs = true} , "unit conversion value is nil") + return + end + + return conversion_function(value, fields.molecular_weights[capability_name]) +end + +function ThermostatUtils.supports_capability_by_id_modular(device, capability, component) + if not device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then + device.log.warn_with({hub_logs = true}, "Device has overriden supports_capability_by_id, but does not have supported capabilities set.") + return false + end + for _, component_capabilities in ipairs(device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES)) do + local comp_id = component_capabilities[1] + local capability_ids = component_capabilities[2] + if (component == nil) or (component == comp_id) then + for _, cap in ipairs(capability_ids) do + if cap == capability then + return true + end + end + end + end + return false +end + +function ThermostatUtils.report_power_consumption_to_st_energy(device, latest_total_imported_energy_wh) + local current_time = os.time() + local last_time = device:get_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP) or 0 + + -- Ensure that the previous report was sent at least 15 minutes ago + if fields.MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then + return + end + + device:set_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) + + -- Calculate the energy delta between reports + local energy_delta_wh = 0.0 + local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, + capabilities.powerConsumptionReport.powerConsumption.NAME) + if previous_imported_report and previous_imported_report.energy then + energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) + end + + local epoch_to_iso8601 = function(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) end + + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + device:emit_component_event(device.profile.components["main"], capabilities.powerConsumptionReport.powerConsumption({ + start = epoch_to_iso8601(last_time), + ["end"] = epoch_to_iso8601(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) +end + +return ThermostatUtils diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index d494e93749..531dc8c36f 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -30,6 +30,24 @@ matterManufacturer: vendorId: 0x130A productId: 0x0060 deviceProfileName: window-covering +# Griesser + - id: "5435/14337" + deviceLabel: MSM-1 + vendorId: 0x153B + productId: 0x3801 + deviceProfileName: window-covering-tilt +# HooRii + - id: "4945/61171" + deviceLabel: HooRii Window Covering + vendorId: 0x1351 + productId: 0xEEF3 + deviceProfileName: window-covering-battery +# Meross + - id: "4933/61453" + deviceLabel: Smart Wi-Fi Roller Shutter Timer + vendorId: 0x1345 + productId: 0xF00D + deviceProfileName: window-covering # Mamaba - id: "4965/4097" deviceLabel: Wi-Fi Curtain @@ -42,6 +60,12 @@ matterManufacturer: vendorId: 0x1500 productId: 0x2711 deviceProfileName: window-covering-battery +# SmartWings + - id: "5231/4097" + deviceLabel: SmartWings Window Covering + vendorId: 0x146F + productId: 0x1001 + deviceProfileName: window-covering-battery #Zemismart - id: "Zemismart MT01 Slide Curtain" deviceLabel: Zemismart MT01 Slide Curtain @@ -83,6 +107,12 @@ matterManufacturer: vendorId: 0x139C productId: 0xFA17 deviceProfileName: window-covering +#WINDOWSTORY + - id: "5496/6657" + deviceLabel: GATEWAY-MT + vendorId: 0x1578 + productId: 0x1A01 + deviceProfileName: window-covering-tilt #WISTAR - id: "5207/3" deviceLabel: WISTAR WSERD16-B Smart Tubular Motor @@ -93,7 +123,7 @@ matterManufacturer: deviceLabel: WISTAR WSERD24 Smart Tubular Motor vendorId: 0x1457 productId: 0x0004 - deviceProfileName: window-covering + deviceProfileName: window-covering - id: "5207/5" deviceLabel: WISTAR WSERD40-B Smart Tubular Motor vendorId: 0x1457 @@ -108,7 +138,7 @@ matterManufacturer: deviceLabel: WISTAR WSERD40-T Smart Tubular Motor vendorId: 0x1457 productId: 0x0007 - deviceProfileName: window-covering + deviceProfileName: window-covering - id: "5207/8" deviceLabel: WISTAR WSERD50-B Smart Tubular Motor vendorId: 0x1457 @@ -123,12 +153,12 @@ matterManufacturer: deviceLabel: WISTAR WSERD50-T Smart Tubular Motor vendorId: 0x1457 productId: 0x0010 - deviceProfileName: window-covering + deviceProfileName: window-covering - id: "5207/19" deviceLabel: WISTAR WSER60 Smart Tubular Motor vendorId: 0x1457 productId: 0x0013 - deviceProfileName: window-covering + deviceProfileName: window-covering - id: "5207/17" deviceLabel: WISTAR WSER40 Smart Tubular Motor vendorId: 0x1457 @@ -144,6 +174,51 @@ matterManufacturer: vendorId: 0x1457 productId: 0x0002 deviceProfileName: window-covering-battery + - id: "5207/22" + deviceLabel: WISTAR WSCMXH Smart Vertical Blind Motor + vendorId: 0x1457 + productId: 0x0016 + deviceProfileName: window-covering-tilt + - id: "5207/23" + deviceLabel: WISTAR WSCMXF Smart Vertical Blind Motor + vendorId: 0x1457 + productId: 0x0017 + deviceProfileName: window-covering-tilt + - id: "5207/24" + deviceLabel: WISTAR WSCMXF-LED Smart Vertical Blind Motor + vendorId: 0x1457 + productId: 0x0018 + deviceProfileName: window-covering-tilt + - id: "5207/20" + deviceLabel: WISTAR WSCMQ Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0014 + deviceProfileName: window-covering + - id: "5207/21" + deviceLabel: WISTAR WSCMXI Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0015 + deviceProfileName: window-covering + - id: "5207/32" + deviceLabel: WISTAR WSCMT Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0020 + deviceProfileName: window-covering + - id: "5207/34" + deviceLabel: WISTAR WSCMXB Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0022 + deviceProfileName: window-covering + - id: "5207/35" + deviceLabel: WISTAR WSCMXC Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0023 + deviceProfileName: window-covering + - id: "5207/38" + deviceLabel: WISTAR WSCMXJ Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0026 + deviceProfileName: window-covering #Yooksmart - id: "5411/1052" deviceLabel: Smart WindowCovering Series diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering-battery.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering-battery.yml index 56cac5ffa4..a1896ca634 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering-battery.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-battery.yml @@ -17,7 +17,5 @@ components: categories: - name: Blind preferences: - - preferenceId: presetPosition - explicit: true - preferenceId: reverse explicit: true diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering-batteryLevel.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering-batteryLevel.yml index 73165b4a90..fa46d87cc6 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering-batteryLevel.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-batteryLevel.yml @@ -17,7 +17,5 @@ components: categories: - name: Blind preferences: - - preferenceId: presetPosition - explicit: true - preferenceId: reverse explicit: true diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering-profile.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering-profile.yml index 4164fb88d0..903565e68c 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering-profile.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-profile.yml @@ -18,7 +18,5 @@ components: categories: - name: Blind preferences: - - preferenceId: presetPosition - explicit: true - preferenceId: reverse explicit: true diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-battery.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-battery.yml index 49b255830e..27f7f1f61a 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-battery.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-battery.yml @@ -19,7 +19,5 @@ components: categories: - name: Blind preferences: - - preferenceId: presetPosition - explicit: true - preferenceId: reverse explicit: true diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt.yml index 4ce9636939..c6a759b610 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt.yml @@ -17,7 +17,5 @@ components: categories: - name: Blind preferences: - - preferenceId: presetPosition - explicit: true - preferenceId: reverse explicit: true diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering.yml index b588b41ec9..fbd40ed08d 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering.yml @@ -15,7 +15,5 @@ components: categories: - name: Blind preferences: - - preferenceId: presetPosition - explicit: true - preferenceId: reverse explicit: true diff --git a/drivers/SmartThings/matter-window-covering/src/init.lua b/drivers/SmartThings/matter-window-covering/src/init.lua index a8e6e80114..6759560c50 100644 --- a/drivers/SmartThings/matter-window-covering/src/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/init.lua @@ -28,6 +28,8 @@ local battery_support = { BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" } local REVERSE_POLARITY = "__reverse_polarity" +local PRESET_LEVEL_KEY = "__preset_level_key" +local DEFAULT_PRESET_LEVEL = 50 local function find_default_endpoint(device, cluster) local res = device.MATTER_DEFAULT_ENDPOINT @@ -69,6 +71,17 @@ end local function device_init(driver, device) device:set_component_to_endpoint_fn(component_to_endpoint) + if device:supports_capability_by_id(capabilities.windowShadePreset.ID) and + device:get_latest_state("main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME) == nil then + -- These should only ever be nil once (and at the same time) for already-installed devices + -- It can be removed after migration is complete + device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed = false}})) + local preset_position = device:get_field(PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + DEFAULT_PRESET_LEVEL + device:emit_event(capabilities.windowShadePreset.position(preset_position, {visibility = {displayed = false}})) + device:set_field(PRESET_LEVEL_KEY, preset_position, {persist = true}) + end device:subscribe() end @@ -118,24 +131,20 @@ local function device_removed(driver, device) log.info("device removed") end -- capability handlers local function handle_preset(driver, device, cmd) + local lift_value = device:get_latest_state( + "main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME + ) or DEFAULT_PRESET_LEVEL + local hundredths_lift_percent = (100 - lift_value) * 100 local endpoint_id = device:component_to_endpoint(cmd.component) - local lift_eps = device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.LIFT}) - local tilt_eps = device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.TILT}) - if #lift_eps > 0 then - local lift_value = 100 - device.preferences.presetPosition - local hundredths_lift_percent = lift_value * 100 - local req = clusters.WindowCovering.server.commands.GoToLiftPercentage( - device, endpoint_id, hundredths_lift_percent - ) - device:send(req) - end - if #tilt_eps > 0 then - -- Use default preset tilt percentage to 50 until a canonical preference is created for preset tilt position - local req = clusters.WindowCovering.server.commands.GoToTiltPercentage( - device, endpoint_id, 50 * 100 - ) - device:send(req) - end + device:send(clusters.WindowCovering.server.commands.GoToLiftPercentage( + device, endpoint_id, hundredths_lift_percent + )) +end + +local function handle_set_preset(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:set_field(PRESET_LEVEL_KEY, cmd.args.position) + device:emit_event_for_endpoint(endpoint_id, capabilities.windowShadePreset.position(cmd.args.position)) end -- close covering @@ -343,11 +352,9 @@ local matter_driver_template = { }, }, capability_handlers = { - [capabilities.refresh.ID] = { - [capabilities.refresh.commands.refresh.NAME] = nil --TODO: define me! - }, [capabilities.windowShadePreset.ID] = { [capabilities.windowShadePreset.commands.presetPosition.NAME] = handle_preset, + [capabilities.windowShadePreset.commands.setPresetPosition.NAME] = handle_set_preset, }, [capabilities.windowShade.ID] = { [capabilities.windowShade.commands.close.NAME] = handle_close, diff --git a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua index 2d7e3946ef..409ebfcb09 100644 --- a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua +++ b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua @@ -17,13 +17,15 @@ local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" local clusters = require "st.matter.clusters" + local WindowCovering = clusters.WindowCovering +test.disable_startup_messages() + local mock_device = test.mock_device.build_test_matter_device( { profile = t_utils.get_profile_definition("window-covering-tilt-battery.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, - preferences = { presetPosition = 30 }, endpoints = { { endpoint_id = 2, @@ -36,44 +38,12 @@ local mock_device = test.mock_device.build_test_matter_device( }, { endpoint_id = 10, - clusters = { -- list the clusters - { - cluster_id = clusters.WindowCovering.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = 3, - }, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} - }, - }, - }, - } -) - -local mock_device_switch_to_battery = test.mock_device.build_test_matter_device( - { - profile = t_utils.get_profile_definition("window-covering.yml"), - manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, - preferences = { presetPosition = 30 }, - endpoints = { - { - endpoint_id = 2, clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - device_type_id = 0x0016, device_type_revision = 1, -- RootNode - } - }, - { - endpoint_id = 10, - clusters = { -- list the clusters { cluster_id = clusters.WindowCovering.ID, cluster_type = "SERVER", cluster_revision = 1, - feature_map = 1, + feature_map = 3, }, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"}, {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} @@ -87,7 +57,6 @@ local mock_device_mains_powered = test.mock_device.build_test_matter_device( { profile = t_utils.get_profile_definition("window-covering.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, - preferences = { presetPosition = 30 }, endpoints = { { endpoint_id = 2, @@ -100,7 +69,7 @@ local mock_device_mains_powered = test.mock_device.build_test_matter_device( }, { endpoint_id = 10, - clusters = { -- list the clusters + clusters = { { cluster_id = clusters.WindowCovering.ID, cluster_type = "SERVER", @@ -129,35 +98,61 @@ local CLUSTER_SUBSCRIBE_LIST_NO_BATTERY = { WindowCovering.server.attributes.OperationalStatus, } +local function set_preset(device) + test.socket.capability:__expect_send( + device:generate_test_message( + "main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed = false}}) + ) + ) + test.socket.capability:__expect_send( + device:generate_test_message( + "main", capabilities.windowShadePreset.position(50, {visibility = {displayed = false}}) + ) + ) +end + local function test_init() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, + {visibility = {displayed = false}}) + ) + ) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + set_preset(mock_device) local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) -end -local function test_init_switch_to_battery() - local subscribe_request = CLUSTER_SUBSCRIBE_LIST_NO_BATTERY[1]:subscribe(mock_device_switch_to_battery) - for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_NO_BATTERY) do - if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_switch_to_battery)) end - end - test.socket.matter:__expect_send({mock_device_switch_to_battery.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_switch_to_battery) - test.socket.device_lifecycle:__queue_receive({ mock_device_switch_to_battery.id, "doConfigure" }) - mock_device_switch_to_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() - test.socket.matter:__expect_send({mock_device_switch_to_battery.id, read_attribute_list}) + test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) end local function test_init_mains_powered() + test.mock_device.add_test_device(mock_device_mains_powered) + test.socket.device_lifecycle:__queue_receive({ mock_device_mains_powered.id, "added" }) + test.socket.capability:__expect_send( + mock_device_mains_powered:generate_test_message( + "main", capabilities.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, + {visibility = {displayed = false}}) + ) + ) + + test.socket.device_lifecycle:__queue_receive({ mock_device_mains_powered.id, "init" }) + set_preset(mock_device_mains_powered) local subscribe_request = CLUSTER_SUBSCRIBE_LIST_NO_BATTERY[1]:subscribe(mock_device_mains_powered) for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_NO_BATTERY) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_mains_powered)) end end test.socket.matter:__expect_send({mock_device_mains_powered.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_mains_powered) + test.socket.device_lifecycle:__queue_receive({ mock_device_mains_powered.id, "doConfigure" }) mock_device_mains_powered:expect_metadata_update({ profile = "window-covering" }) mock_device_mains_powered:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -774,33 +769,40 @@ test.register_coroutine_test("OperationalStatus report contains current position ) end) -test.register_coroutine_test("Handle windowcoveringPreset", function() - test.socket.capability:__queue_receive( - { +test.register_coroutine_test( + "Handle preset commands", + function() + local PRESET_LEVEL = 30 + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = { PRESET_LEVEL }}, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadePreset.position(PRESET_LEVEL) + ) + ) + test.socket.capability:__queue_receive({ mock_device.id, {capability = "windowShadePreset", component = "main", command = "presetPosition", args = {}}, - } - ) - test.socket.matter:__expect_send( - {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 7000)} - ) - test.socket.matter:__expect_send( - {mock_device.id, WindowCovering.server.commands.GoToTiltPercentage(mock_device, 10, 5000)} - ) -end) + }) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, (100 - PRESET_LEVEL) * 100)} + ) + end +) test.register_coroutine_test( "Test profile change to window-covering-battery when battery percent remaining attribute (attribute ID 12) is available", function() test.socket.matter:__queue_receive( { - mock_device_switch_to_battery.id, - clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_switch_to_battery, 10, {uint32(12)}) + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 10, {uint32(12)}) } ) - mock_device_switch_to_battery:expect_metadata_update({ profile = "window-covering-battery" }) - end, - { test_init = test_init_switch_to_battery } + mock_device:expect_metadata_update({ profile = "window-covering-tilt-battery" }) + end ) test.register_coroutine_test( @@ -808,12 +810,11 @@ test.register_coroutine_test( function() test.socket.matter:__queue_receive( { - mock_device_switch_to_battery.id, - clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_switch_to_battery, 10, {uint32(10)}) + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 10, {uint32(10)}) } ) - end, - { test_init = test_init_switch_to_battery } + end ) test.register_coroutine_test( diff --git a/drivers/SmartThings/philips-hue/src/disco/init.lua b/drivers/SmartThings/philips-hue/src/disco/init.lua index 0746e13942..81b74ae9ef 100644 --- a/drivers/SmartThings/philips-hue/src/disco/init.lua +++ b/drivers/SmartThings/philips-hue/src/disco/init.lua @@ -334,8 +334,13 @@ function HueDiscovery.handle_discovered_child_device(driver, bridge_network_id, end for _, svc_info in ipairs(primary_services[primary_service_type]) do + if driver:get_device_by_dni(v1_dni) then + return + end local v2_resource_id = svc_info.rid or "" - if driver:get_device_by_dni(v1_dni) or driver.hue_identifier_to_device_record[v2_resource_id] then return end + if (driver.hue_identifier_to_device_record_by_bridge[bridge_network_id] or {})[v2_resource_id] then + return + end end local api_instance = diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua index d1d054e425..d10ad5ee65 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua @@ -52,8 +52,9 @@ function ButtonLifecycleHandlers.added(driver, device, parent_device_id, resourc end local button_rid_to_index_map = {} + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} if button_info.button then - driver.hue_identifier_to_device_record[button_info.id] = device + hue_id_to_device[button_info.id] = device button_rid_to_index_map[button_info.id] = 1 end @@ -65,7 +66,7 @@ function ButtonLifecycleHandlers.added(driver, device, parent_device_id, resourc local button_id = button_info[button_id_key] if button and button_id then - driver.hue_identifier_to_device_record[button_id] = device + hue_id_to_device[button_id] = device button_rid_to_index_map[button_id] = var local supported_button_values = utils.get_supported_button_values(button.event_values) @@ -86,7 +87,7 @@ function ButtonLifecycleHandlers.added(driver, device, parent_device_id, resourc end if button_info.power_id then - driver.hue_identifier_to_device_record[button_info.power_id] = device + hue_id_to_device[button_info.power_id] = device end log.debug(st_utils.stringify_table(button_rid_to_index_map, "button index map", true)) @@ -98,7 +99,7 @@ function ButtonLifecycleHandlers.added(driver, device, parent_device_id, resourc device:set_field(Fields._ADDED, true, { persist = true }) device:set_field(Fields._REFRESH_AFTER_INIT, true, { persist = true }) - driver.hue_identifier_to_device_record[device_button_resource_id] = device + hue_id_to_device[device_button_resource_id] = device end ---@param driver HueDriver @@ -114,9 +115,29 @@ function ButtonLifecycleHandlers.init(driver, device) log.debug("resource id " .. tostring(device_button_resource_id)) local hue_device_id = device:get_field(Fields.HUE_DEVICE_ID) - if not driver.hue_identifier_to_device_record[device_button_resource_id] then - driver.hue_identifier_to_device_record[device_button_resource_id] = device + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} + if not hue_id_to_device[device_button_resource_id] then + hue_id_to_device[device_button_resource_id] = device end + + local maybe_idx_map = device:get_field(Fields.BUTTON_INDEX_MAP) + local svc_rids_for_device = driver.services_for_device_rid[hue_device_id] or {} + + if not svc_rids_for_device[device_button_resource_id] then + svc_rids_for_device[device_button_resource_id] = HueDeviceTypes.BUTTON + end + + for resource_id, _ in pairs(maybe_idx_map or {}) do + if not hue_id_to_device[resource_id] then + hue_id_to_device[resource_id] = device + end + + if not svc_rids_for_device[resource_id] then + svc_rids_for_device[resource_id] = HueDeviceTypes.BUTTON + end + end + driver.services_for_device_rid[hue_device_id] = svc_rids_for_device + local button_info, err button_info = Discovery.device_state_disco_cache[device_button_resource_id] if not button_info then diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/contact.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/contact.lua index ea5f1ceac2..eaef1a2ccd 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/contact.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/contact.lua @@ -51,8 +51,9 @@ function ContactLifecycleHandlers.added(driver, device, parent_device_id, resour return end - driver.hue_identifier_to_device_record[sensor_info.power_id] = device - driver.hue_identifier_to_device_record[sensor_info.tamper_id] = device + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} + hue_id_to_device[sensor_info.power_id] = device + hue_id_to_device[sensor_info.tamper_id] = device device:set_field(Fields.DEVICE_TYPE, HueDeviceTypes.CONTACT, { persist = true }) device:set_field(Fields.HUE_DEVICE_ID, sensor_info.hue_device_id, { persist = true }) @@ -61,7 +62,7 @@ function ContactLifecycleHandlers.added(driver, device, parent_device_id, resour device:set_field(Fields._ADDED, true, { persist = true }) device:set_field(Fields._REFRESH_AFTER_INIT, true, { persist = true }) - driver.hue_identifier_to_device_record[device_sensor_resource_id] = device + hue_id_to_device[device_sensor_resource_id] = device end ---@param driver HueDriver @@ -77,8 +78,9 @@ function ContactLifecycleHandlers.init(driver, device) log.debug("resource id " .. tostring(device_sensor_resource_id)) local hue_device_id = device:get_field(Fields.HUE_DEVICE_ID) - if not driver.hue_identifier_to_device_record[device_sensor_resource_id] then - driver.hue_identifier_to_device_record[device_sensor_resource_id] = device + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} + if not hue_id_to_device[device_sensor_resource_id] then + hue_id_to_device[device_sensor_resource_id] = device end local sensor_info, err sensor_info = Discovery.device_state_disco_cache[device_sensor_resource_id] diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua index c9d413841d..7724246595 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua @@ -93,8 +93,9 @@ function LifecycleHandlers.device_added(driver, device, ...) if device_type ~= HueDeviceTypes.BRIDGE then ---@cast device HueChildDevice local resource_id = utils.get_hue_rid(device) + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} if resource_id then - driver.hue_identifier_to_device_record[resource_id] = device + hue_id_to_device[resource_id] = device end local resource_state_known = (Discovery.device_state_disco_cache[resource_id] ~= nil) diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua index 00258e5722..edbfb02944 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua @@ -190,7 +190,8 @@ function LightLifecycleHandlers.added(driver, device, parent_device_id, resource device:set_field(Fields._ADDED, true, { persist = true }) device:set_field(Fields._REFRESH_AFTER_INIT, true, { persist = true }) - driver.hue_identifier_to_device_record[device_light_resource_id] = device + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} + hue_id_to_device[device_light_resource_id] = device -- the refresh handler adds lights that don't have a fully initialized bridge to a queue. driver:inject_capability_command(device, { @@ -218,8 +219,9 @@ function LightLifecycleHandlers.init(driver, device) device.device_network_id local hue_device_id = device:get_field(Fields.HUE_DEVICE_ID) - if not driver.hue_identifier_to_device_record[device_light_resource_id] then - driver.hue_identifier_to_device_record[device_light_resource_id] = device + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} + if not hue_id_to_device[device_light_resource_id] then + hue_id_to_device[device_light_resource_id] = device end local svc_rids_for_device = driver.services_for_device_rid[hue_device_id] or {} if not svc_rids_for_device[device_light_resource_id] then diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/motion.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/motion.lua index ceb301dcd4..90b070ff3b 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/motion.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/motion.lua @@ -50,10 +50,11 @@ function MotionLifecycleHandlers.added(driver, device, parent_device_id, resourc }) return end + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} - driver.hue_identifier_to_device_record[sensor_info.power_id] = device - driver.hue_identifier_to_device_record[sensor_info.temperature_id] = device - driver.hue_identifier_to_device_record[sensor_info.light_level_id] = device + hue_id_to_device[sensor_info.power_id] = device + hue_id_to_device[sensor_info.temperature_id] = device + hue_id_to_device[sensor_info.light_level_id] = device device:set_field(Fields.DEVICE_TYPE, HueDeviceTypes.MOTION, { persist = true }) device:set_field(Fields.HUE_DEVICE_ID, sensor_info.hue_device_id, { persist = true }) @@ -62,7 +63,7 @@ function MotionLifecycleHandlers.added(driver, device, parent_device_id, resourc device:set_field(Fields._ADDED, true, { persist = true }) device:set_field(Fields._REFRESH_AFTER_INIT, true, { persist = true }) - driver.hue_identifier_to_device_record[device_sensor_resource_id] = device + hue_id_to_device[device_sensor_resource_id] = device end ---@param driver HueDriver @@ -78,8 +79,9 @@ function MotionLifecycleHandlers.init(driver, device) log.debug("resource id " .. tostring(device_sensor_resource_id)) local hue_device_id = device:get_field(Fields.HUE_DEVICE_ID) - if not driver.hue_identifier_to_device_record[device_sensor_resource_id] then - driver.hue_identifier_to_device_record[device_sensor_resource_id] = device + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} + if not hue_id_to_device[device_sensor_resource_id] then + hue_id_to_device[device_sensor_resource_id] = device end local sensor_info, err sensor_info = Discovery.device_state_disco_cache[device_sensor_resource_id] diff --git a/drivers/SmartThings/philips-hue/src/hue/api.lua b/drivers/SmartThings/philips-hue/src/hue/api.lua index dbcc5c4c19..e05fa3d0a0 100644 --- a/drivers/SmartThings/philips-hue/src/hue/api.lua +++ b/drivers/SmartThings/philips-hue/src/hue/api.lua @@ -132,7 +132,7 @@ function PhilipsHueApi.new_bridge_manager(base_url, api_key, socket_builder) true )) local control_tx, control_rx = channel.new() - control_rx:settimeout(30) + control_rx:settimeout(45) local self = setmetatable( { headers = { [APPLICATION_KEY_HEADER] = api_key or "" }, @@ -217,7 +217,7 @@ end ---@return ... local function do_get(instance, path) local reply_tx, reply_rx = channel.new() - reply_rx:settimeout(10) + reply_rx:settimeout(45) local msg = ControlMessageBuilders.Get(path, reply_tx); try_send(instance, msg) local recv, err = reply_rx:receive() @@ -236,7 +236,7 @@ end ---@return ... local function do_put(instance, path, payload) local reply_tx, reply_rx = channel.new() - reply_rx:settimeout(10) + reply_rx:settimeout(45) local msg = ControlMessageBuilders.Put(path, payload, reply_tx); try_send(instance, msg) local recv, err = reply_rx:receive() @@ -255,7 +255,7 @@ end ---@return ... function PhilipsHueApi.get_bridge_info(bridge_ip, socket_builder) local tx, rx = channel.new() - rx:settimeout(10) + rx:settimeout(45) cosock.spawn( function() tx:send(table.pack(process_rest_response(RestClient.one_shot_get("https://" .. bridge_ip .. "/api/config", nil, @@ -278,7 +278,7 @@ end ---@return ... function PhilipsHueApi.request_api_key(bridge_ip, socket_builder) local tx, rx = channel.new() - rx:settimeout(10) + rx:settimeout(45) cosock.spawn( function() local body = json.encode { devicetype = "smartthings_edge_driver#" .. bridge_ip, generateclientkey = true } diff --git a/drivers/SmartThings/philips-hue/src/hue_driver_template.lua b/drivers/SmartThings/philips-hue/src/hue_driver_template.lua index 1cda7fb921..3ffd02b3b6 100644 --- a/drivers/SmartThings/philips-hue/src/hue_driver_template.lua +++ b/drivers/SmartThings/philips-hue/src/hue_driver_template.lua @@ -70,7 +70,7 @@ local set_color_temp_handler = utils.safe_wrap_handler(command_handlers.set_colo --- @class HueDriver:Driver --- @field public ignored_bridges table --- @field public joined_bridges table ---- @field public hue_identifier_to_device_record table +--- @field public hue_identifier_to_device_record_by_bridge table> --- @field public services_for_device_rid table> Map the device resource ID to another map that goes from service rid to service rtype --- @field public waiting_grandchildren table? --- @field public stray_device_tx table cosock channel @@ -113,7 +113,7 @@ function HueDriver.new_driver_template(dbg_config) ignored_bridges = {}, joined_bridges = {}, - hue_identifier_to_device_record = {}, + hue_identifier_to_device_record_by_bridge = {}, services_for_device_rid = {}, -- the only real way we have to know which bridge a bulb wants to use at migration time -- is by looking at the stored api key so we will make a map to look up bridge IDs with diff --git a/drivers/SmartThings/philips-hue/src/utils/grouped_utils.lua b/drivers/SmartThings/philips-hue/src/utils/grouped_utils.lua index d1f1cd6753..f169381eed 100644 --- a/drivers/SmartThings/philips-hue/src/utils/grouped_utils.lua +++ b/drivers/SmartThings/philips-hue/src/utils/grouped_utils.lua @@ -1,6 +1,7 @@ local log = require "log" local Fields = require "fields" local cosock = require "cosock" +local utils = require "utils" --- Room or zone with the children translated from their hue device id or light resource id to --- their SmartThings represented device object. The grouped light resource id is also moved into @@ -100,10 +101,11 @@ end ---@param group_kind string room or zone ---@param driver HueDriver ---@param hue_id_to_device table +---@param light_id_to_device table ---@param resp table? ---@param err any? ---@return HueLightGroup[]? -local function handle_group_scan_response(group_kind, driver, hue_id_to_device, resp, err) +local function handle_group_scan_response(group_kind, driver, hue_id_to_device, light_id_to_device, resp, err) if err or not resp then log.error(string.format("Failed to scan for %s: %s", group_kind, err or "unknown error")) return nil @@ -121,7 +123,7 @@ local function handle_group_scan_response(group_kind, driver, hue_id_to_device, log.info(string.format("Successfully got %d %s", #resp.data, group_kind)) for _, group in ipairs(resp.data) do - build_hue_light_group(group, hue_id_to_device, driver.hue_identifier_to_device_record) + build_hue_light_group(group, hue_id_to_device, light_id_to_device) log.info(string.format("Found light group %s with %d device records", group.id, #group.devices)) end @@ -134,12 +136,14 @@ end --- @param hue_id_to_device table function grouped_utils.scan_groups(driver, bridge_device, api, hue_id_to_device) local rooms, zones - while not (rooms and zones) do -- TODO: Should this be one and done? Timeout? + -- These are the hue light/other service ids rather than the hue device ids + local light_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, bridge_device) or {} + while not (rooms and zones) do if not rooms then - rooms = handle_group_scan_response("rooms", driver, hue_id_to_device, api:get_rooms()) + rooms = handle_group_scan_response("rooms", driver, hue_id_to_device, light_id_to_device, api:get_rooms()) end if not zones then - zones = handle_group_scan_response("zones", driver, hue_id_to_device, api:get_zones()) + zones = handle_group_scan_response("zones", driver, hue_id_to_device, light_id_to_device, api:get_zones()) end end -- Combine rooms and zones. @@ -204,7 +208,10 @@ function grouped_utils.group_update(driver, bridge_device, to_update) if to_update.children then local devices = build_group_device_table( - to_update, build_hue_id_to_device_map(bridge_device), driver.hue_identifier_to_device_record + to_update, + build_hue_id_to_device_map(bridge_device), + -- These are the hue light/other service ids rather than the hue device ids + utils.get_hue_id_to_device_table_by_bridge(driver, bridge_device) or {} ) -- check if number of children has changed and if we need to move it update_index = #devices ~= #group.devices @@ -241,7 +248,10 @@ function grouped_utils.group_add(driver, bridge_device, to_add) end local hue_light_group = build_hue_light_group( - to_add, build_hue_id_to_device_map(bridge_device), driver.hue_identifier_to_device_record + to_add, + build_hue_id_to_device_map(bridge_device), + -- These are the hue light/other service ids rather than the hue device ids + utils.get_hue_id_to_device_table_by_bridge(driver, bridge_device) or {} ) insert(groups, hue_light_group) log.info(string.format("Adding group %s, %d devices", @@ -277,7 +287,7 @@ function grouped_utils.queue_group_scan(driver, bridge_device) if queue == nil then local tx, rx = cosock.channel.new() -- Set timeout to 30 seconds to allow for other queued scans to come in. - rx:settimeout(30) + rx:settimeout(45) cosock.spawn(function() while true do -- The goal here is to timeout on the receive. If we receive a message then another request diff --git a/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua b/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua index 53d2cd2698..05905b80ff 100644 --- a/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua +++ b/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua @@ -40,6 +40,7 @@ function hue_bridge_utils.do_bridge_network_init(driver, bridge_device, bridge_u { [HueApi.APPLICATION_KEY_HEADER] = api_key }, nil ) + local hue_identifier_to_device_record = utils.get_hue_id_to_device_table_by_bridge(driver, bridge_device) or {} eventsource.onopen = function(msg) log.info_with({ hub_logs = true }, @@ -171,7 +172,7 @@ function hue_bridge_utils.do_bridge_network_init(driver, bridge_device, bridge_u local resource_ids = {} if update_data.type == "zigbee_connectivity" and update_data.owner ~= nil then for rid, rtype in pairs(driver.services_for_device_rid[update_data.owner.rid] or {}) do - if driver.hue_identifier_to_device_record[rid] then + if hue_identifier_to_device_record[rid] then log.debug(string.format("Adding RID %s to resource_ids", rid)) table.insert(resource_ids, rid) end @@ -186,7 +187,7 @@ function hue_bridge_utils.do_bridge_network_init(driver, bridge_device, bridge_u end for _, resource_id in ipairs(resource_ids) do log.debug(string.format("Looking for device record for %s", resource_id)) - local child_device = driver.hue_identifier_to_device_record[resource_id] + local child_device = hue_identifier_to_device_record[resource_id] if child_device ~= nil and child_device.id ~= nil then if update_data.type == "zigbee_connectivity" then log.debug("emitting event for zigbee connectivity") @@ -203,7 +204,7 @@ function hue_bridge_utils.do_bridge_network_init(driver, bridge_device, bridge_u for _, delete_data in ipairs(event.data) do if HueDeviceTypes.can_join_device_for_service(delete_data.type) then local resource_id = delete_data.id - local child_device = driver.hue_identifier_to_device_record[resource_id] + local child_device = hue_identifier_to_device_record[resource_id] if child_device ~= nil and child_device.id ~= nil then log.info( string.format( diff --git a/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/sensor.lua b/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/sensor.lua index a8df7520d6..e9212b2433 100644 --- a/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/sensor.lua +++ b/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/sensor.lua @@ -1,3 +1,5 @@ +local utils = require "utils" + local SensorMultiServiceHelper = {} function SensorMultiServiceHelper.update_multi_service_device_maps(driver, device, hue_device_id, sensor_info) local svc_rids_for_device = driver.services_for_device_rid[hue_device_id] or {} @@ -15,8 +17,9 @@ function SensorMultiServiceHelper.update_multi_service_device_maps(driver, devic end driver.services_for_device_rid[hue_device_id] = svc_rids_for_device + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) for rid, _ in pairs(driver.services_for_device_rid[hue_device_id]) do - driver.hue_identifier_to_device_record[rid] = device + hue_id_to_device[rid] = device end end diff --git a/drivers/SmartThings/philips-hue/src/utils/init.lua b/drivers/SmartThings/philips-hue/src/utils/init.lua index 0819286ab5..3f65c9a4d5 100644 --- a/drivers/SmartThings/philips-hue/src/utils/init.lua +++ b/drivers/SmartThings/philips-hue/src/utils/init.lua @@ -345,6 +345,7 @@ end ---@param driver HueDriver ---@param device HueDevice ---@param parent_device_id string? +---@param quiet boolean? ---@return HueBridgeDevice? bridge_device function utils.get_hue_bridge_for_device(driver, device, parent_device_id, quiet) local _ = quiet or @@ -371,6 +372,27 @@ function utils.get_hue_bridge_for_device(driver, device, parent_device_id, quiet return utils.get_hue_bridge_for_device(driver, parent_device, nil, quiet) end +--- Get the mapping of hue id to device table by associated bridge. The mapping is separated by bridge to account +--- for devices migrated to a new hue bridge. +---@param driver HueDriver +---@param bridge_or_device HueDevice +---@return table? hue_id_to_device +function utils.get_hue_id_to_device_table_by_bridge(driver, bridge_or_device) + -- If bridge_or_device is a bridge this will just return itself + local bridge = utils.get_hue_bridge_for_device(driver, bridge_or_device) + if not bridge then + log.warn(string.format("Failed to lookup bridge for device %s", bridge_or_device.label)) + return nil + end + local bridge_id = bridge:get_field(Fields.BRIDGE_ID) + if not bridge_id then + log.warn(string.format("Failed to get bridge id for %s", bridge.label)) + return nil + end + driver.hue_identifier_to_device_record_by_bridge[bridge_id] = driver.hue_identifier_to_device_record_by_bridge[bridge_id] or {} + return driver.hue_identifier_to_device_record_by_bridge[bridge_id] +end + --- build a exponential backoff time value generator --- ---@param max number the maximum wait interval (not including `rand factor`) diff --git a/drivers/SmartThings/sonos/src/api/cmd_handlers.lua b/drivers/SmartThings/sonos/src/api/cmd_handlers.lua index c2de236044..b5ccfe10b9 100644 --- a/drivers/SmartThings/sonos/src/api/cmd_handlers.lua +++ b/drivers/SmartThings/sonos/src/api/cmd_handlers.lua @@ -24,9 +24,9 @@ local function _do_send_to_group(driver, device, payload) local household_id, group_id = driver.sonos:get_group_for_device(device) payload[1].householdId = household_id payload[1].groupId = group_id - local maybe_token, err = driver:get_oauth_token() + local maybe_token, err = driver:wait_for_oauth_token(30) if err then - log.warn(string.format("notice: get_oauth_token -> %s", err)) + log.warn(string.format("notice: wait_for_oauth_token -> %s", err)) end if maybe_token then @@ -40,9 +40,9 @@ local function _do_send_to_self(driver, device, payload) local household_id, player_id = driver.sonos:get_player_for_device(device) payload[1].householdId = household_id payload[1].playerId = player_id - local maybe_token, err = driver:get_oauth_token() + local maybe_token, err = driver:wait_for_oauth_token(30) if err then - log.warn(string.format("notice: get_oauth_token -> %s", err)) + log.warn(string.format("notice: wait_for_oauth_token -> %s", err)) end if maybe_token then diff --git a/drivers/SmartThings/sonos/src/api/event_handlers.lua b/drivers/SmartThings/sonos/src/api/event_handlers.lua index a4fe85766e..eeb461f7c9 100644 --- a/drivers/SmartThings/sonos/src/api/event_handlers.lua +++ b/drivers/SmartThings/sonos/src/api/event_handlers.lua @@ -1,8 +1,12 @@ local capabilities = require "st.capabilities" +local swGenCapability = capabilities["stus.softwareGeneration"] + local log = require "log" local st_utils = require "st.utils" +local PlayerFields = require "fields".SonosPlayerFields + local CapEventHandlers = {} CapEventHandlers.PlaybackStatus = { @@ -12,45 +16,56 @@ CapEventHandlers.PlaybackStatus = { Playing = "PLAYBACK_STATE_PLAYING", } +local function _do_emit(device, attribute_event) + local bonded = device:get_field(PlayerFields.BONDED) + if not bonded then + device:emit_event(attribute_event) + end +end + +function CapEventHandlers.handle_sw_gen(device, sw_gen) + _do_emit(device, swGenCapability.generation(string.format("%s", sw_gen))) +end + function CapEventHandlers.handle_player_volume(device, new_volume, is_muted) - device:emit_event(capabilities.audioVolume.volume(new_volume)) + _do_emit(device, capabilities.audioVolume.volume(new_volume)) if is_muted then - device:emit_event(capabilities.audioMute.mute.muted()) + _do_emit(device, capabilities.audioMute.mute.muted()) else - device:emit_event(capabilities.audioMute.mute.unmuted()) + _do_emit(device, capabilities.audioMute.mute.unmuted()) end end function CapEventHandlers.handle_group_volume(device, new_volume, is_muted) - device:emit_event(capabilities.mediaGroup.groupVolume(new_volume)) + _do_emit(device, capabilities.mediaGroup.groupVolume(new_volume)) if is_muted then - device:emit_event(capabilities.mediaGroup.groupMute.muted()) + _do_emit(device, capabilities.mediaGroup.groupMute.muted()) else - device:emit_event(capabilities.mediaGroup.groupMute.unmuted()) + _do_emit(device, capabilities.mediaGroup.groupMute.unmuted()) end end function CapEventHandlers.handle_group_role_update(device, group_role) - device:emit_event(capabilities.mediaGroup.groupRole(group_role)) + _do_emit(device, capabilities.mediaGroup.groupRole(group_role)) end function CapEventHandlers.handle_group_coordinator_update(device, coordinator_id) - device:emit_event(capabilities.mediaGroup.groupPrimaryDeviceId(coordinator_id)) + _do_emit(device, capabilities.mediaGroup.groupPrimaryDeviceId(coordinator_id)) end function CapEventHandlers.handle_group_id_update(device, group_id) - device:emit_event(capabilities.mediaGroup.groupId(group_id)) + _do_emit(device, capabilities.mediaGroup.groupId(group_id)) end function CapEventHandlers.handle_group_update(device, group_info) local groupRole, groupPrimaryDeviceId, groupId = table.unpack(group_info) - device:emit_event(capabilities.mediaGroup.groupRole(groupRole)) - device:emit_event(capabilities.mediaGroup.groupPrimaryDeviceId(groupPrimaryDeviceId)) - device:emit_event(capabilities.mediaGroup.groupId(groupId)) + _do_emit(device, capabilities.mediaGroup.groupRole(groupRole)) + _do_emit(device, capabilities.mediaGroup.groupPrimaryDeviceId(groupPrimaryDeviceId)) + _do_emit(device, capabilities.mediaGroup.groupId(groupId)) end function CapEventHandlers.handle_audio_clip_status(device, clips) - for _, clip in ipairs(clips) do + for _, clip in ipairs(clips or {}) do if clip.status == "ACTIVE" then log.debug(st_utils.stringify_table(clip, "Playing Audio Clip: ", false)) elseif clip.status == "DONE" then @@ -61,11 +76,11 @@ end function CapEventHandlers.handle_playback_status(device, playback_state) if playback_state == CapEventHandlers.PlaybackStatus.Playing then - device:emit_event(capabilities.mediaPlayback.playbackStatus.playing()) + _do_emit(device, capabilities.mediaPlayback.playbackStatus.playing()) elseif playback_state == CapEventHandlers.PlaybackStatus.Idle then - device:emit_event(capabilities.mediaPlayback.playbackStatus.stopped()) + _do_emit(device, capabilities.mediaPlayback.playbackStatus.stopped()) elseif playback_state == CapEventHandlers.PlaybackStatus.Paused then - device:emit_event(capabilities.mediaPlayback.playbackStatus.paused()) + _do_emit(device, capabilities.mediaPlayback.playbackStatus.paused()) elseif playback_state == CapEventHandlers.PlaybackStatus.Buffering then -- TODO the DTH doesn't currently do anything w/ buffering; -- might be worth figuring out what to do with this in the future. @@ -74,7 +89,7 @@ function CapEventHandlers.handle_playback_status(device, playback_state) end function CapEventHandlers.update_favorites(device, new_favorites) - device:emit_event(capabilities.mediaPresets.presets(new_favorites)) + _do_emit(device, capabilities.mediaPresets.presets(new_favorites)) end function CapEventHandlers.handle_playback_metadata_update(device, metadata_status_body) @@ -128,7 +143,7 @@ function CapEventHandlers.handle_playback_metadata_update(device, metadata_statu end if type(audio_track_data.title) == "string" then - device:emit_event(capabilities.audioTrackData.audioTrackData(audio_track_data)) + _do_emit(device, capabilities.audioTrackData.audioTrackData(audio_track_data)) end end diff --git a/drivers/SmartThings/sonos/src/api/rest.lua b/drivers/SmartThings/sonos/src/api/rest.lua index bc2dfe498f..93fbcf0f36 100644 --- a/drivers/SmartThings/sonos/src/api/rest.lua +++ b/drivers/SmartThings/sonos/src/api/rest.lua @@ -74,7 +74,7 @@ local SonosRestApi = {} --- Query a Sonos Group IP address for individual player info ---@param url table a URL table created by `net_url` ---@param headers table? ----@return SonosDiscoveryInfo|SonosErrorResponse|nil +---@return SonosDiscoveryInfoObject|SonosErrorResponse|nil ---@return string|nil error function SonosRestApi.get_player_info(url, headers) url.path = "/api/v1/players/local/info" diff --git a/drivers/SmartThings/sonos/src/api/sonos_connection.lua b/drivers/SmartThings/sonos/src/api/sonos_connection.lua index f1773e24aa..471c3a04fb 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_connection.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_connection.lua @@ -55,7 +55,7 @@ local _update_subscriptions_helper = function( command, reply_tx ) - for _, namespace in ipairs(namespaces) do + for _, namespace in ipairs(namespaces or {}) do local wss_msg_header = { namespace = namespace, command = command, @@ -166,8 +166,12 @@ local function _open_coordinator_socket(sonos_conn, household_id, self_player_id return end - _, err = - Router.open_socket_for_player(household_id, coordinator_id, coordinator.websocketUrl, api_key) + _, err = Router.open_socket_for_player( + household_id, + coordinator_id, + coordinator.player.websocket_url, + api_key + ) if err ~= nil then log.error( string.format( @@ -242,11 +246,7 @@ end ---@param sonos_conn SonosConnection local function _oauth_reconnect_task(sonos_conn) log.debug("Spawning reconnect task for ", sonos_conn.device.label) - local check_auth = sonos_conn.driver:check_auth(sonos_conn.device) - local unauthorized = (check_auth == false) - if unauthorized and not sonos_conn.driver:is_waiting_for_oauth_token() then - sonos_conn.driver:request_oauth_token() - end + -- Subscribe first so we get all updates on the bus local token_receive_handle, err = sonos_conn.driver:oauth_token_event_subscribe() if not token_receive_handle then log.warn(string.format("error creating oauth token receive handle for respawn task: %s", err)) @@ -254,8 +254,14 @@ local function _oauth_reconnect_task(sonos_conn) cosock.spawn(function() local backoff = backoff_builder(60, 1, 0.1) while not sonos_conn:is_running() do - if sonos_conn.driver:is_waiting_for_oauth_token() and token_receive_handle then - local token, channel_error = token_receive_handle:receive() + local check_auth = sonos_conn.driver:check_auth(sonos_conn.device) + local unauthorized = (check_auth == false) + + if unauthorized then + sonos_conn.driver:alert_unauthorized() + local token, channel_error = + (token_receive_handle and token_receive_handle:receive()) or nil, + "no token receive handle" if not token then log.warn(string.format("Error requesting token: %s", channel_error)) local _, get_token_err = sonos_conn.driver:get_oauth_token() @@ -271,12 +277,6 @@ local function _oauth_reconnect_task(sonos_conn) end end - check_auth = sonos_conn.driver:check_auth(sonos_conn.device) - unauthorized = (check_auth == false) - - if unauthorized and not sonos_conn.driver:is_waiting_for_oauth_token() then - sonos_conn.driver:request_oauth_token() - end cosock.socket.sleep(backoff()) end sonos_conn._reconnecting = false @@ -302,10 +302,13 @@ end --- @return SonosConnection function SonosConnection.new(driver, device) log.debug(string.format("Creating new SonosConnection for %s", device.label)) - local self = setmetatable( - { driver = driver, device = device, _listener_uuids = {}, _initialized = false, _reconnecting = false }, - SonosConnection - ) + local self = setmetatable({ + driver = driver, + device = device, + _listener_uuids = {}, + _initialized = false, + _reconnecting = false, + }, SonosConnection) -- capture the label here in case something goes wonky like a callback being fired after a -- device is removed @@ -339,10 +342,6 @@ function SonosConnection.new(driver, device) device.log.warn( string.format("WebSocket connection no longer authorized, disconnecting") ) - local _, security_err = driver:request_oauth_token() - if security_err then - log.warn(string.format("Error during request for oauth token: %s", security_err)) - end -- closing the socket directly without calling `:stop()` triggers the reconnect loop, -- which is where we wait for the token to come in. local unique_key, bad_key_part = utils.sonos_unique_key(household_id, player_id) @@ -351,6 +350,7 @@ function SonosConnection.new(driver, device) else Router.close_socket_for_player(unique_key) end + self.driver:alert_unauthorized() end end elseif header.type == "groups" then @@ -358,19 +358,28 @@ function SonosConnection.new(driver, device) local household_id, current_coordinator = self.driver.sonos:get_coordinator_for_device(self.device) local _, player_id = self.driver.sonos:get_player_for_device(self.device) - self.driver.sonos:update_household_info(header.householdId, body, self.device) + self.driver.sonos:update_household_info(header.householdId, body, self.driver) self.driver.sonos:update_device_record_from_state(header.householdId, self.device) local _, updated_coordinator = self.driver.sonos:get_coordinator_for_device(self.device) + local bonded = self.device:get_field(PlayerFields.BONDED) + if bonded then + self:stop() + end + Router.cleanup_unused_sockets(self.driver) - if not self:coordinator_running() then - --TODO this is not infallible - _open_coordinator_socket(self, household_id, player_id) - end + if not bonded then + if not self:coordinator_running() then + --TODO this is not infallible + _open_coordinator_socket(self, household_id, player_id) + end - if current_coordinator ~= updated_coordinator then - self:refresh_subscriptions() + if current_coordinator ~= updated_coordinator then + self:refresh_subscriptions() + end + else + self.device:offline() end elseif header.type == "playerVolume" then log.trace(string.format("PlayerVolume type message for %s", device_name)) @@ -398,8 +407,8 @@ function SonosConnection.new(driver, device) ) return end - local group = household.groups[header.groupId] or { playerIds = {} } - for _, player_id in ipairs(group.playerIds) do + local group = household.groups[header.groupId] or { player_ids = {} } + for _, player_id in ipairs(group.player_ids) do local device_for_player = self.driver:device_for_player(header.householdId, player_id) --- we've seen situations where these messages can be processed while a device --- is being deleted so we check for the presence of emit event as a proxy for @@ -422,8 +431,8 @@ function SonosConnection.new(driver, device) ) return end - local group = household.groups[header.groupId] or { playerIds = {} } - for _, player_id in ipairs(group.playerIds) do + local group = household.groups[header.groupId] or { player_ids = {} } + for _, player_id in ipairs(group.player_ids) do local device_for_player = self.driver:device_for_player(header.householdId, player_id) --- we've seen situations where these messages can be processed while a device --- is being deleted so we check for the presence of emit event as a proxy for @@ -445,8 +454,8 @@ function SonosConnection.new(driver, device) ) return end - local group = household.groups[header.groupId] or { playerIds = {} } - for _, player_id in ipairs(group.playerIds) do + local group = household.groups[header.groupId] or { player_ids = {} } + for _, player_id in ipairs(group.player_ids) do local device_for_player = self.driver:device_for_player(header.householdId, player_id) --- we've seen situations where these messages can be processed while a device --- is being deleted so we check for the presence of emit event as a proxy for @@ -460,9 +469,10 @@ function SonosConnection.new(driver, device) if body.version ~= favorites_version then favorites_version = body.version - local household = self.driver.sonos:get_household(header.householdId) or { groups = {} } + local household = self.driver.sonos:get_household(header.householdId) + or self.driver.sonos.EMPTY_HOUSEHOLD - for group_id, group in pairs(household.groups) do + for group_id, group in pairs(household.groups or {}) do local coordinator_id = self.driver.sonos:get_coordinator_for_group(header.householdId, group_id) local coordinator_player = household.players[coordinator_id] @@ -477,7 +487,7 @@ function SonosConnection.new(driver, device) return end - local url_ip = lb_utils.force_url_table(coordinator_player.websocketUrl).host + local url_ip = lb_utils.force_url_table(coordinator_player.player.websocket_url).host local base_url = lb_utils.force_url_table( string.format("https://%s:%s", url_ip, SonosApi.DEFAULT_SONOS_PORT) ) @@ -503,7 +513,7 @@ function SonosConnection.new(driver, device) end self.driver.sonos:update_household_favorites(header.householdId, new_favorites) - for _, player_id in ipairs(group.playerIds) do + for _, player_id in ipairs(group.player_ids) do local device_for_player = self.driver:device_for_player(header.householdId, player_id) --- we've seen situations where these messages can be processed while a device @@ -590,7 +600,7 @@ function SonosConnection:coordinator_running() ) ) end - return type(unique_key) == "string" and Router.is_connected(unique_key) and self._initialized + return type(unique_key) == "string" and Router.is_connected(unique_key) end function SonosConnection:refresh_subscriptions(maybe_reply_tx) diff --git a/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua b/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua index 88e0cddbd9..ddc1d0842d 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua @@ -9,7 +9,140 @@ local utils = require "utils" local SonosApi = require "api" local SSDP_SCAN_INTERVAL_SECONDS = 600 +local SONOS_DEFAULT_PORT = 1443 +local SONOS_DEFAULT_WSS_PATH = "websocket/api" +local SONOS_DEFAULT_REST_PATH = "api/v1" + +--- Cached information gathered during discovery scanning, created from a subset of the +--- found [SonosSSDPInfo](lua://SonosSSDPInfo) and [SonosDiscoveryInfoObject](lua://SonosDiscoveryInfoObject) +--- +--- @class SpeakerDiscoveryInfo +--- @field public unique_key UniqueKey +--- @field public mac_addr string +--- @field public expires_at integer +--- @field public ipv4 string +--- @field public port integer +--- @field public household_id HouseholdId +--- @field public player_id PlayerId +--- @field public name string +--- @field public model string +--- @field public model_display_name string +--- @field public sw_gen integer +--- @field public wss_url table +--- @field public rest_url table +--- @field public is_group_coordinator boolean +--- @field public group_name string? nil if a speaker is the non-primary in a bonded set +--- @field public group_id GroupId? nil if a speaker is the non-primary in a bonded set +--- @field package wss_path string? nil if equivalent to the default value; does not include leading slash! +--- @field package rest_path string? nil if equivalent to the default value; does not include leading slash! +local SpeakerDiscoveryInfo = {} + +local proxy_index = function(self, k) + if k == "rest_url" and not rawget(self, "rest_url") then + rawset( + self, + "rest_url", + net_url.parse( + string.format( + "https://%s:%s/%s", + self.ipv4, + self.port, + self.rest_path or SONOS_DEFAULT_REST_PATH + ) + ) + ) + end + + if k == "wss_url" and not rawget(self, "wss_url") then + rawset( + self, + "wss_url", + net_url.parse( + string.format( + "https://%s:%s/%s", + self.ipv4, + self.port, + self.wss_path or SONOS_DEFAULT_WSS_PATH + ) + ) + ) + end + + return rawget(self, k) +end + +local proxy_newindex = function(_, _, _) + error("attempt to index a read-only table", 2) +end + +---@param ssdp_info SonosSSDPInfo +---@param discovery_info SonosDiscoveryInfoObject +---@return SpeakerDiscoveryInfo info +function SpeakerDiscoveryInfo.new(ssdp_info, discovery_info) + local mac_addr = utils.extract_mac_addr(discovery_info.device) + local port, rest_path = string.match(discovery_info.restUrl, "^.*:(%d*)/(.*)$") + local _, wss_path = string.match(discovery_info.websocketUrl, "^.*:(%d*)/(.*)$") + port = tonumber(port) or SONOS_DEFAULT_PORT + + local ret = { + unique_key = utils.sonos_unique_key_from_ssdp(ssdp_info), + expires_at = ssdp_info.expires_at, + ipv4 = ssdp_info.ip, + port = port, + mac_addr = mac_addr, + household_id = ssdp_info.household_id, + player_id = ssdp_info.player_id, + name = discovery_info.device.name, + model = discovery_info.device.model, + model_display_name = discovery_info.device.modelDisplayName, + sw_gen = discovery_info.device.swGen, + is_group_coordinator = ssdp_info.is_group_coordinator, + } + + if type(ssdp_info.group_name) == "string" and #ssdp_info.group_name > 0 then + ret.group_name = ssdp_info.group_name + end + + if type(ssdp_info.group_id) == "string" and #ssdp_info.group_id > 0 then + ret.group_id = ssdp_info.group_id + end + + if type(wss_path) == "string" and #wss_path > 0 and wss_path ~= SONOS_DEFAULT_WSS_PATH then + ret.wss_path = wss_path + end + + if type(rest_path) == "string" and #rest_path > 0 and rest_path ~= SONOS_DEFAULT_REST_PATH then + ret.rest_path = rest_path + end + + for k, v in pairs(SpeakerDiscoveryInfo or {}) do + rawset(ret, k, v) + end + + return setmetatable(ret, { __index = proxy_index, __newindex = proxy_newindex }) +end + +function SpeakerDiscoveryInfo:is_bonded() + return (self.group_id == nil) +end + +---@return SonosSSDPInfo +function SpeakerDiscoveryInfo:as_ssdp_info() + ---@type SonosSSDPInfo + return { + ip = self.ipv4, + group_id = self.group_id or "", + group_name = self.group_name or "", + expires_at = self.expires_at, + player_id = self.player_id, + wss_url = self.wss_url:build(), + household_id = self.household_id, + is_group_coordinator = self.is_group_coordinator, + } +end + local sonos_ssdp = {} +sonos_ssdp.SpeakerDiscoveryInfo = SpeakerDiscoveryInfo ---@module 'luncheon.headers' @@ -94,7 +227,7 @@ local function make_persistent_task_impl( log.warn(string.format("Select error: %s", select_err)) end - for _, receiver in ipairs(recv_ready) do + for _, receiver in ipairs(recv_ready or {}) do if receiver == interval_timer then interval_timer:handled() ssdp_search_handle:multicast_m_search() @@ -147,6 +280,18 @@ function sonos_ssdp.ssdp_info_eq(a, b) and (a.wss_url == b.wss_url) end +---@param disco_info SpeakerDiscoveryInfo +---@param ssdp_info SonosSSDPInfo +function sonos_ssdp.known_speaker_matches_ssdp_info(disco_info, ssdp_info) + return (disco_info.group_id == ssdp_info.group_id) + and (disco_info.group_name == ssdp_info.group_name) + and (disco_info.household_id == ssdp_info.household_id) + and (disco_info.ipv4 == ssdp_info.ip) + and (disco_info.is_group_coordinator == ssdp_info.is_group_coordinator) + and (disco_info.player_id == ssdp_info.player_id) + and (disco_info.wss_url:build() == ssdp_info.wss_url) +end + ---@return SsdpSearchTerm the Sonos ssdp search term ---@return SsdpSearchKwargs the default set of keyword arguments for Sonos function sonos_ssdp.new_search_term_context() @@ -160,8 +305,8 @@ end ---@class SonosPersistentSsdpTask ---@field package ssdp_search_handle SsdpSearchHandle ----@field package player_info_by_sonos_ids table ----@field package player_info_by_mac_addrs table +---@field package player_info_by_sonos_ids table +---@field package player_info_by_mac_addrs table ---@field package waiting_for_unique_key table ---@field package waiting_for_mac_addr table ---@field package control_tx table @@ -186,7 +331,7 @@ function SonosPersistentSsdpTask:get_all_known() -- make a shallow copy of the table so it doesn't get clobbered -- the player info itself is a read-only proxy table as well local known = {} - for id, info in pairs(self.player_info_by_sonos_ids) do + for id, info in pairs(self.player_info_by_sonos_ids or {}) do known[id] = info end return known @@ -217,7 +362,7 @@ function SonosPersistentSsdpTask:get_player_info(reply_tx, ...) end local maybe_existing = lookup_table[lookup_key] - if maybe_existing and maybe_existing.ssdp_info.expires_at > os.time() then + if maybe_existing and maybe_existing.expires_at > os.time() then reply_tx:send(maybe_existing) return end @@ -267,11 +412,12 @@ function sonos_ssdp.spawn_persistent_ssdp_task() local maybe_known = task_handle.player_info_by_sonos_ids[unique_key] local is_new_information = not ( maybe_known - and maybe_known.ssdp_info.expires_at > os.time() - and sonos_ssdp.ssdp_info_eq(maybe_known.ssdp_info, sonos_ssdp_info) + and maybe_known.expires_at > os.time() + and sonos_ssdp.known_speaker_matches_ssdp_info(maybe_known, sonos_ssdp_info) ) - local info_to_send + local speaker_info + local event_bus_msg if is_new_information then local headers = SonosApi.make_headers() @@ -283,30 +429,21 @@ function sonos_ssdp.spawn_persistent_ssdp_task() ) if not discovery_info then log.error(string.format("Error getting discovery info from SSDP response: %s", err)) + elseif discovery_info._objectType == "globalError" then + log.error(string.format("Error message in discovery info: %s", discovery_info.errorCode)) else - local unified_info = - { ssdp_info = sonos_ssdp_info, discovery_info = discovery_info, force_refresh = true } - task_handle.player_info_by_sonos_ids[unique_key] = unified_info - info_to_send = unified_info + speaker_info = SpeakerDiscoveryInfo.new(sonos_ssdp_info, discovery_info) + task_handle.player_info_by_sonos_ids[unique_key] = speaker_info + event_bus_msg = { speaker_info = speaker_info, force_refresh = true } end else - info_to_send = { - ssdp_info = maybe_known.ssdp_info, - discovery_info = maybe_known.discovery_info, - force_refresh = false, - } + speaker_info = maybe_known + event_bus_msg = { speaker_info = speaker_info, force_refresh = false } end - if info_to_send then - if not (info_to_send.discovery_info and info_to_send.discovery_info.device) then - log.error_with( - { hub_logs = true }, - st_utils.stringify_table(info_to_send, "Sonos Discovery Info has unexpected structure") - ) - return - end - event_bus:send(info_to_send) - local mac_addr = utils.extract_mac_addr(info_to_send.discovery_info.device) + if speaker_info then + event_bus:send(event_bus_msg) + local mac_addr = speaker_info.mac_addr local waiting_handles = task_handle.waiting_for_unique_key[unique_key] or {} log.debug(st_utils.stringify_table(waiting_handles, "waiting for unique keys", true)) @@ -317,8 +454,8 @@ function sonos_ssdp.spawn_persistent_ssdp_task() log.debug( st_utils.stringify_table(waiting_handles, "waiting for unique keys and mac addresses", true) ) - for _, reply_tx in ipairs(waiting_handles) do - reply_tx:send(info_to_send) + for _, reply_tx in ipairs(waiting_handles or {}) do + reply_tx:send(speaker_info) end task_handle.waiting_for_unique_key[unique_key] = {} diff --git a/drivers/SmartThings/sonos/src/api/sonos_websocket_router.lua b/drivers/SmartThings/sonos/src/api/sonos_websocket_router.lua index 7e15855e89..c19e35274b 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_websocket_router.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_websocket_router.lua @@ -39,7 +39,7 @@ local pending_close = {} cosock.spawn(function() while true do - for _, unique_key in ipairs(pending_close) do -- close any sockets pending close before selecting/receiving on them + for _, unique_key in ipairs(pending_close or {}) do -- close any sockets pending close before selecting/receiving on them local wss = websockets[unique_key] if wss ~= nil then log.trace(string.format("Closing websocket for player %s", unique_key)) @@ -60,7 +60,7 @@ cosock.spawn(function() pending_close = {} local socks = { control_rx } - for _, wss in pairs(websockets) do + for _, wss in pairs(websockets or {}) do table.insert(socks, wss) end local receivers, _, err = socket.select(socks, nil, 10) @@ -71,7 +71,7 @@ cosock.spawn(function() log.error("Error in Websocket Router event loop: " .. err) end else - for _, recv in ipairs(receivers) do + for _, recv in ipairs(receivers or {}) do if recv.link and recv.link.queue and #recv.link.queue == 0 then -- workaround a bug in receiving log.warn("attempting to receive on empty channel") goto continue @@ -93,7 +93,7 @@ cosock.spawn(function() elseif err == "closed" and recv.id then -- closed websocket log.trace(string.format("Websocket %s closed", tostring(recv.id))) local still_open_sockets = {} - for unique_key, wss in pairs(websockets) do + for unique_key, wss in pairs(websockets or {}) do if wss.id ~= recv.id then still_open_sockets[unique_key] = wss end @@ -207,7 +207,7 @@ local function _make_websocket(url_table, api_key) local headers = SonosApi.make_headers(api_key) local config = LustreConfig.default():protocol("v1.api.smartspeaker.audio") - for k, v in pairs(headers) do + for k, v in pairs(headers or {}) do config = config:header(k, v) end @@ -313,7 +313,7 @@ end function SonosWebSocketRouter.cleanup_unused_sockets(driver) log.trace("Begin cleanup of unused websockets") local should_keep = {} - for unique_key, _ in pairs(websockets) do + for unique_key, _ in pairs(websockets or {}) do local household_id, player_id = unique_key:match("(.*)/(.*)") local is_joined = driver.sonos:get_device_id_for_player(household_id, player_id) ~= nil log.debug(string.format("Is Player %s joined? %s", player_id, is_joined)) @@ -322,7 +322,7 @@ function SonosWebSocketRouter.cleanup_unused_sockets(driver) local known_devices = driver:get_devices() - for _, device in ipairs(known_devices) do + for _, device in ipairs(known_devices or {}) do local household_id, coordinator_id = driver.sonos:get_coordinator_for_device(device) local coordinator_unique_key, bad_key_part = utils.sonos_unique_key(household_id, coordinator_id) @@ -340,7 +340,7 @@ function SonosWebSocketRouter.cleanup_unused_sockets(driver) end end - for unique_key, keep in pairs(should_keep) do + for unique_key, keep in pairs(should_keep or {}) do if not keep then SonosWebSocketRouter.close_socket_for_player(unique_key) end diff --git a/drivers/SmartThings/sonos/src/cosock/bus.lua b/drivers/SmartThings/sonos/src/cosock/bus.lua index adb36c4880..f0a248753c 100644 --- a/drivers/SmartThings/sonos/src/cosock/bus.lua +++ b/drivers/SmartThings/sonos/src/cosock/bus.lua @@ -159,7 +159,7 @@ end function __sender_mt:close() self._bus_inner.closed = true local existing_links = self._bus_inner.receiver_links - for _, link in pairs(existing_links) do + for _, link in pairs(existing_links or {}) do if link.waker then link.waker() end @@ -176,7 +176,7 @@ end function __sender_mt:send(msg) if not self._bus_inner.closed then -- wapping in table allows `nil` to be sent as a message - for _, link in pairs(self._bus_inner.receiver_links) do + for _, link in pairs(self._bus_inner.receiver_links or {}) do table.insert(link.queue, { msg = msg }) if link.waker then link.waker() diff --git a/drivers/SmartThings/sonos/src/fields.lua b/drivers/SmartThings/sonos/src/fields.lua index 6a219ef452..11b658a835 100644 --- a/drivers/SmartThings/sonos/src/fields.lua +++ b/drivers/SmartThings/sonos/src/fields.lua @@ -5,6 +5,7 @@ local Fields = {} Fields.SonosPlayerFields = { _IS_INIT = "init", _IS_SCANNING = "scanning", + BONDED = "bonded", CONNECTION = "conn", UNIQUE_KEY = "unique_key", HOUSEHOLD_ID = "householdId", diff --git a/drivers/SmartThings/sonos/src/init.lua b/drivers/SmartThings/sonos/src/init.lua index bfe1c0eb9e..db4e5f6013 100644 --- a/drivers/SmartThings/sonos/src/init.lua +++ b/drivers/SmartThings/sonos/src/init.lua @@ -39,6 +39,6 @@ if api_version < 14 then driver:start_ssdp_event_task() end -log.info "Starting Sonos run loop" +log.info("Starting Sonos run loop") driver:run() -log.info "Exiting Sonos run loop" +log.info("Exiting Sonos run loop") diff --git a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua index 747381f798..1055d53020 100644 --- a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua +++ b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua @@ -68,7 +68,7 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) if not info then device.log.warn(string.format("error receiving device info: %s", recv_err)) else - ---@cast info { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo, force_refresh: boolean } + ---@cast info SpeakerDiscoveryInfo local auth_success, api_key_or_err = driver:check_auth(info) if not auth_success then device:offline() @@ -82,16 +82,10 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) local token, token_recv_err -- max 30 mins local backoff_builder = utils.backoff_builder(60 * 30, 30, 2) - if not driver:is_waiting_for_oauth_token() then - local _, request_token_err = driver:request_oauth_token() - if request_token_err then - log.warn(string.format("Error sending token request: %s", request_token_err)) - end - end + driver:alert_unauthorized() local backoff_timer = nil while not token do - local send_request = false -- we use the backoff to create a timer and utilize a select loop here, instead of -- utilizing a sleep, so that we can create a long delay on our polling of the cloud -- without putting ourselves in a situation where we're sleeping for an extended period @@ -117,7 +111,6 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) -- -- This is just in case both receivers are ready, so that we can prioritize -- handling the token instead of putting another request in flight. - send_request = true backoff_timer:handled() backoff_timer = nil end @@ -137,17 +130,6 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) ) ) end - - if send_request then - if not driver:is_waiting_for_oauth_token() then - local _, request_token_err = driver:request_oauth_token() - if request_token_err then - log.warn( - string.format("Error sending token request: %s", request_token_err) - ) - end - end - end end else device.log.error( @@ -164,10 +146,12 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) return end log.error_with( - { hub_logs = true }, - "Error handling Sonos player initialization: %s, error code: %s", - error, - (error_code or "N/A") + { hub_logs = false }, + string.format( + "Error handling Sonos player initialization: %s, error code: %s", + error, + (error_code or "N/A") + ) ) end end @@ -181,6 +165,7 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) { hub_logs = true }, string.format("Driver wasn't able to spin up SSDP task, cannot initialize devices.") ) + cosock.socket.sleep(30) end end end, diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index c603a7f399..56ccb2335f 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -1,4 +1,6 @@ -local api_version = require "version".api +local version = require "version" +local api_version = version.api +local rpc_version = version.rpc local capabilities = require "st.capabilities" local cosock = require "cosock" local json = require "st.json" @@ -25,8 +27,6 @@ if not security_load_success then security = nil end -local ONE_HOUR_IN_SECONDS = 3600 - ---@class SonosDriver: Driver --- ---@field public datastore table driver persistent store @@ -38,15 +38,32 @@ local ONE_HOUR_IN_SECONDS = 3600 ---@field public sonos SonosState Local state related to the sonos systems ---@field public discovery fun(driver: SonosDriver, opts: table, should_continue: fun(): boolean) ---@field private oauth_token_bus cosock.Bus.Sender bus for broadcasting new oauth tokens that arrive on the environment channel +---@field private oauth_info_bus cosock.Bus.Sender bus for broadcasting new endpoint app info that arrives on the environment channel ---@field private oauth { token: {accessToken: string, expiresAt: number}, endpoint_app_info: { state: "connected"|"disconnected" }, force_oauth: boolean? } cached OAuth info ----@field private waiting_for_oauth_token boolean ---@field private startup_state_received boolean ---@field private devices_waiting_for_startup_state SonosDevice[] +---@field private have_alerted_unauthorized boolean Used to track if we have requested an oauth token, this will trigger the notification used for account linking +---@field package bonded_devices table map of Device device_network_id to a boolean indicating if the device is currently known as a bonded device. --- ---@field public ssdp_task SonosPersistentSsdpTask? ---@field private ssdp_event_thread_handle table? local SonosDriver = {} +---@param device SonosDevice +function SonosDriver:update_bonded_device_tracking(device) + local already_bonded = self.bonded_devices[device.device_network_id] + local currently_bonded = device:get_field(PlayerFields.BONDED) + self.bonded_devices[device.device_network_id] = currently_bonded + + if currently_bonded and not already_bonded then + device:offline() + end + + if already_bonded and not currently_bonded then + SonosDriverLifecycleHandlers.initialize_device(self, device) + end +end + function SonosDriver:has_received_startup_state() return self.startup_state_received end @@ -89,9 +106,9 @@ end function SonosDriver:handle_augmented_data_change(update_key, decoded) if update_key == "endpointAppInfo" then self.oauth.endpoint_app_info = decoded + self.oauth_info_bus:send(decoded) elseif update_key == "sonosOAuthToken" then self.oauth.token = decoded - self.waiting_for_oauth_token = false self.oauth_token_bus:send(decoded) elseif update_key == "force_oauth" then self.oauth.force_oauth = decoded @@ -100,11 +117,6 @@ function SonosDriver:handle_augmented_data_change(update_key, decoded) end end ----@return boolean -function SonosDriver:is_waiting_for_oauth_token() - return (api_version >= 14 and security ~= nil) and self.waiting_for_oauth_token -end - ---@return (cosock.Bus.Subscription)? receiver the subscription receiver if the bus hasn't been closed, nil if closed ---@return nil|"not supported"|"closed" err_msg "not supported" on old API versions, "closed" if the bus is closed, nil on success function SonosDriver:oauth_token_event_subscribe() @@ -114,8 +126,17 @@ function SonosDriver:oauth_token_event_subscribe() return self.oauth_token_bus:subscribe() end +---@return (cosock.Bus.Subscription)? receiver the subscription receiver if the bus hasn't been closed, nil if closed +---@return nil|"not supported"|"closed" err_msg "not supported" on old API versions, "closed" if the bus is closed, nil on success +function SonosDriver:oauth_info_event_subscribe() + if api_version < 14 or security == nil then + return nil, "not supported" + end + return self.oauth_info_bus:subscribe() +end + function SonosDriver:update_after_startup_state_received() - for k, v in pairs(self.hub_augmented_driver_data) do + for k, v in pairs(self.hub_augmented_driver_data or {}) do local decode_success, decoded = pcall(json.decode, v) if decode_success then self:handle_augmented_data_change(k, decoded) @@ -127,13 +148,15 @@ end function SonosDriver:handle_augmented_store_delete(update_key) if update_key == "endpointAppInfo" then if update_key == "endpointAppInfo" then - log.trace "deleting endpoint app info" + log.trace("deleting endpoint app info") self.oauth.endpoint_app_info = nil + self.oauth_info_bus:send(nil) elseif update_key == "sonosOAuthToken" then - log.trace "deleting OAuth Token" + log.trace("deleting OAuth Token") self.oauth.token = nil + self.oauth_token_bus:send(nil) elseif update_key == "force_oauth" then - log.trace "deleting Force OAuth" + log.trace("deleting Force OAuth") self.oauth.force_oauth = nil else log.debug(string.format("received delete of unexpected key: %s", update_key)) @@ -164,9 +187,7 @@ end ---@param update_key "endpointAppInfo"|"sonosOAuthToken" ---@param update_value string function SonosDriver:notify_augmented_data_changed(update_kind, update_key, update_value) - local already_connected = self.oauth - and self.oauth.endpoint_app_info - and self.oauth.endpoint_app_info.state == "connected" + local already_connected = self:oauth_app_connected() log.info(string.format("Already connected? %s", already_connected)) if update_kind == "snapshot" then self:update_after_startup_state_received() @@ -183,24 +204,17 @@ function SonosDriver:notify_augmented_data_changed(update_kind, update_key, upda ) ) end - - if - self.oauth.endpoint_app_info - and self.oauth.endpoint_app_info.state == "connected" - and not already_connected - then - local _, err = self:request_oauth_token() - if err then - log.error(string.format("Request OAuth token error: %s", err)) - end - end end function SonosDriver:handle_startup_state_received() self:start_ssdp_event_task() self:notify_augmented_data_changed "snapshot" + if api_version >= 14 and security ~= nil then + local token_refresher = require "token_refresher" + token_refresher.spawn_token_refresher(self) + end self.startup_state_received = true - for _, device in pairs(self.devices_waiting_for_startup_state) do + for _, device in pairs(self.devices_waiting_for_startup_state or {}) do SonosDriverLifecycleHandlers.initialize_device(self, device) end self.devices_waiting_for_startup_state = {} @@ -220,18 +234,13 @@ end --- Check if the driver is able to authenticate against the given household_id --- with what credentials it currently possesses. ----@param info_or_device SonosDevice | { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo, force_refresh: boolean } +---@param info_or_device SonosDevice | SpeakerDiscoveryInfo ---@return boolean? auth_success true if the driver can authenticate against the provided arguments, false otherwise ---@return string? api_key_or_err if `auth_success` is true, this will be the API key that is known to auth. If `auth_success` is false, this will be nil. If `auth_success` is `nil`, this will be an error message. function SonosDriver:check_auth(info_or_device) local maybe_token, _ = self:get_oauth_token() - local token_valid = (api_version >= 14 and security ~= nil) - and self.oauth - and self.oauth.endpoint_app_info - and self.oauth.endpoint_app_info.state == "connected" - and maybe_token ~= nil - if token_valid then + if maybe_token then return true, SonosApi.api_keys.oauth_key elseif self.oauth.force_oauth then return false @@ -240,13 +249,6 @@ function SonosDriver:check_auth(info_or_device) local rest_url, household_id, sw_gen if type(info_or_device) == "table" then if - type(info_or_device.ssdp_info) == "table" and type(info_or_device.discovery_info) == "table" - then - ---@cast info_or_device { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo } - rest_url = net_url.parse(info_or_device.discovery_info.restUrl) - household_id = info_or_device.ssdp_info.household_id - sw_gen = info_or_device.discovery_info.device.swGen - elseif type(info_or_device.get_field) == "function" and type(info_or_device.set_field) == "function" and info_or_device.id @@ -255,6 +257,11 @@ function SonosDriver:check_auth(info_or_device) rest_url = net_url.parse(info_or_device:get_field(PlayerFields.REST_URL)) household_id = self.sonos:get_sonos_ids_for_device(info_or_device) sw_gen = info_or_device:get_field(PlayerFields.SW_GEN) + else + ---@cast info_or_device SpeakerDiscoveryInfo + rest_url = info_or_device.rest_url + household_id = info_or_device.household_id + sw_gen = info_or_device.sw_gen end end @@ -265,11 +272,7 @@ function SonosDriver:check_auth(info_or_device) ( ( type(info_or_device) == "table" - and ( - info_or_device.label - or info_or_device.id - or info_or_device.discovery_info.device.name - ) + and (info_or_device.label or info_or_device.id or info_or_device.name) ) or "" ) ) @@ -290,7 +293,7 @@ function SonosDriver:check_auth(info_or_device) end local unauthorized = false - for _, api_key in pairs(SonosApi.api_keys) do + for _, api_key in pairs(SonosApi.api_keys or {}) do local headers = SonosApi.make_headers(api_key, maybe_token and maybe_token.accessToken) local response, response_err = SonosApi.RestApi.get_groups_info(rest_url, household_id, headers) @@ -322,45 +325,15 @@ function SonosDriver:check_auth(info_or_device) ) end ----@return any? ret nil on permissions violation ----@return string? error nil on success -function SonosDriver:request_oauth_token() - if api_version < 14 or security == nil then - return nil, "not supported" - end - local maybe_token, maybe_err = self:get_oauth_token() - if maybe_err then - log.warn(string.format("get oauth token error: %s", maybe_err)) - end - if type(maybe_token) == "table" and type(maybe_token.accessToken) == "string" then - self.oauth_token_bus:send(maybe_token) - end - local result, err = security.get_sonos_oauth() - if not result then - return nil, string.format("Error requesting OAuth token via Security API: %s", err) - end - self.waiting_for_oauth_token = true - return result, err -end - ---@return { accessToken: string, expiresAt: number }? the token if a currently valid token is available, nil if not ----@return "token expired"|"no token"|"not supported"|nil reason the reason a token was not provided, nil if there is a valid token available +---@return "token expired"|"no token"|"not supported"|"not connected"|nil reason the reason a token was not provided, nil if there is a valid token available function SonosDriver:get_oauth_token() if api_version < 14 or security == nil then return nil, "not supported" end - self.hub_augmented_driver_data = self.hub_augmented_driver_data or {} - local decode_success, maybe_token = - pcall(json.decode, self.hub_augmented_driver_data.sonosOAuthToken) - if - decode_success - and type(maybe_token) == "table" - and type(maybe_token.accessToken) == "string" - and type(maybe_token.expiresAt) == "number" - then - self.oauth.token = maybe_token - elseif self.hub_augmented_driver_data.sonosOAuthToken ~= nil then - log.warn(string.format("Unable to JSON decode token from hub augmented data: %s", maybe_token)) + + if not self:oauth_app_connected() then + return nil, "not connected" end if self.oauth.token then @@ -368,13 +341,6 @@ function SonosDriver:get_oauth_token() local now = os.time() -- token has not expired yet if now < expiration then - -- token is expiring soon, so we pre-emptively refresh - if math.abs(expiration - now) < ONE_HOUR_IN_SECONDS then - local result, err = security.get_sonos_oauth() - if not result then - log.warn(string.format("Error requesting OAuth token via Security API: %s", err)) - end - end return self.oauth.token else return nil, "token expired" @@ -384,6 +350,60 @@ function SonosDriver:get_oauth_token() return nil, "no token" end +function SonosDriver:wait_for_oauth_token(timeout) + if api_version < 14 or security == nil then + return nil, "not supported" + end + + if not self:oauth_app_connected() then + return nil, "not connected" + end + + -- See if a valid token is already available + local maybe_token, _ = self:get_oauth_token() + if maybe_token then + -- return the valid token + return maybe_token + end + -- Subscribe to the token event bus. A new token has been/will be requested + -- by the token refresher task. + local token_bus, err = self:oauth_token_event_subscribe() + if token_bus then + token_bus:settimeout(timeout) + -- Wait for the new token to come in + token_bus:receive() + -- Call `SonosDriver:get_oauth_token` again to ensure the token is valid. + return self:get_oauth_token() + end + return nil, err +end + +function SonosDriver:oauth_app_connected() + return (api_version >= 14 and security ~= nil) + and self.oauth + and self.oauth.endpoint_app_info + and self.oauth.endpoint_app_info.state == "connected" +end + +--- Used to trigger the notification that the user must link their sonos account. +--- Will request a token a single time which will trigger preinstall isa flow. +function SonosDriver:alert_unauthorized() + if api_version < 14 or security == nil then + return + end + if self.have_alerted_unauthorized then + return + end + -- Do the request regardless if we think oauth is connected, because + -- there is a possibility that we have stale data. + local result, err = security.get_sonos_oauth() + if not result then + log.warn(string.format("Failed to alert unauthorized: %s", err)) + return + end + self.have_alerted_unauthorized = true +end + ---Create a cosock task that handles events from the persistent SSDP task. ---@param driver SonosDriver ---@param discovery_event_subscription cosock.Bus.Subscription @@ -404,13 +424,13 @@ local function make_ssdp_event_handler( local recv_ready, _, select_err = cosock.socket.select(receivers, nil, nil) if recv_ready then - for _, receiver in ipairs(recv_ready) do + for _, receiver in ipairs(recv_ready or {}) do if oauth_token_subscription ~= nil and receiver == oauth_token_subscription then local token_evt, receive_err = oauth_token_subscription:receive() if not token_evt then log.warn(string.format("Error on token event bus receive: %s", receive_err)) else - for _, event in pairs(unauthorized) do + for _, event in pairs(unauthorized or {}) do -- shouldn't need a nil check on the ssdp_task here since this whole function -- won't get called unless the task is successfully spawned. driver.ssdp_task:publish(event) @@ -419,27 +439,32 @@ local function make_ssdp_event_handler( end end if receiver == discovery_event_subscription then - ---@type { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo, force_refresh: boolean }? + ---@type { speaker_info: SpeakerDiscoveryInfo, force_refresh: boolean } local event, recv_err = discovery_event_subscription:receive() if event then - local unique_key = utils.sonos_unique_key_from_ssdp(event.ssdp_info) + local speaker_info = event.speaker_info if - event.force_refresh or not (unauthorized[unique_key] or discovered[unique_key]) + event.force_refresh + or not ( + unauthorized[speaker_info.unique_key] + or discovered[speaker_info.unique_key] + or driver.bonded_devices[speaker_info.mac_addr] + ) then - local _, api_key = driver:check_auth(event) + local _, api_key = driver:check_auth(event.speaker_info) local success, handle_err, err_code = - driver:handle_player_discovery_info(api_key, event) + driver:handle_player_discovery_info(api_key, event.speaker_info) if not success then if err_code == "ERROR_NOT_AUTHORIZED" then - unauthorized[unique_key] = event + unauthorized[speaker_info.unique_key] = event end log.warn_with( - { hub_logs = true }, + { hub_logs = false }, string.format("Failed to handle discovered speaker: %s", handle_err) ) else - discovered[unique_key] = true + discovered[speaker_info.unique_key] = true end end else @@ -456,6 +481,17 @@ local function make_ssdp_event_handler( end function SonosDriver:start_ssdp_event_task() + if self.ssdp_task ~= nil then + return + end + cosock.spawn(function () + while self:start_ssdp_event_task_inner() == false do + cosock.socket.sleep(30) + end + end) +end + +function SonosDriver:start_ssdp_event_task_inner() local ssdp_task, err = sonos_ssdp.spawn_persistent_ssdp_task() if err then log.error_with({ hub_logs = true }, string.format("Unable to create SSDP task: %s", err)) @@ -469,35 +505,35 @@ function SonosDriver:start_ssdp_event_task() end self.ssdp_event_thread_handle = cosock.spawn(make_ssdp_event_handler(self, ssdp_task_subscription, oauth_token_subscription)) + return true end + return false end ---@param api_key string ----@param info { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo, force_refresh: boolean } +---@param info SpeakerDiscoveryInfo ---@param device SonosDevice? ---@return boolean|nil response nil or false on failure ---@return nil|string error the error reason on failure, nil on success ---@return nil|string error_code the Sonos error code, if available function SonosDriver:handle_player_discovery_info(api_key, info, device) - -- If the SSDP Group Info is an empty string, then that means it's the non-primary - -- speaker in a bonded set (e.g. a home theater system, a stereo pair, etc). - -- These aren't the same as speaker groups, and bonded speakers can't be controlled - -- via websocket at all. So we ignore all bonded non-primary speakers - if #info.ssdp_info.group_id == 0 then - return nil, - string.format( - "Player %s is a non-primary bonded Sonos device, ignoring", - info.discovery_info.device.name - ) + local discovery_info_mac_addr = info.mac_addr + local bonded = info:is_bonded() + self.bonded_devices[discovery_info_mac_addr] = bonded + + local maybe_device = self:get_device_by_dni(discovery_info_mac_addr) + if maybe_device then + maybe_device:set_field(PlayerFields.BONDED, bonded, { persist = false }) + self:update_bonded_device_tracking(maybe_device) end api_key = api_key or self:get_fallback_api_key() - local rest_url = net_url.parse(info.discovery_info.restUrl) + local rest_url = info.rest_url local maybe_token, no_token_reason = self:get_oauth_token() local headers = SonosApi.make_headers(api_key, maybe_token and maybe_token.accessToken) local response, response_err = - SonosApi.RestApi.get_groups_info(rest_url, info.ssdp_info.household_id, headers) + SonosApi.RestApi.get_groups_info(rest_url, info.household_id, headers) if response_err then return nil, string.format("Error while making REST API call: %s", response_err) @@ -507,7 +543,7 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) local additional_info = response.reason or response.wwwAuthenticate local error_string = string.format( '`getGroups` response error for player "%s":\n\tError Code: %s', - info.discovery_info.device.name, + info.name, response.errorCode ) @@ -524,7 +560,7 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) return nil, error_string, response.errorCode end - local sw_gen = info.discovery_info.device.swGen + local sw_gen = info.sw_gen local is_s1 = sw_gen == 1 local response_valid if is_s1 then @@ -543,12 +579,11 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) end --- @cast response SonosGroupsResponseBody - self.sonos:update_household_info(info.ssdp_info.household_id, response) + self.sonos:update_household_info(info.household_id, response, self) local device_to_update, device_mac_addr - local maybe_device_id = - self.sonos:get_device_id_for_player(info.ssdp_info.household_id, info.discovery_info.playerId) + local maybe_device_id = self.sonos:get_device_id_for_player(info.household_id, info.player_id) if device then device_to_update = device @@ -562,10 +597,10 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) end if not device_mac_addr then - if not (info and info.discovery_info and info.discovery_info.device) then + if not (info and info.mac_addr) then return nil, st_utils.stringify_table(info, "Sonos Discovery Info has unexpected structure") end - device_mac_addr = utils.extract_mac_addr(info.discovery_info.device) + device_mac_addr = discovery_info_mac_addr end if not device_to_update then @@ -578,11 +613,9 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) if device_to_update then self.dni_to_device_id[device_mac_addr] = device_to_update.id self.sonos:associate_device_record(device_to_update, info) - else - local name = info.discovery_info.device.name - or info.discovery_info.device.modelDisplayName - or "Unknown Sonos Player" - local model = info.discovery_info.device.modelDisplayName or "Unknown Sonos Model" + elseif not bonded then + local name = info.name or info.model_display_name or "Unknown Sonos Player" + local model = info.model_display_name or "Unknown Sonos Model" local try_create_message = { type = "LAN", device_network_id = device_mac_addr, @@ -590,7 +623,7 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) label = name, model = model, profile = "sonos-player", - vendor_provided_label = info.discovery_info.device.model, + vendor_provided_label = info.model, } self:try_create_device(try_create_message) @@ -620,19 +653,77 @@ local function do_refresh(driver, device, cmd) sonos_conn:refresh_subscriptions() end +--- In RPC version 100, the events for augmented driver store were changed +--- to no longer match the parsing done in API version 18 and below. +--- +--- API version 19 will handle both before and after RPC 100 changes so this only needs +--- to be applied for RPC version >= 100 and API version <= 18. +if rpc_version >= 100 and api_version <= 18 then + log.info_with({ hub_logs = true }, "Overriding environment info handler for RPC >= 100 and API <= 18") + function SonosDriver:environment_info_handler(channel) + log.info_with({ hub_logs = true }, "Starting environment info handler for RPC >= 100 and API <= 18") + local msg_type, msg_val = channel:receive() + -- This driver only cares about augmentDriverStore messages currently. + -- Previously, this was augmentDatastore msg_type. + if msg_type == "augmentDriverStore" then + if type(msg_val.payload) ~= "table" then + log.warn( + string.format( + "Unexpected augmentDriverStore payload type: %s", + type(msg_val.payload) + ) + ) + return + end + -- The field evt_kind was renamed to kind and is a string of the enum rather than + -- the integer value of the enum. + if msg_val.kind == "Upsert" then + -- This type was changed to table of u8s instead of a string + local data = "" + for _, v in pairs(msg_val.payload.data_value) do + data = data .. string.char(v) + end + self.hub_augmented_driver_data[msg_val.payload.data_key] = data + -- notify with the updated record + if self.notify_augmented_data_changed ~= nil then + self:notify_augmented_data_changed("upsert", msg_val.payload.data_key, data) + end + elseif msg_val.kind == "Delete" then + self.hub_augmented_driver_data[msg_val.payload.data_key] = nil + -- notify with just the key that got deleted + if self.notify_augmented_data_changed ~= nil then + self:notify_augmented_data_changed("delete", msg_val.payload.data_key) + end + else + log.warn( + string.format( + "Unexpected augmentDriverStore kind: %s", + msg_val.kind + ) + ) + end + end + end +end + function SonosDriver.new_driver_template() local oauth_token_bus = cosock.bus() + local oauth_info_bus = cosock.bus() local template = { sonos = SonosState.instance(), discovery = SonosDisco.discover, oauth_token_bus = oauth_token_bus, + oauth_info_bus = oauth_info_bus, oauth = {}, - waiting_for_oauth_token = false, + have_alerted_unauthorized = false, startup_state_received = false, devices_waiting_for_startup_state = {}, + bonded_devices = utils.new_mac_address_keyed_table(), dni_to_device_id = utils.new_mac_address_keyed_table(), lifecycle_handlers = SonosDriverLifecycleHandlers, + -- Only overriden in the case of RPC version 100+ with API version <= 18 + environment_info_handler = SonosDriver.environment_info_handler, capability_handlers = { [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = do_refresh, @@ -675,7 +766,7 @@ function SonosDriver.new_driver_template() }, } - for k, v in pairs(SonosDriver) do + for k, v in pairs(SonosDriver or {}) do template[k] = v end diff --git a/drivers/SmartThings/sonos/src/sonos_state.lua b/drivers/SmartThings/sonos/src/sonos_state.lua index 25c9e47b2b..faf466c6a3 100644 --- a/drivers/SmartThings/sonos/src/sonos_state.lua +++ b/drivers/SmartThings/sonos/src/sonos_state.lua @@ -1,7 +1,5 @@ -local capabilities = require "st.capabilities" local log = require "log" local st_utils = require "st.utils" -local swGenCapability = capabilities["stus.softwareGeneration"] local utils = require "utils" @@ -12,8 +10,9 @@ local SonosConnection = require "api.sonos_connection" --- @class SonosHousehold --- Information on an entire Sonos system ("household"), such as its current groups, list of players, etc. --- @field public id HouseholdId ---- @field public groups table All of the current groups in the system ---- @field public players table All of the current players in the system +--- @field public groups table All of the current groups in the system +--- @field public players table All of the current players in the system +--- @field public bonded_players table PlayerID's in this map that map to true are non-primary bonded players, and not controllable. --- @field public player_to_group table quick lookup from Player ID -> Group ID --- @field public st_devices table Player ID -> ST Device Record UUID information for the household --- @field public favorites SonosFavorites all of the favorites/presets in the system @@ -23,6 +22,11 @@ function _household_mt:reset() self.groups = utils.new_case_insensitive_table() self.players = utils.new_case_insensitive_table() self.player_to_group = utils.new_case_insensitive_table() + -- previously bonded devices should not be un-bonded after a reset since these should + -- not be treated as distinct devices + if not self.bonded_players then + self.bonded_players = utils.new_case_insensitive_table() + end end _household_mt.__index = _household_mt @@ -46,10 +50,10 @@ local function make_households_table() local households_table_inner = utils.new_case_insensitive_table() local households_table = setmetatable({}, { - __index = function(tbl, key) + __index = function(_, key) return households_table_inner[key] end, - __newindex = function(tbl, key, value) + __newindex = function(_, key, value) households_table_inner[key] = value end, __metatable = "SonosHouseholds", @@ -73,7 +77,7 @@ end local _STATE = { ---@type Households households = make_households_table(), - ---@type table + ---@type table device_record_map = {}, } @@ -81,13 +85,15 @@ local _STATE = { --- @class SonosState local SonosState = {} SonosState.__index = SonosState +SonosState.EMPTY_HOUSEHOLD = _STATE.households:get_or_init("__EMPTY") ---@param device SonosDevice ----@param info { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo } +---@param info SpeakerDiscoveryInfo function SonosState:associate_device_record(device, info) - local household_id = info.ssdp_info.household_id - local group_id = info.ssdp_info.group_id - local player_id = info.discovery_info.playerId + local household_id = info.household_id + local group_id = info.group_id + -- This is the device id even if the device is a seondary in a bonded set + local player_id = info.player_id local household = _STATE.households[household_id] if not household then @@ -100,8 +106,11 @@ function SonosState:associate_device_record(device, info) return end - local group = household.groups[group_id] + if not group_id or #group_id == 0 then + group_id = household.player_to_group[player_id or ""] or "" + end + local group = household.groups[group_id] if not group then log.error( string.format( @@ -112,7 +121,9 @@ function SonosState:associate_device_record(device, info) return end - local player = household.players[player_id] + local player_tbl = household.players[player_id] + local player = (player_tbl or {}).player + local sonos_device = (player_tbl or {}).device if not player then log.error( @@ -124,27 +135,39 @@ function SonosState:associate_device_record(device, info) return end - household.st_devices[player.id] = device.id + household.st_devices[player_id] = device.id - _STATE.device_record_map[device.id] = { group = group, player = player, household = household } + _STATE.device_record_map[device.id] = { + sonos_device_id = player_id, + group = group, + player = player, + household = household, + sonos_device = sonos_device, + } - device:set_field(PlayerFields.SW_GEN, info.discovery_info.device.swGen, { persist = true }) - device:emit_event( - swGenCapability.generation(string.format("%s", info.discovery_info.device.swGen)) - ) + local bonded = household.bonded_players[player_id] and true or false + + local sw_gen_changed = + utils.update_field_if_changed(device, PlayerFields.SW_GEN, info.sw_gen, { persist = true }) + + if sw_gen_changed then + CapEventHandlers.handle_sw_gen(device, info.sw_gen) + end - device:set_field(PlayerFields.REST_URL, info.discovery_info.restUrl, { persist = true }) + device:set_field(PlayerFields.REST_URL, info.rest_url:build(), { persist = true }) local sonos_conn = device:get_field(PlayerFields.CONNECTION) local connected = sonos_conn ~= nil local websocket_url_changed = utils.update_field_if_changed( device, PlayerFields.WSS_URL, - info.ssdp_info.wss_url, + info.wss_url:build(), { persist = true } ) - if websocket_url_changed and connected then + local should_stop_conn = connected and (bonded or websocket_url_changed) + + if should_stop_conn then sonos_conn:stop() sonos_conn = nil device:set_field(PlayerFields.CONNECTION, nil) @@ -158,12 +181,12 @@ function SonosState:associate_device_record(device, info) ) local player_id_changed = - utils.update_field_if_changed(device, PlayerFields.PLAYER_ID, player.id, { persist = true }) + utils.update_field_if_changed(device, PlayerFields.PLAYER_ID, player_id, { persist = true }) local need_refresh = connected and (websocket_url_changed or household_id_changed or player_id_changed) - if sonos_conn == nil then + if not bonded and sonos_conn == nil then sonos_conn = SonosConnection.new(device.driver, device) device:set_field(PlayerFields.CONNECTION, sonos_conn) sonos_conn:start() @@ -175,25 +198,33 @@ function SonosState:associate_device_record(device, info) end self:update_device_record_group_info(household, group, device) + + -- device can't be controlled, mark the device as being offline. + if bonded then + device:offline() + end end ---@param household SonosHousehold ----@param group SonosGroupObject +---@param group SonosGroupInfo ---@param device SonosDevice function SonosState:update_device_record_group_info(household, group, device) local player_id = device:get_field(PlayerFields.PLAYER_ID) + local bonded = ((household or {}).bonded_players or {})[player_id] and true or false local group_role - if + if bonded then + group_role = "auxilary" + elseif ( type(household) == "table" and type(household.groups) == "table" and player_id and group and group.id - and group.coordinatorId - ) and player_id == group.coordinatorId + and group.coordinator_id + ) and player_id == group.coordinator_id then - local player_ids_list = (household.groups[group.id] or {}).playerIds or {} + local player_ids_list = (household.groups[group.id] or {}).player_ids or {} if #player_ids_list > 1 then group_role = "primary" else @@ -205,24 +236,28 @@ function SonosState:update_device_record_group_info(household, group, device) local field_changed = utils.update_field_if_changed(device, PlayerFields.GROUP_ID, group.id, { persist = true }) - if field_changed then + if not bonded and field_changed then CapEventHandlers.handle_group_id_update(device, group.id) end field_changed = utils.update_field_if_changed(device, PlayerFields.GROUP_ROLE, group_role, { persist = true }) - if field_changed then + if not bonded and field_changed then CapEventHandlers.handle_group_role_update(device, group_role) end field_changed = utils.update_field_if_changed( device, PlayerFields.COORDINATOR_ID, - group.coordinatorId, + group.coordinator_id, { persist = true } ) - if field_changed then - CapEventHandlers.handle_group_coordinator_update(device, group.coordinatorId) + if not bonded and field_changed then + CapEventHandlers.handle_group_coordinator_update(device, group.coordinator_id) + end + + if bonded then + device:offline() end end @@ -271,36 +306,96 @@ function SonosState:update_device_record_from_state(household_id, device) self:update_device_record_group_info(household, current_mapping.group, device) end +--- Helper function for when updating household info +---@param driver SonosDriver +---@param player SonosPlayerObject +---@param household SonosHousehold +---@param known_bonded_players table +---@param sonos_device_id PlayerId +local function update_device_info(driver, player, household, known_bonded_players, sonos_device_id) + ---@type SonosDeviceInfo + local device_info = { id = sonos_device_id, primary_device_id = player.id } + ---@type SonosPlayerInfo + local player_info = { id = player.id, websocket_url = player.websocketUrl } + household.players[sonos_device_id] = { + player = player_info, + device = device_info, + } + local previously_bonded = known_bonded_players[sonos_device_id] and true or false + local currently_bonded + local group_id + + -- The primary bonded player will have the same id as the top level player id + if sonos_device_id == player.id then + currently_bonded = false + else + currently_bonded = true + end + group_id = household.player_to_group[player.id] + household.player_to_group[sonos_device_id] = group_id + household.bonded_players[sonos_device_id] = currently_bonded + + local maybe_device_id = household.st_devices[sonos_device_id] + if maybe_device_id then + _STATE.device_record_map[maybe_device_id] = _STATE.device_record_map[maybe_device_id] or {} + _STATE.device_record_map[maybe_device_id].household = household + _STATE.device_record_map[maybe_device_id].group = household.groups[group_id] + _STATE.device_record_map[maybe_device_id].player = player_info + _STATE.device_record_map[maybe_device_id].sonos_device = device_info + if previously_bonded ~= currently_bonded then + local target_device = driver:get_device_info(maybe_device_id) + if target_device then + target_device:set_field(PlayerFields.BONDED, currently_bonded, { persist = false }) + driver:update_bonded_device_tracking(target_device) + end + end + end +end + --- @param id HouseholdId --- @param groups_event SonosGroupsResponseBody -function SonosState:update_household_info(id, groups_event) +--- @param driver SonosDriver +function SonosState:update_household_info(id, groups_event, driver) local household = _STATE.households:get_or_init(id) + local known_bonded_players = household.bonded_players or {} household:reset() local groups, players = groups_event.groups, groups_event.players - for _, group in ipairs(groups) do - household.groups[group.id] = group - for _, playerId in ipairs(group.playerIds) do + for _, group in ipairs(groups or {}) do + household.groups[group.id] = + { id = group.id, coordinator_id = group.coordinatorId, player_ids = group.playerIds } + for _, playerId in ipairs(group.playerIds or {}) do household.player_to_group[playerId] = group.id - - local maybe_device_id = household.st_devices[playerId] - if maybe_device_id then - _STATE.device_record_map[maybe_device_id] = _STATE.device_record_map[maybe_device_id] or {} - _STATE.device_record_map[maybe_device_id].group = group - _STATE.device_record_map[maybe_device_id].household = household - end end end - for _, player in ipairs(players) do - household.players[player.id] = player - local maybe_device_id = household.st_devices[player.id] - if maybe_device_id then - _STATE.device_record_map[maybe_device_id] = _STATE.device_record_map[maybe_device_id] or {} - _STATE.device_record_map[maybe_device_id].player = player + -- Iterate through the players and track all the devices associated with them + -- for bonded set tracking. + local log_devices_error = false + for _, player in ipairs(players or {}) do + -- Prefer devices because deviceIds is deprecated but all we care about is + -- the ID so either way is fine. + if type(player.devices) == "table" then + for _, device in ipairs(player.devices or {}) do + update_device_info(driver, player, household, known_bonded_players, device.id) + end + elseif type(player.deviceIds) == "table" then + for _, device_id in ipairs(player.deviceIds or {}) do + update_device_info(driver, player, household, known_bonded_players, device_id) + end + else + log_devices_error = true + -- We can still track the primary player in this case + update_device_info(driver, player, household, known_bonded_players, player.id) end end + if log_devices_error then + log.warn_with( + { hub_logs = true }, + "Group event contained neither devices nor deviceIds in player" + ) + end household.id = id _STATE.households[id] = household @@ -312,7 +407,7 @@ end --- @return string? error nil on success function SonosState:get_group_for_player(household_id, player_id) log.debug_with( - { hub_logs = true }, + { hub_logs = false }, st_utils.stringify_table( { household_id = household_id, player_id = player_id }, "Get Group For Player", @@ -339,7 +434,7 @@ end --- @return PlayerId?,string? function SonosState:get_coordinator_for_player(household_id, player_id) log.debug_with( - { hub_logs = true }, + { hub_logs = false }, st_utils.stringify_table( { household_id = household_id, player_id = player_id }, "Get Coordinator For Player", @@ -361,7 +456,7 @@ end --- @return PlayerId?,string? function SonosState:get_coordinator_for_group(household_id, group_id) log.debug_with( - { hub_logs = true }, + { hub_logs = false }, st_utils.stringify_table( { household_id = household_id, group_id = group_id }, "Get Coordinator For Group", @@ -395,7 +490,7 @@ function SonosState:get_coordinator_for_group(household_id, group_id) return end - return group.coordinatorId + return group.coordinator_id end --- @param device SonosDevice @@ -432,7 +527,7 @@ function SonosState:get_sonos_ids_for_device(device) -- player id *should* be stable if not player_id then - player_id = sonos_objects.player.id + player_id = sonos_objects.sonos_device_id device:set_field(PlayerFields.PLAYER_ID, player_id, { persist = true }) end @@ -464,7 +559,7 @@ end --- @return nil|string error nil on success function SonosState:get_group_for_device(device) if type(device) ~= "table" then - return nil, string.format("Invalid device argument for get_player_for_device: %s", device) + return nil, string.format("Invalid device argument for get_group_for_device: %s", device) end local household_id, group_id, _, err = self:get_sonos_ids_for_device(device) if err then @@ -479,7 +574,7 @@ end --- @return nil|string error nil on success function SonosState:get_coordinator_for_device(device) if type(device) ~= "table" then - return nil, string.format("Invalid device argument for get_player_for_device: %s", device) + return nil, string.format("Invalid device argument for get_coordinator_for_device: %s", device) end local household_id, group_id, _, err = self:get_sonos_ids_for_device(device) if err then @@ -504,7 +599,7 @@ function SonosState:get_coordinator_for_device(device) ) end - return household_id, group.coordinatorId, nil + return household_id, group.coordinator_id, nil end ---@return SonosState diff --git a/drivers/SmartThings/sonos/src/ssdp.lua b/drivers/SmartThings/sonos/src/ssdp.lua index 7e3b06a198..3e11b92d04 100644 --- a/drivers/SmartThings/sonos/src/ssdp.lua +++ b/drivers/SmartThings/sonos/src/ssdp.lua @@ -64,7 +64,7 @@ end ---@diagnostic disable-next-line: inject-field function Ssdp.check_headers_contain(headers, keys_to_check) local missing = {} - for _, header_key in ipairs(keys_to_check) do + for _, header_key in ipairs(keys_to_check or {}) do if headers:get_one(header_key) == nil then table.insert(missing, header_key) end @@ -197,7 +197,7 @@ end --- the search end time will be pushed out based on the `mx` parameter, extending the amount of --- time that `receive_m_search_response` will return results with a timeout. function _ssdp_mt:multicast_m_search() - for term, _ in pairs(self.search_terms) do + for term, _ in pairs(self.search_terms or {}) do local multicast_msg = table.concat({ "M-SEARCH * HTTP/1.1", "HOST: 239.255.255.250:1900", @@ -284,7 +284,7 @@ function _ssdp_mt:next_msearch_response() ) end - for _, location in ipairs(location_candidates) do + for _, location in ipairs(location_candidates or {}) do local location_host_match = location:match("http://([^,/]+):[^/]+/.+%.xml") if location_host_match ~= nil then possible_locations[location_host_match] = location diff --git a/drivers/SmartThings/sonos/src/token_refresher.lua b/drivers/SmartThings/sonos/src/token_refresher.lua new file mode 100644 index 0000000000..2ba3ad660e --- /dev/null +++ b/drivers/SmartThings/sonos/src/token_refresher.lua @@ -0,0 +1,152 @@ +local cosock = require "cosock" +local utils = require "utils" +local security = require "st.security" +local log = require "log" + +local module = {} + +local ACTIONS = { + -- This action waits for an event via the oauth endpoint app info bus in order to determine + -- when the driver goes from a disconnected to connected state. + WAIT_FOR_CONNECTED = 1, + -- This action will wait for the current valid token to expire. It will also handle a new token + -- event to redetermine the current action. New tokens that come in during this action will likely + -- be from debug testing. + WAIT_FOR_EXPIRE = 2, + -- This action requests a new token and waits for it to come in. + REQUEST_TOKEN = 3, +} + +local ACTION_STRINGIFY = { + [ACTIONS.WAIT_FOR_CONNECTED] = "wait for connected", + [ACTIONS.WAIT_FOR_EXPIRE] = "wait for expire", + [ACTIONS.REQUEST_TOKEN] = "request token", +} + +local Refresher = {} +Refresher.__index = Refresher + +--- Determine which action the refresher should take. +--- This just depends on: +--- - Is Oauth connected? +--- - Do we have a valid token? +function Refresher:determine_action() + if not self.driver:oauth_app_connected() then + -- Oauth is disconnected so no point in trying to request a token until we are connected. + return ACTIONS.WAIT_FOR_CONNECTED + end + local token, _ = self.driver:get_oauth_token() + if token then + local now = os.time() + local expiration = math.floor(token.expiresAt / 1000) + if (expiration - now) > 60 then + -- Token is valid and not expiring in the next 60 seconds. + return ACTIONS.WAIT_FOR_EXPIRE + end + end + -- We don't have a valid token or it is about to expire soon. + return ACTIONS.REQUEST_TOKEN +end + +--- Waits for a token event with a timeout. +--- @param timeout number How long the function will wait for a new token +function Refresher:try_wait_for_token_event(timeout) + local token_bus, err = self.driver:oauth_token_event_subscribe() + if err == "closed" then + self.token_bus_closed = true + end + if token_bus then + token_bus:settimeout(timeout) + token_bus:receive() + end +end + +--- Waits for the current token to expire or a new token event. +--- +--- The likely outcome of this function is to wait the entire expiration timeout. It will +--- also listen for token events just in case a new token with a new expiration is sent to the driver. +--- A new token would most likely come from developer testing, but since the new token requests are +--- not synchronous one could come from an earlier request. +function Refresher:wait_for_expire_or_token_event() + local maybe_token, err = self.driver:get_oauth_token() + if not maybe_token then + -- Something got funky in the state machine, return and re-determine our next action + log.warn(string.format("Tried to wait for expiration of non-existent token: %s", err)) + return + end + -- The token will be refreshed if requested within 1 minute of expiration + local expiration = math.floor(maybe_token.expiresAt / 1000) - 60 + local now = os.time() + local timeout = math.max(expiration - now, 0) + + log.debug(string.format("Token will refresh in %d seconds", timeout)) + -- Wait while trying to receive a token event in case it gets updated for some reason. + self:try_wait_for_token_event(timeout) +end + +--- Waits for an oauth endpoint app info event indefinitely. +--- +--- A new info event indicates that `Refresher:determine_action` should be called to check if oauth +--- is now connected. +function Refresher:wait_for_info_event() + local info_sub, err = self.driver:oauth_info_event_subscribe() + if err == "closed" then + self.info_bus_closed = true + end + if info_sub then + info_sub:receive() + end +end + +--- Requests a token then waits for a new token event. +function Refresher:request_token() + local result, err = security.get_sonos_oauth() + if not result then + log.warn(string.format("Failed to request oauth token: %s", err)) + end + -- Try to receive token even if the request failed. + self:try_wait_for_token_event(10) + local maybe_token, _ = self.driver:get_oauth_token() + if maybe_token then + -- token is valid, reset backoff + self.token_backoff = utils.backoff_builder(30 * 60, 5, 0.1) + else + -- We either didn't receive a token or it is not valid. + -- Backoff and maybe we will receive it in that time, or we retry. + cosock.socket.sleep(self.token_backoff()) + end +end + +function module.spawn_token_refresher(driver) + local refresher = setmetatable({ driver = driver, + token_backoff = utils.backoff_builder(30 * 60, 5, 0.1), + }, + Refresher) + cosock.spawn(function () + while true do + -- We can always determine what we should be doing based off the information we have, + -- any action can proceed action depending on what needs to be done. + local action = refresher:determine_action() + log.info(string.format("Token refresher action: %s", ACTION_STRINGIFY[action])) + if action == ACTIONS.WAIT_FOR_CONNECTED then + refresher:wait_for_info_event() + elseif action == ACTIONS.WAIT_FOR_EXPIRE then + refresher:wait_for_expire_or_token_event() + elseif action == ACTIONS.REQUEST_TOKEN then + refresher:request_token() + else + log.error(string.format("Token refresher task exiting due to bad token refresher action: %s", action)) + return + end + if refresher.token_bus_closed or refresher.info_bus_closed then + log.error(string.format("Token refresher task exiting. Token bus closed: %s Info bus close: %s", + refresher.token_bus_closed, refresher.info_bus_closed)) + return + end + end + end, "token refresher task") +end + +return module + + diff --git a/drivers/SmartThings/sonos/src/types.lua b/drivers/SmartThings/sonos/src/types.lua index 65bcb3041f..c404f92a36 100644 --- a/drivers/SmartThings/sonos/src/types.lua +++ b/drivers/SmartThings/sonos/src/types.lua @@ -5,6 +5,9 @@ --- @alias HouseholdId string --- @alias GroupId string +--------- #region Sonos API Types; the following defintions are from the Sonos API +--------- In particular, anything ending in `Object` is an API object. + --- @alias SonosCapabilities ---| "PLAYBACK" # The player can produce audio. You can target it for playback. ---| "CLOUD" # The player can send commands and receive events over the internet. @@ -17,18 +20,18 @@ ---| "SPEAKER_DETECTION" # The component device is capable of detecting connected speaker drivers. ---| "FIXED_VOLUME" # The device supports fixed volume. ---- @class SonosFeatureInfo +--- @class SonosFeatureInfoObject --- @field public _objectType "feature" --- @field public name string ----@class SonosVersionsInfo +---@class SonosVersionsInfoObject ---@field public _objectType "sdkVersions" ---@field public audioTxProtocol { [1]: integer } ---@field public trueplaySdk { [1]: string } ---@field public controlApi { [1]: string } --- Lua representation of the Sonos `deviceInfo` JSON Object: https://developer.sonos.com/build/control-sonos-players-lan/discover-lan/#deviceInfo-object ---- @class SonosDeviceInfo +--- @class SonosDeviceInfoObject --- @field public _objectType "deviceInfo" --- @field public id PlayerId The playerId. Also known as the deviceId. Used to address Sonos devices in the control API. --- @field public primaryDeviceId string Identifies the primary device in bonded sets. Primary devices leave the value blank, which omits the key from the message. The field is expected for secondary devices in stereo pairs and satellites in home theater configurations. @@ -43,13 +46,13 @@ --- @field public softwareVersion string Stores the software version the player is running. --- @field public hwVersion string Stores the hardware version the player is running. The format is: `{vendor}.{model}.{submodel}.{revision}-{region}.` --- @field public swGen integer Stores the software generation that the player is running. ---- @field public versions SonosVersionsInfo ---- @field public features SonosFeatureInfo[] +--- @field public versions SonosVersionsInfoObject +--- @field public features SonosFeatureInfoObject[] --- Lua representation of the Sonos `discoveryInfo` JSON object: https://developer.sonos.com/build/control-sonos-players-lan/discover-lan/#discoveryInfo-object ---- @class SonosDiscoveryInfo +--- @class SonosDiscoveryInfoObject --- @field public _objectType "discoveryInfo" ---- @field public device SonosDeviceInfo The device object. This object presents immutable data that describes a Sonos device. Use this object to uniquely identify any Sonos device. See below for details. +--- @field public device SonosDeviceInfoObject The device object. This object presents immutable data that describes a Sonos device. Use this object to uniquely identify any Sonos device. See below for details. --- @field public householdId HouseholdId An opaque identifier assigned to the device during registration. This field may be missing prior to registration. --- @field public playerId PlayerId The identifier used to address this particular device in the control API. --- @field public groupId GroupId The currently assigned groupId, an ephemeral opaque identifier. This value is always correct, including for group members. @@ -141,12 +144,9 @@ --- @field public softwareVersion string --- @field public websocketUrl string --- @field public capabilities SonosCapabilities[] +--- @field public devices SonosDeviceInfoObject[] ---- Sonos player local state ---- @class PlayerDiscoveryState ---- @field public info_cache SonosDiscoveryInfo Table representation of the JSON returned by the player REST API info endpoint ---- @field public ipv4 string the ipv4 address of the player on the local network ---- @field public is_coordinator boolean whether or not the player was a coordinator (at time of discovery) +--------- #endregion Sonos API Types --- @class SonosSSDPInfo --- Information parsed from Sonos SSDP reply. Contains most of what is needed to uniquely @@ -164,13 +164,7 @@ --- @field public expires_at integer --- @alias SonosFavorites { id: string, name: string }[] ---- @alias DiscoCallback fun(dni: string, ssdp_group_info: SonosSSDPInfo, player_info: SonosDiscoveryInfo, group_info: SonosGroupsResponseBody): boolean? - ----@class SonosFieldCacheTable ----@field public swGen number ----@field public household_id string ----@field public player_id string ----@field public wss_url string +--- @alias DiscoCallback fun(dni: string, ssdp_group_info: SonosSSDPInfo, player_info: SonosDiscoveryInfoObject, group_info: SonosGroupsResponseBody): boolean? --- Sonos Player device --- @class SonosDevice : st.Device @@ -188,5 +182,18 @@ --- @field public emit_event fun(self: SonosDevice, event: any) --- @field public driver SonosDriver +--- @class SonosGroupInfo +--- @field public id GroupId +--- @field public coordinator_id PlayerId +--- @field public player_ids PlayerId[] + +--- @class SonosDeviceInfo +--- @field public id PlayerId +--- @field public primary_device_id PlayerId? + +--- @class SonosPlayerInfo +--- @field public id PlayerId +--- @field public websocket_url string + --- Sonos JSON commands --- @class SonosCommand diff --git a/drivers/SmartThings/sonos/src/utils.lua b/drivers/SmartThings/sonos/src/utils.lua index 5ffb3a8a9d..0b646252a2 100644 --- a/drivers/SmartThings/sonos/src/utils.lua +++ b/drivers/SmartThings/sonos/src/utils.lua @@ -134,7 +134,7 @@ local function __case_insensitive_key_index(tbl, key) fmt_val = key or "" end log.warn_with( - { hub_logs = true }, + { hub_logs = false }, string.format( "Expected `string` key for CaseInsensitiveKeyTable, received (%s: %s)", fmt_val, @@ -157,7 +157,7 @@ local function __case_insensitive_key_newindex(tbl, key, value) fmt_val = key or "" end log.warn_with( - { hub_logs = true }, + { hub_logs = false }, string.format( "Expected `string` key for CaseInsensitiveKeyTable, received (%s: %s)", fmt_val, @@ -179,11 +179,11 @@ function utils.new_case_insensitive_table() return setmetatable({}, _case_insensitive_key_mt) end ----@param sonos_device_info SonosDeviceInfo +---@param sonos_device_info SonosDeviceInfoObject function utils.extract_mac_addr(sonos_device_info) if type(sonos_device_info) ~= "table" or type(sonos_device_info.serialNumber) ~= "string" then log.error_with( - { hub_logs = true }, + { hub_logs = false }, string.format("Bad sonos device info passed to `extract_mac_addr`: %s", sonos_device_info) ) end @@ -319,7 +319,7 @@ function utils.deep_table_eq(tbl1, tbl2) if tbl1 == tbl2 then return true elseif type(tbl1) == "table" and type(tbl2) == "table" then - for key1, value1 in pairs(tbl1) do + for key1, value1 in pairs(tbl1 or {}) do local value2 = tbl2[key1] if value2 == nil then @@ -337,7 +337,7 @@ function utils.deep_table_eq(tbl1, tbl2) end -- check for missing keys in tbl1 - for key2, _ in pairs(tbl2) do + for key2, _ in pairs(tbl2 or {}) do if tbl1[key2] == nil then return false end diff --git a/drivers/SmartThings/zigbee-air-quality-detector/config.yml b/drivers/SmartThings/zigbee-air-quality-detector/config.yml new file mode 100755 index 0000000000..30e9dceaff --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/config.yml @@ -0,0 +1,6 @@ +name: 'Zigbee Air Quality Detector' +packageKey: 'zigbee-air-quality-detector' +permissions: + zigbee: {} +description: "SmartThings driver for Zigbee air quality detector" +vendorSupportInformation: "https://support.smartthings.com" diff --git a/drivers/SmartThings/zigbee-air-quality-detector/fingerprints.yml b/drivers/SmartThings/zigbee-air-quality-detector/fingerprints.yml new file mode 100755 index 0000000000..b20da987e8 --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/fingerprints.yml @@ -0,0 +1,6 @@ +zigbeeManufacturer: + - id: "MultiIR/PMT1006S-SGM-ZTN" + deviceLabel: MultiIR Air Quality Detector + manufacturer: MultiIR + model: PMT1006S-SGM-ZTN + deviceProfileName: air-quality-detector-MultiIR diff --git a/drivers/SmartThings/zigbee-air-quality-detector/profiles/air-quality-detector-MultiIR.yml b/drivers/SmartThings/zigbee-air-quality-detector/profiles/air-quality-detector-MultiIR.yml new file mode 100755 index 0000000000..7369e1158a --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/profiles/air-quality-detector-MultiIR.yml @@ -0,0 +1,38 @@ +name: air-quality-detector-MultiIR +components: + - id: main + capabilities: + - id: airQualityHealthConcern + version: 1 + - id: temperatureMeasurement + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: carbonDioxideMeasurement + version: 1 + - id: carbonDioxideHealthConcern + version: 1 + - id: fineDustSensor + version: 1 + - id: fineDustHealthConcern + version: 1 + - id: veryFineDustSensor + version: 1 + - id: veryFineDustHealthConcern + version: 1 + - id: dustSensor + version: 1 + - id: dustHealthConcern + version: 1 + - id: formaldehydeMeasurement + version: 1 + - id: tvocMeasurement + version: 1 + - id: tvocHealthConcern + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: AirQualityDetector diff --git a/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/custom_clusters.lua b/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/custom_clusters.lua new file mode 100755 index 0000000000..8e8f117f49 --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/custom_clusters.lua @@ -0,0 +1,72 @@ +-- 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 data_types = require "st.zigbee.data_types" + +local custom_clusters = { + carbonDioxide = { + id = 0xFCC3, + mfg_specific_code = 0x1235, + attributes = { + measured_value = { + id = 0x0000, + value_type = data_types.Uint16, + } + } + }, + particulate_matter = { + id = 0xFCC1, + mfg_specific_code = 0x1235, + attributes = { + pm2_5_MeasuredValue = { + id = 0x0000, + value_type = data_types.Uint16, + }, + pm1_0_MeasuredValue = { + id = 0x0001, + value_type = data_types.Uint16, + }, + pm10_MeasuredValue = { + id = 0x0002, + value_type = data_types.Uint16, + } + } + }, + unhealthy_gas = { + id = 0xFCC2, + mfg_specific_code = 0x1235, + attributes = { + CH2O_MeasuredValue = { + id = 0x0000, + value_type = data_types.SinglePrecisionFloat, + }, + tvoc_MeasuredValue = { + id = 0x0001, + value_type = data_types.SinglePrecisionFloat, + } + } + }, + AQI = { + id = 0xFCC5, + mfg_specific_code = 0x1235, + attributes = { + AQI_value = { + id = 0x0000, + value_type = data_types.Uint16, + } + } + } +} + +return custom_clusters diff --git a/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/init.lua b/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/init.lua new file mode 100755 index 0000000000..3574c06e59 --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/init.lua @@ -0,0 +1,152 @@ +-- 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 capabilities = require "st.capabilities" +local custom_clusters = require "MultiIR/custom_clusters" +local cluster_base = require "st.zigbee.cluster_base" + +local RelativeHumidity = clusters.RelativeHumidity +local TemperatureMeasurement = clusters.TemperatureMeasurement + +local MultiIR_SENSOR_FINGERPRINTS = { + { mfr = "MultiIR", model = "PMT1006S-SGM-ZTN" }--This is not a sleep end device +} + +local function can_handle_MultiIR_sensor(opts, driver, device) + for _, fingerprint in ipairs(MultiIR_SENSOR_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + +local function send_read_attr_request(device, cluster, attr) + device:send( + cluster_base.read_manufacturer_specific_attribute( + device, + cluster.id, + attr.id, + cluster.mfg_specific_code + ) + ) +end + +local function do_refresh(driver, device) + device:send(RelativeHumidity.attributes.MeasuredValue:read(device):to_endpoint(0x01)) + device:send(TemperatureMeasurement.attributes.MeasuredValue:read(device):to_endpoint(0x01)) + send_read_attr_request(device, custom_clusters.particulate_matter, custom_clusters.particulate_matter.attributes.pm2_5_MeasuredValue) + send_read_attr_request(device, custom_clusters.particulate_matter, custom_clusters.particulate_matter.attributes.pm1_0_MeasuredValue) + send_read_attr_request(device, custom_clusters.particulate_matter, custom_clusters.particulate_matter.attributes.pm10_MeasuredValue) + send_read_attr_request(device, custom_clusters.unhealthy_gas, custom_clusters.unhealthy_gas.attributes.CH2O_MeasuredValue) + send_read_attr_request(device, custom_clusters.unhealthy_gas, custom_clusters.unhealthy_gas.attributes.tvoc_MeasuredValue) + send_read_attr_request(device, custom_clusters.carbonDioxide, custom_clusters.carbonDioxide.attributes.measured_value) + send_read_attr_request(device, custom_clusters.AQI, custom_clusters.AQI.attributes.AQI_value) +end + +local function airQualityHealthConcern_attr_handler(driver, device, value, zb_rx) + local airQuality_level = "good" + if value.value >= 51 then + airQuality_level = "moderate" + end + if value.value >= 101 then + airQuality_level = "slightlyUnhealthy" + end + if value.value >= 151 then + airQuality_level = "unhealthy" + end + if value.value >= 201 then + airQuality_level = "veryUnhealthy" + end + if value.value >= 301 then + airQuality_level = "hazardous" + end + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.airQualityHealthConcern.airQualityHealthConcern({value = airQuality_level})) +end + +local function carbonDioxide_attr_handler(driver, device, value, zb_rx) + local level = "unhealthy" + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.carbonDioxideMeasurement.carbonDioxide({value = value.value, unit = "ppm"})) + if value.value <= 1500 then + level = "good" + elseif value.value >= 1501 and value.value <= 2500 then + level = "moderate" + end + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern({value = level})) +end + +local function particulate_matter_attr_handler(cap,Concern,good,bad) + return function(driver, device, value, zb_rx) + local level = "unhealthy" + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, cap({value = value.value})) + if value.value <= good then + level = "good" + elseif bad > 0 and value.value > good and value.value < bad then + level = "moderate" + end + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, Concern({value = level})) + end +end + +local function CH2O_attr_handler(driver, device, value, zb_rx) + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.formaldehydeMeasurement.formaldehydeLevel({value = value.value, unit = "mg/m^3"})) +end + +local function tvoc_attr_handler(driver, device, value, zb_rx) + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.tvocMeasurement.tvocLevel({value = value.value, unit = "ug/m3"})) + local level = "unhealthy" + if value.value < 600.0 then + level = "good" + end + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.tvocHealthConcern.tvocHealthConcern({value = level})) +end + +local function added_handler(self, device) + do_refresh() +end + +local MultiIR_sensor = { + NAME = "MultiIR air quality detector", + lifecycle_handlers = { + added = added_handler + }, + zigbee_handlers = { + attr = { + [custom_clusters.carbonDioxide.id] = { + [custom_clusters.carbonDioxide.attributes.measured_value.id] = carbonDioxide_attr_handler + }, + [custom_clusters.particulate_matter.id] = { + [custom_clusters.particulate_matter.attributes.pm2_5_MeasuredValue.id] = particulate_matter_attr_handler(capabilities.fineDustSensor.fineDustLevel,capabilities.fineDustHealthConcern.fineDustHealthConcern,75,115),--75 115 is a comparative value of good moderate unhealthy, and 0 is no comparison + [custom_clusters.particulate_matter.attributes.pm1_0_MeasuredValue.id] = particulate_matter_attr_handler(capabilities.veryFineDustSensor.veryFineDustLevel,capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern,100,0), + [custom_clusters.particulate_matter.attributes.pm10_MeasuredValue.id] = particulate_matter_attr_handler(capabilities.dustSensor.dustLevel,capabilities.dustHealthConcern.dustHealthConcern,150,0) + }, + [custom_clusters.unhealthy_gas.id] = { + [custom_clusters.unhealthy_gas.attributes.CH2O_MeasuredValue.id] = CH2O_attr_handler, + [custom_clusters.unhealthy_gas.attributes.tvoc_MeasuredValue.id] = tvoc_attr_handler + }, + [custom_clusters.AQI.id] = { + [custom_clusters.AQI.attributes.AQI_value.id] = airQualityHealthConcern_attr_handler + } + } + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh, + } + }, + can_handle = can_handle_MultiIR_sensor +} + +return MultiIR_sensor diff --git a/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua b/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua new file mode 100755 index 0000000000..993ba2a96d --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua @@ -0,0 +1,41 @@ +-- 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 ZigbeeDriver = require "st.zigbee" +local capabilities = require "st.capabilities" +local defaults = require "st.zigbee.defaults" + +local zigbee_air_quality_detector_template = { + supported_capabilities = { + capabilities.airQualityHealthConcern, + capabilities.temperatureMeasurement, + capabilities.relativeHumidityMeasurement, + capabilities.carbonDioxideMeasurement, + capabilities.carbonDioxideHealthConcern, + capabilities.fineDustSensor, + capabilities.fineDustHealthConcern, + capabilities.veryFineDustSensor, + capabilities.veryFineDustHealthConcern, + capabilities.dustSensor, + capabilities.dustHealthConcern, + capabilities.formaldehydeMeasurement, + capabilities.tvocMeasurement, + capabilities.tvocHealthConcern + }, + sub_drivers = { require("MultiIR") } +} + +defaults.register_for_default_handlers(zigbee_air_quality_detector_template, zigbee_air_quality_detector_template.supported_capabilities) +local zigbee_air_quality_detector_driver = ZigbeeDriver("zigbee-air-quality-detector", zigbee_air_quality_detector_template) +zigbee_air_quality_detector_driver:run() diff --git a/drivers/SmartThings/zigbee-air-quality-detector/src/test/test_MultiIR_air_quality_detector.lua b/drivers/SmartThings/zigbee-air-quality-detector/src/test/test_MultiIR_air_quality_detector.lua new file mode 100755 index 0000000000..09f539b53b --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/src/test/test_MultiIR_air_quality_detector.lua @@ -0,0 +1,424 @@ +-- 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. + +-- Mock out globals +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" +local SinglePrecisionFloat = require "st.zigbee.data_types.SinglePrecisionFloat" + +local profile_def = t_utils.get_profile_definition("air-quality-detector-MultiIR.yml") +local MFG_CODE = 0x1235 + +local mock_device = test.mock_device.build_test_zigbee_device( +{ + label = "air quality detector", + profile = profile_def, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "MultiIR", + model = "PMT1006S-SGM-ZTN", + server_clusters = { 0x0000, 0x0402,0x0405,0xFCC1, 0xFCC2,0xFCC3,0xFCC5} + } + } +}) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) + zigbee_test_utils.init_noop_health_check_timer() +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "capability - refresh", + function() + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } }) + local read_RelativeHumidity_messge = clusters.RelativeHumidity.attributes.MeasuredValue:read(mock_device) + local read_TemperatureMeasurement_messge = clusters.TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + local read_pm2_5_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC1, 0x0000, MFG_CODE) + local read_pm1_0_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC1, 0x0001, MFG_CODE) + local read_pm10_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC1, 0x0002, MFG_CODE) + local read_ch2o_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC2, 0x0000, MFG_CODE) + local read_tvoc_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC2, 0x0001, MFG_CODE) + local read_carbonDioxide_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC3, 0x0000, MFG_CODE) + local read_AQI_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC5, 0x0000, MFG_CODE) + + test.socket.zigbee:__expect_send({mock_device.id, read_RelativeHumidity_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_TemperatureMeasurement_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_pm2_5_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_pm1_0_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_pm10_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_ch2o_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_tvoc_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_carbonDioxide_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_AQI_messge}) + end +) + +test.register_message_test( + "Relative humidity reports should generate correct messages", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + clusters.RelativeHumidity.attributes.MeasuredValue:build_test_attr_report(mock_device, 40*100) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 40 })) + } + } +) + +test.register_message_test( + "Temperature reports should generate correct messages", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + } + } +) + +test.register_coroutine_test( + "Device reported carbonDioxide and driver emit carbonDioxide and carbonDioxideHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 1400 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC3, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.carbonDioxideMeasurement.carbonDioxide({value = 1400, unit = "ppm"}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern({value = "good"}))) + end +) + +test.register_coroutine_test( + "Device reported pm2.5 and driver emit pm2.5 and fineDustHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 74 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.fineDustSensor.fineDustLevel({value = 74 }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.fineDustHealthConcern.fineDustHealthConcern.good())) + end +) + +test.register_coroutine_test( + "Device reported pm1.0 and driver emit pm1.0 and veryFineDustHealthConcern", + function() + local attr_report_data = { + { 0x0001, data_types.Uint16.ID, 69 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.veryFineDustSensor.veryFineDustLevel({value = 69 }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern.good())) + end +) + +test.register_coroutine_test( + "Device reported pm10 and driver emit pm10 and dustHealthConcern", + function() + local attr_report_data = { + { 0x0002, data_types.Uint16.ID, 69 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.dustSensor.dustLevel({value = 69 }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.dustHealthConcern.dustHealthConcern.good())) + end +) + +test.register_coroutine_test( + "Device reported ch2o and driver emit ch2o", + function() + local attr_report_data = { + { 0x0000, data_types.SinglePrecisionFloat.ID, SinglePrecisionFloat(0, 9, 0.953125) } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC2, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.formaldehydeMeasurement.formaldehydeLevel({value = 1000.0, unit = "mg/m^3"}))) + end +) + +test.register_coroutine_test( + "Device reported tvoc and driver emit tvoc", + function() + local attr_report_data = { + { 0x0001, data_types.SinglePrecisionFloat.ID, SinglePrecisionFloat(0, 9, 0.953125) } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC2, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tvocMeasurement.tvocLevel({value = 1000.0, unit = "ug/m3"}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tvocHealthConcern.tvocHealthConcern({value = "unhealthy"}))) + end +) + +test.register_coroutine_test( + "Device reported AQI and driver emit airQualityHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 50 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC5, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "good"}))) + end +) + +test.register_coroutine_test( + "AQI moderate (51-100) emits moderate airQualityHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 75 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC5, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "moderate"}))) + end +) + +test.register_coroutine_test( + "AQI slightlyUnhealthy (101-150) emits slightlyUnhealthy airQualityHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 125 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC5, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "slightlyUnhealthy"}))) + end +) + +test.register_coroutine_test( + "AQI unhealthy (151-200) emits unhealthy airQualityHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 175 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC5, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "unhealthy"}))) + end +) + +test.register_coroutine_test( + "AQI veryUnhealthy (201-300) emits veryUnhealthy airQualityHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 250 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC5, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "veryUnhealthy"}))) + end +) + +test.register_coroutine_test( + "AQI hazardous (>=301) emits hazardous airQualityHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 350 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC5, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "hazardous"}))) + end +) + +test.register_coroutine_test( + "carbonDioxide moderate (1501-2500) emits moderate carbonDioxideHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 2000 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC3, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.carbonDioxideMeasurement.carbonDioxide({value = 2000, unit = "ppm"}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern({value = "moderate"}))) + end +) + +test.register_coroutine_test( + "carbonDioxide unhealthy (>2500) emits unhealthy carbonDioxideHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 3000 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC3, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.carbonDioxideMeasurement.carbonDioxide({value = 3000, unit = "ppm"}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern({value = "unhealthy"}))) + end +) + +test.register_coroutine_test( + "pm2.5 moderate emits moderate fineDustHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 90 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.fineDustSensor.fineDustLevel({value = 90}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.fineDustHealthConcern.fineDustHealthConcern({value = "moderate"}))) + end +) + +test.register_coroutine_test( + "pm2.5 unhealthy (>=115) emits unhealthy fineDustHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 120 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.fineDustSensor.fineDustLevel({value = 120}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.fineDustHealthConcern.fineDustHealthConcern({value = "unhealthy"}))) + end +) + +test.register_coroutine_test( + "pm1.0 unhealthy (>100) emits unhealthy veryFineDustHealthConcern", + function() + local attr_report_data = { + { 0x0001, data_types.Uint16.ID, 150 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.veryFineDustSensor.veryFineDustLevel({value = 150}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern({value = "unhealthy"}))) + end +) + +test.register_coroutine_test( + "pm10 unhealthy (>150) emits unhealthy dustHealthConcern", + function() + local attr_report_data = { + { 0x0002, data_types.Uint16.ID, 200 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.dustSensor.dustLevel({value = 200}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.dustHealthConcern.dustHealthConcern({value = "unhealthy"}))) + end +) + +test.register_coroutine_test( + "tvoc good (<600) emits good tvocHealthConcern", + function() + local attr_report_data = { + { 0x0001, data_types.SinglePrecisionFloat.ID, SinglePrecisionFloat(0, 8, 0.953125) } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC2, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tvocMeasurement.tvocLevel({value = 500.0, unit = "ug/m3"}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tvocHealthConcern.tvocHealthConcern({value = "good"}))) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-bed/src/init.lua b/drivers/SmartThings/zigbee-bed/src/init.lua index d59cf4e012..9f464c38ce 100755 --- a/drivers/SmartThings/zigbee-bed/src/init.lua +++ b/drivers/SmartThings/zigbee-bed/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -20,9 +10,7 @@ local zigbee_bed_template = { supported_capabilities = { capabilities.refresh, }, - sub_drivers = { - require("shus-mattress"), - }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-bed/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-bed/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-bed/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-bed/src/shus-mattress/can_handle.lua b/drivers/SmartThings/zigbee-bed/src/shus-mattress/can_handle.lua new file mode 100644 index 0000000000..0adcc5e46e --- /dev/null +++ b/drivers/SmartThings/zigbee-bed/src/shus-mattress/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_shus_products(opts, driver, device) + local FINGERPRINTS = require("shus-mattress.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("shus-mattress") + end + end + return false +end + +return is_shus_products diff --git a/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_capabilities.lua b/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_capabilities.lua index 2c3dab5292..fdd5ab0004 100755 --- a/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_capabilities.lua +++ b/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_capabilities.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_clusters.lua b/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_clusters.lua index 35baddbaa9..db220da460 100755 --- a/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_clusters.lua +++ b/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_clusters.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local data_types = require "st.zigbee.data_types" diff --git a/drivers/SmartThings/zigbee-bed/src/shus-mattress/fingerprints.lua b/drivers/SmartThings/zigbee-bed/src/shus-mattress/fingerprints.lua new file mode 100644 index 0000000000..2d6c80ee1a --- /dev/null +++ b/drivers/SmartThings/zigbee-bed/src/shus-mattress/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "SHUS", model = "SX-1" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua b/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua index c11354a8d2..65cf64e446 100755 --- a/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua +++ b/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua @@ -1,25 +1,12 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cluster_base = require "st.zigbee.cluster_base" local custom_clusters = require "shus-mattress/custom_clusters" local custom_capabilities = require "shus-mattress/custom_capabilities" -local FINGERPRINTS = { - { mfr = "SHUS", model = "SX-1" } -} -- ############################# -- # Attribute handlers define # @@ -36,7 +23,7 @@ end local function process_control_attr_factory(cmd) return function(driver, device, value, zb_rx) - device:emit_event(cmd.idle()) + device:emit_event(cmd("idle", { visibility = { displayed = false }})) end end @@ -129,7 +116,7 @@ local function process_capabilities_hardness_factory(cap,attrs,cap_attr) ) --A button that can be triggered continuously local evt_ctrl = cap_attr.soft() - local evt_idle = cap_attr.idle() + local evt_idle = cap_attr("idle", { visibility = { displayed = false }}) if cmd.args[cap] == "hard" then evt_ctrl = cap_attr.hard() end @@ -148,21 +135,13 @@ local function device_init(driver, device) end local function device_added(driver, device) - device:emit_event(custom_capabilities.yoga.supportedYogaState({"stop", "left", "right"})) + device:emit_event(custom_capabilities.yoga.supportedYogaState({"stop", "left", "right"}, { visibility = { displayed = false }})) do_refresh(driver, device) end local function do_configure(driver, device) end -local function is_shus_products(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end -- ################# -- # Handlers bind # @@ -229,7 +208,7 @@ local shus_smart_mattress = { ["stateControl"] = process_capabilities_factory("stateControl","yoga") } }, - can_handle = is_shus_products + can_handle = require("shus-mattress.can_handle"), } return shus_smart_mattress diff --git a/drivers/SmartThings/zigbee-bed/src/sub_drivers.lua b/drivers/SmartThings/zigbee-bed/src/sub_drivers.lua new file mode 100644 index 0000000000..3e78841f3b --- /dev/null +++ b/drivers/SmartThings/zigbee-bed/src/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("shus-mattress"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua b/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua index 0ba0441719..a3227b04bc 100755 --- a/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua +++ b/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -59,7 +48,7 @@ test.register_coroutine_test( function() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.yoga.supportedYogaState({"stop", "left", "right"}) )) + custom_capabilities.yoga.supportedYogaState({"stop", "left", "right"}, { visibility = { displayed = false }}) )) local read_0x0006_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0006, MFG_CODE) local read_0x0007_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0007, MFG_CODE) local read_0x0009_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0009, MFG_CODE) @@ -140,7 +129,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "Device reported leftback 0 and driver emit custom_capabilities.left_control.leftback.idle()", + "Device reported leftback 0 and driver emit custom_capabilities.left_control.leftback.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0000, data_types.Uint8.ID, 0 } @@ -150,12 +139,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.leftback.idle())) + custom_capabilities.left_control.leftback.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported leftback 1 and driver emit custom_capabilities.left_control.leftback.idle()", + "Device reported leftback 1 and driver emit custom_capabilities.left_control.leftback.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0000, data_types.Uint8.ID, 1 } @@ -165,12 +154,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.leftback.idle())) + custom_capabilities.left_control.leftback.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported leftwaist 0 and driver emit custom_capabilities.left_control.leftwaist.idle()", + "Device reported leftwaist 0 and driver emit custom_capabilities.left_control.leftwaist.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0001, data_types.Uint8.ID, 0 } @@ -180,12 +169,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.leftwaist.idle())) + custom_capabilities.left_control.leftwaist.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported leftwaist 1 and driver emit custom_capabilities.left_control.leftwaist.idle()", + "Device reported leftwaist 1 and driver emit custom_capabilities.left_control.leftwaist.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0001, data_types.Uint8.ID, 1 } @@ -195,12 +184,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.leftwaist.idle())) + custom_capabilities.left_control.leftwaist.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported lefthip 0 and driver emit custom_capabilities.left_control.lefthip.idle()", + "Device reported lefthip 0 and driver emit custom_capabilities.left_control.lefthip.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0002, data_types.Uint8.ID, 0 } @@ -210,12 +199,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.lefthip.idle())) + custom_capabilities.left_control.lefthip.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported lefthip 1 and driver emit custom_capabilities.left_control.lefthip.idle()", + "Device reported lefthip 1 and driver emit custom_capabilities.left_control.lefthip.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0002, data_types.Uint8.ID, 1 } @@ -225,12 +214,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.lefthip.idle())) + custom_capabilities.left_control.lefthip.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported rightback 0 and driver emit custom_capabilities.right_control.rightback.idle()", + "Device reported rightback 0 and driver emit custom_capabilities.right_control.rightback.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0003, data_types.Uint8.ID, 0 } @@ -240,12 +229,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.rightback.idle())) + custom_capabilities.right_control.rightback.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported rightback 1 and driver emit custom_capabilities.right_control.rightback.idle()", + "Device reported rightback 1 and driver emit custom_capabilities.right_control.rightback.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0003, data_types.Uint8.ID, 1 } @@ -255,12 +244,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.rightback.idle())) + custom_capabilities.right_control.rightback.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported rightwaist 0 and driver emit custom_capabilities.right_control.rightwaist.idle()", + "Device reported rightwaist 0 and driver emit custom_capabilities.right_control.rightwaist.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0004, data_types.Uint8.ID, 0 } @@ -270,12 +259,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.rightwaist.idle())) + custom_capabilities.right_control.rightwaist.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported rightwaist 1 and driver emit custom_capabilities.right_control.rightwaist.idle()", + "Device reported rightwaist 1 and driver emit custom_capabilities.right_control.rightwaist.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0004, data_types.Uint8.ID, 1 } @@ -285,12 +274,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.rightwaist.idle())) + custom_capabilities.right_control.rightwaist.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported righthip 0 and driver emit custom_capabilities.right_control.righthip.idle()", + "Device reported righthip 0 and driver emit custom_capabilities.right_control.righthip.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0005, data_types.Uint8.ID, 0 } @@ -300,12 +289,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.righthip.idle())) + custom_capabilities.right_control.righthip.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported righthip 1 and driver emit custom_capabilities.right_control.righthip.idle()", + "Device reported righthip 1 and driver emit custom_capabilities.right_control.righthip.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0005, data_types.Uint8.ID, 1 } @@ -315,7 +304,7 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.righthip.idle())) + custom_capabilities.right_control.righthip.idle({ visibility = { displayed = false }}))) end ) @@ -409,6 +398,21 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Device reported yoga 3 and driver emit custom_capabilities.yoga.state.both()", + function() + local attr_report_data = { + { 0x0008, data_types.Uint8.ID, 3 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + custom_capabilities.yoga.state.both())) + end +) + test.register_coroutine_test( "Device reported yoga 2 and driver emit custom_capabilities.yoga.state.right()", function() @@ -921,4 +925,26 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "capability left_control backControl soft emits idle event after delay", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = custom_capabilities.left_control.ID, component = "main", command ="backControl" , args = {"soft"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + 0x0000, MFG_CODE, data_types.Uint8, 0) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + custom_capabilities.left_control.leftback.soft())) + test.wait_for_events() + + test.mock_time.advance_time(1) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + custom_capabilities.left_control.leftback("idle", { visibility = { displayed = false }}))) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-button/fingerprints.yml b/drivers/SmartThings/zigbee-button/fingerprints.yml index 422901141f..c73109ebca 100644 --- a/drivers/SmartThings/zigbee-button/fingerprints.yml +++ b/drivers/SmartThings/zigbee-button/fingerprints.yml @@ -8,12 +8,32 @@ zigbeeManufacturer: deviceLabel: Aqara Wireless Mini Switch T1 manufacturer: LUMI model: lumi.remote.b1acn02 - deviceProfileName: one-button-battery + deviceProfileName: one-button-batteryLevel - id: "LUMI/lumi.remote.acn003" deviceLabel: Aqara Wireless Remote Switch E1 (Single Rocker) manufacturer: LUMI model: lumi.remote.acn003 - deviceProfileName: one-button-battery + deviceProfileName: one-button-batteryLevel + - id: "LUMI/lumi.remote.b186acn03" + deviceLabel: Aqara Wireless Remote Switch T1 (Single Rocker) + manufacturer: LUMI + model: lumi.remote.b186acn03 + deviceProfileName: one-button-batteryLevel + - id: "LUMI/lumi.remote.b286acn03" + deviceLabel: Aqara Wireless Remote Switch T1 (Double Rocker) + manufacturer: LUMI + model: lumi.remote.b286acn03 + deviceProfileName: aqara-double-buttons + - id: "LUMI/lumi.remote.b18ac1" + deviceLabel: Aqara Wireless Remote Switch H1 (Single Rocker) + manufacturer: LUMI + model: lumi.remote.b18ac1 + deviceProfileName: aqara-single-button-mode + - id: "LUMI/lumi.remote.b28ac1" + deviceLabel: Aqara Wireless Remote Switch H1 (Double Rocker) + manufacturer: LUMI + model: lumi.remote.b28ac1 + deviceProfileName: aqara-double-buttons-mode - id: "HEIMAN/SOS-EM" deviceLabel: HEIMAN Button manufacturer: HEIMAN @@ -23,7 +43,12 @@ zigbeeManufacturer: deviceLabel: frient Button manufacturer: frient A/S model: MBTZB-110 - deviceProfileName: button-profile + deviceProfileName: button-profile-frient + - id: "frient A/S/SBTZB-110" + deviceLabel: frient Button + manufacturer: frient A/S + model: SBTZB-110 + deviceProfileName: button-profile-frient - id: "LDS/ZBT-CCTSwitch-D0001" deviceLabel: EcoSmart Remote Control manufacturer: LDS @@ -195,7 +220,7 @@ zigbeeManufacturer: model: WB01 deviceProfileName: one-button-battery - id: eWeLink/SNZB-01P - deviceLabel: eWeLink Button + deviceLabel: Zigbee Wireless Switch manufacturer: eWeLink model: SNZB-01P deviceProfileName: one-button-battery diff --git a/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons-mode.yml b/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons-mode.yml new file mode 100644 index 0000000000..f19f19da57 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons-mode.yml @@ -0,0 +1,35 @@ +name: aqara-double-buttons-mode +components: + - id: main + capabilities: + - id: button + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button1 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: all + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +preferences: + - preferenceId: stse.allowOperationModeChange + explicit: true diff --git a/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons.yml b/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons.yml new file mode 100644 index 0000000000..07a4882783 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons.yml @@ -0,0 +1,32 @@ +name: aqara-double-buttons +components: + - id: main + capabilities: + - id: button + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button1 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: all + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/zigbee-button/profiles/aqara-single-button-mode.yml b/drivers/SmartThings/zigbee-button/profiles/aqara-single-button-mode.yml new file mode 100644 index 0000000000..904de72281 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/aqara-single-button-mode.yml @@ -0,0 +1,17 @@ +name: aqara-single-button-mode +components: + - id: main + capabilities: + - id: button + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Button +preferences: + - preferenceId: stse.allowOperationModeChange + explicit: true diff --git a/drivers/SmartThings/zigbee-button/profiles/button-profile-frient.yml b/drivers/SmartThings/zigbee-button/profiles/button-profile-frient.yml new file mode 100644 index 0000000000..c817709b4d --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/button-profile-frient.yml @@ -0,0 +1,46 @@ +name: button-profile-frient +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController +preferences: + - title: "LED Color" + name: ledColor + description: "Color of LED when button is pressed (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: enumeration + definition: + options: + 0: "Off" + 1: "Red" + 2: "Green" + 3: "Yellow" + default: "2" + - title: "Button press delay (ms)" + name: buttonDelay + description: "Delay before button press is registered in milliseconds (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 8000 + default: 100 + - title: "Use as Panic Button" + name: panicButton + description: "Use as Panic Button (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting. May take up to 1 minute to reconfigure the device)." + required: false + preferenceType: enumeration + definition: + options: + 0xFFFF: "Off" + 0x002C: "On" + default: "0xFFFF" \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-button/profiles/button-profile-panic-frient.yml b/drivers/SmartThings/zigbee-button/profiles/button-profile-panic-frient.yml new file mode 100644 index 0000000000..61bb0d0add --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/button-profile-panic-frient.yml @@ -0,0 +1,85 @@ +name: button-profile-panic-frient +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + - id: panicAlarm + version: 1 + categories: + - name: RemoteController +preferences: + - title: "LED Color" + name: ledColor + description: "Color of LED when button is pressed (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: enumeration + definition: + options: + 0: "Off" + 1: "Red" + 2: "Green" + 3: "Yellow" + default: "2" + - title: "Button press delay (ms)" + name: buttonDelay + description: "Delay before button press is registered in milliseconds (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 8000 + default: 100 + - title: "Use as Panic Button" + name: panicButton + description: "Use as Panic Button (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting. May take up to 1 minute to reconfigure the device)." + required: false + preferenceType: enumeration + definition: + options: + 0xFFFF: "Off" + 0x002C: "On" + default: "0x002C" + - title: "Hold to activate alarm (ms)" + name: buttonAlarmDelay + description: "Delay before button press is registered as alarm in milliseconds (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 8000 + default: 2000 + - title: "Hold to cancel alarm (ms)" + name: buttonCancelDelay + description: "Delay before button press cancels alarm in milliseconds (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 8000 + default: 2000 + - title: "Auto alarm cancel (s)" + name: autoCancel + description: "Set the time the alarm will be automatically canceled in seconds (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65536 + default: 10 + - title: "Alarm LED behavior" + name: alarmBehavior + description: "Choose the behavior of the LED when the alarm is triggered (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: enumeration + definition: + options: + 0: "Immediate" + 1: "Wait for confirmation" + default: "0" diff --git a/drivers/SmartThings/zigbee-button/profiles/one-button-batteryLevel.yml b/drivers/SmartThings/zigbee-button/profiles/one-button-batteryLevel.yml new file mode 100644 index 0000000000..005e41256f --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/one-button-batteryLevel.yml @@ -0,0 +1,14 @@ +name: one-button-batteryLevel +components: + - id: main + capabilities: + - id: button + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Button diff --git a/drivers/SmartThings/zigbee-button/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-button/src/aqara/can_handle.lua new file mode 100644 index 0000000000..3dc96661cd --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/aqara/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_aqara_products = function(opts, driver, device) + local FINGERPRINTS = require "aqara.fingerprints" + if FINGERPRINTS[device:get_model()] and FINGERPRINTS[device:get_model()].mfr == device:get_manufacturer() then + return true, require("aqara") + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-button/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..83f7c77050 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/aqara/fingerprints.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + ["lumi.remote.b1acn02"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2032", quantity = 1 }, -- Aqara Wireless Mini Switch T1 + ["lumi.remote.acn003"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2032", quantity = 1 }, -- Aqara Wireless Remote Switch E1 (Single Rocker) + ["lumi.remote.b186acn03"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2032", quantity = 1 }, -- Aqara Wireless Remote Switch T1 (Single Rocker) + ["lumi.remote.b286acn03"] = { mfr = "LUMI", btn_cnt = 3, type = "CR2032", quantity = 1 }, -- Aqara Wireless Remote Switch T1 (Double Rocker) + ["lumi.remote.b18ac1"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2450", quantity = 1 }, -- Aqara Wireless Remote Switch H1 (Single Rocker) + ["lumi.remote.b28ac1"] = { mfr = "LUMI", btn_cnt = 3, type = "CR2450", quantity = 1 } -- Aqara Wireless Remote Switch H1 (Double Rocker) +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/aqara/init.lua b/drivers/SmartThings/zigbee-button/src/aqara/init.lua index 5fd1076b4a..b9545e4721 100644 --- a/drivers/SmartThings/zigbee-button/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-button/src/aqara/init.lua @@ -1,102 +1,187 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local battery_defaults = require "st.zigbee.defaults.battery_defaults" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" local capabilities = require "st.capabilities" +local button_utils = require "button_utils" +local MODE = "devicemode" +local MODE_CHANGE = "stse.allowOperationModeChange" +local SUPPORTED_BUTTON = { { "pushed" }, { "pushed", "held", "double" } } local PowerConfiguration = clusters.PowerConfiguration local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID_T1 = 0x0009 local PRIVATE_ATTRIBUTE_ID_E1 = 0x0125 +local PRIVATE_ATTRIBUTE_ID_ALIVE = 0x00F7 local MFG_CODE = 0x115F local MULTISTATE_INPUT_CLUSTER_ID = 0x0012 local PRESENT_ATTRIBUTE_ID = 0x0055 -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.remote.b1acn02" }, - { mfr = "LUMI", model = "lumi.remote.acn003" } -} +local COMP_LIST = { "button1", "button2", "all" } +local FINGERPRINTS = require "aqara.fingerprints" local configuration = { - { - cluster = MULTISTATE_INPUT_CLUSTER_ID, - attribute = PRESENT_ATTRIBUTE_ID, - minimum_interval = 3, - maximum_interval = 7200, - data_type = data_types.Uint16, - reportable_change = 1 - }, - { - cluster = PowerConfiguration.ID, - attribute = PowerConfiguration.attributes.BatteryVoltage.ID, - minimum_interval = 30, - maximum_interval = 3600, - data_type = PowerConfiguration.attributes.BatteryVoltage.base_type, - reportable_change = 1 - } + { + cluster = MULTISTATE_INPUT_CLUSTER_ID, + attribute = PRESENT_ATTRIBUTE_ID, + minimum_interval = 3, + maximum_interval = 7200, + data_type = data_types.Uint16, + reportable_change = 1 + }, + { + cluster = PowerConfiguration.ID, + attribute = PowerConfiguration.attributes.BatteryVoltage.ID, + minimum_interval = 30, + maximum_interval = 3600, + data_type = PowerConfiguration.attributes.BatteryVoltage.base_type, + reportable_change = 1 + } } local function present_value_attr_handler(driver, device, value, zb_rx) + if value.value < 0xFF then + local end_point = zb_rx.address_header.src_endpoint.value + local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 + local evt = capabilities.button.button.held({ state_change = true }) if value.value == 1 then - device:emit_event(capabilities.button.button.pushed({state_change = true})) + evt = capabilities.button.button.pushed({ state_change = true }) elseif value.value == 2 then - device:emit_event(capabilities.button.button.double({state_change = true})) - elseif value.value == 0 then - device:emit_event(capabilities.button.button.held({state_change = true})) + evt = capabilities.button.button.double({ state_change = true }) + end + device:emit_event(evt) + if btn_evt_cnt > 1 then + device:emit_component_event(device.profile.components[COMP_LIST[end_point]], evt) end + end +end + +local function calc_battery_percentage(voltage) + local millivolt = voltage * 100 + local percentage = 0 + if millivolt >= 3000 then + percentage = 100 + elseif millivolt >= 2600 then + local fVoltage = (millivolt * millivolt) * 0.00045; + percentage = fVoltage - 2.277 * millivolt + 2880 + end + + return math.floor(percentage) +end + +local function battery_level_handler(driver, device, value, zb_rx) + local voltage = value.value + local batteryLevel = "normal" + + if voltage <= 25 then + batteryLevel = "critical" + elseif voltage < 28 then + batteryLevel = "warning" + end + + -- Note that all aqara buttons use batteryLevel and not battery capability. + if device:supports_capability_by_id(capabilities.battery.ID) then + device:emit_event(capabilities.battery.battery(calc_battery_percentage(voltage))) + elseif device:supports_capability_by_id(capabilities.batteryLevel.ID) then + device:emit_event(capabilities.batteryLevel.battery(batteryLevel)) + end end -local is_aqara_products = function(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true +local function mode_switching_handler(driver, device, value, zb_rx) + local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 + local allow = device.preferences[MODE_CHANGE] or false + if allow then + local mode = device:get_field(MODE) or 1 + mode = 3 - mode + device:set_field(MODE, mode, { persist = true }) + device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, + MFG_CODE, data_types.Uint8, mode)) + device:emit_event(capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], + { visibility = { displayed = false } })) + device:emit_event(capabilities.button.numberOfButtons({ value = 1 })) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, + capabilities.button.button.pushed({ state_change = false })) + if btn_evt_cnt > 1 then + for i = 1, btn_evt_cnt do + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], + { visibility = { displayed = false } })) + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.numberOfButtons({ value = 1 })) + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.button.pushed({ state_change = false })) + button_utils.emit_event_if_latest_state_missing(device, COMP_LIST[i], capabilities.button, + capabilities.button.button.NAME, capabilities.button.button.pushed({ state_change = false })) end end - return false + end end local function device_init(driver, device) - battery_defaults.build_linear_voltage_init(2.6, 3.0)(driver, device) - if configuration ~= nil then - for _, attribute in ipairs(configuration) do - device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) - end + battery_defaults.build_linear_voltage_init(2.6, 3.0)(driver, device) + if configuration ~= nil then + for _, attribute in ipairs(configuration) do + device:add_configured_attribute(attribute) end + end end local function added_handler(self, device) - device:emit_event(capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.numberOfButtons({value = 1})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) - device:emit_event(capabilities.battery.battery(100)) + local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 + local mode = device:get_field(MODE) or 0 + local model = device:get_model() + local type = FINGERPRINTS[device:get_model()].type or "CR2032" + local quantity = FINGERPRINTS[device:get_model()].quantity or 1 + + if mode == 0 then + if model == "lumi.remote.b18ac1" or model == "lumi.remote.b28ac1" then + mode = 1 + else + mode = 2 + end + end + device:set_field(MODE, mode, { persist = true }) + device:emit_event(capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], + { visibility = { displayed = false } })) + device:emit_event(capabilities.button.numberOfButtons({ value = 1 })) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, + capabilities.button.button.pushed({ state_change = false })) + device:emit_event(capabilities.batteryLevel.battery.normal()) + device:emit_event(capabilities.batteryLevel.type(type)) + device:emit_event(capabilities.batteryLevel.quantity(quantity)) + + if btn_evt_cnt > 1 then + for i = 1, btn_evt_cnt do + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], + { visibility = { displayed = false } })) + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.numberOfButtons({ value = 1 })) + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.button.pushed({ state_change = false })) + button_utils.emit_event_if_latest_state_missing(device, COMP_LIST[i], capabilities.button, + capabilities.button.button.NAME, capabilities.button.button.pushed({ state_change = false })) + end + end end local function do_configure(driver, device) + local ATTR_ID = PRIVATE_ATTRIBUTE_ID_T1 + local cmd_value = 1 + device:configure() - if device:get_model() == "lumi.remote.b1acn02" then - device:send(cluster_base.write_manufacturer_specific_attribute(device, - PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_T1, MFG_CODE, data_types.Uint8, 1)) - elseif device:get_model() == "lumi.remote.acn003" then - device:send(cluster_base.write_manufacturer_specific_attribute(device, - PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, MFG_CODE, data_types.Uint8, 2)) + if device:get_model() == "lumi.remote.acn003" then + ATTR_ID = PRIVATE_ATTRIBUTE_ID_E1 + cmd_value = 2 end + device:send(cluster_base.write_manufacturer_specific_attribute(device, + PRIVATE_CLUSTER_ID, ATTR_ID, MFG_CODE, data_types.Uint8, cmd_value)) -- when the wireless switch T1 accesses the network, the gateway sends -- private attribute 0009 to make the device no longer distinguish -- between the standard gateway and the aqara gateway. @@ -105,20 +190,26 @@ local function do_configure(driver, device) end local aqara_wireless_switch_handler = { - NAME = "Aqara Wireless Switch Handler", - lifecycle_handlers = { - init = device_init, - added = added_handler, - doConfigure = do_configure - }, - zigbee_handlers = { - attr = { - [MULTISTATE_INPUT_CLUSTER_ID] = { - [PRESENT_ATTRIBUTE_ID] = present_value_attr_handler - } + NAME = "Aqara Wireless Switch Handler", + lifecycle_handlers = { + init = device_init, + added = added_handler, + doConfigure = do_configure + }, + zigbee_handlers = { + attr = { + [MULTISTATE_INPUT_CLUSTER_ID] = { + [PRESENT_ATTRIBUTE_ID] = present_value_attr_handler + }, + [PowerConfiguration.ID] = { + [PowerConfiguration.attributes.BatteryVoltage.ID] = battery_level_handler + }, + [PRIVATE_CLUSTER_ID] = { + [PRIVATE_ATTRIBUTE_ID_ALIVE] = mode_switching_handler } - }, - can_handle = is_aqara_products + } + }, + can_handle = require("aqara.can_handle"), } return aqara_wireless_switch_handler diff --git a/drivers/SmartThings/zigbee-button/src/button_utils.lua b/drivers/SmartThings/zigbee-button/src/button_utils.lua index b7499684cd..10ca4ab11a 100644 --- a/drivers/SmartThings/zigbee-button/src/button_utils.lua +++ b/drivers/SmartThings/zigbee-button/src/button_utils.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local log = require "log" @@ -79,4 +68,10 @@ button_utils.build_button_handler = function(button_name, pressed_type) end end +button_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + return button_utils diff --git a/drivers/SmartThings/zigbee-button/src/dimming-remote/can_handle.lua b/drivers/SmartThings/zigbee-button/src/dimming-remote/can_handle.lua new file mode 100644 index 0000000000..d3a469e214 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/dimming-remote/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zigbee_dimming_remote(opts, driver, device, ...) + local FINGERPRINTS = require("dimming-remote.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("dimming-remote") + end + end + return false +end + +return can_handle_zigbee_dimming_remote diff --git a/drivers/SmartThings/zigbee-button/src/dimming-remote/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/dimming-remote/fingerprints.lua new file mode 100644 index 0000000000..439694ebaf --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/dimming-remote/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIBEE_DIMMING_SWITCH_FINGERPRINTS = { + { mfr = "OSRAM", model = "LIGHTIFY Dimming Switch" }, + { mfr = "CentraLite", model = "3130" } +} + +return ZIBEE_DIMMING_SWITCH_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua b/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua index 45809d728c..54ca675ccd 100644 --- a/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua +++ b/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -22,19 +12,7 @@ local battery_defaults = require "st.zigbee.defaults.battery_defaults" local button_utils = require "button_utils" -local ZIBEE_DIMMING_SWITCH_FINGERPRINTS = { - { mfr = "OSRAM", model = "LIGHTIFY Dimming Switch" }, - { mfr = "CentraLite", model = "3130" } -} -local function can_handle_zigbee_dimming_remote(opts, driver, device, ...) - for _, fingerprint in ipairs(ZIBEE_DIMMING_SWITCH_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function button_pushed_handler(button_number) return function(self, device, value, zb_rx) @@ -54,7 +32,7 @@ local function added_handler(self, device) device:emit_component_event(component, capabilities.button.numberOfButtons({value = number_of_buttons}, {visibility = { displayed = false }})) end device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local function do_configure(self, device) @@ -84,7 +62,7 @@ local dimming_remote = { } } }, - can_handle = can_handle_zigbee_dimming_remote + can_handle = require("dimming-remote.can_handle"), } return dimming_remote diff --git a/drivers/SmartThings/zigbee-button/src/ewelink/can_handle.lua b/drivers/SmartThings/zigbee-button/src/ewelink/can_handle.lua new file mode 100644 index 0000000000..6a9426b75d --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/ewelink/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_ewelink_button(opts, driver, device, ...) + local FINGERPRINTS = require("ewelink.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("ewelink") + end + end + return false +end + +return can_handle_ewelink_button diff --git a/drivers/SmartThings/zigbee-button/src/ewelink/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/ewelink/fingerprints.lua new file mode 100644 index 0000000000..3e21d092d3 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/ewelink/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local EWELINK_BUTTON_FINGERPRINTS = { + { mfr = "eWeLink", model = "WB01" }, + { mfr = "eWeLink", model = "SNZB-01P" } +} + +return EWELINK_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/ewelink/init.lua b/drivers/SmartThings/zigbee-button/src/ewelink/init.lua index 334dd79108..b503b68c64 100644 --- a/drivers/SmartThings/zigbee-button/src/ewelink/init.lua +++ b/drivers/SmartThings/zigbee-button/src/ewelink/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -19,19 +9,7 @@ local device_management = require "st.zigbee.device_management" local OnOff = clusters.OnOff local button = capabilities.button.button -local EWELINK_BUTTON_FINGERPRINTS = { - { mfr = "eWeLink", model = "WB01" }, - { mfr = "eWeLink", model = "SNZB-01P" } -} -local function can_handle_ewelink_button(opts, driver, device, ...) - for _, fingerprint in ipairs(EWELINK_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function do_configure(driver, device) device:configure() @@ -61,7 +39,7 @@ local ewelink_button = { } } }, - can_handle = can_handle_ewelink_button + can_handle = require("ewelink.can_handle"), } -return ewelink_button \ No newline at end of file +return ewelink_button diff --git a/drivers/SmartThings/zigbee-button/src/ezviz/can_handle.lua b/drivers/SmartThings/zigbee-button/src/ezviz/can_handle.lua new file mode 100644 index 0000000000..b046b8966c --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/ezviz/can_handle.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_ezviz_button = function(opts, driver, device) + + local EZVIZ_PRIVATE_BUTTON_CLUSTER = 0xFE05 + local EZVIZ_PRIVATE_STANDARD_CLUSTER = 0xFE00 + local EZVIZ_MFR = "EZVIZ" + + local support_button_cluster = device:supports_server_cluster(EZVIZ_PRIVATE_BUTTON_CLUSTER) + local support_standard_cluster = device:supports_server_cluster(EZVIZ_PRIVATE_STANDARD_CLUSTER) + if device:get_manufacturer() == EZVIZ_MFR and support_button_cluster and support_standard_cluster then + return true, require("ezviz") + end + return false +end + +return is_ezviz_button diff --git a/drivers/SmartThings/zigbee-button/src/ezviz/init.lua b/drivers/SmartThings/zigbee-button/src/ezviz/init.lua index 5b630843a2..5bea38ffb4 100644 --- a/drivers/SmartThings/zigbee-button/src/ezviz/init.lua +++ b/drivers/SmartThings/zigbee-button/src/ezviz/init.lua @@ -1,31 +1,12 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local EZVIZ_PRIVATE_BUTTON_CLUSTER = 0xFE05 -local EZVIZ_PRIVATE_STANDARD_CLUSTER = 0xFE00 local EZVIZ_PRIVATE_BUTTON_ATTRIBUTE = 0x0000 -local EZVIZ_MFR = "EZVIZ" -local is_ezviz_button = function(opts, driver, device) - local support_button_cluster = device:supports_server_cluster(EZVIZ_PRIVATE_BUTTON_CLUSTER) - local support_standard_cluster = device:supports_server_cluster(EZVIZ_PRIVATE_STANDARD_CLUSTER) - if device:get_manufacturer() == EZVIZ_MFR and support_button_cluster and support_standard_cluster then - return true - end -end local ezviz_private_cluster_button_handler = function(driver, device, zb_rx) local event @@ -53,6 +34,6 @@ local ezviz_button_handler = { } } }, - can_handle = is_ezviz_button + can_handle = require("ezviz.can_handle"), } -return ezviz_button_handler \ No newline at end of file +return ezviz_button_handler diff --git a/drivers/SmartThings/zigbee-button/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-button/src/frient/can_handle.lua new file mode 100644 index 0000000000..924c595fd3 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/frient/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function frient_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "frient A/S" and (device:get_model() == "SBTZB-110" or device:get_model() == "MBTZB-110") then + return true, require("frient") + end + return false +end + +return frient_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/frient/init.lua b/drivers/SmartThings/zigbee-button/src/frient/init.lua index eeed6e36a2..bf622cd694 100644 --- a/drivers/SmartThings/zigbee-button/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-button/src/frient/init.lua @@ -1,23 +1,23 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" local capabilities = require "st.capabilities" -local device_management = require "st.zigbee.device_management" local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local data_types = require "st.zigbee.data_types" +local button_utils = require "button_utils" local BasicInput = zcl_clusters.BasicInput local PowerConfiguration = zcl_clusters.PowerConfiguration +local OnOff = zcl_clusters.OnOff +local panicAlarm = capabilities.panicAlarm +local IASZone = zcl_clusters.IASZone + +local DEVELCO_MANUFACTURER_CODE = 0x1015 +local BUTTON_LED_COLOR = 0x8002 +local BUTTON_PRESS_DELAY = 0x8001 +local PANIC_BUTTON = 0x8000 local battery_table = { [2.90] = 100, @@ -34,51 +34,194 @@ local battery_table = { [0.00] = 0 } -local function present_value_attr_handler(driver, device, value, zb_rx) - local event - local additional_fields = { - state_change = true +local CONFIGURATIONS = { + { + cluster = BasicInput.ID, + attribute = BasicInput.attributes.PresentValue.ID, + minimum_interval = 0, + maximum_interval = 21600, + data_type = BasicInput.attributes.PresentValue.base_type, + reportable_change = 1, + endpoint = 0x20 + }, + { + cluster = PowerConfiguration.ID, + attribute = PowerConfiguration.attributes.BatteryVoltage.ID, + minimum_interval = 30, + maximum_interval = 21600, + reportable_change = 1, } +} + +local PREFERENCE_TABLES = { + ledColor = { + clusterId = OnOff.ID, + attributeId = BUTTON_LED_COLOR, + dataType = data_types.Enum8, + mfg_code = DEVELCO_MANUFACTURER_CODE, + endpoint = 0x20, + frame_ctrl = 0x0C + }, + buttonDelay = { + clusterId = OnOff.ID, + attributeId = BUTTON_PRESS_DELAY, + dataType = data_types.Uint16, + mfg_code = DEVELCO_MANUFACTURER_CODE, + endpoint = 0x20, + frame_ctrl = 0x0C + }, + panicButton = { + clusterId = BasicInput.ID, + attributeId = PANIC_BUTTON, + dataType = data_types.Uint16, + mfg_code = DEVELCO_MANUFACTURER_CODE, + endpoint = 0x20, + frame_ctrl = 0x04 + }, + buttonAlarmDelay = { + clusterId = IASZone.ID, + attributeId = 0x8002, + dataType = data_types.Uint16, + mfg_code = DEVELCO_MANUFACTURER_CODE, + endpoint = 0x23, + frame_ctrl = 0x04 + }, + buttonCancelDelay = { + clusterId = IASZone.ID, + attributeId = 0x8003, + dataType= data_types.Uint16, + mfg_code = DEVELCO_MANUFACTURER_CODE, + endpoint = 0x23, + frame_ctrl = 0x04 + }, + autoCancel = { + clusterId = IASZone.ID, + attributeId = 0x8004, + dataType = data_types.Uint16, + mfg_code = DEVELCO_MANUFACTURER_CODE, + endpoint = 0x23, + frame_ctrl = 0x04 + }, + alarmBehavior = { + clusterId = IASZone.ID, + attributeId = 0x8005, + dataType = data_types.Enum8, + mfg_code = DEVELCO_MANUFACTURER_CODE, + endpoint = 0x23, + frame_ctrl = 0x04 + }, +} + +local function generate_event_from_zone_status(driver, device, zone_status, zigbee_message) + if device:supports_capability(panicAlarm) then + if zone_status:is_alarm2_set() then + device:emit_event(panicAlarm.panicAlarm.panic({state_change = true})) + else + device:emit_event(panicAlarm.panicAlarm.clear({state_change = true})) + end + end +end + +local function configure_ias_zone_settings(driver, device) + device:send(cluster_base.write_manufacturer_specific_attribute(device, IASZone.ID, 0x8002, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 2000):to_endpoint(0x23)) + device:send(cluster_base.write_manufacturer_specific_attribute(device, IASZone.ID, 0x8003, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 2000):to_endpoint(0x23)) + device:send(cluster_base.write_manufacturer_specific_attribute(device, IASZone.ID, 0x8004, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 10):to_endpoint(0x23)) + device:send(cluster_base.write_manufacturer_specific_attribute(device, IASZone.ID, 0x8005, DEVELCO_MANUFACTURER_CODE, data_types.Enum8, 0):to_endpoint(0x23)) +end + +local function present_value_attr_handler(driver, device, value, zb_rx) if value.value == true then - event = capabilities.button.button.pushed(additional_fields) - device:emit_event(event) + device:emit_event(capabilities.button.button.pushed({state_change = true})) end end -local function init_handler(self, device, event, args) +local function ias_zone_status_attr_handler(driver, device, zone_status, zb_rx) + generate_event_from_zone_status(driver, device, zone_status, zb_rx) +end + +local function ias_zone_status_change_handler(driver, device, zb_rx) + local zone_status = zb_rx.body.zcl_body.zone_status + generate_event_from_zone_status(driver, device, zone_status, zb_rx) +end + +local function init_handler(self, device) + for _,attribute in ipairs(CONFIGURATIONS) do + device:add_configured_attribute(attribute) + end battery_defaults.enable_battery_voltage_table(device, battery_table) end local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end -local configure_handler = function(self, device) - device:send(device_management.build_bind_request(device, BasicInput.ID, self.environment_info.hub_zigbee_eui)) - device:send(BasicInput.attributes.PresentValue:configure_reporting(device, 0, 600, 1)) - device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) - device:send(PowerConfiguration.attributes.BatteryVoltage:configure_reporting(device, 30, 21600, 1)) +local function do_configure(driver, device, event, args) + device:configure() + device:send(cluster_base.write_manufacturer_specific_attribute(device, OnOff.ID, BUTTON_LED_COLOR, DEVELCO_MANUFACTURER_CODE, data_types.Enum8, 2):to_endpoint(0x20)) + device:send(cluster_base.write_manufacturer_specific_attribute(device, OnOff.ID, BUTTON_PRESS_DELAY, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 100):to_endpoint(0x20)) + device:send(cluster_base.write_manufacturer_specific_attribute(device, BasicInput.ID, PANIC_BUTTON, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 0xFFFF):to_endpoint(0x20)) +end + +local function info_changed(driver, device, event, args) + for name, info in pairs(PREFERENCE_TABLES) do + if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then + local input = device.preferences[name] + local payload = tonumber(input) + + if (name == "panicButton") then + if (input == "0x002C")then + device:try_update_metadata({profile = "button-profile-panic-frient"}) + device.thread:call_with_delay(5, function() + device:emit_event(panicAlarm.panicAlarm.clear({state_change = true})) + configure_ias_zone_settings(driver,device) + end) + else + device:try_update_metadata({profile = "button-profile-frient"}) + end + end + + if (payload ~= nil) then + local message = cluster_base.write_manufacturer_specific_attribute( + device, + info.clusterId, + info.attributeId, + info.mfg_code, + info.dataType, + payload + ) + message.address_header.dest_endpoint.value = info.endpoint + message.body.zcl_header.frame_ctrl.value = info.frame_ctrl + device:send(message) + end + end + end end local frient_button = { NAME = "Frient Button Handler", lifecycle_handlers = { added = added_handler, - doConfigure = configure_handler, - init = init_handler + doConfigure = do_configure, + init = init_handler, + infoChanged = info_changed }, zigbee_handlers = { + cluster = { + [IASZone.ID] = { + [IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler + } + }, attr = { + [IASZone.ID] = { + [IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler + }, [BasicInput.ID] = { [BasicInput.attributes.PresentValue.ID] = present_value_attr_handler } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "frient A/S" and device:get_model() == "MBTZB-110" - end + can_handle = require("frient.can_handle"), } - return frient_button diff --git a/drivers/SmartThings/zigbee-button/src/init.lua b/drivers/SmartThings/zigbee-button/src/init.lua index de5d3bf755..8ed0db27db 100644 --- a/drivers/SmartThings/zigbee-button/src/init.lua +++ b/drivers/SmartThings/zigbee-button/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -18,6 +8,7 @@ local defaults = require "st.zigbee.defaults" local constants = require "st.zigbee.constants" local IASZone = (require "st.zigbee.zcl.clusters").IASZone local TemperatureMeasurement = (require "st.zigbee.zcl.clusters").TemperatureMeasurement +local button_utils = require "button_utils" local temperature_measurement_defaults = { MIN_TEMP = "MIN_TEMP", @@ -109,7 +100,7 @@ end local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) if device:supports_server_cluster(TemperatureMeasurement.ID) then device:send(TemperatureMeasurement.attributes.MaxMeasuredValue:read(device)) device:send(TemperatureMeasurement.attributes.MinMeasuredValue:read(device)) @@ -120,7 +111,8 @@ local zigbee_button_driver_template = { supported_capabilities = { capabilities.button, capabilities.battery, - capabilities.temperatureMeasurement + capabilities.panicAlarm, + capabilities.temperatureMeasurement, }, zigbee_handlers = { attr = { @@ -138,18 +130,7 @@ local zigbee_button_driver_template = { } } }, - sub_drivers = { - require("aqara"), - require("pushButton"), - require("frient"), - require("zigbee-multi-button"), - require("dimming-remote"), - require("iris"), - require("samjin"), - require("ewelink"), - require("thirdreality"), - require("ezviz") - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { added = added_handler, }, diff --git a/drivers/SmartThings/zigbee-button/src/iris/can_handle.lua b/drivers/SmartThings/zigbee-button/src/iris/can_handle.lua new file mode 100644 index 0000000000..618bc6eb5a --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/iris/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_iris_button(opts, driver, device, ...) + local FINGERPRINTS = require("iris.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("iris") + end + end + return false +end + +return can_handle_iris_button diff --git a/drivers/SmartThings/zigbee-button/src/iris/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/iris/fingerprints.lua new file mode 100644 index 0000000000..4ae1761e06 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/iris/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local IRIS_BUTTON_FINGERPRINTS = { + { mfr = "CentraLite", model = "3455-L" }, + { mfr = "CentraLite", model = "3460-L" } +} + +return IRIS_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/iris/init.lua b/drivers/SmartThings/zigbee-button/src/iris/init.lua index 1baa2f8ced..a6eac49870 100644 --- a/drivers/SmartThings/zigbee-button/src/iris/init.lua +++ b/drivers/SmartThings/zigbee-button/src/iris/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -21,19 +11,7 @@ local device_management = require "st.zigbee.device_management" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local button_utils = require "button_utils" -local IRIS_BUTTON_FINGERPRINTS = { - { mfr = "CentraLite", model = "3455-L" }, - { mfr = "CentraLite", model = "3460-L" } -} -local function can_handle_iris_button(opts, driver, device, ...) - for _, fingerprint in ipairs(IRIS_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function button_pressed_handler(self, device, value, zb_rx) button_utils.init_button_press(device) @@ -62,7 +40,7 @@ end local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) end @@ -94,7 +72,7 @@ local iris_button = { } } }, - can_handle = can_handle_iris_button + can_handle = require("iris.can_handle"), } return iris_button diff --git a/drivers/SmartThings/zigbee-button/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-button/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-button/src/pushButton/can_handle.lua b/drivers/SmartThings/zigbee-button/src/pushButton/can_handle.lua new file mode 100644 index 0000000000..f25592fb01 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/pushButton/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function pushButton_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "HEIMAN" and device:get_model() == "SOS-EM" then + return true, require("pushButton") + end + return false +end + +return pushButton_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/pushButton/init.lua b/drivers/SmartThings/zigbee-button/src/pushButton/init.lua index 8d09684fd7..170307a600 100644 --- a/drivers/SmartThings/zigbee-button/src/pushButton/init.lua +++ b/drivers/SmartThings/zigbee-button/src/pushButton/init.lua @@ -1,34 +1,15 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + --- Copyright 2020 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 capabilities = require "st.capabilities" +local button_utils = require "button_utils" local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local push_button = { @@ -37,9 +18,7 @@ local push_button = { added = added_handler, }, sub_drivers = {}, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "HEIMAN" and device:get_model() == "SOS-EM" - end + can_handle = require("pushButton.can_handle"), } return push_button diff --git a/drivers/SmartThings/zigbee-button/src/samjin/can_handle.lua b/drivers/SmartThings/zigbee-button/src/samjin/can_handle.lua new file mode 100644 index 0000000000..0886bedb9c --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/samjin/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function samjin_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Samjin" and device:get_model() == "button" then + return true, require("samjin") + end + return false +end + +return samjin_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/samjin/init.lua b/drivers/SmartThings/zigbee-button/src/samjin/init.lua index 66ae410169..bd254091a6 100644 --- a/drivers/SmartThings/zigbee-button/src/samjin/init.lua +++ b/drivers/SmartThings/zigbee-button/src/samjin/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" @@ -21,7 +11,6 @@ battery_config.data_type = zcl_clusters.PowerConfiguration.attributes.BatteryVol local function init_handler(self, device) device:add_configured_attribute(battery_config) - device:add_monitored_attribute(battery_config) end local samjin_button = { @@ -29,9 +18,7 @@ local samjin_button = { lifecycle_handlers = { init = init_handler }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Samjin" and device:get_model() == "button" - end + can_handle = require("samjin.can_handle"), } return samjin_button diff --git a/drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua b/drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua index 9d009d47cb..3d832ac180 100644 --- a/drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua +++ b/drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua @@ -1,16 +1,5 @@ --- Copyright 2021 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. +-- Copyright 2021 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local data_types = require "st.zigbee.data_types" local utils = require "st.zigbee.utils" local zdo_commands = require "st.zigbee.zdo.commands" @@ -152,4 +141,4 @@ end setmetatable(zdo_messages.ZdoMessageBody, { __call = zdo_messages.ZdoMessageBody.from_values }) -return zdo_messages \ No newline at end of file +return zdo_messages diff --git a/drivers/SmartThings/zigbee-button/src/sub_drivers.lua b/drivers/SmartThings/zigbee-button/src/sub_drivers.lua new file mode 100644 index 0000000000..47fe5ff9c4 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/sub_drivers.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("aqara"), + lazy_load_if_possible("pushButton"), + lazy_load_if_possible("frient"), + lazy_load_if_possible("zigbee-multi-button"), + lazy_load_if_possible("dimming-remote"), + lazy_load_if_possible("iris"), + lazy_load_if_possible("samjin"), + lazy_load_if_possible("ewelink"), + lazy_load_if_possible("thirdreality"), + lazy_load_if_possible("ezviz"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-button/src/test/test_SLED_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_SLED_button.lua index fecbeeabb7..cfbd4a6845 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_SLED_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_SLED_button.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zigbee-button/src/test/test_aduro_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_aduro_button.lua index 2736ea3d96..27b8da764b 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_aduro_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_aduro_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" diff --git a/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua index ba0f87741f..b2d5a986d6 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" @@ -28,10 +17,28 @@ local MFG_CODE = 0x115F local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID_T1 = 0x0009 local PRIVATE_ATTRIBUTE_ID_E1 = 0x0125 +local PRIVATE_ATTRIBUTE_ID_ALIVE = 0x00F7 +local MODE_CHANGE = "stse.allowOperationModeChange" + +local COMP_LIST = { "button1", "button2", "all" } + +local mock_device_h1_single = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("aqara-single-button-mode.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "LUMI", + model = "lumi.remote.b18ac1", + server_clusters = { 0x0001, 0x0012 } + } + } + } +) local mock_device_e1 = test.mock_device.build_test_zigbee_device( { - profile = t_utils.get_profile_definition("one-button-battery.yml"), + profile = t_utils.get_profile_definition("one-button-batteryLevel.yml"), zigbee_endpoints = { [1] = { id = 1, @@ -43,14 +50,14 @@ local mock_device_e1 = test.mock_device.build_test_zigbee_device( } ) -local mock_device_t1 = test.mock_device.build_test_zigbee_device( +local mock_device_h1_double_rocker = test.mock_device.build_test_zigbee_device( { - profile = t_utils.get_profile_definition("one-button-battery.yml"), + profile = t_utils.get_profile_definition("aqara-double-buttons-mode.yml"), zigbee_endpoints = { [1] = { id = 1, manufacturer = "LUMI", - model = "lumi.remote.b1acn02", + model = "lumi.remote.b286acn03", server_clusters = { 0x0001, 0x0012 } } } @@ -59,30 +66,37 @@ local mock_device_t1 = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() + test.mock_device.add_test_device(mock_device_h1_single) test.mock_device.add_test_device(mock_device_e1) - test.mock_device.add_test_device(mock_device_t1)end + test.mock_device.add_test_device(mock_device_h1_double_rocker) +end test.set_test_init_function(test_init) test.register_coroutine_test( - "Handle added lifecycle -- e1", + "Handle added lifecycle - T1 double rocker", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_e1.id, "added" }) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }}))) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.battery.battery(100))) - end -) - -test.register_coroutine_test( - "Handle added lifecycle -- t1", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device_t1.id, "added" }) - test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }}))) - test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) - test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.battery.battery(100))) + test.socket.device_lifecycle:__queue_receive({ mock_device_h1_double_rocker.id, "added" }) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 1 }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.pushed({ state_change = false }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.batteryLevel.battery.normal())) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.batteryLevel.type("CR2032"))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.batteryLevel.quantity(1))) + for i = 1, 3 do + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.numberOfButtons({ value = 1 }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.button.pushed({ state_change = false }))) + end end ) @@ -104,12 +118,14 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device_e1.id, - zigbee_test_utils.build_attr_config(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, 0x0003, 0x1C20, data_types.Uint16, 0x0001) + zigbee_test_utils.build_attr_config(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, 0x0003, + 0x1C20, data_types.Uint16, 0x0001) }) test.socket.zigbee:__expect_send({ mock_device_e1.id, - cluster_base.write_manufacturer_specific_attribute(mock_device_e1, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, MFG_CODE, - data_types.Uint8, 2) }) - mock_device_e1:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + cluster_base.write_manufacturer_specific_attribute(mock_device_e1, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, + MFG_CODE, + data_types.Uint8, 2) }) + mock_device_e1:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) @@ -117,27 +133,31 @@ test.register_coroutine_test( test.register_coroutine_test( "Handle doConfigure lifecycle -- t1", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_t1.id, "doConfigure" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_h1_double_rocker.id, "doConfigure" }) test.socket.zigbee:__expect_send({ - mock_device_t1.id, - zigbee_test_utils.build_bind_request(mock_device_t1, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_bind_request(mock_device_h1_double_rocker, zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID) }) test.socket.zigbee:__expect_send({ - mock_device_t1.id, - PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device_t1, 30, 3600, 1) + mock_device_h1_double_rocker.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device_h1_double_rocker, 30, 3600, 1) }) test.socket.zigbee:__expect_send({ - mock_device_t1.id, - zigbee_test_utils.build_bind_request(mock_device_t1, zigbee_test_utils.mock_hub_eui, MULTISTATE_INPUT_CLUSTER_ID) + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_bind_request(mock_device_h1_double_rocker, zigbee_test_utils.mock_hub_eui, + MULTISTATE_INPUT_CLUSTER_ID) }) test.socket.zigbee:__expect_send({ - mock_device_t1.id, - zigbee_test_utils.build_attr_config(mock_device_t1, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, 0x0003, 0x1C20, data_types.Uint16, 0x0001) + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attr_config(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, + 0x0003, 0x1C20, data_types.Uint16, 0x0001) }) - test.socket.zigbee:__expect_send({ mock_device_t1.id, - cluster_base.write_manufacturer_specific_attribute(mock_device_t1, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_T1, MFG_CODE, - data_types.Uint8, 1) }) - mock_device_t1:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.zigbee:__expect_send({ mock_device_h1_double_rocker.id, + cluster_base.write_manufacturer_specific_attribute(mock_device_h1_double_rocker, PRIVATE_CLUSTER_ID, + PRIVATE_ATTRIBUTE_ID_T1, MFG_CODE, + data_types.Uint8, 1) }) + mock_device_h1_double_rocker:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) @@ -148,11 +168,14 @@ test.register_coroutine_test( { PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0001 } } test.socket.zigbee:__queue_receive({ - mock_device_e1.id, - zigbee_test_utils.build_attribute_report(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE) + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, + attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", - capabilities.button.button.pushed({state_change = true}))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.pushed({ state_change = true }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("button1", + capabilities.button.button.pushed({ state_change = true }))) end ) @@ -163,11 +186,14 @@ test.register_coroutine_test( { PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0002 } } test.socket.zigbee:__queue_receive({ - mock_device_e1.id, - zigbee_test_utils.build_attribute_report(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE) + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, + attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", - capabilities.button.button.double({state_change = true}))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.double({ state_change = true }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("button1", + capabilities.button.button.double({ state_change = true }))) end ) @@ -178,16 +204,19 @@ test.register_coroutine_test( { PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0000 } } test.socket.zigbee:__queue_receive({ - mock_device_e1.id, - zigbee_test_utils.build_attribute_report(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE) + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, + attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", - capabilities.button.button.held({state_change = true}))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.held({ state_change = true }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("button1", + capabilities.button.button.held({ state_change = true }))) end ) test.register_message_test( - "Battery voltage report should be handled", + "Battery Level - Normal", { { channel = "zigbee", @@ -197,9 +226,99 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_device_e1:generate_test_message("main", capabilities.battery.battery(100)) + message = mock_device_e1:generate_test_message("main", capabilities.batteryLevel.battery("normal")) } } ) +test.register_message_test( + "Battery Level - Warning", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device_e1.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device_e1, 27) } + }, + { + channel = "capability", + direction = "send", + message = mock_device_e1:generate_test_message("main", capabilities.batteryLevel.battery("warning")) + } + } +) +test.register_message_test( + "Battery Level - Critical", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device_e1.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device_e1, 20) } + }, + { + channel = "capability", + direction = "send", + message = mock_device_e1:generate_test_message("main", capabilities.batteryLevel.battery("critical")) + } + } +) + +test.register_coroutine_test( + "Wireless Remote Switch H1 Mode Change", + function() + local mode = 2 + local updates = { + preferences = { + [MODE_CHANGE] = true + } + } + test.socket.device_lifecycle:__queue_receive(mock_device_h1_double_rocker:generate_info_changed(updates)) + mock_device_h1_double_rocker:set_field("devicemode", 1, { persist = true }) + local attr_report_data = { + { PRIVATE_ATTRIBUTE_ID_ALIVE, data_types.OctetString.ID, "\x01\x21\xB8\x0B\x03\x28\x19\x04\x21\xA8\x13\x05\x21\x45\x08\x06\x24\x07\x00\x00\x00\x00\x08\x21\x15\x01\x0A\x21\xF5\x65\x0C\x20\x01\x64\x20\x01\x66\x20\x03\x67\x20\x01\x68\x21\xA8\x00" } + } + test.wait_for_events() + test.socket.zigbee:__queue_receive({ + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, PRIVATE_CLUSTER_ID, attr_report_data, + MFG_CODE) + }) + test.socket.zigbee:__expect_send({ mock_device_h1_double_rocker.id, cluster_base + .write_manufacturer_specific_attribute(mock_device_h1_double_rocker, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, + MFG_CODE, data_types.Uint8, mode) }) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 1 }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.pushed({ state_change = false }))) + + for i = 1, 3 do + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.numberOfButtons({ value = 1 }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.button.pushed({ state_change = false }))) + end + end +) + +test.register_coroutine_test( + "Handle added lifecycle - H1 single rocker (sets mode=1)", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_h1_single.id, "added" }) + test.socket.capability:__expect_send(mock_device_h1_single:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device_h1_single:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 1 }))) + test.socket.capability:__expect_send(mock_device_h1_single:generate_test_message("main", + capabilities.button.button.pushed({ state_change = false }))) + test.socket.capability:__expect_send(mock_device_h1_single:generate_test_message("main", + capabilities.batteryLevel.battery.normal())) + test.socket.capability:__expect_send(mock_device_h1_single:generate_test_message("main", + capabilities.batteryLevel.type("CR2450"))) + test.socket.capability:__expect_send(mock_device_h1_single:generate_test_message("main", + capabilities.batteryLevel.quantity(1))) + end +) test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-button/src/test/test_centralite_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_centralite_button.lua index 6c2e46f724..c8d5ff87ae 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_centralite_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_centralite_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" diff --git a/drivers/SmartThings/zigbee-button/src/test/test_dimming_remote.lua b/drivers/SmartThings/zigbee-button/src/test/test_dimming_remote.lua index 32e686a9f5..c552322c9b 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_dimming_remote.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_dimming_remote.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" @@ -176,6 +165,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( @@ -221,7 +211,49 @@ test.register_coroutine_test( attribute_id = "button", state = { value = "pushed" } } }) - + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + }) + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 2 }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button1", + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button1", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button2", + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button2", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) @@ -229,4 +261,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-button/src/test/test_ewelink_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_ewelink_button.lua index d08e38391a..5562eced88 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_ewelink_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_ewelink_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -151,4 +140,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-button/src/test/test_ezviz_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_ezviz_button.lua index 77b6768149..be613bbc14 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_ezviz_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_ezviz_button.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zigbee_test_utils = require "integration_test.zigbee_test_utils" @@ -196,4 +185,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua index 725e0fa737..aa6f211d65 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua @@ -1,43 +1,75 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" -local PowerConfiguration = clusters.PowerConfiguration local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local cluster_base = require "st.zigbee.cluster_base" + +local IASZone = clusters.IASZone +local PowerConfiguration = clusters.PowerConfiguration local BasicInput = clusters.BasicInput +local OnOff = clusters.OnOff + +local panicAlarm = capabilities.panicAlarm.panicAlarm local button_attr = capabilities.button.button -local t_utils = require "integration_test.utils" + + +local DEVELCO_MANUFACTURER_CODE = 0x1015 + +local data_types = require "st.zigbee.data_types" local mock_device = test.mock_device.build_test_zigbee_device( { - profile = t_utils.get_profile_definition("button-profile.yml"), + profile = t_utils.get_profile_definition("button-profile-frient.yml"), zigbee_endpoints = { [1] = { id = 1, manufacturer = "frient A/S", - model = "MBTZB-110", - server_clusters = {0x0001,0x0019} + model = "SBTZB-110", + server_clusters = {OnOff.ID}, + }, + [0x20] = { + id = 0x20, + server_clusters = {BasicInput.ID, PowerConfiguration.ID}, + client_clusters = {OnOff.ID}, + + }, + } + } +) +local mock_device_panic = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("button-profile-panic-frient.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "frient A/S", + model = "SBTZB-110", + server_clusters = {OnOff.ID}, + }, + [0x20] = { + id = 0x20, + server_clusters = {BasicInput.ID, PowerConfiguration.ID}, + client_clusters = {OnOff.ID}, + + }, + [0x23] = { + id = 0x23, + server_clusters = {IASZone.ID} } } } ) zigbee_test_utils.prepare_zigbee_env_info() local function test_init() - test.mock_device.add_test_device(mock_device)end + test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_device_panic) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) @@ -57,68 +89,210 @@ test.register_message_test( } ) +test.register_message_test("Refresh should read all necessary attributes", { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = {mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device)} + }, + { + channel = "zigbee", + direction = "send", + message = {mock_device.id, BasicInput.attributes.PresentValue:read(mock_device)} + }, +}) + +test.register_coroutine_test("panicAlarm should be triggered and cleared", function() + + local panic_report = IASZone.attributes.ZoneStatus.build_test_attr_report( + IASZone.attributes.ZoneStatus, + mock_device_panic, + 0x0002 + ) + + test.socket.zigbee:__queue_receive({ + mock_device_panic.id, + panic_report + }) + + test.socket.capability:__expect_send(mock_device_panic:generate_test_message("main", panicAlarm.panic({value = "panic", state_change = true}))) + + test.wait_for_events() + + local clear_report = IASZone.attributes.ZoneStatus.build_test_attr_report( + IASZone.attributes.ZoneStatus, + mock_device_panic, + 0x0001 + ) + test.socket.zigbee:__queue_receive({ + mock_device_panic.id, + clear_report + }) + + test.socket.capability:__expect_send(mock_device_panic:generate_test_message("main", panicAlarm.clear({value = "clear", state_change = true}))) + test.wait_for_events() + +end) + test.register_coroutine_test( "Battery Voltage test cases", function() - local battery_test_map = { - [33] = 100, - [32] = 100, - [27] = 50, - [26] = 30, - [23] = 10, - [15] = 0, - [10] = 0 - } + local battery_table = { + [33] = 100, + [32] = 100, + [27] = 50, + [26] = 30, + [23] = 10, + [15] = 0, + [10] = 0 + } + -- The initial button pushed event should be send during the device's first time onboarding + test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = { displayed = false }}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({ state_change = false}))) + test.wait_for_events() - for voltage, batt_perc in pairs(battery_test_map) do - test.socket.zigbee:__queue_receive({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, voltage) }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(batt_perc)) ) + for voltage, batt_perc in pairs(battery_table) do + test.socket.zigbee:__queue_receive({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, voltage) }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(batt_perc))) + end + test.wait_for_events() + -- Avoid sending the button pushed contactSensor event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = { displayed = false }}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) test.wait_for_events() - end end ) test.register_coroutine_test( - "Configure should configure all necessary attributes", + "added , init, and doConfigure should configure all necessary attributes", function() - test.wait_for_events() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.zigbee:__set_channel_ordering("relaxed") - test.socket.zigbee:__expect_send( - { - mock_device.id, - PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device, - 30, - 21600, - 1) - } - ) - test.socket.zigbee:__expect_send( - { - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - PowerConfiguration.ID) - } - ) - test.socket.zigbee:__expect_send( - { - mock_device.id, - BasicInput.attributes.PresentValue:configure_reporting(mock_device, 0, 600, 1) - } - ) - test.socket.zigbee:__expect_send( - { - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - BasicInput.ID) - } - ) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = { displayed = false }}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({ state_change = false}))) + + test.socket.device_lifecycle:__queue_receive({mock_device.id, "doConfigure"}) + + test.socket.zigbee:__expect_send({mock_device.id, zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID, + 0x20 + ):to_endpoint(0x20)}) + + test.socket.zigbee:__expect_send({mock_device.id, zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + BasicInput.ID, + 0x20 + ):to_endpoint(0x20)}) + + test.socket.zigbee:__expect_send({mock_device.id, PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device, 30,21600, 1 ):to_endpoint(0x20)}) + test.socket.zigbee:__expect_send({mock_device.id, BasicInput.attributes.PresentValue:configure_reporting(mock_device, 0,21600, 1 ):to_endpoint(0x20)}) + + test.socket.zigbee:__expect_send({mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, OnOff.ID, 0x8002, DEVELCO_MANUFACTURER_CODE, data_types.Enum8, 2):to_endpoint(0x20)}) + test.socket.zigbee:__expect_send({mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, OnOff.ID, 0x8001, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 100):to_endpoint(0x20)}) + test.socket.zigbee:__expect_send({mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, BasicInput.ID, 0x8000, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 65535):to_endpoint(0x20)}) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) +test.register_coroutine_test("info_changed for OnOff cluster attributes should run properly", +function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed( + { + preferences = { + ledColor = 1, + buttonDelay = 300, + } + } + )) + + local ledColor_msg = cluster_base.write_manufacturer_specific_attribute(mock_device,OnOff.ID, 0x8002, DEVELCO_MANUFACTURER_CODE, data_types.Enum8, 1) + ledColor_msg.body.zcl_header.frame_ctrl.value = 0x0C + ledColor_msg.address_header.dest_endpoint.value = 0x20 + test.socket.zigbee:__expect_send({mock_device.id, ledColor_msg}) + + local buttonDelay_msg = cluster_base.write_manufacturer_specific_attribute(mock_device,OnOff.ID, 0x8001, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 0x012C) + buttonDelay_msg.body.zcl_header.frame_ctrl.value = 0x0C + buttonDelay_msg.address_header.dest_endpoint.value = 0x20 + test.socket.zigbee:__expect_send({mock_device.id, buttonDelay_msg}) +end) + +test.register_coroutine_test(" Configuration and Switching to button-profile-panic-frient deviceProfile should be triggered", function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed( + { + preferences = { + panicButton = "0x002C" + } + } + )) + mock_device:expect_metadata_update({ profile = "button-profile-panic-frient" }) + test.socket.zigbee:__expect_send({mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, BasicInput.ID, 0x8000, DEVELCO_MANUFACTURER_CODE, data_types.Uint16,0x002C)}) + + local attributes = { + {attr = 0x8002, payload = 0x07D0, data_type = data_types.Uint16}, + {attr = 0x8003, payload = 0x07D0, data_type = data_types.Uint16}, + {attr = 0x8004, payload = 0x0A, data_type = data_types.Uint16}, + {attr = 0x8005, payload = 0, data_type = data_types.Enum8} + } + -- waiting for IASzone configuration execution + test.mock_time.advance_time(5) + for _, attr in ipairs(attributes) do + local msg = cluster_base.write_manufacturer_specific_attribute(mock_device,IASZone.ID, attr.attr, DEVELCO_MANUFACTURER_CODE, attr.data_type, attr.payload) + msg.address_header.dest_endpoint.value = 0x23 + test.socket.zigbee:__expect_send({mock_device.id, msg}) + end + -- Unable to check if the emit went through successfully due to the framework limitations in swapping mock device's deviceProfile + --test.socket.capability:__expect_send({mock_device.id, capabilities.panicAlarm.panicAlarm.clear({state_change = true})}) +end) + +test.register_coroutine_test("Switching from button-profile-panic-frient to button-profile-frient should work", function() + test.socket.device_lifecycle:__queue_receive(mock_device_panic:generate_info_changed( + { + preferences = { + panicButton = "0xFFFF" + }, + } + )) + mock_device_panic:expect_metadata_update({ profile = "button-profile-frient" }) + test.socket.zigbee:__expect_send({mock_device_panic.id, cluster_base.write_manufacturer_specific_attribute(mock_device_panic,BasicInput.ID,0x8000,DEVELCO_MANUFACTURER_CODE,data_types.Uint16,0xFFFF)}) +end) + +test.register_coroutine_test("New preferences after switching the profile should work", function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive(mock_device_panic:generate_info_changed( + { + preferences = { + buttonAlarmDelay = 1, + buttonCancelDelay = 300, + autoCancel = 20, + alarmBehavior = 1 + } + } + )) + test.socket.zigbee:__expect_send({mock_device_panic.id, cluster_base.write_manufacturer_specific_attribute(mock_device_panic, IASZone.ID,0x8002,DEVELCO_MANUFACTURER_CODE,data_types.Uint16, 1)}) + test.socket.zigbee:__expect_send({mock_device_panic.id, cluster_base.write_manufacturer_specific_attribute(mock_device_panic, IASZone.ID,0x8003,DEVELCO_MANUFACTURER_CODE,data_types.Uint16, 300)}) + test.socket.zigbee:__expect_send({mock_device_panic.id, cluster_base.write_manufacturer_specific_attribute(mock_device_panic, IASZone.ID,0x8004,DEVELCO_MANUFACTURER_CODE,data_types.Uint16, 20)}) + test.socket.zigbee:__expect_send({mock_device_panic.id, cluster_base.write_manufacturer_specific_attribute(mock_device_panic, IASZone.ID,0x8005,DEVELCO_MANUFACTURER_CODE,data_types.Enum8, 1)}) +end) test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-button/src/test/test_heiman_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_heiman_button.lua index f0604a0cd8..0cc2c734cf 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_heiman_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_heiman_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" diff --git a/drivers/SmartThings/zigbee-button/src/test/test_ikea_on_off.lua b/drivers/SmartThings/zigbee-button/src/test/test_ikea_on_off.lua index f685d6fbec..82f477f426 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_ikea_on_off.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_ikea_on_off.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" @@ -211,6 +200,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send({ mock_device.id, @@ -251,6 +241,48 @@ test.register_coroutine_test( attribute_id = "button", state = { value = "pushed" } } }) + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) + test.wait_for_events() + + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = "main", + attribute_id = "supportedButtonValues", state = { value = { "pushed", "held" } } + } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = "main", + attribute_id = "numberOfButtons", state = { value = 2 } + } + }) + for button_name, _ in pairs(mock_device.profile.components) do + if button_name ~= "main" then + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = button_name, + attribute_id = "supportedButtonValues", state = { value = { "pushed", "held" } } + } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = button_name, + attribute_id = "numberOfButtons", state = { value = 1 } + } + }) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.zigbee:__expect_send({ diff --git a/drivers/SmartThings/zigbee-button/src/test/test_ikea_open_close.lua b/drivers/SmartThings/zigbee-button/src/test/test_ikea_open_close.lua index ec5cb8a1dc..3d5c7ab58d 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_ikea_open_close.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_ikea_open_close.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" @@ -155,6 +144,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send({ mock_device.id, @@ -195,6 +185,47 @@ test.register_coroutine_test( attribute_id = "button", state = { value = "pushed" } } }) + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) + test.wait_for_events() + + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = "main", + attribute_id = "supportedButtonValues", state = { value = { "pushed" } } + } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = "main", + attribute_id = "numberOfButtons", state = { value = 2 } + } + }) + for button_name, _ in pairs(mock_device.profile.components) do + if button_name ~= "main" then + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = button_name, + attribute_id = "supportedButtonValues", state = { value = { "pushed" } } + } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = button_name, + attribute_id = "numberOfButtons", state = { value = 1 } + } + }) + end + end test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.zigbee:__expect_send({ diff --git a/drivers/SmartThings/zigbee-button/src/test/test_ikea_remote_control.lua b/drivers/SmartThings/zigbee-button/src/test/test_ikea_remote_control.lua index dade3489c5..ef08a1d7e5 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_ikea_remote_control.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_ikea_remote_control.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" @@ -188,6 +177,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( mock_device:generate_test_message( @@ -230,14 +220,59 @@ test.register_coroutine_test( mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) - - test.socket.capability:__expect_send({ + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.capability:__expect_send({ mock_device.id, { capability_id = "button", component_id = "main", attribute_id = "button", state = { value = "pushed" } } }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.wait_for_events() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 5 }, { visibility = { displayed = false } }) + ) + ) + for button_name, _ in pairs(mock_device.profile.components) do + if button_name ~= "main" then + if button_name ~= "button5" then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + else + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed"}, { visibility = { displayed = false } }) + ) + ) + end + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + end + end + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.wait_for_events() end diff --git a/drivers/SmartThings/zigbee-button/src/test/test_iris_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_iris_button.lua index 032b9c18d1..64630415a0 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_iris_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_iris_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" @@ -129,6 +118,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( @@ -150,7 +140,25 @@ test.register_coroutine_test( attribute_id = "button", state = { value = "pushed" } } }) - + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + }) + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_linxura_aura_smart_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_linxura_aura_smart_button.lua index 4b9ca6dfd9..8e3ff6e001 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_linxura_aura_smart_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_linxura_aura_smart_button.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -119,4 +108,4 @@ test.register_coroutine_test( ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-button/src/test/test_linxura_smart_controller_4x_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_linxura_smart_controller_4x_button.lua index a53d5f3851..27ec0a8e44 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_linxura_smart_controller_4x_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_linxura_smart_controller_4x_button.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -119,4 +108,4 @@ test.register_coroutine_test( ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-button/src/test/test_push_only_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_push_only_button.lua index ed046c5781..4af8ef8c3c 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_push_only_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_push_only_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-button/src/test/test_robb_4x_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_robb_4x_button.lua index 733e79e304..4888ae5f7e 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_robb_4x_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_robb_4x_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" @@ -218,6 +207,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__set_channel_ordering("relaxed") @@ -271,6 +261,57 @@ test.register_coroutine_test( mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__set_channel_ordering("relaxed") + + for button_name, _ in pairs(mock_device.profile.components) do + if button_name ~= "main" and (button_name == "button1" or button_name == "button3") then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed", "up_hold" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + elseif button_name ~= "main" and (button_name == "button2" or button_name == "button4") then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed", "down_hold" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + else + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "up_hold", "down_hold" }, + { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 4 }, { visibility = { displayed = false } }) + ) + ) + end + end + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) end ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_robb_8x_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_robb_8x_button.lua index bbac26d61c..ebb324a3dc 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_robb_8x_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_robb_8x_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" @@ -335,6 +324,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__set_channel_ordering("relaxed") @@ -388,6 +378,58 @@ test.register_coroutine_test( mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__set_channel_ordering("relaxed") + + for button_name, _ in pairs(mock_device.profile.components) do + if button_name ~= "main" and (button_name == "button1" or button_name == "button3" or button_name == "button5" or button_name == "button7") then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed", "up_hold" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + elseif button_name ~= "main" and (button_name == "button2" or button_name == "button4" or button_name == "button6" or button_name == "button8") then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed", "down_hold" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + else + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "up_hold", "down_hold" }, + { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 8 }, { visibility = { displayed = false } }) + ) + ) + end + end + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) end ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_samjin_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_samjin_button.lua index cf3bd7fe3d..1a23d68d83 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_samjin_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_samjin_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-button/src/test/test_shinasystem_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_shinasystem_button.lua index 6f677e80b7..5278edbf8e 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_shinasystem_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_shinasystem_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" diff --git a/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_1_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_1_button.lua index 83e9fec30d..7e7e155867 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_1_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_1_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_4_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_4_button.lua index c0329f5561..d358db3fcc 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_4_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_4_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-button/src/test/test_thirdreality_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_thirdreality_button.lua index bdb8e51bf6..a102e30e51 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_thirdreality_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_thirdreality_button.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local data_types = require "st.zigbee.data_types" diff --git a/drivers/SmartThings/zigbee-button/src/test/test_vimar_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_vimar_button.lua index e1d35fb6bc..575175aa47 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_vimar_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_vimar_button.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-button/src/test/test_wallhero_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_wallhero_button.lua index e62cbd7055..98d8efbdfd 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_wallhero_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_wallhero_button.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-button/src/test/test_zigbee_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_zigbee_button.lua index 97feca9121..92a50636a4 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_zigbee_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_zigbee_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-button/src/test/test_zigbee_ecosmart_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_zigbee_ecosmart_button.lua index 8f956e9203..c6f28dfe44 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_zigbee_ecosmart_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_zigbee_ecosmart_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-button/src/test/test_zunzunbee_8_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_zunzunbee_8_button.lua index a92e1c6285..ae579dd671 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_zunzunbee_8_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_zunzunbee_8_button.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -203,4 +192,4 @@ test.register_coroutine_test( ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-button/src/thirdreality/can_handle.lua b/drivers/SmartThings/zigbee-button/src/thirdreality/can_handle.lua new file mode 100644 index 0000000000..77d97bb6a5 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/thirdreality/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function thirdreality_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Third Reality, Inc" and device:get_model() == "3RSB22BZ" then + return true, require("thirdreality") + end + return false +end + +return thirdreality_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/thirdreality/init.lua b/drivers/SmartThings/zigbee-button/src/thirdreality/init.lua index c4ce10f1bf..f56745c0c1 100644 --- a/drivers/SmartThings/zigbee-button/src/thirdreality/init.lua +++ b/drivers/SmartThings/zigbee-button/src/thirdreality/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local MULTISTATE_INPUT_ATTR = 0x0012 @@ -41,9 +44,7 @@ local thirdreality_device_handler = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Third Reality, Inc" and device:get_model() == "3RSB22BZ" - end + can_handle = require("thirdreality.can_handle"), } return thirdreality_device_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/SLED/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/SLED/can_handle.lua new file mode 100644 index 0000000000..4e96689e08 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/SLED/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function SLED_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Samsung Electronics" and device:get_model() == "SAMSUNG-ITM-Z-005" then + return true, require("zigbee-multi-button.SLED") + end + return false +end + +return SLED_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/SLED/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/SLED/init.lua index ea6a9d5faa..cab7458507 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/SLED/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/SLED/init.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -72,9 +62,7 @@ local SLED_button = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Samsung Electronics" and device:get_model() == "SAMSUNG-ITM-Z-005" - end + can_handle = require("zigbee-multi-button.SLED.can_handle"), } return SLED_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/can_handle.lua new file mode 100644 index 0000000000..9b4eedd1ff --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_aduro_button = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-multi-button.adurosmart.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.adurosmart") + end + end + return false +end + +return is_aduro_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/fingerprints.lua new file mode 100644 index 0000000000..c5870be270 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ADURO_BUTTON_FINGERPRINTS = { + { mfr = "AduroSmart Eria", model = "ADUROLIGHT_CSC" }, + { mfr = "ADUROLIGHT", model = "ADUROLIGHT_CSC" }, + { mfr = "AduroSmart Eria", model = "Adurolight_NCC" }, + { mfr = "ADUROLIGHT", model = "Adurolight_NCC" } +} + +return ADURO_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/init.lua index 76a6ba6aae..56a29a1d0a 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -25,21 +15,7 @@ local ADURO_NUM_ENDPOINT = 0x04 local ADURO_MANUFACTURER_SPECIFIC_CLUSTER = 0xFCCC local ADURO_MANUFACTURER_SPECIFIC_CMD = 0x00 -local ADURO_BUTTON_FINGERPRINTS = { - { mfr = "AduroSmart Eria", model = "ADUROLIGHT_CSC" }, - { mfr = "ADUROLIGHT", model = "ADUROLIGHT_CSC" }, - { mfr = "AduroSmart Eria", model = "Adurolight_NCC" }, - { mfr = "ADUROLIGHT", model = "Adurolight_NCC" } -} -local is_aduro_button = function(opts, driver, device) - for _, fingerprint in ipairs(ADURO_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local do_configuration = function(self, device) for endpoint = 1,ADURO_NUM_ENDPOINT do @@ -83,7 +59,7 @@ local aduro_device_handler = { } } }, - can_handle = is_aduro_button + can_handle = require("zigbee-multi-button.adurosmart.can_handle"), } return aduro_device_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/can_handle.lua new file mode 100644 index 0000000000..bd1c57940f --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zigbee_multi_button(opts, driver, device, ...) + local FINGERPRINTS = require("zigbee-multi-button.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button") + end + end + return false +end + +return can_handle_zigbee_multi_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/can_handle.lua new file mode 100644 index 0000000000..0216d3729c --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_centralite_button = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-multi-button.centralite.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.centralite") + end + end + return false +end + +return is_centralite_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/fingerprints.lua new file mode 100644 index 0000000000..d742cb6c64 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local CENTRALITE_BUTTON_FINGERPRINTS = { + { mfr = "CentraLite", model = "3450-L" }, + { mfr = "CentraLite", model = "3450-L2" } +} + +return CENTRALITE_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/init.lua index 8eb3762be4..d291efb0cb 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local device_management = require "st.zigbee.device_management" @@ -30,19 +20,7 @@ local EP_BUTTON_COMPONENT_MAP = { [0x04] = 2 } -local CENTRALITE_BUTTON_FINGERPRINTS = { - { mfr = "CentraLite", model = "3450-L" }, - { mfr = "CentraLite", model = "3450-L2" } -} -local is_centralite_button = function(opts, driver, device) - for _, fingerprint in ipairs(CENTRALITE_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local do_configuration = function(self, device) device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) @@ -75,7 +53,7 @@ local centralite_device_handler = { } } }, - can_handle = is_centralite_button + can_handle = require("zigbee-multi-button.centralite.can_handle"), } return centralite_device_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ecosmart/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ecosmart/can_handle.lua new file mode 100644 index 0000000000..217df6b26f --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ecosmart/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function ecosmart_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "LDS" and device:get_model() == "ZBT-CCTSwitch-D0001" then + return true, require("zigbee-multi-button.ecosmart") + end + return false +end + +return ecosmart_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ecosmart/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ecosmart/init.lua index 8f7d4ec6d8..215adba69c 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ecosmart/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ecosmart/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -126,9 +116,7 @@ local ecosmart_button = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "LDS" and device:get_model() == "ZBT-CCTSwitch-D0001" - end + can_handle = require("zigbee-multi-button.ecosmart.can_handle"), } return ecosmart_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/fingerprints.lua new file mode 100644 index 0000000000..cf3903152d --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/fingerprints.lua @@ -0,0 +1,42 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_MULTI_BUTTON_FINGERPRINTS = { + { mfr = "CentraLite", model = "3450-L" }, + { mfr = "CentraLite", model = "3450-L2" }, + { mfr = "AduroSmart Eria", model = "ADUROLIGHT_CSC" }, + { mfr = "ADUROLIGHT", model = "ADUROLIGHT_CSC" }, + { mfr = "AduroSmart Eria", model = "Adurolight_NCC" }, + { mfr = "ADUROLIGHT", model = "Adurolight_NCC" }, + { mfr = "HEIMAN", model = "SceneSwitch-EM-3.0" }, + { mfr = "HEIMAN", model = "HS6SSA-W-EF-3.0" }, + { mfr = "HEIMAN", model = "HS6SSB-W-EF-3.0" }, + { mfr = "IKEA of Sweden", model = "TRADFRI on/off switch" }, + { mfr = "IKEA of Sweden", model = "TRADFRI open/close remote" }, + { mfr = "IKEA of Sweden", model = "TRADFRI remote control" }, + { mfr = "KE", model = "TRADFRI open/close remote" }, + { mfr = "\x02KE", model = "TRADFRI open/close remote" }, + { mfr = "SOMFY", model = "Situo 1 Zigbee" }, + { mfr = "SOMFY", model = "Situo 4 Zigbee" }, + { mfr = "LDS", model = "ZBT-CCTSwitch-D0001" }, + { mfr = "ShinaSystem", model = "MSM-300Z" }, + { mfr = "ShinaSystem", model = "BSM-300Z" }, + { mfr = "ShinaSystem", model = "SBM300ZB1" }, + { mfr = "ShinaSystem", model = "SBM300ZB2" }, + { mfr = "ShinaSystem", model = "SBM300ZB3" }, + { mfr = "ShinaSystem", model = "SBM300ZC1" }, + { mfr = "ShinaSystem", model = "SBM300ZC2" }, + { mfr = "ShinaSystem", model = "SBM300ZC3" }, + { mfr = "ShinaSystem", model = "SBM300ZC4" }, + { mfr = "ShinaSystem", model = "SQM300ZC4" }, + { mfr = "ROBB smarrt", model = "ROB_200-007-0" }, + { mfr = "ROBB smarrt", model = "ROB_200-008-0" }, + { mfr = "WALL HERO", model = "ACL-401SCA4" }, + { mfr = "Samsung Electronics", model = "SAMSUNG-ITM-Z-005" }, + { mfr = "Vimar", model = "RemoteControl_v1.0" }, + { mfr = "Linxura", model = "Smart Controller" }, + { mfr = "Linxura", model = "Aura Smart Button" }, + { mfr = "zunzunbee", model = "SSWZ8T" } +} + +return ZIGBEE_MULTI_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/can_handle.lua new file mode 100644 index 0000000000..09e0575266 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_heiman_button = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-multi-button.heiman.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.heiman") + end + end + return false +end + +return is_heiman_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/fingerprints.lua new file mode 100644 index 0000000000..68723ba700 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local HEIMAN_BUTTON_FINGERPRINTS = { + { mfr = "HEIMAN", model = "SceneSwitch-EM-3.0", endpoint_num = 0x04 }, + { mfr = "HEIMAN", model = "HS6SSA-W-EF-3.0", endpoint_num = 0x04 }, + { mfr = "HEIMAN", model = "HS6SSB-W-EF-3.0", endpoint_num = 0x03 }, +} + +return HEIMAN_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/init.lua index 3c32e33fbe..980472671f 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -22,25 +12,12 @@ local OnOff = clusters.OnOff local PowerConfiguration = clusters.PowerConfiguration local Scenes = clusters.Scenes -local HEIMAN_GROUP_CONFIGURE = "is_group_configured" - -local HEIMAN_BUTTON_FINGERPRINTS = { - { mfr = "HEIMAN", model = "SceneSwitch-EM-3.0", endpoint_num = 0x04 }, - { mfr = "HEIMAN", model = "HS6SSA-W-EF-3.0", endpoint_num = 0x04 }, - { mfr = "HEIMAN", model = "HS6SSB-W-EF-3.0", endpoint_num = 0x03 }, -} +local FINGERPRINTS = require("zigbee-multi-button.heiman.fingerprints") -local is_heiman_button = function(opts, driver, device) - for _, fingerprint in ipairs(HEIMAN_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end +local HEIMAN_GROUP_CONFIGURE = "is_group_configured" local function get_endpoint_num(device) - for _, fingerprint in ipairs(HEIMAN_BUTTON_FINGERPRINTS) do + for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_model() == fingerprint.model then return fingerprint.endpoint_num end @@ -123,7 +100,7 @@ local heiman_device_handler = { } } }, - can_handle = is_heiman_button + can_handle = require("zigbee-multi-button.heiman.can_handle"), } return heiman_device_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch.lua deleted file mode 100644 index c6862ef533..0000000000 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch.lua +++ /dev/null @@ -1,41 +0,0 @@ --- Copyright 2022 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 capabilities = require "st.capabilities" -local clusters = require "st.zigbee.zcl.clusters" -local button_utils = require "button_utils" - -local Level = clusters.Level -local OnOff = clusters.OnOff - -local on_off_switch = { - NAME = "On/Off Switch", - zigbee_handlers = { - cluster = { - [OnOff.ID] = { - [OnOff.server.commands.Off.ID] = button_utils.build_button_handler("button1", capabilities.button.button.pushed), - [OnOff.server.commands.On.ID] = button_utils.build_button_handler("button2", capabilities.button.button.pushed) - }, - [Level.ID] = { - [Level.server.commands.Move.ID] = button_utils.build_button_handler("button1", capabilities.button.button.held), - [Level.server.commands.MoveWithOnOff.ID] = button_utils.build_button_handler("button2", capabilities.button.button.held) - }, - } - }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "TRADFRI on/off switch" - end -} - -return on_off_switch diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch/can_handle.lua new file mode 100644 index 0000000000..1c9621e53b --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch/can_handle.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + if device:get_model() == "TRADFRI on/off switch" then + return true, require("zigbee-multi-button.ikea.TRADFRI_on_off_switch") + end + return false +end diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch/init.lua new file mode 100644 index 0000000000..87dcfcd47a --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch/init.lua @@ -0,0 +1,28 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local button_utils = require "button_utils" + +local Level = clusters.Level +local OnOff = clusters.OnOff + +local on_off_switch = { + NAME = "On/Off Switch", + zigbee_handlers = { + cluster = { + [OnOff.ID] = { + [OnOff.server.commands.Off.ID] = button_utils.build_button_handler("button1", capabilities.button.button.pushed), + [OnOff.server.commands.On.ID] = button_utils.build_button_handler("button2", capabilities.button.button.pushed) + }, + [Level.ID] = { + [Level.server.commands.Move.ID] = button_utils.build_button_handler("button1", capabilities.button.button.held), + [Level.server.commands.MoveWithOnOff.ID] = button_utils.build_button_handler("button2", capabilities.button.button.held) + }, + } + }, + can_handle = require "zigbee-multi-button.ikea.TRADFRI_on_off_switch.can_handle" +} + +return on_off_switch diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote.lua deleted file mode 100644 index bed6086e43..0000000000 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote.lua +++ /dev/null @@ -1,36 +0,0 @@ --- Copyright 2022 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 capabilities = require "st.capabilities" -local clusters = require "st.zigbee.zcl.clusters" -local button_utils = require "button_utils" - -local WindowCovering = clusters.WindowCovering - -local open_close_remote = { - NAME = "Open/Close Remote", - zigbee_handlers = { - cluster = { - [WindowCovering.ID] = { - [WindowCovering.server.commands.UpOrOpen.ID] = button_utils.build_button_handler("button1", capabilities.button.button.pushed), - [WindowCovering.server.commands.DownOrClose.ID] = button_utils.build_button_handler("button2", capabilities.button.button.pushed) - } - } - }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "TRADFRI open/close remote" - end -} - -return open_close_remote diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote/can_handle.lua new file mode 100644 index 0000000000..aed0256524 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote/can_handle.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + if device:get_model() == "TRADFRI open/close remote" then + return true, require("zigbee-multi-button.ikea.TRADFRI_open_close_remote") + end + return false +end diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote/init.lua new file mode 100644 index 0000000000..23a622eb43 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote/init.lua @@ -0,0 +1,23 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local button_utils = require "button_utils" + +local WindowCovering = clusters.WindowCovering + +local open_close_remote = { + NAME = "Open/Close Remote", + zigbee_handlers = { + cluster = { + [WindowCovering.ID] = { + [WindowCovering.server.commands.UpOrOpen.ID] = button_utils.build_button_handler("button1", capabilities.button.button.pushed), + [WindowCovering.server.commands.DownOrClose.ID] = button_utils.build_button_handler("button2", capabilities.button.button.pushed) + } + } + }, + can_handle = require "zigbee-multi-button.ikea.TRADFRI_open_close_remote.can_handle", +} + +return open_close_remote diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua deleted file mode 100644 index 46cc01c4f9..0000000000 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua +++ /dev/null @@ -1,93 +0,0 @@ --- Copyright 2022 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 capabilities = require "st.capabilities" -local clusters = require "st.zigbee.zcl.clusters" -local log = require "log" -local button_utils = require "button_utils" - -local Level = clusters.Level -local OnOff = clusters.OnOff -local Scenes = clusters.Scenes -local PowerConfiguration = clusters.PowerConfiguration - -local function build_button_payload_handler(pressed_type) - return function(driver, device, zb_rx) - local additional_fields = { - state_change = true - } - local bytes = zb_rx.body.zcl_body.body_bytes - local payload_id = bytes:byte(1) - local button_name = - payload_id == 0x00 and "button2" or "button4" - local event = pressed_type(additional_fields) - local comp = device.profile.components[button_name] - if comp ~= nil then - device:emit_component_event(comp, event) - if button_name ~= "main" then - device:emit_event(event) - end - else - log.warn("Attempted to emit button event for unknown button: " .. button_name) - end - end -end - -local function added_handler(self, device) - for comp_name, comp in pairs(device.profile.components) do - if comp_name == "button5" then - device:emit_component_event(comp, capabilities.button.supportedButtonValues({"pushed"}, {visibility = { displayed = false }})) - else - device:emit_component_event(comp, capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = { displayed = false }})) - end - if comp_name == "main" then - device:emit_component_event(comp, capabilities.button.numberOfButtons({value = 5}, {visibility = { displayed = false }})) - else - device:emit_component_event(comp, capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - end - end - device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) - device:emit_event(capabilities.button.button.pushed({state_change = false})) -end - -local remote_control = { - NAME = "Remote Control", - zigbee_handlers = { - cluster = { - [OnOff.ID] = { - [OnOff.server.commands.Toggle.ID] = button_utils.build_button_handler("button5", capabilities.button.button.pushed) - }, - [Level.ID] = { - [Level.server.commands.Move.ID] = button_utils.build_button_handler("button3", capabilities.button.button.held), - [Level.server.commands.Step.ID] = button_utils.build_button_handler("button3", capabilities.button.button.pushed), - [Level.server.commands.MoveWithOnOff.ID] = button_utils.build_button_handler("button1", capabilities.button.button.held), - [Level.server.commands.StepWithOnOff.ID] = button_utils.build_button_handler("button1", capabilities.button.button.pushed) - }, - -- Manufacturer command id used in ikea - [Scenes.ID] = { - [0x07] = build_button_payload_handler(capabilities.button.button.pushed), - [0x08] = build_button_payload_handler(capabilities.button.button.held) - } - } - }, - lifecycle_handlers = { - added = added_handler - }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "TRADFRI remote control" - end -} - - -return remote_control diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control/can_handle.lua new file mode 100644 index 0000000000..d45924cfd1 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control/can_handle.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + if device:get_model() == "TRADFRI remote control" then + return true, require("zigbee-multi-button.ikea.TRADFRI_remote_control") + end + return false +end diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control/init.lua new file mode 100644 index 0000000000..bc608749b0 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control/init.lua @@ -0,0 +1,80 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local log = require "log" +local button_utils = require "button_utils" + +local Level = clusters.Level +local OnOff = clusters.OnOff +local Scenes = clusters.Scenes +local PowerConfiguration = clusters.PowerConfiguration + +local function build_button_payload_handler(pressed_type) + return function(driver, device, zb_rx) + local additional_fields = { + state_change = true + } + local bytes = zb_rx.body.zcl_body.body_bytes + local payload_id = bytes:byte(1) + local button_name = + payload_id == 0x00 and "button2" or "button4" + local event = pressed_type(additional_fields) + local comp = device.profile.components[button_name] + if comp ~= nil then + device:emit_component_event(comp, event) + if button_name ~= "main" then + device:emit_event(event) + end + else + log.warn("Attempted to emit button event for unknown button: " .. button_name) + end + end +end + +local function added_handler(self, device) + for comp_name, comp in pairs(device.profile.components) do + if comp_name == "button5" then + device:emit_component_event(comp, capabilities.button.supportedButtonValues({"pushed"}, {visibility = { displayed = false }})) + else + device:emit_component_event(comp, capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = { displayed = false }})) + end + if comp_name == "main" then + device:emit_component_event(comp, capabilities.button.numberOfButtons({value = 5}, {visibility = { displayed = false }})) + else + device:emit_component_event(comp, capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) + end + end + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) +end + +local remote_control = { + NAME = "Remote Control", + zigbee_handlers = { + cluster = { + [OnOff.ID] = { + [OnOff.server.commands.Toggle.ID] = button_utils.build_button_handler("button5", capabilities.button.button.pushed) + }, + [Level.ID] = { + [Level.server.commands.Move.ID] = button_utils.build_button_handler("button3", capabilities.button.button.held), + [Level.server.commands.Step.ID] = button_utils.build_button_handler("button3", capabilities.button.button.pushed), + [Level.server.commands.MoveWithOnOff.ID] = button_utils.build_button_handler("button1", capabilities.button.button.held), + [Level.server.commands.StepWithOnOff.ID] = button_utils.build_button_handler("button1", capabilities.button.button.pushed) + }, + -- Manufacturer command id used in ikea + [Scenes.ID] = { + [0x07] = build_button_payload_handler(capabilities.button.button.pushed), + [0x08] = build_button_payload_handler(capabilities.button.button.held) + } + } + }, + lifecycle_handlers = { + added = added_handler + }, + can_handle = require "zigbee-multi-button.ikea.TRADFRI_remote_control.can_handle" +} + + +return remote_control diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/can_handle.lua new file mode 100644 index 0000000000..9e1b4676f2 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local can_handle_ikea = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-multi-button.ikea.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr then + return true, require("zigbee-multi-button.ikea") + end + end + return false +end + +return can_handle_ikea diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/fingerprints.lua new file mode 100644 index 0000000000..f15d195eef --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local IKEA_MFG = { + { mfr = "IKEA of Sweden" }, + { mfr = "KE" }, + { mfr = "\02KE" } +} + +return IKEA_MFG diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua index f01f29715f..97de983eff 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local constants = require "st.zigbee.constants" @@ -22,6 +12,7 @@ local mgmt_bind_req = require "st.zigbee.zdo.mgmt_bind_request" local utils = require 'st.utils' local zdo_messages = require "st.zigbee.zdo" local supported_values = require "zigbee-multi-button.supported_values" +local button_utils = require "button_utils" local OnOff = clusters.OnOff local PowerConfiguration = clusters.PowerConfiguration @@ -29,20 +20,7 @@ local Groups = clusters.Groups local ENTRIES_READ = "ENTRIES_READ" -local IKEA_MFG = { - { mfr = "IKEA of Sweden" }, - { mfr = "KE" }, - { mfr = "\02KE" } -} -local can_handle_ikea = function(opts, driver, device) - for _, fingerprint in ipairs(IKEA_MFG) do - if device:get_manufacturer() == fingerprint.mfr then - return true - end - end - return false -end local do_configure = function(self, device) device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) @@ -80,8 +58,8 @@ local function added_handler(self, device) device:emit_component_event(component, capabilities.button.numberOfButtons({value = number_of_buttons})) end device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) - device:emit_event(capabilities.button.button.pushed({state_change = false})) -end + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) + end local function zdo_binding_table_handler(driver, device, zb_rx) for _, binding_table in pairs(zb_rx.body.zdo_body.binding_table_entries) do @@ -139,12 +117,8 @@ local ikea_of_sweden = { } } }, - sub_drivers = { - require("zigbee-multi-button.ikea.TRADFRI_remote_control"), - require("zigbee-multi-button.ikea.TRADFRI_on_off_switch"), - require("zigbee-multi-button.ikea.TRADFRI_open_close_remote") - }, - can_handle = can_handle_ikea + sub_drivers = require("zigbee-multi-button.ikea.sub_drivers"), + can_handle = require("zigbee-multi-button.ikea.can_handle"), } return ikea_of_sweden diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/sub_drivers.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/sub_drivers.lua new file mode 100644 index 0000000000..6d7d567775 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/sub_drivers.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_sub_driver = require "lazy_load_subdriver" + +local sub_drivers = { + lazy_load_sub_driver("zigbee-multi-button.ikea.TRADFRI_remote_control"), + lazy_load_sub_driver("zigbee-multi-button.ikea.TRADFRI_on_off_switch"), + lazy_load_sub_driver("zigbee-multi-button.ikea.TRADFRI_open_close_remote") +} + +return sub_drivers diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua index 369b3aaaf9..84dc2af26e 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua @@ -1,66 +1,13 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" local supported_values = require "zigbee-multi-button.supported_values" +local button_utils = require "button_utils" -local ZIGBEE_MULTI_BUTTON_FINGERPRINTS = { - { mfr = "CentraLite", model = "3450-L" }, - { mfr = "CentraLite", model = "3450-L2" }, - { mfr = "AduroSmart Eria", model = "ADUROLIGHT_CSC" }, - { mfr = "ADUROLIGHT", model = "ADUROLIGHT_CSC" }, - { mfr = "AduroSmart Eria", model = "Adurolight_NCC" }, - { mfr = "ADUROLIGHT", model = "Adurolight_NCC" }, - { mfr = "HEIMAN", model = "SceneSwitch-EM-3.0" }, - { mfr = "HEIMAN", model = "HS6SSA-W-EF-3.0" }, - { mfr = "HEIMAN", model = "HS6SSB-W-EF-3.0" }, - { mfr = "IKEA of Sweden", model = "TRADFRI on/off switch" }, - { mfr = "IKEA of Sweden", model = "TRADFRI open/close remote" }, - { mfr = "IKEA of Sweden", model = "TRADFRI remote control" }, - { mfr = "KE", model = "TRADFRI open/close remote" }, - { mfr = "\x02KE", model = "TRADFRI open/close remote" }, - { mfr = "SOMFY", model = "Situo 1 Zigbee" }, - { mfr = "SOMFY", model = "Situo 4 Zigbee" }, - { mfr = "LDS", model = "ZBT-CCTSwitch-D0001" }, - { mfr = "ShinaSystem", model = "MSM-300Z" }, - { mfr = "ShinaSystem", model = "BSM-300Z" }, - { mfr = "ShinaSystem", model = "SBM300ZB1" }, - { mfr = "ShinaSystem", model = "SBM300ZB2" }, - { mfr = "ShinaSystem", model = "SBM300ZB3" }, - { mfr = "ShinaSystem", model = "SBM300ZC1" }, - { mfr = "ShinaSystem", model = "SBM300ZC2" }, - { mfr = "ShinaSystem", model = "SBM300ZC3" }, - { mfr = "ShinaSystem", model = "SBM300ZC4" }, - { mfr = "ShinaSystem", model = "SQM300ZC4" }, - { mfr = "ROBB smarrt", model = "ROB_200-007-0" }, - { mfr = "ROBB smarrt", model = "ROB_200-008-0" }, - { mfr = "WALL HERO", model = "ACL-401SCA4" }, - { mfr = "Samsung Electronics", model = "SAMSUNG-ITM-Z-005" }, - { mfr = "Vimar", model = "RemoteControl_v1.0" }, - { mfr = "Linxura", model = "Smart Controller" }, - { mfr = "Linxura", model = "Aura Smart Button" }, - { mfr = "zunzunbee", model = "SSWZ8T" } -} -local function can_handle_zigbee_multi_button(opts, driver, device, ...) - for _, fingerprint in ipairs(ZIGBEE_MULTI_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function added_handler(self, device) local config = supported_values.get_device_parameters(device) @@ -78,7 +25,7 @@ local function added_handler(self, device) capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } })) end end - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local zigbee_multi_button = { @@ -86,22 +33,8 @@ local zigbee_multi_button = { lifecycle_handlers = { added = added_handler }, - can_handle = can_handle_zigbee_multi_button, - sub_drivers = { - require("zigbee-multi-button.ikea"), - require("zigbee-multi-button.somfy"), - require("zigbee-multi-button.ecosmart"), - require("zigbee-multi-button.centralite"), - require("zigbee-multi-button.adurosmart"), - require("zigbee-multi-button.heiman"), - require("zigbee-multi-button.shinasystems"), - require("zigbee-multi-button.robb"), - require("zigbee-multi-button.wallhero"), - require("zigbee-multi-button.SLED"), - require("zigbee-multi-button.vimar"), - require("zigbee-multi-button.linxura"), - require("zigbee-multi-button.zunzunbee") - } + can_handle = require("zigbee-multi-button.can_handle"), + sub_drivers = require("zigbee-multi-button.sub_drivers"), } return zigbee_multi_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/can_handle.lua new file mode 100644 index 0000000000..5d8aa4bd2e --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_linxura_button = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-multi-button.linxura.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.linxura") + end + end + return false +end + +return is_linxura_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/fingerprints.lua new file mode 100644 index 0000000000..0320a5f2dd --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local LINXURA_BUTTON_FINGERPRINTS = { + { mfr = "Linxura", model = "Smart Controller"}, + { mfr = "Linxura", model = "Aura Smart Button"} +} + +return LINXURA_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/init.lua index 2be3564a93..cb1dd362a2 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/init.lua @@ -1,25 +1,11 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local IASZone = (require "st.zigbee.zcl.clusters").IASZone local log = require "log" -local LINXURA_BUTTON_FINGERPRINTS = { - { mfr = "Linxura", model = "Smart Controller"}, - { mfr = "Linxura", model = "Aura Smart Button"} -} local configuration = { { @@ -31,14 +17,6 @@ local configuration = { reportable_change = 1 } } -local is_linxura_button = function(opts, driver, device) - for _, fingerprint in ipairs(LINXURA_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function present_value_attr_handler(driver, device, zone_status, zb_rx) log.info("present_value_attr_handler The current value is: ", zone_status.value) @@ -67,7 +45,6 @@ end local function device_init(driver, device) for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end @@ -85,7 +62,7 @@ local linxura_device_handler = { } }, - can_handle = is_linxura_button + can_handle = require("zigbee-multi-button.linxura.can_handle"), } return linxura_device_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/can_handle.lua new file mode 100644 index 0000000000..7b30164eaa --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, ...) + local ROBB_MFR_STRING = "ROBB smarrt" + local WIRELESS_REMOTE_FINGERPRINTS = require "zigbee-multi-button.robb.fingerprints" + + if device:get_manufacturer() == ROBB_MFR_STRING and WIRELESS_REMOTE_FINGERPRINTS[device:get_model()] then + return true, require("zigbee-multi-button.robb") + else + return false + end +end + +return can_handle diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/fingerprints.lua new file mode 100644 index 0000000000..0b9e1b3f51 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/fingerprints.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local WIRELESS_REMOTE_FINGERPRINTS = { + ["ROB_200-008-0"] = { + endpoints = 2, + buttons = 4 + }, + ["ROB_200-007-0"] = { + endpoints = 4, + buttons = 8 + } +} + +return WIRELESS_REMOTE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua index 8775e27bdc..4aca7d95aa 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -6,6 +9,7 @@ local Level = zcl_clusters.Level local OnOff = zcl_clusters.OnOff local PowerConfiguration = zcl_clusters.PowerConfiguration local capabilities = require "st.capabilities" +local button_utils = require "button_utils" --[[ The ROBB Wireless Remote Control has 4 or 8 buttons. They are arranged in two columns: @@ -17,7 +21,6 @@ Each button-row represents one endpoint. The 8x remote control has four endpoint That means each endpoint has two buttons. --]] -local ROBB_MFR_STRING = "ROBB smarrt" local WIRELESS_REMOTE_FINGERPRINTS = { ["ROB_200-008-0"] = { endpoints = 2, @@ -29,13 +32,6 @@ local WIRELESS_REMOTE_FINGERPRINTS = { } } -local function can_handle(opts, driver, device, ...) - if device:get_manufacturer() == ROBB_MFR_STRING and WIRELESS_REMOTE_FINGERPRINTS[device:get_model()] then - return true - else - return false - end -end local button_push_handler = function(addF) return function(driver, device, zb_rx) @@ -130,7 +126,7 @@ local function added_handler(self, device) device:emit_component_event(comp, capabilities.button.numberOfButtons({ value = number_of_buttons }, { visibility = { displayed = false } })) end - device:emit_event(capabilities.button.button.pushed({ state_change = false })) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) end @@ -185,7 +181,7 @@ local robb_wireless_control = { } } }, - can_handle = can_handle + can_handle = require("zigbee-multi-button.robb.can_handle"), } return robb_wireless_control diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/can_handle.lua new file mode 100644 index 0000000000..d62ba886ad --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_shinasystem_button = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-multi-button.shinasystems.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.shinasystems") + end + end + return false +end + +return is_shinasystem_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/fingerprints.lua new file mode 100644 index 0000000000..5d0d1abb6f --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/fingerprints.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SHINASYSTEM_BUTTON_FINGERPRINTS = { + { mfr = "ShinaSystem", model = "MSM-300Z", endpoint_num = 0x04 }, + { mfr = "ShinaSystem", model = "BSM-300Z", endpoint_num = 0x01 }, + { mfr = "ShinaSystem", model = "SBM300ZB1", endpoint_num = 0x01 }, + { mfr = "ShinaSystem", model = "SBM300ZB2", endpoint_num = 0x02 }, + { mfr = "ShinaSystem", model = "SBM300ZB3", endpoint_num = 0x03 }, + { mfr = "ShinaSystem", model = "SBM300ZC1", endpoint_num = 0x01 }, + { mfr = "ShinaSystem", model = "SBM300ZC2", endpoint_num = 0x02 }, + { mfr = "ShinaSystem", model = "SBM300ZC3", endpoint_num = 0x03 }, + { mfr = "ShinaSystem", model = "SBM300ZC4", endpoint_num = 0x04 }, + { mfr = "ShinaSystem", model = "SQM300ZC4", endpoint_num = 0x04 } +} + +return SHINASYSTEM_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/init.lua index 33528b3641..5d5b920db9 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -20,30 +10,11 @@ local OnOff = clusters.OnOff local device_management = require "st.zigbee.device_management" local Groups = clusters.Groups -local SHINASYSTEM_BUTTON_FINGERPRINTS = { - { mfr = "ShinaSystem", model = "MSM-300Z", endpoint_num = 0x04 }, - { mfr = "ShinaSystem", model = "BSM-300Z", endpoint_num = 0x01 }, - { mfr = "ShinaSystem", model = "SBM300ZB1", endpoint_num = 0x01 }, - { mfr = "ShinaSystem", model = "SBM300ZB2", endpoint_num = 0x02 }, - { mfr = "ShinaSystem", model = "SBM300ZB3", endpoint_num = 0x03 }, - { mfr = "ShinaSystem", model = "SBM300ZC1", endpoint_num = 0x01 }, - { mfr = "ShinaSystem", model = "SBM300ZC2", endpoint_num = 0x02 }, - { mfr = "ShinaSystem", model = "SBM300ZC3", endpoint_num = 0x03 }, - { mfr = "ShinaSystem", model = "SBM300ZC4", endpoint_num = 0x04 }, - { mfr = "ShinaSystem", model = "SQM300ZC4", endpoint_num = 0x04 } -} -local is_shinasystem_button = function(opts, driver, device) - for _, fingerprint in ipairs(SHINASYSTEM_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function get_ep_num_shinasystem_button(device) - for _, fingerprint in ipairs(SHINASYSTEM_BUTTON_FINGERPRINTS) do + local FINGERPRINTS = require("zigbee-multi-button.shinasystems.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_model() == fingerprint.model then return fingerprint.endpoint_num end @@ -92,7 +63,7 @@ local shinasystem_device_handler = { } } }, - can_handle = is_shinasystem_button + can_handle = require("zigbee-multi-button.shinasystems.can_handle"), } return shinasystem_device_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/can_handle.lua new file mode 100644 index 0000000000..55e2919fef --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function somfy_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "SOMFY" then + return true, require("zigbee-multi-button.somfy") + end + return false +end + +return somfy_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/init.lua index 7236d22491..f4b7951a8f 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local constants = require "st.zigbee.constants" @@ -65,13 +55,8 @@ local somfy = { [mgmt_bind_resp.MGMT_BIND_RESPONSE] = zdo_binding_table_handler } }, - sub_drivers = { - require("zigbee-multi-button.somfy.somfy_situo_1"), - require("zigbee-multi-button.somfy.somfy_situo_4") - }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "SOMFY" - end + sub_drivers = require("zigbee-multi-button.somfy.sub_drivers"), + can_handle = require("zigbee-multi-button.somfy.can_handle"), } return somfy diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1.lua deleted file mode 100644 index de20408306..0000000000 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1.lua +++ /dev/null @@ -1,70 +0,0 @@ --- Copyright 2022 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 capabilities = require "st.capabilities" -local constants = require "st.zigbee.constants" -local clusters = require "st.zigbee.zcl.clusters" -local device_management = require "st.zigbee.device_management" -local messages = require "st.zigbee.messages" -local mgmt_bind_req = require "st.zigbee.zdo.mgmt_bind_request" -local zdo_messages = require "st.zigbee.zdo" -local button_utils = require "button_utils" -local PowerConfiguration = clusters.PowerConfiguration -local WindowCovering = clusters.WindowCovering - -local do_configure = function(self, device) - device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) - device:send(device_management.build_bind_request(device, WindowCovering.ID, self.environment_info.hub_zigbee_eui)) - device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device):to_endpoint(0xE8)) - device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 30, 21600, 1):to_endpoint(0xE8)) - -- Read binding table - local addr_header = messages.AddressHeader( - constants.HUB.ADDR, - constants.HUB.ENDPOINT, - device:get_short_address(), - device.fingerprinted_endpoint_id, - constants.ZDO_PROFILE_ID, - mgmt_bind_req.BINDING_TABLE_REQUEST_CLUSTER_ID - ) - local binding_table_req = mgmt_bind_req.MgmtBindRequest(0) -- Single argument of the start index to query the table - local message_body = zdo_messages.ZdoMessageBody({ - zdo_body = binding_table_req - }) - local binding_table_cmd = messages.ZigbeeMessageTx({ - address_header = addr_header, - body = message_body - }) - device:send(binding_table_cmd) -end - -local somfy_handler = { - NAME = "SOMFY Remote Control - 3 buttons", - lifecycle_handlers = { - doConfigure = do_configure - }, - zigbee_handlers = { - cluster = { - [WindowCovering.ID] = { - [WindowCovering.server.commands.UpOrOpen.ID] = button_utils.build_button_handler("button1", capabilities.button.button.pushed), - [WindowCovering.server.commands.DownOrClose.ID] = button_utils.build_button_handler("button3", capabilities.button.button.pushed), - [WindowCovering.server.commands.Stop.ID] = button_utils.build_button_handler("button2", capabilities.button.button.pushed) - } - } - }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "Situo 1 Zigbee" - end -} - -return somfy_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1/can_handle.lua new file mode 100644 index 0000000000..45584d2f13 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1/can_handle.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + if device:get_model() == "Situo 1 Zigbee" then + return true, require("zigbee-multi-button.somfy.somfy_situo_1") + end + return false +end diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1/init.lua new file mode 100644 index 0000000000..b81791038e --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1/init.lua @@ -0,0 +1,57 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local constants = require "st.zigbee.constants" +local clusters = require "st.zigbee.zcl.clusters" +local device_management = require "st.zigbee.device_management" +local messages = require "st.zigbee.messages" +local mgmt_bind_req = require "st.zigbee.zdo.mgmt_bind_request" +local zdo_messages = require "st.zigbee.zdo" +local button_utils = require "button_utils" +local PowerConfiguration = clusters.PowerConfiguration +local WindowCovering = clusters.WindowCovering + +local do_configure = function(self, device) + device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) + device:send(device_management.build_bind_request(device, WindowCovering.ID, self.environment_info.hub_zigbee_eui)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device):to_endpoint(0xE8)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 30, 21600, 1):to_endpoint(0xE8)) + -- Read binding table + local addr_header = messages.AddressHeader( + constants.HUB.ADDR, + constants.HUB.ENDPOINT, + device:get_short_address(), + device.fingerprinted_endpoint_id, + constants.ZDO_PROFILE_ID, + mgmt_bind_req.BINDING_TABLE_REQUEST_CLUSTER_ID + ) + local binding_table_req = mgmt_bind_req.MgmtBindRequest(0) -- Single argument of the start index to query the table + local message_body = zdo_messages.ZdoMessageBody({ + zdo_body = binding_table_req + }) + local binding_table_cmd = messages.ZigbeeMessageTx({ + address_header = addr_header, + body = message_body + }) + device:send(binding_table_cmd) +end + +local somfy_handler = { + NAME = "SOMFY Remote Control - 3 buttons", + lifecycle_handlers = { + doConfigure = do_configure + }, + zigbee_handlers = { + cluster = { + [WindowCovering.ID] = { + [WindowCovering.server.commands.UpOrOpen.ID] = button_utils.build_button_handler("button1", capabilities.button.button.pushed), + [WindowCovering.server.commands.DownOrClose.ID] = button_utils.build_button_handler("button3", capabilities.button.button.pushed), + [WindowCovering.server.commands.Stop.ID] = button_utils.build_button_handler("button2", capabilities.button.button.pushed) + } + } + }, + can_handle = require("zigbee-multi-button.somfy.somfy_situo_1.can_handle"), +} + +return somfy_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4.lua deleted file mode 100644 index cd9c59bf90..0000000000 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4.lua +++ /dev/null @@ -1,111 +0,0 @@ --- Copyright 2022 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 capabilities = require "st.capabilities" -local constants = require "st.zigbee.constants" -local clusters = require "st.zigbee.zcl.clusters" -local device_management = require "st.zigbee.device_management" -local messages = require "st.zigbee.messages" -local mgmt_bind_req = require "st.zigbee.zdo.mgmt_bind_request" -local log = require "log" -local zdo_messages = require "st.zigbee.zdo" - -local PowerConfiguration = clusters.PowerConfiguration -local WindowCovering = clusters.WindowCovering - --- Src_ep, buttonName -local UP_MAPPING = { - [1] = "button1", - [2] = "button4", - [3] = "button7", - [4] = "button10" -} - -local DOWN_MAPPING = { - [1] = "button3", - [2] = "button6", - [3] = "button9", - [4] = "button12" -} - -local STOP_MAPPING = { - [1] = "button2", - [2] = "button5", - [3] = "button8", - [4] = "button11" -} - -local function build_button_handler(MAPPING, pressed_type) - return function(driver, device, zb_rx) - local additional_fields = { - state_change = true - } - local event = pressed_type(additional_fields) - local button_name = MAPPING[zb_rx.address_header.src_endpoint.value] - local comp = device.profile.components[button_name] - if comp ~= nil then - device:emit_component_event(comp, event) - else - log.warn("Attempted to emit button event for unknown button: " .. button_name) - end - end -end - -local do_configure = function(self, device) - device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device):to_endpoint(0xE8)) - device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 30, 21600, 1):to_endpoint(0xE8)) - device:send(device_management.build_bind_request(device, WindowCovering.ID, self.environment_info.hub_zigbee_eui, 1)) - device:send(device_management.build_bind_request(device, WindowCovering.ID, self.environment_info.hub_zigbee_eui, 2)) - device:send(device_management.build_bind_request(device, WindowCovering.ID, self.environment_info.hub_zigbee_eui, 3)) - device:send(device_management.build_bind_request(device, WindowCovering.ID, self.environment_info.hub_zigbee_eui, 4)) - -- Read binding table - local addr_header = messages.AddressHeader( - constants.HUB.ADDR, - constants.HUB.ENDPOINT, - device:get_short_address(), - device.fingerprinted_endpoint_id, - constants.ZDO_PROFILE_ID, - mgmt_bind_req.BINDING_TABLE_REQUEST_CLUSTER_ID - ) - local binding_table_req = mgmt_bind_req.MgmtBindRequest(0) -- Single argument of the start index to query the table - local message_body = zdo_messages.ZdoMessageBody({ - zdo_body = binding_table_req - }) - local binding_table_cmd = messages.ZigbeeMessageTx({ - address_header = addr_header, - body = message_body - }) - device:send(binding_table_cmd) -end - -local somfy_situo_4_handler = { - NAME = "SOMFY Situo 4 Remote Control", - lifecycle_handlers = { - doConfigure = do_configure - }, - zigbee_handlers = { - cluster = { - [WindowCovering.ID] = { - [WindowCovering.server.commands.UpOrOpen.ID] = build_button_handler(UP_MAPPING, capabilities.button.button.pushed), - [WindowCovering.server.commands.DownOrClose.ID] = build_button_handler(DOWN_MAPPING, capabilities.button.button.pushed), - [WindowCovering.server.commands.Stop.ID] = build_button_handler(STOP_MAPPING, capabilities.button.button.pushed) - } - } - }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "Situo 4 Zigbee" - end -} - -return somfy_situo_4_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4/can_handle.lua new file mode 100644 index 0000000000..15b70ad23e --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4/can_handle.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + if device:get_model() == "Situo 4 Zigbee" then + return true, require("zigbee-multi-button.somfy.somfy_situo_4") + end + return false +end diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4/init.lua new file mode 100644 index 0000000000..ca7892d578 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4/init.lua @@ -0,0 +1,98 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local constants = require "st.zigbee.constants" +local clusters = require "st.zigbee.zcl.clusters" +local device_management = require "st.zigbee.device_management" +local messages = require "st.zigbee.messages" +local mgmt_bind_req = require "st.zigbee.zdo.mgmt_bind_request" +local log = require "log" +local zdo_messages = require "st.zigbee.zdo" + +local PowerConfiguration = clusters.PowerConfiguration +local WindowCovering = clusters.WindowCovering + +-- Src_ep, buttonName +local UP_MAPPING = { + [1] = "button1", + [2] = "button4", + [3] = "button7", + [4] = "button10" +} + +local DOWN_MAPPING = { + [1] = "button3", + [2] = "button6", + [3] = "button9", + [4] = "button12" +} + +local STOP_MAPPING = { + [1] = "button2", + [2] = "button5", + [3] = "button8", + [4] = "button11" +} + +local function build_button_handler(MAPPING, pressed_type) + return function(driver, device, zb_rx) + local additional_fields = { + state_change = true + } + local event = pressed_type(additional_fields) + local button_name = MAPPING[zb_rx.address_header.src_endpoint.value] + local comp = device.profile.components[button_name] + if comp ~= nil then + device:emit_component_event(comp, event) + else + log.warn("Attempted to emit button event for unknown button: " .. button_name) + end + end +end + +local do_configure = function(self, device) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device):to_endpoint(0xE8)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 30, 21600, 1):to_endpoint(0xE8)) + device:send(device_management.build_bind_request(device, WindowCovering.ID, self.environment_info.hub_zigbee_eui, 1)) + device:send(device_management.build_bind_request(device, WindowCovering.ID, self.environment_info.hub_zigbee_eui, 2)) + device:send(device_management.build_bind_request(device, WindowCovering.ID, self.environment_info.hub_zigbee_eui, 3)) + device:send(device_management.build_bind_request(device, WindowCovering.ID, self.environment_info.hub_zigbee_eui, 4)) + -- Read binding table + local addr_header = messages.AddressHeader( + constants.HUB.ADDR, + constants.HUB.ENDPOINT, + device:get_short_address(), + device.fingerprinted_endpoint_id, + constants.ZDO_PROFILE_ID, + mgmt_bind_req.BINDING_TABLE_REQUEST_CLUSTER_ID + ) + local binding_table_req = mgmt_bind_req.MgmtBindRequest(0) -- Single argument of the start index to query the table + local message_body = zdo_messages.ZdoMessageBody({ + zdo_body = binding_table_req + }) + local binding_table_cmd = messages.ZigbeeMessageTx({ + address_header = addr_header, + body = message_body + }) + device:send(binding_table_cmd) +end + +local somfy_situo_4_handler = { + NAME = "SOMFY Situo 4 Remote Control", + lifecycle_handlers = { + doConfigure = do_configure + }, + zigbee_handlers = { + cluster = { + [WindowCovering.ID] = { + [WindowCovering.server.commands.UpOrOpen.ID] = build_button_handler(UP_MAPPING, capabilities.button.button.pushed), + [WindowCovering.server.commands.DownOrClose.ID] = build_button_handler(DOWN_MAPPING, capabilities.button.button.pushed), + [WindowCovering.server.commands.Stop.ID] = build_button_handler(STOP_MAPPING, capabilities.button.button.pushed) + } + } + }, + can_handle = require("zigbee-multi-button.somfy.somfy_situo_4.can_handle"), +} + +return somfy_situo_4_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/sub_drivers.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/sub_drivers.lua new file mode 100644 index 0000000000..c1c88d1e7c --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/sub_drivers.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_subdriver = require "lazy_load_subdriver" + +local sub_drivers = { + lazy_load_subdriver("zigbee-multi-button.somfy.somfy_situo_1"), + lazy_load_subdriver("zigbee-multi-button.somfy.somfy_situo_4"), +} + +return sub_drivers diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sub_drivers.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sub_drivers.lua new file mode 100644 index 0000000000..d8d3611ba3 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sub_drivers.lua @@ -0,0 +1,20 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("zigbee-multi-button.ikea"), + lazy_load_if_possible("zigbee-multi-button.somfy"), + lazy_load_if_possible("zigbee-multi-button.ecosmart"), + lazy_load_if_possible("zigbee-multi-button.centralite"), + lazy_load_if_possible("zigbee-multi-button.adurosmart"), + lazy_load_if_possible("zigbee-multi-button.heiman"), + lazy_load_if_possible("zigbee-multi-button.shinasystems"), + lazy_load_if_possible("zigbee-multi-button.robb"), + lazy_load_if_possible("zigbee-multi-button.wallhero"), + lazy_load_if_possible("zigbee-multi-button.SLED"), + lazy_load_if_possible("zigbee-multi-button.vimar"), + lazy_load_if_possible("zigbee-multi-button.linxura"), + lazy_load_if_possible("zigbee-multi-button.zunzunbee"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua index 172a7c1ca3..813859f891 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local devices = { BUTTON_PUSH_HELD_2 = { diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/vimar/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/vimar/can_handle.lua new file mode 100644 index 0000000000..8317f95893 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/vimar/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function vimar_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Vimar" and device:get_model() == "RemoteControl_v1.0" then + return true, require("zigbee-multi-button.vimar") + end + return false +end + +return vimar_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/vimar/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/vimar/init.lua index 3df8cc4d71..aa7ba729a1 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/vimar/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/vimar/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -85,9 +75,7 @@ local vimar_remote_control = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Vimar" and device:get_model() == "RemoteControl_v1.0" - end + can_handle = require("zigbee-multi-button.vimar.can_handle"), } return vimar_remote_control diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/can_handle.lua new file mode 100644 index 0000000000..633871874d --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_wallhero_button(opts, driver, device, ...) + local FINGERPRINTS = require("zigbee-multi-button.wallhero.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.wallhero") + end + end + return false +end + +return can_handle_wallhero_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/fingerprints.lua new file mode 100644 index 0000000000..6b6ca8b00f --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "WALL HERO", model = "ACL-401SCA4" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/init.lua index 71b01c84f8..e562aef77d 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/init.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local log = require "log" @@ -19,18 +9,7 @@ local zcl_clusters = require "st.zigbee.zcl.clusters" local Scenes = zcl_clusters.Scenes -local FINGERPRINTS = { - { mfr = "WALL HERO", model = "ACL-401SCA4" } -} -local function can_handle_wallhero_button(opts, driver, device, ...) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function scenes_cluster_handler(driver, device, zb_rx) local additional_fields = { @@ -97,7 +76,7 @@ local wallhero_button = { } } }, - can_handle = can_handle_wallhero_button + can_handle = require("zigbee-multi-button.wallhero.can_handle"), } return wallhero_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/can_handle.lua new file mode 100644 index 0000000000..0277725380 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Check if a given device matches the supported fingerprints +local function is_zunzunbee_button(opts, driver, device) + local ZUNZUNBEE_BUTTON_FINGERPRINTS = require "zigbee-multi-button.zunzunbee.fingerprints" + for _, fingerprint in ipairs(ZUNZUNBEE_BUTTON_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.zunzunbee") + end + end + return false +end + +return is_zunzunbee_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/fingerprints.lua new file mode 100644 index 0000000000..b893f4c1e6 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- List of supported device fingerprints +local ZUNZUNBEE_BUTTON_FINGERPRINTS = { + { mfr = "zunzunbee", model = "SSWZ8T" } +} + +return ZUNZUNBEE_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/init.lua index d5355c3b10..e8fa24a63e 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/init.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local Tone = capabilities.tone @@ -30,25 +20,9 @@ local battery_config = utils.deep_copy(battery_defaults.default_percentage_confi battery_config.reportable_change = 0x02 battery_config.data_type = clusters.PowerConfiguration.attributes.BatteryVoltage.base_type --- List of supported device fingerprints -local ZUNZUNBEE_BUTTON_FINGERPRINTS = { - { mfr = "zunzunbee", model = "SSWZ8T" } -} - -- Initialize device attributes local function init_handler(self, device) device:add_configured_attribute(battery_config) - device:add_monitored_attribute(battery_config) -end - --- Check if a given device matches the supported fingerprints -local function is_zunzunbee_button(opts, driver, device) - for _, fingerprint in ipairs(ZUNZUNBEE_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false end -- Generate and emit button events based on IAS Zone status attribute @@ -164,7 +138,7 @@ local zunzunbee_device_handler = { } } }, - can_handle = is_zunzunbee_button + can_handle = require("zigbee-multi-button.zunzunbee.can_handle"), } return zunzunbee_device_handler diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/can_handle.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/can_handle.lua new file mode 100644 index 0000000000..95ad1a88fa --- /dev/null +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_climax_technology_carbon_monoxide = function(opts, driver, device) + local FINGERPRINTS = require("ClimaxTechnology.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("ClimaxTechnology") + end + end + + return false +end + +return is_climax_technology_carbon_monoxide diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/fingerprints.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/fingerprints.lua new file mode 100644 index 0000000000..d72389f772 --- /dev/null +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local CLIMAX_TECHNOLOGY_CARBON_MONOXIDE_FINGERPRINTS = { + { mfr = "ClimaxTechnology", model = "CO_00.00.00.22TC" }, + { mfr = "ClimaxTechnology", model = "CO_00.00.00.15TC" } +} + +return CLIMAX_TECHNOLOGY_CARBON_MONOXIDE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/init.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/init.lua index cf96fb7162..1f18983c4b 100644 --- a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/init.lua +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/init.lua @@ -1,33 +1,10 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -local capabilities = require "st.capabilities" -local CLIMAX_TECHNOLOGY_CARBON_MONOXIDE_FINGERPRINTS = { - { mfr = "ClimaxTechnology", model = "CO_00.00.00.22TC" }, - { mfr = "ClimaxTechnology", model = "CO_00.00.00.15TC" } -} +local capabilities = require "st.capabilities" -local is_climax_technology_carbon_monoxide = function(opts, driver, device) - for _, fingerprint in ipairs(CLIMAX_TECHNOLOGY_CARBON_MONOXIDE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local device_added = function(self, device) device:emit_event(capabilities.battery.battery(100)) @@ -38,7 +15,7 @@ local climax_technology_carbon_monoxide = { lifecycle_handlers = { added = device_added }, - can_handle = is_climax_technology_carbon_monoxide + can_handle = require("ClimaxTechnology.can_handle"), } return climax_technology_carbon_monoxide diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua index 21c170151e..8ef8a50795 100644 --- a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local ZigbeeDriver = require "st.zigbee" local capabilities = require "st.capabilities" @@ -24,8 +14,8 @@ local zigbee_carbon_monoxide_driver_template = { capabilities.battery, }, ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, - sub_drivers = { require("ClimaxTechnology") }, health_check = false, + sub_drivers = require("sub_drivers"), } defaults.register_for_default_handlers(zigbee_carbon_monoxide_driver_template, diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/sub_drivers.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/sub_drivers.lua new file mode 100644 index 0000000000..6a7a185392 --- /dev/null +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/sub_drivers.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" + +local sub_drivers = { + lazy_load_if_possible("ClimaxTechnology") +} + +return sub_drivers diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_climax_technology_carbon_monoxide.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_climax_technology_carbon_monoxide.lua index a2ba57dfed..78164924d0 100644 --- a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_climax_technology_carbon_monoxide.lua +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_climax_technology_carbon_monoxide.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_zigbee_carbon_monoxide.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_zigbee_carbon_monoxide.lua index 313f3ea9ec..07933958de 100644 --- a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_zigbee_carbon_monoxide.lua +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_zigbee_carbon_monoxide.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-contact/fingerprints.yml b/drivers/SmartThings/zigbee-contact/fingerprints.yml index dbb88f33a6..c30c3296a2 100644 --- a/drivers/SmartThings/zigbee-contact/fingerprints.yml +++ b/drivers/SmartThings/zigbee-contact/fingerprints.yml @@ -149,6 +149,16 @@ zigbeeManufacturer: manufacturer: frient A/S model: WISZB-121 deviceProfileName: contact-battery-profile + - id: "frient A/S/WISZB-131" + deviceLabel: frient Entry Sensor 2 Pro + manufacturer: frient A/S + model: WISZB-131 + deviceProfileName: frient-contact-battery-temperature + - id: "frient A/S/WISZB-137" + deviceLabel: frient Vibration Sensor + manufacturer: frient A/S + model: WISZB-137 + deviceProfileName: acceleration-motion-temperature-battery - id: "Compacta/ZBWDS" deviceLabel: Smartenit Open/Closed Sensor manufacturer: Compacta @@ -194,6 +204,11 @@ zigbeeManufacturer: manufacturer: Third Reality, Inc model: 3RVS01031Z deviceProfileName: thirdreality-multi-sensor + - id: "Aug. Winkhaus SE/FM.V.ZB" + deviceLabel: Funkkontakt FM.V.ZB + manufacturer: Aug. Winkhaus SE + model: FM.V.ZB + deviceProfileName: contact-battery-profile zigbeeGeneric: - id: "contact-generic" deviceLabel: "Zigbee Contact Sensor" diff --git a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml new file mode 100644 index 0000000000..4f90f8a11e --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml @@ -0,0 +1,50 @@ +name: acceleration-motion-temperature-battery +components: +- id: main + capabilities: + - id: accelerationSensor + version: 1 + - id: motionSensor + version: 1 + - id: threeAxis + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: ContactSensor +preferences: + - preferenceId: tempOffset + explicit: true + - title: "Temperature sensitivity (°)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 + - title: "Sensitivity level" + name: sensitivityLevel + description: "How sensitivite the device is to vibrations" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 15 + default: 10 + - title: "Use with Contact Sensor" + name: garageSensor + required: false + preferenceType: enumeration + definition: + options: + "Yes": "Yes" + "No": "No" + default: "No" \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml new file mode 100644 index 0000000000..c40eb49b9e --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml @@ -0,0 +1,89 @@ +name: acceleration-motion-temperature-contact-battery +components: +- id: main + capabilities: + - id: accelerationSensor + version: 1 + - id: motionSensor + version: 1 + - id: threeAxis + version: 1 + - id: contactSensor + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: ContactSensor +preferences: + - preferenceId: tempOffset + explicit: true + - title: "Temperature sensitivity (°)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 + - title: "Sensitivity level" + name: sensitivityLevel + description: "How sensitivite the device is to vibrations" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 15 + default: 10 + - title: "Use with Contact Sensor" + name: garageSensor + required: false + preferenceType: enumeration + definition: + options: + "Yes": "Yes" + "No": "No" + default: "Yes" + - title: "Axis to activate Contact Sensor" + name: contactSensorAxis + required: false + preferenceType: enumeration + definition: + options: + "X": "X" + "Y": "Y" + "Z": "Z" + default: "Z" + - title: "Initial position (closed state)" + name: sensorInitialPosition + description: "Initial position of the device in the chosen axis" + required: false + preferenceType: number + definition: + minimum: -2000 + maximum: 2000 + default: 0 + - title: "Contact Sensor threshold (open)" + name: contactSensorValue + description: "Value change required to trigger contact sensor" + required: false + preferenceType: number + definition: + minimum: 20 + maximum: 4000 + default: 900 + - title: "Measurement tolerance" + name: tolerance + description: "Set the tolerance in percentage of the threshold" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 20 + default: 0 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-contact/profiles/frient-contact-battery-temperature.yml b/drivers/SmartThings/zigbee-contact/profiles/frient-contact-battery-temperature.yml new file mode 100644 index 0000000000..6eea0a0c2d --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/profiles/frient-contact-battery-temperature.yml @@ -0,0 +1,28 @@ +name: frient-contact-battery-temperature +components: +- id: main + capabilities: + - id: contactSensor + version: 1 + - id: battery + version: 1 + - id: temperatureMeasurement + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: ContactSensor +preferences: + - preferenceId: tempOffset + explicit: true + - title: "Temperature Sensitivity (°C)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 diff --git a/drivers/SmartThings/zigbee-contact/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/aqara/can_handle.lua new file mode 100644 index 0000000000..7877f2c180 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_aqara_products = function(opts, driver, device, ...) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-contact/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-contact/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..bcad0d779d --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/aqara/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.magnet.agl02" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-contact/src/aqara/init.lua b/drivers/SmartThings/zigbee-contact/src/aqara/init.lua index 46da673401..9093ee7bb9 100644 --- a/drivers/SmartThings/zigbee-contact/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/aqara/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" @@ -13,9 +16,6 @@ local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 local PRIVATE_HEART_BATTERY_ENERGY_ID = 0x00F7 -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.magnet.agl02" } -} local CONFIGURATIONS = { { @@ -36,14 +36,6 @@ local CONFIGURATIONS = { } } -local is_aqara_products = function(opts, driver, device, ...) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function device_init(driver, device) device:remove_configured_attribute(IASZone.ID, IASZone.attributes.ZoneStatus.ID) @@ -53,7 +45,6 @@ local function device_init(driver, device) for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end @@ -63,11 +54,17 @@ local function do_configure(self, device) PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0x01)) end +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + local function added_handler(driver, device) device:emit_event(capabilities.batteryLevel.type("CR1632")) device:emit_event(capabilities.batteryLevel.quantity(1)) device:emit_event(capabilities.batteryLevel.battery("normal")) - device:emit_event(capabilities.contactSensor.contact.closed()) + emit_event_if_latest_state_missing(device, "main", capabilities.contactSensor, capabilities.contactSensor.contact.NAME, capabilities.contactSensor.contact.open()) end local function contact_status_handler(self, device, value, zb_rx) @@ -131,7 +128,7 @@ local aqara_contact_handler = { doConfigure = do_configure, added = added_handler, }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } return aqara_contact_handler diff --git a/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/can_handle.lua new file mode 100644 index 0000000000..ceb3f4d933 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aurora_contact(opts, driver, device, ...) + local FINGERPRINTS = require("aurora-contact-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aurora-contact-sensor") + end + end + return false +end + +return can_handle_aurora_contact diff --git a/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/fingerprints.lua b/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/fingerprints.lua new file mode 100644 index 0000000000..022bbc83d5 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local AURORA_CONTACT_FINGERPRINTS = { + { mfr = "Aurora", model = "DoorSensor50AU" }, -- Aurora Smart Door/Window Sensor + { mfr = "Aurora", model = "WindowSensor51AU" } -- Aurora Smart Door/Window Sensor +} + +return AURORA_CONTACT_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/init.lua b/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/init.lua index 43002ec0ad..b5ac4fe8bb 100644 --- a/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/init.lua @@ -1,26 +1,12 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local IASZone = clusters.IASZone -local AURORA_CONTACT_FINGERPRINTS = { - { mfr = "Aurora", model = "DoorSensor50AU" }, -- Aurora Smart Door/Window Sensor - { mfr = "Aurora", model = "WindowSensor51AU" } -- Aurora Smart Door/Window Sensor -} local AURORA_CONTACT_CONFIGURATION = { { @@ -33,21 +19,12 @@ local AURORA_CONTACT_CONFIGURATION = { } } -local function can_handle_aurora_contact(opts, driver, device, ...) - for _, fingerprint in ipairs(AURORA_CONTACT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function device_init(driver, device) battery_defaults.use_battery_voltage_handling(device) for _, attribute in ipairs(AURORA_CONTACT_CONFIGURATION) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end @@ -56,7 +33,7 @@ local aurora_contact = { lifecycle_handlers = { init = device_init }, - can_handle = can_handle_aurora_contact + can_handle = require("aurora-contact-sensor.can_handle"), } return aurora_contact diff --git a/drivers/SmartThings/zigbee-contact/src/configurations.lua b/drivers/SmartThings/zigbee-contact/src/configurations.lua index 6acaeb5e04..cd5ede1b6e 100644 --- a/drivers/SmartThings/zigbee-contact/src/configurations.lua +++ b/drivers/SmartThings/zigbee-contact/src/configurations.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" @@ -92,6 +81,7 @@ local devices = { { mfr = "Sercomm Corp.", model = "SZ-DWS04" }, { mfr = "DAWON_DNS", model = "SS-B100-ZB" }, { mfr = "frient A/S", model = "WISZB-120" }, + { mfr = "frient A/S", model = "WISZB-131" }, { mfr = "Compacta", model = "ZBWDS" } }, CONFIGURATION = { @@ -128,6 +118,22 @@ local devices = { } } }, + FRIENT_VIBRATION_SENSOR_WISZB_137 = { + FINGERPRINTS = { + { mfr = "frient A/S", model = "WISZB-137" } + }, + CONFIGURATION = { + { + cluster = IASZone.ID, + attribute = IASZone.attributes.ZoneStatus.ID, + minimum_interval = 0, + maximum_interval = 3600, + data_type = IASZone.attributes.ZoneStatus.base_type, + reportable_change = 1, + endpoint = 0x2D + } + } + } } local configurations = {} diff --git a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/can_handle.lua new file mode 100644 index 0000000000..2c9f359dbe --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_contact_temperature_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("contact-temperature-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("contact-temperature-sensor") + end + end + return false +end + +return can_handle_contact_temperature_sensor diff --git a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/can_handle.lua new file mode 100644 index 0000000000..b17c1efb21 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_ecolink_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("contact-temperature-sensor.ecolink-contact.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("contact-temperature-sensor.ecolink-contact") + end + end + return false +end + +return can_handle_ecolink_sensor diff --git a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/fingerprints.lua b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/fingerprints.lua new file mode 100644 index 0000000000..c86e6e073f --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ECOLINK_CONTACT_TEMPERATURE_FINGERPRINTS = { + { mfr = "Ecolink", model = "4655BC0-R" }, + { mfr = "Ecolink", model = "DWZB1-ECO" } +} + +return ECOLINK_CONTACT_TEMPERATURE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/init.lua b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/init.lua index 22999c59aa..31dc67fd3e 100644 --- a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local device_management = require "st.zigbee.device_management" @@ -22,19 +12,7 @@ local SHORT_POLL_INTERVAL = 0x0200 local LONG_POLL_INTERVAL = 0xB1040000 local FAST_POLL_TIMEOUT = 0x0028 -local ECOLINK_CONTACT_TEMPERATURE_FINGERPRINTS = { - { mfr = "Ecolink", model = "4655BC0-R" }, - { mfr = "Ecolink", model = "DWZB1-ECO" } -} -local function can_handle_ecolink_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(ECOLINK_CONTACT_TEMPERATURE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function do_configure(driver, device) device:configure() @@ -51,7 +29,7 @@ local ecolink_sensor = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_ecolink_sensor + can_handle = require("contact-temperature-sensor.ecolink-contact.can_handle"), } return ecolink_sensor diff --git a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/fingerprints.lua b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/fingerprints.lua new file mode 100644 index 0000000000..4efc09684b --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/fingerprints.lua @@ -0,0 +1,24 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local CONTACT_TEMPERATURE_SENSOR_FINGERPRINTS = { + { mfr = "CentraLite", model = "3300-S" }, + { mfr = "CentraLite", model = "3300" }, + { mfr = "CentraLite", model = "3320-L" }, + { mfr = "CentraLite", model = "3323-G" }, + { mfr = "CentraLite", model = "Contact Sensor-A" }, + { mfr = "Visonic", model = "MCT-340 E" }, + { mfr = "Visonic", model = "MCT-340 SMA" }, + { mfr = "Ecolink", model = "4655BC0-R" }, + { mfr = "Ecolink", model = "DWZB1-ECO" }, + { mfr = "iMagic by GreatStar", model = "1116-S" }, + { mfr = "Bosch", model = "RFMS-ZBMS" }, + { mfr = "Megaman", model = "MS601/z1" }, + { mfr = "AduroSmart Eria", model = "CSW_ADUROLIGHT" }, + { mfr = "ADUROLIGHT", model = "CSW_ADUROLIGHT" }, + { mfr = "Sercomm Corp.", model = "SZ-DWS04" }, + { mfr = "DAWON_DNS", model = "SS-B100-ZB" }, + { mfr = "Compacta", model = "ZBWDS" } +} + +return CONTACT_TEMPERATURE_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/init.lua b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/init.lua index 52319ad34d..76f822fedd 100644 --- a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/init.lua @@ -1,48 +1,12 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local battery_defaults = require "st.zigbee.defaults.battery_defaults" local configurationMap = require "configurations" -local CONTACT_TEMPERATURE_SENSOR_FINGERPRINTS = { - { mfr = "CentraLite", model = "3300-S" }, - { mfr = "CentraLite", model = "3300" }, - { mfr = "CentraLite", model = "3320-L" }, - { mfr = "CentraLite", model = "3323-G" }, - { mfr = "CentraLite", model = "Contact Sensor-A" }, - { mfr = "Visonic", model = "MCT-340 E" }, - { mfr = "Visonic", model = "MCT-340 SMA" }, - { mfr = "Ecolink", model = "4655BC0-R" }, - { mfr = "Ecolink", model = "DWZB1-ECO" }, - { mfr = "iMagic by GreatStar", model = "1116-S" }, - { mfr = "Bosch", model = "RFMS-ZBMS" }, - { mfr = "Megaman", model = "MS601/z1" }, - { mfr = "AduroSmart Eria", model = "CSW_ADUROLIGHT" }, - { mfr = "ADUROLIGHT", model = "CSW_ADUROLIGHT" }, - { mfr = "Sercomm Corp.", model = "SZ-DWS04" }, - { mfr = "DAWON_DNS", model = "SS-B100-ZB" }, - { mfr = "Compacta", model = "ZBWDS" } -} -local function can_handle_contact_temperature_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(CONTACT_TEMPERATURE_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function device_init(driver, device) local configuration = configurationMap.get_device_configuration(device) @@ -52,7 +16,6 @@ local function device_init(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end @@ -62,10 +25,8 @@ local contact_temperature_sensor = { lifecycle_handlers = { init = device_init }, - sub_drivers = { - require("contact-temperature-sensor/ecolink-contact") - }, - can_handle = can_handle_contact_temperature_sensor + sub_drivers = require("contact-temperature-sensor.sub_drivers"), + can_handle = require("contact-temperature-sensor.can_handle"), } return contact_temperature_sensor diff --git a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/sub_drivers.lua b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/sub_drivers.lua new file mode 100644 index 0000000000..d47c4ad123 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("contact-temperature-sensor.ecolink-contact"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-contact/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/frient/can_handle.lua new file mode 100644 index 0000000000..e2d56c3dc0 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/frient/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function frient_can_handle(opts, driver, device, ...) + if (device:get_manufacturer() == "frient A/S") and + (device:get_model() == "WISZB-120" or + device:get_model() == "WISZB-121" or + device:get_model() == "WISZB-131" or + device:get_model() == "WISZB-137") then + return true, require("frient") + end + return false +end + +return frient_can_handle diff --git a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/can_handle.lua new file mode 100644 index 0000000000..14ecae440a --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FRIENT_DEVICE_FINGERPRINTS = { + { mfr = "frient A/S", model = "WISZB-137", }, +} + +return function(opts, driver, device, ...) + for _, fingerprint in ipairs(FRIENT_DEVICE_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("frient.frient-vibration") + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua new file mode 100644 index 0000000000..bba3dc0bdf --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua @@ -0,0 +1,212 @@ +-- 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 capabilities = require "st.capabilities" +local zcl_commands = require "st.zigbee.zcl.global_commands" +local zcl_clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local device_management = require "st.zigbee.device_management" +local data_types = require "st.zigbee.data_types" +local threeAxis = capabilities.threeAxis + +local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement +local IASZone = zcl_clusters.IASZone +local PowerConfiguration = zcl_clusters.PowerConfiguration +local POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT = 0x2D +local TEMPERATURE_ENDPOINT = 0x26 + +local Frient_AccelerationMeasurementCluster = { + ID = 0xFC04, + ManufacturerSpecificCode = 0x1015, + attributes = { + MeasuredValueX = { ID = 0x0000, data_type = data_types.Int16 }, + MeasuredValueY = { ID = 0x0001, data_type = data_types.Int16 }, + MeasuredValueZ = { ID = 0x0002, data_type = data_types.Int16 } + }, +} + +local function acceleration_measure_report_handler(driver, device, zb_rx) + local measured_x, measured_y, measured_z + + for _, attribute_record in ipairs(zb_rx.body.zcl_body.attr_records) do + local attribute_id = attribute_record.attr_id.value + local axis_value = attribute_record.data.value + + if attribute_id == Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID then + measured_x = axis_value + elseif attribute_id == Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID then + measured_y = axis_value + elseif attribute_id == Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID then + measured_z = axis_value + end + end + + if measured_x and measured_y and measured_z then + device:emit_event(threeAxis.threeAxis({measured_x, measured_y, measured_z})) + + if device:supports_capability(capabilities.contactSensor) then + local garageAxis = measured_x + if device.preferences.contactSensorAxis == "Y" then + garageAxis = measured_y + elseif device.preferences.contactSensorAxis == "Z" then + garageAxis = measured_z + end + local initial_position = device.preferences.sensorInitialPosition or 0 + if math.abs(initial_position - garageAxis) >= device.preferences.contactSensorValue - device.preferences.contactSensorValue * (device.preferences.tolerance / 100) then + device:emit_event(capabilities.contactSensor.contact.open()) + else + device:emit_event(capabilities.contactSensor.contact.closed()) + end + end + end +end + +local function get_cluster_configurations() + return { + { + cluster = Frient_AccelerationMeasurementCluster.ID, + attribute = Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID, + minimum_interval = 0, + maximum_interval = 300, + reportable_change = 0x0001, + data_type = data_types.Int16, + mfg_code = Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + }, + { + cluster = Frient_AccelerationMeasurementCluster.ID, + attribute = Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID, + minimum_interval = 0, + maximum_interval = 300, + reportable_change = 0x0001, + data_type = data_types.Int16, + mfg_code = Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + }, + { + cluster = Frient_AccelerationMeasurementCluster.ID, + attribute = Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID, + minimum_interval = 0, + maximum_interval = 300, + reportable_change = 0x0001, + data_type = data_types.Int16, + mfg_code = Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + } + } +end + +local function generate_event_from_zone_status(driver, device, zone_status, zb_rx) + device:emit_event(zone_status:is_alarm1_set() and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) + device:emit_event(zone_status:is_alarm2_set() and capabilities.accelerationSensor.acceleration.active() or capabilities.accelerationSensor.acceleration.inactive()) +end + +local function ias_zone_status_attr_handler(driver, device, attr_val, zb_rx) + generate_event_from_zone_status(driver, device, attr_val, zb_rx) +end + +local function ias_zone_status_change_handler(driver, device, zb_rx) + generate_event_from_zone_status(driver, device, zb_rx.body.zcl_body.zone_status, zb_rx) +end + +local function device_init(driver, device) + battery_defaults.build_linear_voltage_init(2.3, 3.0)(driver, device) + --Add the manufacturer-specific attributes to generate their configure reporting and bind requests + for _, config in pairs(get_cluster_configurations()) do + device:add_configured_attribute(config) + end +end + +local function do_refresh(driver, device) + device:send(IASZone.attributes.ZoneStatus:read(device):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(cluster_base.read_manufacturer_specific_attribute(device, Frient_AccelerationMeasurementCluster.ID, Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID, Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(cluster_base.read_manufacturer_specific_attribute(device, Frient_AccelerationMeasurementCluster.ID, Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID, Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(cluster_base.read_manufacturer_specific_attribute(device, Frient_AccelerationMeasurementCluster.ID, Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID, Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(TemperatureMeasurement.attributes.MeasuredValue:read(device):to_endpoint(TEMPERATURE_ENDPOINT)) + device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) +end + +local function do_configure(driver, device, event, args) + device:configure() + + device:send(device_management.build_bind_request(device, zcl_clusters.IASZone.ID, driver.environment_info.hub_zigbee_eui, POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(IASZone.attributes.ZoneStatus:configure_reporting(device, 0, 1*60*60, 1):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(device_management.build_bind_request(device, Frient_AccelerationMeasurementCluster.ID, driver.environment_info.hub_zigbee_eui, POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + + local sensitivityLevel = device.preferences.sensitivityLevel or 10 + device:send(IASZone.attributes.CurrentZoneSensitivityLevel:write(device, sensitivityLevel):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + + local sensitivity = math.floor((device.preferences.temperatureSensitivity or 0.1) * 100 + 0.5) + device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 1 * 60 * 60, sensitivity):to_endpoint(TEMPERATURE_ENDPOINT)) + + device.thread:call_with_delay(5, function() + device:refresh() + end) +end + +local function info_changed(driver, device, event, args) + if args and args.old_st_store then + if args.old_st_store.preferences.sensitivityLevel ~= device.preferences.sensitivityLevel then + local sensitivityLevel = device.preferences.sensitivityLevel or 10 + device:send(IASZone.attributes.CurrentZoneSensitivityLevel:write(device, sensitivityLevel):to_endpoint(0x2D)) + end + if args.old_st_store.preferences.temperatureSensitivity ~= device.preferences.temperatureSensitivity then + local sensitivity = math.floor((device.preferences.temperatureSensitivity or 0.1)*100 + 0.5) + device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 1*60*60, sensitivity):to_endpoint(0x26)) + end + if args.old_st_store.preferences.garageSensor ~= device.preferences.garageSensor then + if device.preferences.garageSensor == "Yes" then + device:try_update_metadata({profile = "acceleration-motion-temperature-contact-battery"}) + elseif device.preferences.garageSensor == "No" then + device:try_update_metadata({profile = "acceleration-motion-temperature-battery"}) + end + end + device.thread:call_with_delay(5, function() + device:refresh() + end) + end +end + +local frient_vibration_driver_template = { + NAME = "frient vibration driver", + lifecycle_handlers = { + init = device_init, + doConfigure = do_configure, + infoChanged = info_changed + }, + zigbee_handlers = { + global = { + [Frient_AccelerationMeasurementCluster.ID] = { + [zcl_commands.ReportAttribute.ID] = acceleration_measure_report_handler, + [zcl_commands.ReadAttributeResponse.ID] = acceleration_measure_report_handler + } + }, + cluster = { + [IASZone.ID] = { + [IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler + } + }, + attr = { + [IASZone.ID] = { + [IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler + } + } + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh, + } + }, + can_handle = require ("frient.frient-vibration.can_handle") +} + +return frient_vibration_driver_template diff --git a/drivers/SmartThings/zigbee-contact/src/frient/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/init.lua index 6e5ff2af00..f28cad8cdb 100644 --- a/drivers/SmartThings/zigbee-contact/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/frient/init.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" @@ -44,7 +34,6 @@ local function device_init(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end @@ -81,6 +70,7 @@ local frient_sensor = { doConfigure = do_configure, infoChanged = info_changed }, + sub_drivers = require("frient.sub_drivers"), zigbee_handlers = { cluster = { [IASZone.ID] = { @@ -93,9 +83,7 @@ local frient_sensor = { } } }, - can_handle = function(opts, driver, device, ...) - return (device:get_manufacturer() == "frient A/S" and (device:get_model() == "WISZB-120" or device:get_model() == "WISZB-121")) - end + can_handle = require("frient.can_handle"), } return frient_sensor diff --git a/drivers/SmartThings/zigbee-contact/src/frient/sub_drivers.lua b/drivers/SmartThings/zigbee-contact/src/frient/sub_drivers.lua new file mode 100644 index 0000000000..cfaca8fca3 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/frient/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("frient.frient-vibration"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-contact/src/init.lua b/drivers/SmartThings/zigbee-contact/src/init.lua index 7efb05179d..63a7bf7565 100644 --- a/drivers/SmartThings/zigbee-contact/src/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -65,7 +55,6 @@ local function device_init(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end @@ -98,15 +87,7 @@ local zigbee_contact_driver_template = { init = device_init, added = added_handler, }, - sub_drivers = { - require("aqara"), - require("aurora-contact-sensor"), - require("contact-temperature-sensor"), - require("multi-sensor"), - require("smartsense-multi"), - require("sengled"), - require("frient") - }, + sub_drivers = require("sub_drivers"), ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, } diff --git a/drivers/SmartThings/zigbee-contact/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-contact/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/can_handle.lua new file mode 100644 index 0000000000..0065632f88 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zigbee_multi_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("multi-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("multi-sensor") + end + end + return false +end + +return can_handle_zigbee_multi_sensor diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/centralite-multi/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/centralite-multi/can_handle.lua new file mode 100644 index 0000000000..94e73b1691 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/centralite-multi/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function centralite_multi_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "CentraLite" then + return true, require("multi-sensor.centralite-multi") + end + return false +end + +return centralite_multi_can_handle diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/centralite-multi/init.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/centralite-multi/init.lua index 1e75aa6b0d..0f7b7b7f25 100644 --- a/drivers/SmartThings/zigbee-contact/src/multi-sensor/centralite-multi/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/centralite-multi/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local battery_defaults = require "st.zigbee.defaults.battery_defaults" local multi_utils = require "multi-sensor/multi_utils" @@ -67,9 +57,7 @@ local centralite_handler = { [capabilities.refresh.commands.refresh.NAME] = do_refresh, } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "CentraLite" - end + can_handle = require("multi-sensor.centralite-multi.can_handle"), } return centralite_handler diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/fingerprints.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/fingerprints.lua new file mode 100644 index 0000000000..7abfe772b5 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/fingerprints.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local MULTI_SENSOR_FINGERPRINTS = { + { mfr = "CentraLite", model = "3320" }, + { mfr = "CentraLite", model = "3321" }, + { mfr = "CentraLite", model = "3321-S" }, + { mfr = "SmartThings", model = "multiv4" }, + { mfr = "Samjin", model = "multi" }, + { mfr = "Third Reality, Inc", model = "3RVS01031Z" } +} + +return MULTI_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/init.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/init.lua index a440156c11..4cc8f88efa 100644 --- a/drivers/SmartThings/zigbee-contact/src/multi-sensor/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local zcl_commands = require "st.zigbee.zcl.global_commands" local multi_utils = require "multi-sensor/multi_utils" @@ -18,23 +9,7 @@ local zcl_clusters = require "st.zigbee.zcl.clusters" local contactSensor_defaults = require "st.zigbee.defaults.contactSensor_defaults" local capabilities = require "st.capabilities" -local MULTI_SENSOR_FINGERPRINTS = { - { mfr = "CentraLite", model = "3320" }, - { mfr = "CentraLite", model = "3321" }, - { mfr = "CentraLite", model = "3321-S" }, - { mfr = "SmartThings", model = "multiv4" }, - { mfr = "Samjin", model = "multi" }, - { mfr = "Third Reality, Inc", model = "3RVS01031Z" } -} -local function can_handle_zigbee_multi_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(MULTI_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function multi_sensor_report_handler(driver, device, zb_rx) local x, y, z @@ -98,12 +73,7 @@ local multi_sensor = { } } }, - sub_drivers = { - require("multi-sensor/smartthings-multi"), - require("multi-sensor/samjin-multi"), - require("multi-sensor/centralite-multi"), - require("multi-sensor/thirdreality-multi") - }, - can_handle = can_handle_zigbee_multi_sensor + sub_drivers = require("multi-sensor.sub_drivers"), + can_handle = require("multi-sensor.can_handle"), } return multi_sensor diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/multi_utils.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/multi_utils.lua index e24ec1097b..b1b3e18be4 100644 --- a/drivers/SmartThings/zigbee-contact/src/multi-sensor/multi_utils.lua +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/multi_utils.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cluster_base = require "st.zigbee.cluster_base" @@ -152,4 +141,4 @@ multi_utils.convert_to_signedInt16 = function(byte1, byte2) end -return multi_utils \ No newline at end of file +return multi_utils diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/samjin-multi/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/samjin-multi/can_handle.lua new file mode 100644 index 0000000000..e524845f40 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/samjin-multi/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function samjin_multi_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Samjin" then + return true, require("multi-sensor.samjin-multi") + end + return false +end + +return samjin_multi_can_handle diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/samjin-multi/init.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/samjin-multi/init.lua index 935d1cfc20..3bc3edbcf9 100644 --- a/drivers/SmartThings/zigbee-contact/src/multi-sensor/samjin-multi/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/samjin-multi/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local data_types = require "st.zigbee.data_types" @@ -54,9 +44,7 @@ local samjin_driver = { [capabilities.refresh.commands.refresh.NAME] = do_refresh, } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Samjin" - end + can_handle = require("multi-sensor.samjin-multi.can_handle"), } return samjin_driver diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/smartthings-multi/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/smartthings-multi/can_handle.lua new file mode 100644 index 0000000000..6c99521efc --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/smartthings-multi/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function smartthings_multi_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "SmartThings" then + return true, require("multi-sensor.smartthings-multi") + end + return false +end + +return smartthings_multi_can_handle diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/smartthings-multi/init.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/smartthings-multi/init.lua index 69d0b7f3f4..24234ef6de 100644 --- a/drivers/SmartThings/zigbee-contact/src/multi-sensor/smartthings-multi/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/smartthings-multi/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local battery_defaults = require "st.zigbee.defaults.battery_defaults" local zcl_commands = require "st.zigbee.zcl.global_commands" @@ -91,9 +81,7 @@ local smartthings_multi = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "SmartThings" - end + can_handle = require("multi-sensor.smartthings-multi.can_handle"), } return smartthings_multi diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/sub_drivers.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/sub_drivers.lua new file mode 100644 index 0000000000..3769ab89d1 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/sub_drivers.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("multi-sensor/smartthings-multi"), + lazy_load_if_possible("multi-sensor/samjin-multi"), + lazy_load_if_possible("multi-sensor/centralite-multi"), + lazy_load_if_possible("multi-sensor/thirdreality-multi"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/thirdreality-multi/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/thirdreality-multi/can_handle.lua new file mode 100644 index 0000000000..bdfc4f75b5 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/thirdreality-multi/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function thirdreality_multi_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Third Reality, Inc" then + return true, require("multi-sensor.thirdreality-multi") + end + return false +end + +return thirdreality_multi_can_handle diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/thirdreality-multi/init.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/thirdreality-multi/init.lua index c63596cab5..1be7988fac 100644 --- a/drivers/SmartThings/zigbee-contact/src/multi-sensor/thirdreality-multi/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/thirdreality-multi/init.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_commands = require "st.zigbee.zcl.global_commands" local multi_utils = require "multi-sensor/multi_utils" @@ -47,9 +37,7 @@ local thirdreality_multi = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Third Reality, Inc" - end + can_handle = require("multi-sensor.thirdreality-multi.can_handle"), } return thirdreality_multi diff --git a/drivers/SmartThings/zigbee-contact/src/sengled/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/sengled/can_handle.lua new file mode 100644 index 0000000000..68d774fb8f --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/sengled/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_sengled_products = function(opts, driver, device, ...) + local FINGERPRINTS = require("sengled.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("sengled") + end + end + return false +end + +return is_sengled_products diff --git a/drivers/SmartThings/zigbee-contact/src/sengled/fingerprints.lua b/drivers/SmartThings/zigbee-contact/src/sengled/fingerprints.lua new file mode 100644 index 0000000000..4d015324b3 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/sengled/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "sengled", model = "E2D-G73" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-contact/src/sengled/init.lua b/drivers/SmartThings/zigbee-contact/src/sengled/init.lua index afe1f29770..6a4b4c6814 100644 --- a/drivers/SmartThings/zigbee-contact/src/sengled/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/sengled/init.lua @@ -1,12 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local IASZone = clusters.IASZone local PowerConfiguration = clusters.PowerConfiguration -local FINGERPRINTS = { - { mfr = "sengled", model = "E2D-G73" } -} local CONFIGURATIONS = { { @@ -27,21 +27,12 @@ local CONFIGURATIONS = { } } -local is_sengled_products = function(opts, driver, device, ...) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function device_init(driver, device) battery_defaults.build_linear_voltage_init(2.1, 3.0)(driver, device) for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end @@ -50,7 +41,7 @@ local sengled_contact_handler = { lifecycle_handlers = { init = device_init }, - can_handle = is_sengled_products + can_handle = require("sengled.can_handle"), } return sengled_contact_handler diff --git a/drivers/SmartThings/zigbee-contact/src/smartsense-multi/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/smartsense-multi/can_handle.lua new file mode 100644 index 0000000000..f5c226cc55 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/smartsense-multi/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, ...) + local SMARTSENSE_PROFILE_ID = 0xFC01 + local FINGERPRINTS = require("smartsense-multi.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("smartsense-multi") + end + end + local endpoint = device.zigbee_endpoints[1] or device.zigbee_endpoints["1"] + if endpoint.profile_id == SMARTSENSE_PROFILE_ID then return true end + return false +end + +return can_handle diff --git a/drivers/SmartThings/zigbee-contact/src/smartsense-multi/fingerprints.lua b/drivers/SmartThings/zigbee-contact/src/smartsense-multi/fingerprints.lua new file mode 100644 index 0000000000..fff1b601a3 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/smartsense-multi/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SMARTSENSE_MULTI_FINGERPRINTS = { + { mfr = "SmartThings", model = "PGC313" }, + { mfr = "SmartThings", model = "PGC313EU" } +} + +return SMARTSENSE_MULTI_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-contact/src/smartsense-multi/init.lua b/drivers/SmartThings/zigbee-contact/src/smartsense-multi/init.lua index cbe59d7c09..d5030278e8 100644 --- a/drivers/SmartThings/zigbee-contact/src/smartsense-multi/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/smartsense-multi/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local multi_utils = require "multi-sensor/multi_utils" @@ -24,23 +13,6 @@ local SMARTSENSE_MULTI_ACC_CMD = 0x00 local SMARTSENSE_MULTI_XYZ_CMD = 0x05 local SMARTSENSE_MULTI_STATUS_CMD = 0x07 local SMARTSENSE_MULTI_STATUS_REPORT_CMD = 0x09 -local SMARTSENSE_PROFILE_ID = 0xFC01 - -local SMARTSENSE_MULTI_FINGERPRINTS = { - { mfr = "SmartThings", model = "PGC313" }, - { mfr = "SmartThings", model = "PGC313EU" } -} - -local function can_handle(opts, driver, device, ...) - for _, fingerprint in ipairs(SMARTSENSE_MULTI_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - local endpoint = device.zigbee_endpoints[1] or device.zigbee_endpoints["1"] - if endpoint.profile_id == SMARTSENSE_PROFILE_ID then return true end - return false -end local function acceleration_handler(driver, device, zb_rx) -- This is a custom cluster command for the kickstarter multi. @@ -182,7 +154,7 @@ local smartsense_multi = { } } }, - can_handle = can_handle + can_handle = require("smartsense-multi.can_handle"), } return smartsense_multi diff --git a/drivers/SmartThings/zigbee-contact/src/sub_drivers.lua b/drivers/SmartThings/zigbee-contact/src/sub_drivers.lua new file mode 100644 index 0000000000..394de5a72d --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/sub_drivers.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("aqara"), + lazy_load_if_possible("aurora-contact-sensor"), + lazy_load_if_possible("contact-temperature-sensor"), + lazy_load_if_possible("multi-sensor"), + lazy_load_if_possible("smartsense-multi"), + lazy_load_if_possible("sengled"), + lazy_load_if_possible("frient"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_aqara_contact_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_aqara_contact_sensor.lua index 0fd017d6b0..7f2dae99fe 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_aqara_contact_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_aqara_contact_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -81,13 +70,21 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle handler", function() + -- The initial contactSensor event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.batteryLevel.type("CR1632"))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.batteryLevel.quantity(1))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.batteryLevel.battery("normal"))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.contactSensor.contact.closed())) + capabilities.contactSensor.contact.open())) + test.wait_for_events() + -- Avoid sending the initial contactSensor event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.batteryLevel.type("CR1632"))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.batteryLevel.quantity(1))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.batteryLevel.battery("normal"))) end ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_aurora_contact_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_aurora_contact_sensor.lua index 40cc2c47be..46fc40f686 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_aurora_contact_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_aurora_contact_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_centralite_multi_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_centralite_multi_sensor.lua index e3628f6ec7..d914712e06 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_centralite_multi_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_centralite_multi_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_contact_temperature_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_contact_temperature_sensor.lua index e05233ad48..075b923bed 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_contact_temperature_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_contact_temperature_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_ecolink_contact.lua b/drivers/SmartThings/zigbee-contact/src/test/test_ecolink_contact.lua index a05f420166..716828541a 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_ecolink_contact.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_ecolink_contact.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_ewelink_heiman_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_ewelink_heiman_sensor.lua index 58c2c8d6b2..f983211856 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_ewelink_heiman_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_ewelink_heiman_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor.lua index 7d1319abc0..b73f7ee3fe 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" @@ -176,36 +165,6 @@ test.register_message_test( } ) --- test.register_coroutine_test( --- "Health check should check all relevant attributes", --- function() --- test.wait_for_events() - --- test.mock_time.advance_time(50000) -- battery is 21600 for max reporting interval --- test.socket.zigbee:__set_channel_ordering("relaxed") - --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- PowerConfiguration.attributes.BatteryVoltage:read(mock_device) --- } --- ) - --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- IASZone.attributes.ZoneStatus:read(mock_device) --- } --- ) --- end, --- { --- test_init = function() --- test.mock_device.add_test_device(mock_device) --- test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") --- end --- } --- ) - test.register_message_test( "Refresh should read all necessary attributes", { @@ -271,4 +230,36 @@ test.register_message_test( } ) +test.register_message_test( + "ZoneStatusChangeNotification should be handled: contact/open", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0001, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) + } + } +) + +test.register_message_test( + "ZoneStatusChangeNotification should be handled: contact/closed", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0000, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + } + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_2_pro.lua b/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_2_pro.lua new file mode 100644 index 0000000000..4b0363caba --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_2_pro.lua @@ -0,0 +1,402 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" + +local IASZone = clusters.IASZone +local PowerConfiguration = clusters.PowerConfiguration +local TemperatureMeasurement = clusters.TemperatureMeasurement + +local POWER_CONFIGURATION_ENDPOINT = 0x23 +local IASZONE_ENDPOINT = 0x23 +local TEMPERATURE_MEASUREMENT_ENDPOINT = 0x26 + +local base64 = require "base64" +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("frient-contact-battery-temperature.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "WISZB-131", + server_clusters = { 0x0005, 0x0006 } + }, + [0x23] = { + id = 0x23, + server_clusters = { 0x0000, 0x0001, 0x0003, 0x000f, 0x0020, 0x0500 } + }, + [0x26] = { + id = 0x26, + server_clusters = { 0x0000, 0x0003, 0x0402 } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Refresh necessary attributes", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} } }) + test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.ZoneStatus:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) + end +) + +test.register_coroutine_test( + "init and doConfigure lifecycles should be handled properly", + function() + test.socket.environment_update:__queue_receive({ "zigbee", { hub_zigbee_id = base64.encode(zigbee_test_utils.mock_hub_eui) } }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + test.wait_for_events() + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MinMeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MaxMeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID, + POWER_CONFIGURATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device, + 30, + 21600, + 1 + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + TemperatureMeasurement.ID, + TEMPERATURE_MEASUREMENT_ENDPOINT + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 1800, + 100 + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + IASZONE_ENDPOINT + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device, + 30, + 300, + 0 + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write( + mock_device, + zigbee_test_utils.mock_hub_eui + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device):to_endpoint(IASZONE_ENDPOINT) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } + } + } +) + +test.register_message_test( + "Min battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 23) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +test.register_message_test( + "Max battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 30) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + +-- test.register_coroutine_test( +-- "Health check should check all relevant attributes", +-- function() +-- test.wait_for_events() + +-- test.mock_time.advance_time(50000) -- battery is 21600 for max reporting interval +-- test.socket.zigbee:__set_channel_ordering("relaxed") + +-- test.socket.zigbee:__expect_send( +-- { +-- mock_device.id, +-- PowerConfiguration.attributes.BatteryVoltage:read(mock_device) +-- } +-- ) + +-- test.socket.zigbee:__expect_send( +-- { +-- mock_device.id, +-- TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) +-- } +-- ) + +-- test.socket.zigbee:__expect_send( +-- { +-- mock_device.id, +-- IASZone.attributes.ZoneStatus:read(mock_device) +-- } +-- ) +-- end, +-- { +-- test_init = function() +-- test.mock_device.add_test_device(mock_device) +-- test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") +-- end +-- } +-- ) + +test.register_message_test( + "Refresh should read all necessary attributes", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_message_test( + "Reported ZoneStatus should be handled: contact/closed", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0000) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + } + } +) + +test.register_message_test( + "Reported ZoneStatus should be handled: contact/open", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0001) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) + } + } +) + +test.register_message_test( + "Reported ZoneStatus should be handled: contact/open", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0005) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) + } + } +) + +test.register_message_test( + "Reported ZoneStatus should be handled: contact/closed", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0004) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + } + } +) + +test.register_coroutine_test( + "infochanged to check for necessary preferences settings: Temperature Sensitivity", + function() + local updates = { + preferences = { + temperatureSensitivity = 0.9 + } + } + test.socket.zigbee:__set_channel_ordering("relaxed") + test.wait_for_events() + + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + + local temperatureSensitivity = math.floor(0.9 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 1800, + temperatureSensitivity + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_pro.lua b/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_pro.lua index 19673a958a..6a7876541f 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_pro.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_pro.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" @@ -199,6 +188,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua new file mode 100644 index 0000000000..2e704a61ca --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua @@ -0,0 +1,510 @@ + +-- 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 clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" + +local IASZone = clusters.IASZone +local PowerConfiguration = clusters.PowerConfiguration +local TemperatureMeasurement = clusters.TemperatureMeasurement +local POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT = 0x2D +local TEMPERATURE_ENDPOINT = 0x26 + +local base64 = require "base64" +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("acceleration-motion-temperature-battery.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "WISZB-137", + server_clusters = { 0x0003, 0x0005, 0x0006 } + }, + [0x2D] = { + id = 0x2D, + server_clusters = { 0x0000, 0x0001, 0x0003, 0x0020, 0x0500, 0xFC04 } + }, + [0x26] = { + id = 0x26, + server_clusters = { 0x0402 } + } + } + } +) + +local mock_device_contact = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("acceleration-motion-temperature-contact-battery.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "WISZB-137", + server_clusters = { 0x0003, 0x0005, 0x0006 } + }, + [0x2D] = { + id = 0x2D, + server_clusters = { 0x0000, 0x0001, 0x0003, 0x0020, 0x0500, 0xFC04 } + }, + [0x26] = { + id = 0x26, + server_clusters = { 0x0402 } + } + } + } +) + +local Frient_AccelerationMeasurementCluster = { + ID = 0xFC04, + ManufacturerSpecificCode = 0x1015, + attributes = { + MeasuredValueX = { ID = 0x0000, data_type = data_types.name_to_id_map["Int16"] }, + MeasuredValueY = { ID = 0x0001, data_type = data_types.name_to_id_map["Int16"] }, + MeasuredValueZ = { ID = 0x0002, data_type = data_types.name_to_id_map["Int16"] } + }, +} + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_device_contact) +end + +test.set_test_init_function(test_init) + +local function custom_configure_reporting(device, cluster, attribute, data_type, min_interval, max_interval, reportable_change, mfg_code) + local message = cluster_base.configure_reporting(device, + data_types.ClusterId(cluster), + data_types.AttributeId(attribute), + data_type, + min_interval, + max_interval, + reportable_change) + + -- Set the manufacturer-specific bit and add the manufacturer code + message.body.zcl_header.frame_ctrl:set_mfg_specific() + message.body.zcl_header.mfg_code = data_types.validate_or_build_type(mfg_code, data_types.Uint16, "mfg_code") + + return message +end + +test.register_coroutine_test( + "init and doConfigure lifecycles should be handled properly", + function() + test.socket.environment_update:__queue_receive({ "zigbee", { hub_zigbee_id = base64.encode(zigbee_test_utils.mock_hub_eui) } }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.device_lifecycle:__queue_receive({ mock_device_contact.id, "init" }) + + test.wait_for_events() + + --test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_contact.id, "doConfigure" }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device_contact, + 30, + 21600, + 1 + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + zigbee_test_utils.mock_hub_eui, + TemperatureMeasurement.ID, + TEMPERATURE_ENDPOINT + ):to_endpoint(TEMPERATURE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device_contact, + 0x001E, + 0x012C, + 1 + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device_contact, + 30, + 600, + 100 + ):to_endpoint(TEMPERATURE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + IASZone.attributes.IASCIEAddress:write( + mock_device_contact, + zigbee_test_utils.mock_hub_eui + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + zigbee_test_utils.mock_hub_eui, + Frient_AccelerationMeasurementCluster.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + custom_configure_reporting( + mock_device_contact, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.data_type, + 0x0000, + 0x012C, + 0x0001, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + custom_configure_reporting( + mock_device_contact, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.data_type, + 0x0000, + 0x012C, + 0x0001, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + custom_configure_reporting( + mock_device_contact, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.data_type, + 0x0000, + 0x012C, + 0x0001, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device_contact, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ) + }) + + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + IASZone.attributes.CurrentZoneSensitivityLevel:write( + mock_device_contact, + 0x000A + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device_contact, + 0x001E, + 0x0E10, + 100 + ):to_endpoint(TEMPERATURE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device_contact, + 0, + 3600, + 0 + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + zigbee_test_utils.mock_hub_eui, + Frient_AccelerationMeasurementCluster.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ) + }) + + mock_device_contact:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature measurement cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2300)} + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 23.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { "register_native_capability_attr_handler", + { + device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" + } + } + } + } +) + +test.register_message_test( + "Battery min voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = {mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 23)} + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +test.register_message_test( + "Battery max voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = {mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 30)} + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + +test.register_coroutine_test( +"Refresh necessary attributes", +function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} } }) + test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.ZoneStatus:read(mock_device):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) }) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) }) + test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_ENDPOINT) }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) +end +) + +test.register_message_test( + "Reported ZoneStatus change should be handled: active motion and inactive acceleration", + { + { + channel = "zigbee", + direction = "receive", + message = {mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0001)} + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.active(mock_device)) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.accelerationSensor.acceleration.inactive(mock_device)) + } + } +) + +test.register_message_test( + "Reported ZoneStatus change should be handled: inactive motion and active acceleration", + { + { + channel = "zigbee", + direction = "receive", + message = {mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0002)} + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive(mock_device)) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.accelerationSensor.acceleration.active(mock_device)) + } + } +) + +test.register_coroutine_test( + "Three Axis report should be correctly handled", + function() + local attr_report_data = { + { 0x0000, data_types.Int16.ID, 300}, + { 0x0001, data_types.Int16.ID, 200}, + { 0x0002, data_types.Int16.ID, 100}, + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Frient_AccelerationMeasurementCluster.ID, attr_report_data, 0x1015) + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) + ) + end +) + +test.register_coroutine_test( + "Contact sensor open events should be correctly handled when preference is set", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__set_channel_ordering("relaxed") + local attr_report_data = { + { 0x0000, data_types.Int16.ID, 300}, + { 0x0001, data_types.Int16.ID, 200}, + { 0x0002, data_types.Int16.ID, -902}, + } + test.socket.zigbee:__queue_receive({ + mock_device_contact.id, + zigbee_test_utils.build_attribute_report(mock_device_contact, Frient_AccelerationMeasurementCluster.ID, attr_report_data, 0x1015) + }) + + test.socket.capability:__expect_send( + mock_device_contact:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, -902})) + ) + + test.socket.capability:__expect_send( + mock_device_contact:generate_test_message("main", capabilities.contactSensor.contact.open()) + ) + end +) + +test.register_coroutine_test( + "Contact sensor close events should be correctly handled when preference is set", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__set_channel_ordering("relaxed") + local attr_report_data = { + { 0x0000, data_types.Int16.ID, 300}, + { 0x0001, data_types.Int16.ID, 200}, + { 0x0002, data_types.Int16.ID, 100}, + } + test.socket.zigbee:__queue_receive({ + mock_device_contact.id, + zigbee_test_utils.build_attribute_report(mock_device_contact, Frient_AccelerationMeasurementCluster.ID, attr_report_data, 0x1015) + }) + + test.socket.capability:__expect_send( + mock_device_contact:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) + ) + + test.socket.capability:__expect_send( + mock_device_contact:generate_test_message("main", capabilities.contactSensor.contact.closed()) + ) + end +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_orvibo_contact_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_orvibo_contact_sensor.lua index 458af19546..291755bb17 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_orvibo_contact_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_orvibo_contact_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_samjin_multi_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_samjin_multi_sensor.lua index d8a23f4af5..2e0c67469d 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_samjin_multi_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_samjin_multi_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_sengled_contact_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_sengled_contact_sensor.lua index 1950a22649..39f127dcf4 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_sengled_contact_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_sengled_contact_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_smartsense_multi.lua b/drivers/SmartThings/zigbee-contact/src/test/test_smartsense_multi.lua index 9db8033062..d5482e7d6e 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_smartsense_multi.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_smartsense_multi.lua @@ -1,21 +1,13 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" + +local IASZone = clusters.IASZone local SMARTSENSE_PROFILE_ID = 0xFC01 local MFG_CODE = 0x110A @@ -437,4 +429,68 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.register_message_test( + "ZoneStatusChangeNotification should generate contact event when garageSensor not set: open", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0001, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) + } + } +) + +test.register_message_test( + "ZoneStatusChangeNotification should generate contact event when garageSensor not set: closed", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0000, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + } + } +) + +test.register_message_test( + "ZoneStatus attr report should generate contact event when garageSensor not set: open", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0001) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) + } + } +) + +test.register_message_test( + "ZoneStatus attr report should generate contact event when garageSensor not set: closed", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0000) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + } + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_smartthings_multi_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_smartthings_multi_sensor.lua index 05e5fdd0d4..c86078f224 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_smartthings_multi_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_smartthings_multi_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" @@ -230,6 +219,38 @@ test.register_coroutine_test( end ) +test.register_message_test( + "ZoneStatusChangeNotification should generate contact event when garageSensor not set: open", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0001, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) + } + } +) + +test.register_message_test( + "ZoneStatusChangeNotification should generate contact event when garageSensor not set: closed", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0000, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + } + } +) + test.register_coroutine_test( "Refresh necessary attributes", function() diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_third_reality_contact.lua b/drivers/SmartThings/zigbee-contact/src/test/test_third_reality_contact.lua index 07beb7c0dd..a6d989609f 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_third_reality_contact.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_third_reality_contact.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact.lua b/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact.lua index b01e88e62b..8db97fd224 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -147,6 +136,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_battery.lua b/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_battery.lua index 44570bcce1..5e3404b2a0 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_battery.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_battery.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_tyco.lua b/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_tyco.lua index 79e5c68fa5..0ee62b73ee 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_tyco.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_tyco.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" @@ -80,6 +69,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -97,6 +94,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() end ) diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua index 9e45b9dbad..be7f8ad147 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -18,12 +8,12 @@ local defaults = require "st.zigbee.defaults" local zcl_clusters = require "st.zigbee.zcl.clusters" local battery_attribute_configuration = { - cluster = zcl_clusters.PowerConfiguration.ID, - attribute = zcl_clusters.PowerConfiguration.attributes.BatteryPercentageRemaining.ID, - minimum_interval = 30, - maximum_interval = 14300, -- ~4hrs - data_type = zcl_clusters.PowerConfiguration.attributes.BatteryPercentageRemaining.base_type, - reportable_change = 1 + cluster = zcl_clusters.PowerConfiguration.ID, + attribute = zcl_clusters.PowerConfiguration.attributes.BatteryPercentageRemaining.ID, + minimum_interval = 30, + maximum_interval = 14300, -- ~4hrs + data_type = zcl_clusters.PowerConfiguration.attributes.BatteryPercentageRemaining.base_type, + reportable_change = 1 } local function device_init(driver, device) @@ -38,9 +28,9 @@ local zigbee_dimmer_remote_driver_template = { capabilities.switchLevel }, lifecycle_handlers = { - init = device_init, + init = device_init, }, - sub_drivers = { require("zigbee-accessory-dimmer"), require("zigbee-battery-accessory-dimmer")}, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/sub_drivers.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/sub_drivers.lua new file mode 100644 index 0000000000..c489a2b5d2 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/sub_drivers.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" + +local sub_drivers = { + lazy_load_if_possible("zigbee-accessory-dimmer"), + lazy_load_if_possible("zigbee-battery-accessory-dimmer"), +} + +return sub_drivers diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_accessory_dimmer.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_accessory_dimmer.lua index 477b2eac71..751fc1bee9 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_accessory_dimmer.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_accessory_dimmer.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2022 SmartThings, Inc. -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -217,4 +217,81 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "On command when current level is 0 should reset level then toggle switch", + function() + mock_device:set_field("current_level", 0) + test.socket.zigbee:__queue_receive({ mock_device.id, OnOff.server.commands.On.build_test_rx(mock_device) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switchLevel.level(10))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) + end +) + +test.register_coroutine_test( + "Step command(MoveStepMode.DOWN) to level 0 should emit switch off and level 0", + function() + mock_device:set_field("current_level", 10) + local step_command = Level.server.commands.Step.build_test_rx(mock_device, Level.types.MoveStepMode.DOWN, 0x00, + 0x0000, 0x00, 0x00) + local frm_ctrl = FrameCtrl(0x01) + step_command.body.zcl_header.frame_ctrl = frm_ctrl + test.socket.zigbee:__queue_receive({ mock_device.id, step_command }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switchLevel.level(0))) + end +) + +test.register_coroutine_test( + "Step command(MoveStepMode.UP) when device is off should emit switch on", + function() + mock_device:set_field("current_status", "off") + local step_command = Level.server.commands.Step.build_test_rx(mock_device, Level.types.MoveStepMode.UP, 0x00, + 0x0000, 0x00, 0x00) + local frm_ctrl = FrameCtrl(0x01) + step_command.body.zcl_header.frame_ctrl = frm_ctrl + test.socket.zigbee:__queue_receive({ mock_device.id, step_command }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.on())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switchLevel.level(100))) + end +) + +test.register_coroutine_test( + "Capability command setLevel should be handled", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 50 } } }) + test.wait_for_events() + test.mock_time.advance_time(1) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switchLevel.level(50))) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Capability command setLevel 0 should restore previous level", + function() + mock_device:set_field("current_level", 30) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 0 } } }) + test.wait_for_events() + test.mock_time.advance_time(1) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switchLevel.level(30))) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "device added lifecycle should initialize device state", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.on())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switchLevel.level(100))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "held"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = true }))) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.wait_for_events() + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_battery_accessory_dimmer.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_battery_accessory_dimmer.lua index b16fe8cd11..243f559466 100644 Binary files a/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_battery_accessory_dimmer.lua and b/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_battery_accessory_dimmer.lua differ diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/can_handle.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/can_handle.lua new file mode 100644 index 0000000000..6e87ef0c4b --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_accessory_dimmer = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-accessory-dimmer.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-accessory-dimmer") + end + end + return false +end + +return is_zigbee_accessory_dimmer diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/fingerprints.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/fingerprints.lua new file mode 100644 index 0000000000..6528171f35 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_ACCESSORY_DIMMER_FINGERPRINTS = { + { mfr = "Aurora", model = "Remote50AU" }, + { mfr = "LDS", model = "ZBT-DIMController-D0800" } +} + +return ZIGBEE_ACCESSORY_DIMMER_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua index 3e02125863..b37298802a 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -29,10 +19,6 @@ local CURRENT_LEVEL = "current_level" local CURRENT_STATUS = "current_status" local STEP = 10 -local ZIGBEE_ACCESSORY_DIMMER_FINGERPRINTS = { - { mfr = "Aurora", model = "Remote50AU" }, - { mfr = "LDS", model = "ZBT-DIMController-D0800" } -} local generate_switch_onoff_event = function(device, value) if value == "on" then @@ -122,11 +108,17 @@ local switch_level_set_level_command_handler = function(driver, device, command) end local device_added = function(self, device) - generate_switch_onoff_event(device, "on") - generate_switch_level_event(device, 100) + if device:get_latest_state("main", capabilities.switch.ID, capabilities.switch.switch.NAME) == nil then + generate_switch_onoff_event(device, "on") + end + if device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) == nil then + generate_switch_level_event(device, DEFAULT_LEVEL) + end device:emit_event(capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } })) device:emit_event(capabilities.button.supportedButtonValues({"pushed", "held"}, { visibility = { displayed = false } })) - device:emit_event(capabilities.button.button.pushed({state_change = true})) + if device:get_latest_state("main", capabilities.button.ID, capabilities.button.button.NAME) == nil then + device:emit_event(capabilities.button.button.pushed({state_change = true})) + end end local do_configure = function(self, device) @@ -136,15 +128,6 @@ local do_configure = function(self, device) end -local is_zigbee_accessory_dimmer = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_ACCESSORY_DIMMER_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - - return false -end local zigbee_accessory_dimmer = { NAME = "zigbee accessory dimmer", @@ -177,7 +160,7 @@ local zigbee_accessory_dimmer = { added = device_added, doConfigure = do_configure }, - can_handle = is_zigbee_accessory_dimmer + can_handle = require("zigbee-accessory-dimmer.can_handle"), } return zigbee_accessory_dimmer diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/can_handle.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/can_handle.lua new file mode 100644 index 0000000000..c3dd2870d4 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_centralite_systems = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-battery-accessory-dimmer.CentraliteSystems.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-battery-accessory-dimmer.CentraliteSystems") + end + end + + return false +end + +return is_centralite_systems diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/fingerprints.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/fingerprints.lua new file mode 100644 index 0000000000..a8d90850b9 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local CENTRALITE_SYSTEMS_FINGERPRINTS = { + { mfr = "Centralite Systems", model = "3131-G" } +} + +return CENTRALITE_SYSTEMS_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/init.lua index 0734bf57e8..a7df73aec5 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local battery_defaults = require "st.zigbee.defaults.battery_defaults" @@ -24,9 +14,6 @@ local capabilities = require "st.capabilities" local DEFAULT_LEVEL = 100 local DOUBLE_STEP = 10 -local CENTRALITE_SYSTEMS_FINGERPRINTS = { - { mfr = "Centralite Systems", model = "3131-G" } -} local generate_switch_level_event = function(device, value) device:emit_event(capabilities.switchLevel.level(value)) @@ -77,15 +64,6 @@ local do_configure = function(self, device) device:send(device_management.build_bind_request(device, Level.ID, self.environment_info.hub_zigbee_eui)) end -local is_centralite_systems = function(opts, driver, device) - for _, fingerprint in ipairs(CENTRALITE_SYSTEMS_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - - return false -end local voltage_configuration = { cluster = zcl_clusters.PowerConfiguration.ID, @@ -98,7 +76,6 @@ local voltage_configuration = { local function device_init(driver, device) device:add_configured_attribute(voltage_configuration) - device:add_monitored_attribute(voltage_configuration) device:remove_monitored_attribute(zcl_clusters.PowerConfiguration.ID, zcl_clusters.PowerConfiguration.attributes.BatteryPercentageRemaining.ID) device:remove_configured_attribute(zcl_clusters.PowerConfiguration.ID, zcl_clusters.PowerConfiguration.attributes.BatteryPercentageRemaining.ID) device:set_field(battery_defaults.DEVICE_MIN_VOLTAGE_KEY, 2.3) @@ -119,7 +96,7 @@ local centralite_systems = { init = device_init, doConfigure = do_configure }, - can_handle = is_centralite_systems + can_handle = require("zigbee-battery-accessory-dimmer.CentraliteSystems.can_handle"), } return centralite_systems diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/can_handle.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/can_handle.lua new file mode 100644 index 0000000000..888e889e6d --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_ikea_of_sweden = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-battery-accessory-dimmer.IKEAofSweden.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-battery-accessory-dimmer.IKEAofSweden") + end + end + + return false +end + +return is_ikea_of_sweden diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/fingerprints.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/fingerprints.lua new file mode 100644 index 0000000000..429684c8c7 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local IKEA_OF_SWEDEN_FINGERPRINTS = { + { mfr = "IKEA of Sweden", model = "TRADFRI wireless dimmer" } +} + +return IKEA_OF_SWEDEN_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/init.lua index 8edf50b296..4c87146ea2 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local utils = require 'st.utils' local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -23,9 +13,6 @@ local capabilities = require "st.capabilities" local DEFAULT_LEVEL = 100 local DOUBLE_STEP = 10 -local IKEA_OF_SWEDEN_FINGERPRINTS = { - { mfr = "IKEA of Sweden", model = "TRADFRI wireless dimmer" } -} local generate_switch_level_event = function(device, value) device:emit_event(capabilities.switchLevel.level(value)) @@ -99,15 +86,6 @@ local battery_perc_attr_handler = function(driver, device, value, zb_rx) device:emit_event(capabilities.battery.battery(utils.clamp_value(value.value, 0, 100))) end -local is_ikea_of_sweden = function(opts, driver, device) - for _, fingerprint in ipairs(IKEA_OF_SWEDEN_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - - return false -end local ikea_of_sweden = { NAME = "IKEA of Sweden", @@ -126,7 +104,7 @@ local ikea_of_sweden = { } } }, - can_handle = is_ikea_of_sweden + can_handle = require("zigbee-battery-accessory-dimmer.IKEAofSweden.can_handle"), } return ikea_of_sweden diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/can_handle.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/can_handle.lua new file mode 100644 index 0000000000..7b8f11b278 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_battery_accessory_dimmer = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-battery-accessory-dimmer.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-battery-accessory-dimmer") + end + end + + return false +end + +return is_zigbee_battery_accessory_dimmer diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/fingerprints.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/fingerprints.lua new file mode 100644 index 0000000000..16c4a7f7b4 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_BATTERY_ACCESSORY_DIMMER_FINGERPRINTS = { + { mfr = "sengled", model = "E1E-G7F" }, + { mfr = "IKEA of Sweden", model = "TRADFRI wireless dimmer" }, + { mfr = "Centralite Systems", model = "3131-G" } +} + +return ZIGBEE_BATTERY_ACCESSORY_DIMMER_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua index c14e4263c3..01417c1a64 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local zcl_clusters = require "st.zigbee.zcl.clusters" local OnOff = zcl_clusters.OnOff @@ -22,11 +13,6 @@ local SwitchLevel = capabilities.switchLevel local DEFAULT_LEVEL = 100 local DOUBLE_STEP = 10 -local ZIGBEE_BATTERY_ACCESSORY_DIMMER_FINGERPRINTS = { - { mfr = "sengled", model = "E1E-G7F" }, - { mfr = "IKEA of Sweden", model = "TRADFRI wireless dimmer" }, - { mfr = "Centralite Systems", model = "3131-G" } -} local generate_switch_level_event = function(device, value) device:emit_event(capabilities.switchLevel.level(value)) @@ -81,20 +67,15 @@ local switch_level_set_level_command_handler = function(driver, device, command) end local device_added = function(self, device) - generate_switch_onoff_event(device, "on") - generate_switch_level_event(device, DEFAULT_LEVEL) -end - -local is_zigbee_battery_accessory_dimmer = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_BATTERY_ACCESSORY_DIMMER_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end + if device:get_latest_state("main", capabilities.switch.ID, capabilities.switch.switch.NAME) == nil then + generate_switch_onoff_event(device, "on") + end + if device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) == nil then + generate_switch_level_event(device, DEFAULT_LEVEL) end - - return false end + local zigbee_battery_accessory_dimmer = { NAME = "zigbee battery accessory dimmer", zigbee_handlers = { @@ -117,8 +98,7 @@ local zigbee_battery_accessory_dimmer = { lifecycle_handlers = { added = device_added }, - sub_drivers = { require("zigbee-battery-accessory-dimmer/CentraliteSystems"), require("zigbee-battery-accessory-dimmer/IKEAofSweden"), require("zigbee-battery-accessory-dimmer/sengled") }, - can_handle = is_zigbee_battery_accessory_dimmer + sub_drivers = require("zigbee-battery-accessory-dimmer.sub_drivers"), } return zigbee_battery_accessory_dimmer diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/can_handle.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/can_handle.lua new file mode 100644 index 0000000000..5fa64577ca --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_sengled = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-battery-accessory-dimmer.sengled.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-battery-accessory-dimmer.sengled") + end + end + + return false +end + +return is_sengled diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/fingerprints.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/fingerprints.lua new file mode 100644 index 0000000000..252c48b6d4 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SENGLED_FINGERPRINTS = { + { mfr = "sengled", model = "E1E-G7F" } +} + +return SENGLED_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/init.lua index 08d3dfc4a8..7877c41b68 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" @@ -20,9 +10,6 @@ local DOUBLE_STEP = 10 local SENGLED_MFR_SPECIFIC_CLUSTER = 0xFC10 local SENGLED_MFR_SPECIFIC_COMMAND = 0x00 -local SENGLED_FINGERPRINTS = { - { mfr = "sengled", model = "E1E-G7F" } -} local generate_switch_level_event = function(device, value) device:emit_event(capabilities.switchLevel.level(value)) @@ -84,15 +71,6 @@ local sengled_mfr_specific_command_handler = function(driver, device, zb_rx) end end -local is_sengled = function(opts, driver, device) - for _, fingerprint in ipairs(SENGLED_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - - return false -end local sengled = { NAME = "sengled", @@ -103,7 +81,7 @@ local sengled = { } } }, - can_handle = is_sengled + can_handle = require("zigbee-battery-accessory-dimmer.sengled.can_handle"), } return sengled diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sub_drivers.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sub_drivers.lua new file mode 100644 index 0000000000..0669f18b25 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sub_drivers.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" + +local sub_drivers = { + lazy_load_if_possible("zigbee-battery-accessory-dimmer.CentraliteSystems"), + lazy_load_if_possible("zigbee-battery-accessory-dimmer.IKEAofSweden"), + lazy_load_if_possible("zigbee-battery-accessory-dimmer.sengled"), +} + +return sub_drivers diff --git a/drivers/SmartThings/zigbee-fan/src/configurations.lua b/drivers/SmartThings/zigbee-fan/src/configurations.lua index c2cbb57de0..f6bcbe7bf5 100644 --- a/drivers/SmartThings/zigbee-fan/src/configurations.lua +++ b/drivers/SmartThings/zigbee-fan/src/configurations.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-fan/src/fan-light/can_handle.lua b/drivers/SmartThings/zigbee-fan/src/fan-light/can_handle.lua new file mode 100644 index 0000000000..46523f65f9 --- /dev/null +++ b/drivers/SmartThings/zigbee-fan/src/fan-light/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_itm_fanlight(opts, driver, device) + local FINGERPRINTS = require("fan-light.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("fan-light") + end + end + return false +end + +return can_handle_itm_fanlight diff --git a/drivers/SmartThings/zigbee-fan/src/fan-light/fingerprints.lua b/drivers/SmartThings/zigbee-fan/src/fan-light/fingerprints.lua new file mode 100644 index 0000000000..bf93802c10 --- /dev/null +++ b/drivers/SmartThings/zigbee-fan/src/fan-light/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "Samsung Electronics", model = "SAMSUNG-ITM-Z-003" }, +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-fan/src/fan-light/init.lua b/drivers/SmartThings/zigbee-fan/src/fan-light/init.lua index 95329c1ddf..341876c676 100644 --- a/drivers/SmartThings/zigbee-fan/src/fan-light/init.lua +++ b/drivers/SmartThings/zigbee-fan/src/fan-light/init.lua @@ -1,34 +1,13 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local FanControl = clusters.FanControl local Level = clusters.Level local OnOff = clusters.OnOff -local FINGERPRINTS = { - { mfr = "Samsung Electronics", model = "SAMSUNG-ITM-Z-003" }, -} -local function can_handle_itm_fanlight(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end -- CAPABILITY HANDLERS @@ -74,7 +53,11 @@ local function zb_fan_control_handler(driver, device, value, zb_rx) end local function zb_level_handler(driver, device, value, zb_rx) - local evt = capabilities.switchLevel.level(math.floor((value.value / 254.0 * 100) + 0.5)) + local level = value.value + if level > 0 then + level = math.max(1, math.floor((level / 254.0 * 100) + 0.5)) + end + local evt = capabilities.switchLevel.level(level) device:emit_component_event(device.profile.components.light, evt) end @@ -111,9 +94,7 @@ local itm_fan_light = { [capabilities.fanSpeed.commands.setFanSpeed.NAME] = fan_speed_handler } }, - can_handle = can_handle_itm_fanlight + can_handle = require("fan-light.can_handle"), } return itm_fan_light - - diff --git a/drivers/SmartThings/zigbee-fan/src/init.lua b/drivers/SmartThings/zigbee-fan/src/init.lua index 1766be9ff1..c4a7185838 100644 --- a/drivers/SmartThings/zigbee-fan/src/init.lua +++ b/drivers/SmartThings/zigbee-fan/src/init.lua @@ -1,28 +1,17 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" -local configurationMap = require "configurations" local device_init = function(self, device) + local configurationMap = require "configurations" local configuration = configurationMap.get_device_configuration(device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end @@ -33,9 +22,7 @@ local zigbee_fan_driver = { capabilities.switchLevel, capabilities.fanspeed }, - sub_drivers = { - require("fan-light") - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { init = device_init }, @@ -45,4 +32,3 @@ local zigbee_fan_driver = { defaults.register_for_default_handlers(zigbee_fan_driver,zigbee_fan_driver.supported_capabilities) local fan = ZigbeeDriver("zigbee-fan", zigbee_fan_driver) fan:run() - diff --git a/drivers/SmartThings/zigbee-fan/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-fan/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-fan/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-fan/src/sub_drivers.lua b/drivers/SmartThings/zigbee-fan/src/sub_drivers.lua new file mode 100644 index 0000000000..f43392d622 --- /dev/null +++ b/drivers/SmartThings/zigbee-fan/src/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("fan-light"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-fan/src/test/test_fan_light.lua b/drivers/SmartThings/zigbee-fan/src/test/test_fan_light.lua index 4ef4ce37e6..1a61602c1e 100644 --- a/drivers/SmartThings/zigbee-fan/src/test/test_fan_light.lua +++ b/drivers/SmartThings/zigbee-fan/src/test/test_fan_light.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" local clusters = require "st.zigbee.zcl.clusters" @@ -442,4 +432,46 @@ test.register_message_test( } ) +test.register_message_test( + "Fan switch on command from main component", + { + { + channel = "capability", + direction = "receive", + message = { mock_base_device.id, { capability = "switch", component = "main", command = "on", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_base_device.id, FanControl.attributes.FanMode:write(mock_base_device, 1) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_base_device.id, FanControl.attributes.FanMode:read(mock_base_device) } + } + } +) + +test.register_message_test( + "Fan switch off command from main component", + { + { + channel = "capability", + direction = "receive", + message = { mock_base_device.id, { capability = "switch", component = "main", command = "off", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_base_device.id, FanControl.attributes.FanMode:write(mock_base_device, 0x00) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_base_device.id, FanControl.attributes.FanMode:read(mock_base_device) } + } + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml index c6dd3efcca..e5ad9e571a 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml +++ b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml @@ -58,18 +58,28 @@ zigbeeManufacturer: manufacturer: HEIMAN model: HT-EF-3.0 deviceProfileName: humidity-temp-battery + - id: frient/AQSZB-110 + deviceLabel: Air Quality Sensor + manufacturer: frient A/S + model: AQSZB-110 + deviceProfileName: frient-airquality-humidity-temperature-battery - id: frient/HMSZB-110 - deviceLabel: frient Multipurpose Sensor + deviceLabel: frient Humidity Sensor manufacturer: frient A/S model: HMSZB-110 - deviceProfileName: humidity-temp-battery + deviceProfileName: frient-humidity-temperature-battery + - id: frient/HMSZB-120 + deviceLabel: frient Humidity Sensor + manufacturer: frient A/S + model: HMSZB-120 + deviceProfileName: frient-humidity-temperature-battery - id: eWeLink/TH01 deviceLabel: eWeLink Multipurpose Sensor manufacturer: eWeLink model: TH01 deviceProfileName: humidity-temp-battery - id: eWeLink/SNZB-02P - deviceLabel: eWeLink Multipurpose Sensor + deviceLabel: SONOFF Zigbee Temperature and Humidity Sensor manufacturer: eWeLink model: SNZB-02P deviceProfileName: humidity-temp-battery @@ -83,13 +93,18 @@ zigbeeManufacturer: manufacturer: Third Reality, Inc model: 3RSM0147Z deviceProfileName: humidity-temp-battery + - id: "NodOn/STPH-4-1-00" + deviceLabel: Zigbee Temperature and Humidity Sensor + manufacturer: NodOn + model: STPH-4-1-00 + deviceProfileName: humidity-temp-battery zigbeeGeneric: - id: "HumidityTempGeneric" deviceLabel: Multipurpose Sensor deviceIdentifiers: - 0x0302 - clusters: - server: + clusters: + server: - 0x0402 # Temperature Measurement Cluster - 0x0405 # Relative Humidity Measurement Cluster deviceProfileName: humidity-temperature diff --git a/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-airquality-humidity-temperature-battery.yml b/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-airquality-humidity-temperature-battery.yml new file mode 100644 index 0000000000..08774c0d31 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-airquality-humidity-temperature-battery.yml @@ -0,0 +1,52 @@ +name: frient-airquality-humidity-temperature-battery +components: +- id: main + capabilities: + - id: airQualitySensor + version: 1 + - id: tvocMeasurement + version: 1 + - id: tvocHealthConcern + version: 1 + config: + values: + - key: "tvocHealthConcern.value" + enabledValues: + - good + - moderate + - slightlyUnhealthy + - unhealthy + - veryUnhealthy + - id: relativeHumidityMeasurement + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 +preferences: + - preferenceId: humidityOffset + explicit: true + - title: "Humidity Sensitivity (%)" + name: humiditySensitivity + description: "Minimum change in humidity level to report" + required: false + preferenceType: number + definition: + minimum: 1 + maximum: 50 + default: 3 + - preferenceId: tempOffset + explicit: true + - title: "Temperature Sensitivity (°)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 diff --git a/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-humidity-temperature-battery.yml b/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-humidity-temperature-battery.yml new file mode 100644 index 0000000000..cd186812bf --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-humidity-temperature-battery.yml @@ -0,0 +1,39 @@ +name: frient-humidity-temperature-battery +components: +- id: main + capabilities: + - id: relativeHumidityMeasurement + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: TempHumiditySensor +preferences: + - preferenceId: humidityOffset + explicit: true + - title: "Humidity Sensitivity (%)" + name: humiditySensitivity + description: "Minimum change in humidity level to report" + required: false + preferenceType: number + definition: + minimum: 1 + maximum: 50 + default: 3 + - preferenceId: tempOffset + explicit: true + - title: "Temperature Sensitivity (°C)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/can_handle.lua new file mode 100644 index 0000000000..da7a97c64b --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_aqara_products = function(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..8e6df9f7d6 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.sensor_ht.agl02" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/init.lua index e712f7711f..d3a8c7f89d 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" @@ -11,9 +14,6 @@ local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 local MFG_CODE = 0x115F -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.sensor_ht.agl02" } -} -- temperature: 0.5C, humidity: 2% local configuration = { @@ -43,14 +43,6 @@ local configuration = { } } -local is_aqara_products = function(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function battery_level_handler(driver, device, value, zb_rx) local voltage = value.value @@ -67,7 +59,6 @@ local function device_init(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end @@ -99,7 +90,7 @@ local aqara_humidity_handler = { init = device_init, added = added_handler }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } return aqara_humidity_handler diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/can_handle.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/can_handle.lua new file mode 100644 index 0000000000..a36cc01536 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_centralite_sensor(opts, driver, device) + local FINGERPRINTS = require("centralite-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("centralite-sensor") + end + end + return false +end + +return can_handle_centralite_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/fingerprints.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/fingerprints.lua new file mode 100644 index 0000000000..df4f8a34a3 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local CENTRALITE_SENSOR_FINGERPRINTS = { + { mfr = "CentraLite", model = "3310-S" }, + { mfr = "CentraLite", model = "3310-G" }, + { mfr = "CentraLite", model = "3310" } +} + +return CENTRALITE_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/init.lua index e1c11dd03b..11fedcfe24 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -23,20 +13,7 @@ local configurationMap = require "configurations" local HUMIDITY_CLUSTER_ID = 0xFC45 local HUMIDITY_MEASURE_ATTR_ID = 0x0000 -local CENTRALITE_SENSOR_FINGERPRINTS = { - { mfr = "CentraLite", model = "3310-S" }, - { mfr = "CentraLite", model = "3310-G" }, - { mfr = "CentraLite", model = "3310" } -} -local function can_handle_centralite_sensor(opts, driver, device) - for _, fingerprint in ipairs(CENTRALITE_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function device_init(driver, device) device:remove_configured_attribute(clusters.RelativeHumidity.ID, clusters.RelativeHumidity.attributes.MeasuredValue.ID) @@ -89,7 +66,7 @@ local centralite_sensor = { } } }, - can_handle = can_handle_centralite_sensor + can_handle = require("centralite-sensor.can_handle"), } return centralite_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua index 5c8f6834e9..8bc35c8765 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2022 SmartThings, Inc. -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -22,24 +22,34 @@ local PowerConfiguration = clusters.PowerConfiguration local devices = { FRIENT_HUMIDITY_TEMP_SENSOR = { FINGERPRINTS = { - { mfr = "frient A/S", model = "HMSZB-110" } + { mfr = "frient A/S", model = "HMSZB-110" }, + { mfr = "frient A/S", model = "HMSZB-120" }, + { mfr = "frient A/S", model = "AQSZB-110" } }, CONFIGURATION = { { cluster = RelativeHumidity.ID, attribute = RelativeHumidity.attributes.MeasuredValue.ID, minimum_interval = 60, - maximum_interval = 600, + maximum_interval = 3600, data_type = RelativeHumidity.attributes.MeasuredValue.base_type, - reportable_change = 100 + reportable_change = 300 }, { cluster = TemperatureMeasurement.ID, attribute = TemperatureMeasurement.attributes.MeasuredValue.ID, - minimum_interval = 60, - maximum_interval = 600, + minimum_interval = 30, + maximum_interval = 3600, data_type = TemperatureMeasurement.attributes.MeasuredValue.base_type, - reportable_change = 10 + reportable_change = 100 + }, + { + cluster = PowerConfiguration.ID, + attribute = PowerConfiguration.attributes.BatteryVoltage.ID, + minimum_interval = 30 , + maximum_interval = 21600, + data_type = PowerConfiguration.attributes.BatteryVoltage.base_type, + reportable_change = 1 } } }, diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/can_handle.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/can_handle.lua new file mode 100644 index 0000000000..f82f572b43 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_frient(opts, driver, device, ...) + local FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS = require ("frient-sensor.air-quality.fingerprints") + for _, fingerprint in ipairs(FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model and fingerprint.subdriver == "airquality" then + return true, require("frient-sensor.air-quality") + end + end + return false +end + +return can_handle_frient diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/fingerprints.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/fingerprints.lua new file mode 100644 index 0000000000..82c98c39fe --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS = { + { mfr = "frient A/S", model = "AQSZB-110", subdriver = "airquality" } +} + +return FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua new file mode 100644 index 0000000000..25ff436988 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua @@ -0,0 +1,147 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local util = require "st.utils" +local data_types = require "st.zigbee.data_types" +local zcl_clusters = require "st.zigbee.zcl.clusters" +local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement +local HumidityMeasurement = zcl_clusters.RelativeHumidity +local PowerConfiguration = zcl_clusters.PowerConfiguration +local device_management = require "st.zigbee.device_management" +local cluster_base = require "st.zigbee.cluster_base" +local battery_defaults = require "st.zigbee.defaults.battery_defaults" + +local Frient_VOCMeasurement = { + ID = 0xFC03, + ManufacturerSpecificCode = 0x1015, + attributes = { + MeasuredValue = { ID = 0x0000, base_type = data_types.Uint16 }, + MinMeasuredValue = { ID = 0x0001, base_type = data_types.Uint16 }, + MaxMeasuredValue = { ID = 0x0002, base_type = data_types.Uint16 }, + Resolution = { ID = 0x0003, base_type = data_types.Uint16 }, + }, +} + +Frient_VOCMeasurement.attributes.MeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.MinMeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.MaxMeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.Resolution._cluster = Frient_VOCMeasurement + +local MAX_VOC_REPORTABLE_VALUE = 5500 -- Max VOC reportable value + +--- Table to map VOC (ppb) to HealthConcern +local VOC_TO_HEALTHCONCERN_MAPPING = { + [2201] = "veryUnhealthy", + [661] = "unhealthy", + [221] = "slightlyUnhealthy", + [66] = "moderate", + [0] = "good", +} + +--- Map VOC (ppb) to HealthConcern +local function voc_to_healthconcern(raw_voc) + for voc, perc in util.rkeys(VOC_TO_HEALTHCONCERN_MAPPING) do + if raw_voc >= voc then + return perc + end + end +end +--- Map VOC (ppb) to CAQI +local function voc_to_caqi(raw_voc) + if (raw_voc > 5500) then + return 100 + else + return math.floor(raw_voc*99/5500) + end +end + +-- May take around 8 minutes for the first valid VOC measurement to be reported after the device is powered on +local function voc_measure_value_attr_handler(driver, device, attr_val, zb_rx) + local voc_value = attr_val.value + if (voc_value < 65535) then -- ignore it if it's outside the limits + voc_value = util.clamp_value(voc_value, 0, MAX_VOC_REPORTABLE_VALUE) + device:emit_event(capabilities.airQualitySensor.airQuality({ value = voc_to_caqi(voc_value)})) + device:emit_event(capabilities.tvocHealthConcern.tvocHealthConcern(voc_to_healthconcern(voc_value))) + device:emit_event(capabilities.tvocMeasurement.tvocLevel({ value = voc_value, unit = "ppb" })) + end +end + +-- The device sends the value of MeasuredValue to be 0x8000, which corresponds to -327.68C, until it gets the first valid measurement. Therefore we don't emit event before the value is correct. It may take up to 4 minutes +local function temperatureHandler(driver, device, attr_val, zb_rx) + local temp_value = attr_val.value + if (temp_value > -32768) then + device:emit_event(capabilities.temperatureMeasurement.temperature({ value = temp_value / 100, unit = "C" })) + end +end + +local function device_init(driver, device) + local configurationMap = require "configurations" + battery_defaults.build_linear_voltage_init(2.3, 3.0)(driver, device) + local configuration = configurationMap.get_device_configuration(device) + if configuration ~= nil then + for _, attribute in ipairs(configuration) do + device:add_configured_attribute(attribute) + end + end +end + +local function device_added(driver, device) + device:emit_event(capabilities.airQualitySensor.airQuality(voc_to_caqi(0))) + device:emit_event(capabilities.tvocHealthConcern.tvocHealthConcern(voc_to_healthconcern(0))) + device:emit_event(capabilities.tvocMeasurement.tvocLevel({ value = 0, unit = "ppb" })) +end + +local function do_refresh(driver, device) + local FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS = require "frient-sensor.air-quality.fingerprints" + for _, fingerprint in ipairs(FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + device:send(cluster_base.read_manufacturer_specific_attribute(device, Frient_VOCMeasurement.ID, Frient_VOCMeasurement.attributes.MeasuredValue.ID, Frient_VOCMeasurement.ManufacturerSpecificCode):to_endpoint(0x26)) + device:send(TemperatureMeasurement.attributes.MeasuredValue:read(device):to_endpoint(0x26)) + device:send(HumidityMeasurement.attributes.MeasuredValue:read(device):to_endpoint(0x26)) + device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) + end + end +end + +local function do_configure(driver, device) + device:configure() + device:send(device_management.build_bind_request(device, Frient_VOCMeasurement.ID, driver.environment_info.hub_zigbee_eui, 0x26)) + + device:send( + cluster_base.configure_reporting( + device, + data_types.ClusterId(Frient_VOCMeasurement.ID), + Frient_VOCMeasurement.attributes.MeasuredValue.ID, + Frient_VOCMeasurement.attributes.MeasuredValue.base_type.ID, + 60, 600, 10 + ):to_endpoint(0x26) + ) + + device.thread:call_with_delay(5, function() + do_refresh(driver, device) + end) +end + +local frient_airquality_sensor = { + NAME = "frient Air Quality Sensor", + lifecycle_handlers = { + init = device_init, + added = device_added, + doConfigure = do_configure, + }, + zigbee_handlers = { + cluster = {}, + attr = { + [Frient_VOCMeasurement.ID] = { + [Frient_VOCMeasurement.attributes.MeasuredValue.ID] = voc_measure_value_attr_handler, + }, + [TemperatureMeasurement.ID] = { + [TemperatureMeasurement.attributes.MeasuredValue.ID] = temperatureHandler, + }, + } + }, + can_handle = require("frient-sensor.air-quality.can_handle") +} + +return frient_airquality_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/can_handle.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/can_handle.lua new file mode 100644 index 0000000000..060f58f76e --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_frient_sensor(opts, driver, device) + local FINGERPRINTS = require("frient-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("frient-sensor") + end + end + return false +end + +return can_handle_frient_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/fingerprints.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/fingerprints.lua new file mode 100644 index 0000000000..f7969cafdd --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FRIENT_TEMP_HUMUDITY_SENSOR_FINGERPRINTS = { + { mfr = "frient A/S", model = "HMSZB-110" }, + { mfr = "frient A/S", model = "HMSZB-120" }, + { mfr = "frient A/S", model = "AQSZB-110" } +} + +return FRIENT_TEMP_HUMUDITY_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua index c63b319cea..d0d6190ad0 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua @@ -1,50 +1,55 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -local battery_defaults = require "st.zigbee.defaults.battery_defaults" -local configurationMap = require "configurations" - -local FRIENT_TEMP_HUMUDITY_SENSOR_FINGERPRINTS = { - { mfr = "frient A/S", model = "HMSZB-110" }, -} - -local function can_handle_frient_sensor(opts, driver, device) - for _, fingerprint in ipairs(FRIENT_TEMP_HUMUDITY_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end +local zcl_clusters = require "st.zigbee.zcl.clusters" +local HumidityMeasurement = zcl_clusters.RelativeHumidity +local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement local function device_init(driver, device) + local configurationMap = require "configurations" + local battery_defaults = require "st.zigbee.defaults.battery_defaults" battery_defaults.build_linear_voltage_init(2.3,3.0)(driver, device) local configuration = configurationMap.get_device_configuration(device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end +local function do_configure(driver, device, event, args) + device:configure() + device.thread:call_with_delay(5, function() + device:refresh() + end) +end + +local function info_changed(driver, device, event, args) + for name, value in pairs(device.preferences) do + if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then + local sensitivity = math.floor((device.preferences[name]) * 100 + 0.5) + if (name == "temperatureSensitivity") then + device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 3600, sensitivity)) + end + if (name == "humiditySensitivity") then + device:send(HumidityMeasurement.attributes.MeasuredValue:configure_reporting(device, 60, 3600, sensitivity)) + end + end + end + device.thread:call_with_delay(5, function() + device:refresh() + end) +end + local frient_sensor = { NAME = "Frient Humidity Sensor", lifecycle_handlers = { - init = device_init + init = device_init, + doConfigure = do_configure, + infoChanged = info_changed }, - can_handle = can_handle_frient_sensor + sub_drivers = require("frient-sensor.sub_drivers"), + can_handle = require("frient-sensor.can_handle"), } return frient_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/sub_drivers.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/sub_drivers.lua new file mode 100644 index 0000000000..cc45e1b394 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("frient-sensor.air-quality") +} + +return sub_drivers diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/can_handle.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/can_handle.lua new file mode 100644 index 0000000000..4e3aff5517 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_heiman_sensor(opts, driver, device) + local FINGERPRINTS = require("heiman-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("heiman-sensor") + end + end + return false +end + +return can_handle_heiman_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/fingerprints.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/fingerprints.lua new file mode 100644 index 0000000000..b0f9e1c7ab --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local HEIMAN_TEMP_HUMUDITY_SENSOR_FINGERPRINTS = { + { mfr = "Heiman", model = "b467083cfc864f5e826459e5d8ea6079" }, + { mfr = "HEIMAN", model = "888a434f3cfc47f29ec4a3a03e9fc442" }, + { mfr = "HEIMAN", model = "HT-EM" }, + { mfr = "HEIMAN", model = "HT-EF-3.0" } +} + +return HEIMAN_TEMP_HUMUDITY_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/init.lua index 10a60c8392..8be2857f70 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -20,21 +10,7 @@ local RelativeHumidity = clusters.RelativeHumidity local TemperatureMeasurement = clusters.TemperatureMeasurement local PowerConfiguration = clusters.PowerConfiguration -local HEIMAN_TEMP_HUMUDITY_SENSOR_FINGERPRINTS = { - { mfr = "Heiman", model = "b467083cfc864f5e826459e5d8ea6079" }, - { mfr = "HEIMAN", model = "888a434f3cfc47f29ec4a3a03e9fc442" }, - { mfr = "HEIMAN", model = "HT-EM" }, - { mfr = "HEIMAN", model = "HT-EF-3.0" } -} -local function can_handle_heiman_sensor(opts, driver, device) - for _, fingerprint in ipairs(HEIMAN_TEMP_HUMUDITY_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function do_refresh(driver, device) device:send(RelativeHumidity.attributes.MeasuredValue:read(device):to_endpoint(0x02)) @@ -59,7 +35,7 @@ local heiman_sensor = { [capabilities.refresh.commands.refresh.NAME] = do_refresh, } }, - can_handle = can_handle_heiman_sensor + can_handle = require("heiman-sensor.can_handle"), } return heiman_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua index cf136ae1fd..9ca7cd734d 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -51,7 +41,6 @@ local function device_init(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end @@ -79,17 +68,10 @@ local zigbee_humidity_driver = { init = device_init, added = added_handler, }, - sub_drivers = { - require("aqara"), - require("plant-link"), - require("plaid-systems"), - require("centralite-sensor"), - require("heiman-sensor"), - require("frient-sensor") - }, + sub_drivers = require("sub_drivers"), health_check = false, } -defaults.register_for_default_handlers(zigbee_humidity_driver, zigbee_humidity_driver.supported_capabilities) +defaults.register_for_default_handlers(zigbee_humidity_driver, zigbee_humidity_driver.supported_capabilities, {native_capability_attrs_enabled = true}) local driver = ZigbeeDriver("zigbee-humidity-sensor", zigbee_humidity_driver) driver:run() diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/can_handle.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/can_handle.lua new file mode 100644 index 0000000000..a11116318e --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_plaid_systems_humidity_sensor = function(opts, driver, device) + local FINGERPRINTS = require("plaid-systems.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("plaid-systems") + end + end + + return false +end + +return is_zigbee_plaid_systems_humidity_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/fingerprints.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/fingerprints.lua new file mode 100644 index 0000000000..fbacec9269 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS = { + { mfr = "PLAID SYSTEMS", model = "PS-SPRZMS-01" }, + { mfr = "PLAID SYSTEMS", model = "PS-SPRZMS-SLP1" }, + { mfr = "PLAID SYSTEMS", model = "PS-SPRZMS-SLP3" }, +} + +return ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/init.lua index 2233aab2a3..093007adf9 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local device_management = require "st.zigbee.device_management" @@ -21,21 +11,7 @@ local PowerConfiguration = zcl_clusters.PowerConfiguration local RelativeHumidity = zcl_clusters.RelativeHumidity local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement -local ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS = { - { mfr = "PLAID SYSTEMS", model = "PS-SPRZMS-01" }, - { mfr = "PLAID SYSTEMS", model = "PS-SPRZMS-SLP1" }, - { mfr = "PLAID SYSTEMS", model = "PS-SPRZMS-SLP3" }, -} -local is_zigbee_plaid_systems_humidity_sensor = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - - return false -end local battery_mains_voltage_attr_handler = function(driver, device, value, zb_rx) local min = 2500 @@ -87,7 +63,7 @@ local plaid_systems_humdity_sensor = { } } }, - can_handle = is_zigbee_plaid_systems_humidity_sensor + can_handle = require("plaid-systems.can_handle"), } return plaid_systems_humdity_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/can_handle.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/can_handle.lua new file mode 100644 index 0000000000..9dcbc3e407 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_plant_link_humidity_sensor = function(opts, driver, device) + local FINGERPRINTS = require("plant-link.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model and device:supports_server_cluster(fingerprint.cluster_id) then + return true, require("plant-link") + end + end + + return false +end + +return is_zigbee_plant_link_humidity_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/fingerprints.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/fingerprints.lua new file mode 100644 index 0000000000..3b5e5f648d --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/fingerprints.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local zcl_clusters = require "st.zigbee.zcl.clusters" +local PLANT_LINK_MANUFACTURER_SPECIFIC_CLUSTER = 0xFC08 + +local ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS = { + { mfr = "", model = "", cluster_id = PLANT_LINK_MANUFACTURER_SPECIFIC_CLUSTER }, + { mfr = "", model = "", cluster_id = zcl_clusters.ElectricalMeasurement.ID } +} + +return ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/init.lua index 0a2d7fb225..07ab4a095e 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -18,12 +8,7 @@ local PowerConfiguration = zcl_clusters.PowerConfiguration local RelativeHumidity = zcl_clusters.RelativeHumidity local utils = require "st.utils" -local PLANT_LINK_MANUFACTURER_SPECIFIC_CLUSTER = 0xFC08 -local ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS = { - { mfr = "", model = "", cluster_id = PLANT_LINK_MANUFACTURER_SPECIFIC_CLUSTER }, - { mfr = "", model = "", cluster_id = zcl_clusters.ElectricalMeasurement.ID } -} local humidity_value_attr_handler = function(driver, device, value, zb_rx) -- adc reading of 0x1ec0 produces a plant fuel level near 0 @@ -44,15 +29,6 @@ local battery_mains_voltage_attr_handler = function(driver, device, value, zb_rx device:emit_event(capabilities.battery.battery(percent)) end -local is_zigbee_plant_link_humidity_sensor = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model and device:supports_server_cluster(fingerprint.cluster_id) then - return true - end - end - - return false -end local plant_link_humdity_sensor = { NAME = "PlantLink Soil Moisture Sensor", @@ -70,7 +46,7 @@ local plant_link_humdity_sensor = { } } }, - can_handle = is_zigbee_plant_link_humidity_sensor + can_handle = require("plant-link.can_handle"), } return plant_link_humdity_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/sub_drivers.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/sub_drivers.lua new file mode 100644 index 0000000000..6f49a5f18c --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/sub_drivers.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("aqara"), + lazy_load_if_possible("plant-link"), + lazy_load_if_possible("plaid-systems"), + lazy_load_if_possible("centralite-sensor"), + lazy_load_if_possible("heiman-sensor"), + lazy_load_if_possible("frient-sensor"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_aqara_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_aqara_sensor.lua index 104c36c3e5..5ced35a0d3 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_aqara_sensor.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_aqara_sensor.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2022 SmartThings, Inc. -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -18,11 +18,17 @@ local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" local PowerConfiguration = clusters.PowerConfiguration local TemperatureMeasurement = clusters.TemperatureMeasurement local RelativeHumidity = clusters.RelativeHumidity +local PRIVATE_CLUSTER_ID = 0xFCC0 +local PRIVATE_ATTRIBUTE_ID = 0x0009 +local MFG_CODE = 0x115F + local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("humidity-temp-battery-aqara.yml"), @@ -167,6 +173,14 @@ test.register_message_test( direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -186,6 +200,7 @@ test.register_coroutine_test( ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() end ) @@ -247,4 +262,23 @@ test.register_message_test( } ) +test.register_coroutine_test( + "Added handler should send manufacturer attribute and initialize device state", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, + PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.temperatureMeasurement.temperature({ value = 0, unit = "C" }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.relativeHumidityMeasurement.humidity(0))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.batteryLevel.battery("normal"))) + test.wait_for_events() + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_centralite_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_centralite_sensor.lua index 626d704f2a..c9b469376c 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_centralite_sensor.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_centralite_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_ewelink_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_ewelink_sensor.lua index 17d9afd2a5..35f557e850 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_ewelink_sensor.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_ewelink_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_air_quality_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_air_quality_sensor.lua new file mode 100644 index 0000000000..063afbdf9f --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_air_quality_sensor.lua @@ -0,0 +1,329 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" + +local PowerConfiguration = clusters.PowerConfiguration +local TemperatureMeasurement = clusters.TemperatureMeasurement +local HumidityMeasurement = clusters.RelativeHumidity + +local Frient_VOCMeasurement = { + ID = 0xFC03, + ManufacturerSpecificCode = 0x1015, + attributes = { + MeasuredValue = { ID = 0x0000, base_type = data_types.Uint16 }, + MinMeasuredValue = { ID = 0x0001, base_type = data_types.Uint16 }, + MaxMeasuredValue = { ID = 0x0002, base_type = data_types.Uint16 }, + Resolution = { ID = 0x0003, base_type = data_types.Uint16 }, + }, +} + +Frient_VOCMeasurement.attributes.MeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.MinMeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.MaxMeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.Resolution._cluster = Frient_VOCMeasurement + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("frient-airquality-humidity-temperature-battery.yml"), + zigbee_endpoints = { + [0x26] = { + id = 0x26, + manufacturer = "frient A/S", + model = "AQSZB-110", + server_clusters = {0x0001, 0x0402, 0x0405, 0xFC03} + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "Refresh should read all necessary attributes", + { + { + channel = "capability", + direction = "receive", + message = {mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_message_test( + "Min battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 23) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +test.register_message_test( + "Max battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 30) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + +test.register_coroutine_test( + "Configure should configure all necessary attributes", + function() + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device, 30, 21600, 1) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + TemperatureMeasurement.ID + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 0x001E, 0x0E10, 100) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + HumidityMeasurement.ID + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 60, 3600, 300) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + Frient_VOCMeasurement.ID, + 38 + ):to_endpoint(0x26) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.configure_reporting( + mock_device, + data_types.ClusterId(Frient_VOCMeasurement.ID), + Frient_VOCMeasurement.attributes.MeasuredValue.ID, + Frient_VOCMeasurement.attributes.MeasuredValue.base_type.ID, + 60, 600, 10 + ):to_endpoint(0x26) + }) + + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + + --refresh happens after configure + test.mock_time.advance_time(5) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Frient_VOCMeasurement.ID, Frient_VOCMeasurement.attributes.MeasuredValue.ID, Frient_VOCMeasurement.ManufacturerSpecificCode):to_endpoint(0x26) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(0x26) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(0x26) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + }) + end +) + +test.register_message_test( + "Humidity report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 0x1950) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 65 })) + } + } +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + } + } +) + +test.register_coroutine_test( + "info_changed to check for necessary preferences settings: Temperature Sensitivity", + function() + local updates = { + preferences = { + temperatureSensitivity = 0.9, + humiditySensitivity = 10 + } + } + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + local temperatureSensitivity = math.floor(0.9 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 3600, + temperatureSensitivity + ) + }) + local humiditySensitivity = math.floor(10 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 60, + 3600, + humiditySensitivity + ) + }) + test.wait_for_events() + end +) + +test.register_message_test( + "VOC measurement report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, cluster_base.build_test_attr_report(Frient_VOCMeasurement.attributes.MeasuredValue, mock_device, 300) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.airQualitySensor.airQuality({ value = 5 })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tvocHealthConcern.tvocHealthConcern({ value = "slightlyUnhealthy" })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tvocMeasurement.tvocLevel({ value = 300, unit = "ppb" })) + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_coroutine_test( + "Added handler should initialize VOC and air quality state", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.airQualitySensor.airQuality({ value = 0 }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tvocHealthConcern.tvocHealthConcern({ value = "good" }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tvocMeasurement.tvocLevel({ value = 0, unit = "ppb" }))) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_sensor.lua index e61ae9e22d..7d925ae94d 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_sensor.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_sensor.lua @@ -1,36 +1,26 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" local PowerConfiguration = clusters.PowerConfiguration local TemperatureMeasurement = clusters.TemperatureMeasurement -local RelativeHumidity = clusters.RelativeHumidity +local HumidityMeasurement = clusters.RelativeHumidity local mock_device = test.mock_device.build_test_zigbee_device( { - profile = t_utils.get_profile_definition("humidity-temp-battery.yml"), + profile = t_utils.get_profile_definition("frient-humidity-temperature-battery.yml"), zigbee_endpoints = { - [1] = { - id = 1, + [26] = { + id = 26, manufacturer = "frient A/S", - model = "HMSZB-110", + model = "HMSZB-120", server_clusters = {0x0001, 0x0402, 0x0405} - } + }, } } ) @@ -63,7 +53,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - RelativeHumidity.attributes.MeasuredValue:read(mock_device) + HumidityMeasurement.attributes.MeasuredValue:read(mock_device) } }, { @@ -80,16 +70,49 @@ test.register_message_test( } ) +test.register_message_test( + "Min battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 23) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +test.register_message_test( + "Max battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 30) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + test.register_coroutine_test( "Configure should configure all necessary attributes", function() + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, - RelativeHumidity.ID + HumidityMeasurement.ID ) }) test.socket.zigbee:__expect_send({ @@ -108,7 +131,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - RelativeHumidity.attributes.MeasuredValue:configure_reporting(mock_device, 60, 600, 100) + HumidityMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 60, 3600, 300) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -116,13 +139,118 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 60, 600, 10) + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 0x001E, 0x0E10, 100) }) - test.socket.zigbee:__expect_send({ mock_device.id, RelativeHumidity.attributes.MeasuredValue:read(mock_device) }) - test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) - test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + + test.mock_time.advance_time(5) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + }) + test.wait_for_events() end ) +test.register_message_test( + "Humidity report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 0x1950) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 65 })) + } + } +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } + } + } +) + +test.register_coroutine_test( + "info_changed to check for necessary preferences settings: Temperature Sensitivity", + function() + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + local updates = { + preferences = { + temperatureSensitivity = 0.9, + humiditySensitivity = 10 + } + } + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + local temperatureSensitivity = math.floor(0.9 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 3600, + temperatureSensitivity + ) + }) + local humiditySensitivity = math.floor(10 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 60, + 3600, + humiditySensitivity + ) + }) + test.wait_for_events() + + test.mock_time.advance_time(5) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + }) + test.wait_for_events() + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_heiman_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_heiman_sensor.lua index 3dd7f5c719..773bd3e9e4 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_heiman_sensor.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_heiman_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_battery_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_battery_sensor.lua index 62f6421fa7..f914f0a898 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_battery_sensor.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_battery_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_plaid_systems.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_plaid_systems.lua index 9334c03045..6bca6c4e1a 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_plaid_systems.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_plaid_systems.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local base64 = require "st.base64" local capabilities = require "st.capabilities" @@ -116,6 +105,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -156,6 +153,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() end ) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature.lua index ca121baa9b..6de1dceb18 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" @@ -48,6 +37,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -151,6 +148,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() test.socket.zigbee:__queue_receive( { diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_battery.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_battery.lua index f3d30ab0c8..341cd4bc20 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_battery.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_battery.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" @@ -60,6 +49,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -171,6 +168,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() test.socket.zigbee:__queue_receive( { diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_sensor.lua index 7da190b523..4282db0ffd 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_sensor.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -55,6 +44,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/can_handle.lua new file mode 100644 index 0000000000..e4453597ed --- /dev/null +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_aqara_products(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..69218a197a --- /dev/null +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.sen_ill.agl01" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/init.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/init.lua index a345d816c8..0eb5e1b059 100644 --- a/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local zcl_commands = require "st.zigbee.zcl.global_commands" @@ -19,9 +22,6 @@ local FREQUENCY_ATTRIBUTE_ID = 0x0000 local FREQUENCY_DEFAULT_VALUE = 5 local FREQUENCY_PREF = "frequencyPref" -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.sen_ill.agl01" } -} local configuration = { { @@ -42,14 +42,6 @@ local configuration = { } } -local function is_aqara_products(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function detection_frequency_capability_handler(driver, device, command) local frequency = command.args.frequency @@ -69,7 +61,6 @@ local function device_init(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end @@ -105,7 +96,7 @@ local aqara_illuminance_handler = { } } }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } return aqara_illuminance_handler diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua index 45a3ade62b..7f84eb68dc 100644 --- a/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua @@ -1,29 +1,25 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" +local do_configure = function(self, device) + device:configure() + device:refresh() +end + local zigbee_illuminance_driver = { supported_capabilities = { capabilities.illuminanceMeasurement, capabilities.battery }, - sub_drivers = { - require("aqara") + lifecycle_handlers = { + doConfigure = do_configure, }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/sub_drivers.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/sub_drivers.lua new file mode 100644 index 0000000000..6211f46578 --- /dev/null +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("aqara"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor.lua index 02c3c63a55..92e93119f5 100644 --- a/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor.lua +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -107,6 +96,8 @@ test.register_coroutine_test( zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) } ) + test.socket.zigbee:__expect_send({ mock_device.id, IlluminanceMeasurement.attributes.MeasuredValue:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) end ) diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor_aqara.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor_aqara.lua index 11d66478ba..81fee9249f 100644 --- a/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor_aqara.lua +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor_aqara.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -20,6 +9,10 @@ local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" +local messages = require "st.zigbee.messages" +local zb_const = require "st.zigbee.constants" +local write_attribute_response = require "st.zigbee.zcl.global_commands.write_attribute_response" +local zcl_messages = require "st.zigbee.zcl" test.add_package_capability("detectionFrequency.yaml") local IlluminanceMeasurement = clusters.IlluminanceMeasurement @@ -30,8 +23,10 @@ local detectionFrequency = capabilities["stse.detectionFrequency"] local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 local MFG_CODE = 0x115F +local FREQUENCY_ATTRIBUTE_ID = 0x0000 local FREQUENCY_DEFAULT_VALUE = 5 +local FREQUENCY_PREF = "frequencyPref" local mock_device = test.mock_device.build_test_zigbee_device( { @@ -142,4 +137,50 @@ test.register_message_test( } ) +local function build_write_attr_res(cluster, status) + local addr_header = messages.AddressHeader( + mock_device:get_short_address(), + mock_device.fingerprinted_endpoint_id, + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + cluster + ) + local write_attribute_body = write_attribute_response.WriteAttributeResponse(status, {}) + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(write_attribute_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = write_attribute_body + }) + return messages.ZigbeeMessageRx({ + address_header = addr_header, + body = message_body + }) +end + +test.register_coroutine_test( + "Handle setDetectionFrequency capability command", + function() + local frequency = 10 + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "stse.detectionFrequency", component = "main", command = "setDetectionFrequency", args = { frequency } } }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, FREQUENCY_ATTRIBUTE_ID, + MFG_CODE, data_types.Uint16, frequency) }) + end +) + +test.register_coroutine_test( + "Handle write attr res on PRIVATE_CLUSTER_ID emits detectionFrequency", + function() + mock_device:set_field(FREQUENCY_PREF, FREQUENCY_DEFAULT_VALUE) + test.socket.zigbee:__queue_receive({ mock_device.id, + build_write_attr_res(PRIVATE_CLUSTER_ID, 0x00) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + detectionFrequency.detectionFrequency(FREQUENCY_DEFAULT_VALUE, { visibility = { displayed = false } }))) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/fingerprints.yml b/drivers/SmartThings/zigbee-lock/fingerprints.yml index 86870aa962..083dd3a11e 100644 --- a/drivers/SmartThings/zigbee-lock/fingerprints.yml +++ b/drivers/SmartThings/zigbee-lock/fingerprints.yml @@ -1,4 +1,15 @@ zigbeeManufacturer: + # GELUBU + - id: "GELUBU/S93" + deviceLabel: GELUBU Door Lock S93 + manufacturer: GELUBU + model: S93 + deviceProfileName: lock-battery + - id: "GELUBU/G30" + deviceLabel: GELUBU Door Lock G30 + manufacturer: GELUBU + model: G30 + deviceProfileName: lock-battery # YALE - id: "Yale YRD220/240" deviceLabel: "Yale Door Lock" diff --git a/drivers/SmartThings/zigbee-lock/src/configurations.lua b/drivers/SmartThings/zigbee-lock/src/configurations.lua index a2429252b0..88e4e59f80 100644 --- a/drivers/SmartThings/zigbee-lock/src/configurations.lua +++ b/drivers/SmartThings/zigbee-lock/src/configurations.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index ce6894b868..94f5adc0c4 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Zigbee Driver utilities local defaults = require "st.zigbee.defaults" @@ -445,12 +435,7 @@ local zigbee_lock_driver = { [capabilities.refresh.commands.refresh.NAME] = refresh } }, - sub_drivers = { - require("samsungsds"), - require("yale"), - require("yale-fingerprint-lock"), - require("lock-without-codes") - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { doConfigure = do_configure, added = device_added, diff --git a/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua new file mode 100644 index 0000000000..543e43a8b1 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_lock_without_codes(opts, driver, device) + local FINGERPRINTS = require("lock-without-codes.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("lock-without-codes") + end + end + return false +end + +return can_handle_lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua new file mode 100644 index 0000000000..63ae82b46c --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local LOCK_WITHOUT_CODES_FINGERPRINTS = { + { model = "E261-KR0B0Z0-HA" }, + { mfr = "Danalock", model = "V3-BTZB" } +} + +return LOCK_WITHOUT_CODES_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua index 46f0867857..e5c6de3408 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local configurationMap = require "configurations" local clusters = require "st.zigbee.zcl.clusters" @@ -19,26 +9,13 @@ local capabilities = require "st.capabilities" local DoorLock = clusters.DoorLock local PowerConfiguration = clusters.PowerConfiguration -local LOCK_WITHOUT_CODES_FINGERPRINTS = { - { model = "E261-KR0B0Z0-HA" }, - { mfr = "Danalock", model = "V3-BTZB" } -} -local function can_handle_lock_without_codes(opts, driver, device) - for _, fingerprint in ipairs(LOCK_WITHOUT_CODES_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function device_init(driver, device) local configuration = configurationMap.get_device_configuration(device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end @@ -96,7 +73,7 @@ local lock_without_codes = { } } }, - can_handle = can_handle_lock_without_codes + can_handle = require("lock-without-codes.can_handle"), } return lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua index 0a36a9685e..a02a59963c 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local utils = require "st.utils" local capabilities = require "st.capabilities" local json = require "st.json" diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua new file mode 100644 index 0000000000..c483b2fe27 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function samsungsds_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "SAMSUNG SDS" then + return true, require("samsungsds") + end + return false +end + +return samsungsds_can_handle diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua index c0a597a99d..fff290df5d 100644 --- a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local clusters = require "st.zigbee.zcl.clusters" @@ -57,9 +47,15 @@ local refresh = function(driver, device, cmd) -- do nothing in refresh capability handler end +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + local device_added = function(self, device) lock_utils.populate_state_from_data(device) - device:emit_event(capabilities.lock.lock.unlocked()) + emit_event_if_latest_state_missing(device, "main", capabilities.lock, capabilities.lock.lock.NAME, capabilities.lock.lock.unlocked()) device:emit_event(capabilities.battery.battery(100)) end @@ -106,9 +102,7 @@ local samsung_sds_driver = { added = device_added, init = device_init }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "SAMSUNG SDS" - end + can_handle = require("samsungsds.can_handle"), } return samsung_sds_driver diff --git a/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua new file mode 100644 index 0000000000..ff4bf8980d --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("samsungsds"), + lazy_load_if_possible("yale"), + lazy_load_if_possible("yale-fingerprint-lock"), + lazy_load_if_possible("lock-without-codes"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua index b6fa3d1323..146c628b8b 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua b/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua index ed4ce6e3cc..f287300f60 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -45,4 +34,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua b/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua index d499d7ff66..4f50c3c24a 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua index 80d10d092e..2fd55fd3f0 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -782,4 +771,88 @@ test.register_coroutine_test( end ) +test.register_message_test( + "Alarm code 0 should generate lock unknown event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Alarm.client.commands.Alarm.build_test_rx(mock_device, 0x00, DoorLock.ID) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown()) + } + } +) + +test.register_message_test( + "Alarm code 1 should generate lock unknown event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Alarm.client.commands.Alarm.build_test_rx(mock_device, 0x01, DoorLock.ID) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown()) + } + } +) + +test.register_message_test( + "Pin response for unoccupied slot with no existing code should generate unset event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x05, + DoorLockUserStatus.OCCUPIED_DISABLED, + DoorLockUserType.UNRESTRICTED, + "" + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("5 unset", + { data = { codeName = "Code 5" }, state_change = true })) + } + } +) + +test.register_coroutine_test( + "Pin response for already-set slot should use changed change type", + function() + init_code_slot(1, "Code 1", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) + test.wait_for_events() + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 changed", { data = { codeName = "Code 1" }, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua index 1aa9432933..7950e3f62d 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_v10.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_v10.lua new file mode 100644 index 0000000000..c4e28dcd0d --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_v10.lua @@ -0,0 +1,778 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Mock out globals +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" + +local clusters = require "st.zigbee.zcl.clusters" +local PowerConfiguration = clusters.PowerConfiguration +local DoorLock = clusters.DoorLock +local Alarm = clusters.Alarms +local capabilities = require "st.capabilities" +-- Note: This is not the proper way to test against previous versions. +-- Instead, testing should be run against different lua lib artifacts +local version = require "version" +version.api = 10 + +local DoorLockState = DoorLock.attributes.LockState +local OperationEventCode = DoorLock.types.OperationEventCode +local DoorLockUserStatus = DoorLock.types.DrlkUserStatus +local DoorLockUserType = DoorLock.types.DrlkUserType +local ProgrammingEventCode = DoorLock.types.ProgramEventCode + +local json = require "dkjson" + +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("base-lock.yml") } +) +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device)end + +test.set_test_init_function(test_init) + +local expect_reload_all_codes_messages = function() + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, + true) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.MaxPINCodeLength:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.MinPINCodeLength:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:read(mock_device) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false } }))) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 0) }) +end + +test.register_coroutine_test( + "Configure should configure all necessary attributes and begin reading codes", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.wait_for_events() + + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(mock_device, + 600, + 21600, + 1) }) + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + DoorLock.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:configure_reporting(mock_device, + 0, + 3600, + 0) }) + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + Alarm.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:configure_reporting(mock_device, + 0, + 21600, + 0) }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + + test.mock_time.advance_time(2) + expect_reload_all_codes_messages() + + end +) + +test.register_coroutine_test( + "Refresh should read expected attributes", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} }}) + + test.socket.zigbee:__expect_send({mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device)}) + test.socket.zigbee:__expect_send({mock_device.id, DoorLock.attributes.LockState:read(mock_device)}) + test.socket.zigbee:__expect_send({mock_device.id, Alarm.attributes.AlarmCount:read(mock_device)}) + end +) + +test.register_message_test( + "Lock status reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, DoorLock.attributes.LockState:build_test_attr_report(mock_device, + DoorLockState.LOCKED) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.locked()) + } + } +) + +test.register_message_test( + "Battery percentage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, + 55) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(28)) + } + } +) + +test.register_message_test( + "Lock operation event reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, + DoorLock.client.commands.OperatingEventNotification.build_test_rx( + mock_device, + 0x02, + OperationEventCode.LOCK, + 0x0000, + "", + 0x0000, + "") } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({ data = { method = "manual" } })) + } + } +) + +test.register_message_test( + "Pin response reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x02, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("2 set", + { data = { codeName = "Code 2" }, state_change = true })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({["2"] = "Code 2"} ), { visibility = { displayed = false } })) + } + } +) + +test.register_message_test( + "Sending the lock command should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "lock", component = "main", command = "lock", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, DoorLock.server.commands.LockDoor(mock_device) } + } + } +) + +test.register_message_test( + "Min lock code length report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report(mock_device, 4) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.minCodeLength(4, { visibility = { displayed = false }})) + } + } +) + +test.register_message_test( + "Max lock code length report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, DoorLock.attributes.MaxPINCodeLength:build_test_attr_report(mock_device, 4) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.maxCodeLength(4, { visibility = { displayed = false }})) + } + } +) + +test.register_message_test( + "Max user code number report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, + 16) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(16, { visibility = { displayed = false }})) + } + } +) + +test.register_coroutine_test( + "Reloading all codes of an unconfigured lock should generate correct attribute checks", + function() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "reloadAllCodes", args = {} } }) + expect_reload_all_codes_messages() + end +) + +test.register_message_test( + "Requesting a user code should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = capabilities.lockCodes.ID, command = "requestCode", args = { 1 } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) } + } + } +) + +test.register_coroutine_test( + "Deleting a user code should be handled", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 set", + { data = { codeName = "Code 1" }, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode( {["1"] = "Code 1"} ), { visibility = { displayed = false }}) + )) + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "deleteCode", args = { 1 } } }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, + true) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 1) }) + test.wait_for_events() + + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) }) + test.socket.zigbee:__queue_receive({ mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.AVAILABLE, + "")}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 deleted", + { data = { codeName = "Code 1"}, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({} ), { visibility = { displayed = false } }) + )) + end +) + +test.register_coroutine_test( + "Setting a user code should result in the named code changed event firing", + function() + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.wait_for_events() + + test.mock_time.advance_time(4) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 1) + } + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "test" }, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }))) + end +) + +local function init_code_slot(slot_number, name, device) + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.capability:__queue_receive({ device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { slot_number, "1234", name } } }) + test.socket.zigbee:__expect_send( + { + device.id, + DoorLock.server.commands.SetPINCode(device, + slot_number, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.wait_for_events() + test.mock_time.advance_time(4) + test.socket.zigbee:__expect_send( + { + device.id, + DoorLock.server.commands.GetPINCode(device, slot_number) + } + ) + test.wait_for_events() + test.socket.zigbee:__queue_receive( + { + device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + device, + slot_number, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.socket.capability:__expect_send(device:generate_test_message("main", + capabilities.lockCodes.codeChanged(slot_number .. " set", { data = { codeName = name }, state_change = true })) + ) +end + +test.register_coroutine_test( + "Setting a user code name should be handled", + function() + init_code_slot(1, "initialName", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false } }))) + test.wait_for_events() + + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "nameSlot", args = { 1, "foo" } } }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 renamed", {state_change = true}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false } }))) + end +) + +test.register_coroutine_test( + "Setting a user code name via setCode should be handled", + function() + init_code_slot(1, "initialName", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false } }))) + test.wait_for_events() + + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "", "foo"} } }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 renamed", {state_change = true}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false } }))) + end +) + +test.register_coroutine_test( + "Calling updateCodes should send properly spaced commands", + function () + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "updateCodes", args = {{code1 = "1234", code2 = "2345", code3 = "3456", code4 = ""}}}}) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 2, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "2345" + ) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 3, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "3456" + ) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 4) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 1) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 2) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 3) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 4) + }) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Setting all user codes should result in a code set event for each", + function () + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "updateCodes", args = {{code1 = "1234", code2 = "2345", code3 = "3456", code4 = ""}}}}) + test.socket.zigbee:__expect_send({mock_device.id, DoorLock.server.commands.SetPINCode(mock_device, 1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234")}) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({mock_device.id, DoorLock.server.commands.SetPINCode(mock_device, 2, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "2345")}) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 2) }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({mock_device.id, DoorLock.server.commands.SetPINCode(mock_device, 3, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "3456")}) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 3) }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 4) }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 4) }) + test.wait_for_events() + end +) + +test.register_message_test( + "Master code programming event should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x00, + ProgrammingEventCode.MASTER_CODE_CHANGED, + 0, + "1234", + DoorLockUserType.MASTER_USER, + DoorLockUserStatus.OCCUPIED_ENABLED, + 0x0000, + "data" + ) + } + }, + + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("0 set", { data = { codeName = "Master Code"}, state_change = true }) + ) + } + } +) + +test.register_message_test( + "The lock reporting a single code has been set should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x0, + ProgrammingEventCode.PIN_CODE_ADDED, + 1, + "1234", + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.OCCUPIED_ENABLED, + 0x0000, + "data" + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "Code 1"}, state_change = true })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } })) + } + } +) + +test.register_coroutine_test( + "The lock reporting a code has been deleted should be handled", + function() + init_code_slot(1, "Code 1", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x0, + ProgrammingEventCode.PIN_CODE_DELETED, + 1, + "1234", + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.AVAILABLE, + 0x0000, + "data" + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "Code 1"}, state_change = true }) + ) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({}), { visibility = { displayed = false } }))) + end +) + +test.register_coroutine_test( + "The lock reporting that all codes have been deleted should be handled", + function() + init_code_slot(1, "Code 1", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) + init_code_slot(2, "Code 2", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1", ["2"] = "Code 2"}), { visibility = { displayed = false } }))) + init_code_slot(3, "Code 3", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1", ["2"] = "Code 2", ["3"] = "Code 3"}), { visibility = { displayed = false } }))) + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x0, + ProgrammingEventCode.PIN_CODE_DELETED, + 0xFF, + "1234", + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.AVAILABLE, + 0x0000, + "data" + ) + } + ) + + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "Code 1"}, state_change = true }) + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("2 deleted", { data = { codeName = "Code 2"}, state_change = true }) + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("3 deleted", { data = { codeName = "Code 3"}, state_change = true }) + ) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({}), { visibility = { displayed = false } }))) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "The lock reporting unlock via code should include the code info in the report", + function() + init_code_slot(1, "Code 1", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.OperatingEventNotification.build_test_rx( + mock_device, + 0x00, -- 0 = keypad + OperationEventCode.UNLOCK, + 0x0001, + "1234", + 0x0000, + "" + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) + ) + ) + end +) + +test.register_coroutine_test( + "Lock state attribute reports (after the first) should be delayed if they come before event notifications ", + function() + init_code_slot(1, "Code 1", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) + test.socket.zigbee:__queue_receive({mock_device.id, DoorLock.attributes.LockState:build_test_attr_report(mock_device, DoorLockState.UNLOCKED)}) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked() + ) + ) + test.mock_time.advance_time(2) + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.OperatingEventNotification.build_test_rx( + mock_device, + 0x00, -- 0 = keypad + OperationEventCode.UNLOCK, + 0x0001, + "1234", + 0x0000, + "" + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) + ) + ) + test.mock_time.advance_time(2) + test.timer.__create_and_queue_test_time_advance_timer(2.5, "oneshot") + test.socket.zigbee:__queue_receive({mock_device.id, DoorLock.attributes.LockState:build_test_attr_report(mock_device, DoorLockState.LOCKED)}) + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.OperatingEventNotification.build_test_rx( + mock_device, + 0x00, -- 0 = keypad + OperationEventCode.LOCK, + 0x0001, + "1234", + 0x0000, + "" + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.locked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) + ) + ) + test.mock_time.advance_time(2.5) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.locked() + ) + ) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_samsungsds.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_samsungsds.lua index 1667b0ecb8..bac1554790 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_samsungsds.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_samsungsds.lua @@ -1377,11 +1377,17 @@ test.register_coroutine_test( test.register_coroutine_test( "Device added function handler", function() + -- The initial lock event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added"}) test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(100))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked())) test.wait_for_events() + -- Avoid sending the initial lock event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added"}) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(100))) + test.wait_for_events() end ) diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-bad-battery-reporter.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-bad-battery-reporter.lua index ee1745e3b7..b8f4c386d9 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-bad-battery-reporter.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-bad-battery-reporter.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua index 2255c063a3..7cda71cdb3 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua index 34b6881028..931a4b143c 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -330,4 +319,98 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Setting a user code for a slot that is empty should indicate failure and unset", + function() + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.wait_for_events() + test.mock_time.advance_time(4) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 1, + DoorLockUserStatus.OCCUPIED_DISABLED, + DoorLockUserType.UNRESTRICTED, + "" + ) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 failed", { state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 is not set", { state_change = true }))) + end +) + +test.register_coroutine_test( + "Pin response for already-set slot without pending operation should use changed change type", + function() + init_code_slot(1, "initialName", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) + test.wait_for_events() + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 changed", { data = { codeName = "initialName" }, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) + end +) + +test.register_coroutine_test( + "Pin response for already-set slot that is now empty should delete the code", + function() + init_code_slot(1, "initialName", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) + test.wait_for_events() + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 1, + DoorLockUserStatus.OCCUPIED_DISABLED, + DoorLockUserType.UNRESTRICTED, + "" + ) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "initialName" }, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({}), { visibility = { displayed = false }}))) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua new file mode 100644 index 0000000000..a80632bf80 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local yale_fingerprint_lock_models = function(opts, driver, device) + local FINGERPRINTS = require("yale-fingerprint-lock.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("yale-fingerprint-lock") + end + end + return false +end + +return yale_fingerprint_lock_models diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua new file mode 100644 index 0000000000..b3db27d719 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local YALE_FINGERPRINT_LOCK = { + { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +return YALE_FINGERPRINT_LOCK diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua index 9d0a0b4148..b78d043784 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -19,21 +9,7 @@ local LockCodes = capabilities.lockCodes local YALE_FINGERPRINT_MAX_CODES = 0x1E -local YALE_FINGERPRINT_LOCK = { - { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, - { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, - { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, - { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } -} -local yale_fingerprint_lock_models = function(opts, driver, device) - for _, fingerprint in ipairs(YALE_FINGERPRINT_LOCK) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local handle_max_codes = function(driver, device, value) device:emit_event(LockCodes.maxCodes(YALE_FINGERPRINT_MAX_CODES), { visibility = { displayed = false } }) @@ -48,7 +24,7 @@ local yale_fingerprint_lock_driver = { } } }, - can_handle = yale_fingerprint_lock_models + can_handle = require("yale-fingerprint-lock.can_handle"), } return yale_fingerprint_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua new file mode 100644 index 0000000000..54340c7811 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function yale_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" then + return true, require("yale") + end + return false +end + +return yale_can_handle diff --git a/drivers/SmartThings/zigbee-lock/src/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/yale/init.lua index 73e984036e..c315fbfa06 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + -- Zigbee Spec Utils local clusters = require "st.zigbee.zcl.clusters" @@ -151,11 +142,8 @@ local yale_door_lock_driver = { [LockCodes.commands.setCode.NAME] = set_code } }, - - sub_drivers = { require("yale.yale-bad-battery-reporter") }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" - end + sub_drivers = require("yale.sub_drivers"), + can_handle = require("yale.can_handle"), } return yale_door_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua new file mode 100644 index 0000000000..4b546979d3 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("yale.yale-bad-battery-reporter"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua new file mode 100644 index 0000000000..67169e9268 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_bad_yale_lock_models = function(opts, driver, device) + local FINGERPRINTS = require("yale.yale-bad-battery-reporter.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("yale.yale-bad-battery-reporter") + end + end + return false +end + +return is_bad_yale_lock_models diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua new file mode 100644 index 0000000000..cbb7c3404f --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local BAD_YALE_LOCK_FINGERPRINTS = { + { mfr = "Yale", model = "YRD220/240 TSDB" }, + { mfr = "Yale", model = "YRL220 TS LL" }, + { mfr = "Yale", model = "YRD210 PB DB" }, + { mfr = "Yale", model = "YRL210 PB LL" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +return BAD_YALE_LOCK_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua index 59fdbf228b..3b77f32563 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua @@ -1,37 +1,11 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" -local BAD_YALE_LOCK_FINGERPRINTS = { - { mfr = "Yale", model = "YRD220/240 TSDB" }, - { mfr = "Yale", model = "YRL220 TS LL" }, - { mfr = "Yale", model = "YRD210 PB DB" }, - { mfr = "Yale", model = "YRL210 PB LL" }, - { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, - { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } -} -local is_bad_yale_lock_models = function(opts, driver, device) - for _, fingerprint in ipairs(BAD_YALE_LOCK_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local battery_report_handler = function(driver, device, value) device:emit_event(capabilities.battery.battery(value.value)) @@ -46,7 +20,7 @@ local bad_yale_driver = { } } }, - can_handle = is_bad_yale_lock_models + can_handle = require("yale.yale-bad-battery-reporter.can_handle"), } return bad_yale_driver diff --git a/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml b/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml index c1a3b0fda3..e71ab0c5ac 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml +++ b/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml @@ -24,6 +24,11 @@ zigbeeManufacturer: manufacturer: eWeLink model: MSO1 deviceProfileName: motion-battery + - id: eWeLink/SNZB-03P + deviceLabel: Zigbee Motion Sensor + manufacturer: eWeLink + model: SNZB-03P + deviceProfileName: motion - id: "ORVIBO/895a2d80097f4ae2b2d40500d5e0" deviceLabel: Orvibo Motion Sensor manufacturer: ORVIBO @@ -168,6 +173,11 @@ zigbeeManufacturer: manufacturer: frient A/S model: MOSZB-141 deviceProfileName: frient-motion-battery + - id: frientA/S/153 + deviceLabel: frient Motion Sensor 2 Pet + manufacturer: frient A/S + model: MOSZB-153 + deviceProfileName: frient-motion-temp-illuminance-battery - id: Compacta/ZBMS3-1 deviceLabel: Smartenit Motion Sensor manufacturer: Compacta diff --git a/drivers/SmartThings/zigbee-motion-sensor/profiles/frient-motion-temp-illuminance-battery.yml b/drivers/SmartThings/zigbee-motion-sensor/profiles/frient-motion-temp-illuminance-battery.yml new file mode 100644 index 0000000000..1c8f50f822 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/profiles/frient-motion-temp-illuminance-battery.yml @@ -0,0 +1,57 @@ +name: frient-motion-temp-illuminance-battery +components: +- id: main + capabilities: + - id: motionSensor + version: 1 + - id: temperatureMeasurement + version: 1 + - id: illuminanceMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: MotionSensor +preferences: + - preferenceId: tempOffset + explicit: true + - title: "Temperature Sensitivity (°C)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 + - title: "Motion Turn-Off Delay (s)" + name: occupiedToUnoccupiedD + description: "Delay in seconds to report after no motion is detected" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 240 + - title: "Motion Turn-On Delay (s)" + name: unoccupiedToOccupiedD + description: "Delay in seconds to report after motion is detected" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 0 + - title: "Movement Threshold in Turn-On Delay" + name: unoccupiedToOccupiedT + description: "Number of movements to detect before reporting motion during the Motion Turn-On Delay" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 254 + default: 1 diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/aqara_utils.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/aqara_utils.lua index e48f8fde92..8a1b184693 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/aqara_utils.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/aqara_utils.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local aqara_utils = {} diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/can_handle.lua new file mode 100644 index 0000000000..da7a97c64b --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_aqara_products = function(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..705fa72746 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.motion.ac02" }, + { mfr = "LUMI", model = "lumi.motion.agl02" }, + { mfr = "LUMI", model = "lumi.motion.agl04" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/high-precision-motion/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/high-precision-motion/can_handle.lua new file mode 100644 index 0000000000..02c2f313cb --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/high-precision-motion/can_handle.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + if device:get_model() == "lumi.motion.agl04" then + return true, require("aqara.high-precision-motion") + end + return false +end diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/high-precision-motion/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/high-precision-motion/init.lua index 8c70857bdb..d6c89e3e0c 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/high-precision-motion/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/high-precision-motion/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" @@ -118,9 +121,7 @@ local aqara_high_precision_motion_handler = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "lumi.motion.agl04" - end + can_handle = require("aqara.high-precision-motion.can_handle") } return aqara_high_precision_motion_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/init.lua index e5649dab4d..e4586d4b0f 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_commands = require "st.zigbee.zcl.global_commands" local clusters = require "st.zigbee.zcl.clusters" @@ -16,11 +19,6 @@ local FREQUENCY_ATTRIBUTE_ID = 0x0102 local MOTION_DETECTED_UINT32 = 65536 -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.motion.ac02" }, - { mfr = "LUMI", model = "lumi.motion.agl02" }, - { mfr = "LUMI", model = "lumi.motion.agl04" } -} local CONFIGURATIONS = { { @@ -33,14 +31,6 @@ local CONFIGURATIONS = { } } -local is_aqara_products = function(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function motion_illuminance_attr_handler(driver, device, value, zb_rx) -- The low 16 bits for Illuminance @@ -96,7 +86,6 @@ local function device_init(driver, device) for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end @@ -124,10 +113,8 @@ local aqara_motion_handler = { } } }, - sub_drivers = { - require("aqara.high-precision-motion") - }, - can_handle = is_aqara_products + sub_drivers = require("aqara.sub_drivers"), + can_handle = require("aqara.can_handle"), } return aqara_motion_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/sub_drivers.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/sub_drivers.lua new file mode 100644 index 0000000000..a0e42d2085 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("aqara.high-precision-motion") +} diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aurora/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aurora/can_handle.lua new file mode 100644 index 0000000000..f58429a53c --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aurora/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function aurora_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Aurora" and device:get_model() == "MotionSensor51AU" then + return true, require("aurora") + end + return false +end + +return aurora_can_handle diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aurora/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aurora/init.lua index 060ed9d308..d08eb13c36 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/aurora/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aurora/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local function added_handler(self, device) @@ -24,9 +14,7 @@ local aurora_motion_driver = { lifecycle_handlers = { added = added_handler, }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Aurora" and device:get_model() == "MotionSensor51AU" - end + can_handle = require("aurora.can_handle"), } return aurora_motion_driver diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/can_handle.lua new file mode 100644 index 0000000000..01bf292b1a --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local can_handle_battery_voltage = function(opts, driver, device, ...) + local FINGERPRINTS = require("battery-voltage.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("battery-voltage") + end + end + return false +end + +return can_handle_battery_voltage diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/fingerprints.lua new file mode 100644 index 0000000000..4d3006c943 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/fingerprints.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local DEVICES_REPORTING_BATTERY_VOLTAGE = { + { mfr = "Bosch", model = "RFPR-ZB" }, + { mfr = "Bosch", model = "RFDL-ZB-MS" }, + { mfr = "Ecolink", model = "PIRZB1-ECO" }, + { mfr = "ADUROLIGHT", model = "VMS_ADUROLIGHT" }, + { mfr = "AduroSmart Eria", model = "VMS_ADUROLIGHT" } +} + +return DEVICES_REPORTING_BATTERY_VOLTAGE diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/init.lua index 316b626afe..d031aee153 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/init.lua @@ -1,35 +1,8 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -local battery_defaults = require "st.zigbee.defaults.battery_defaults" - -local DEVICES_REPORTING_BATTERY_VOLTAGE = { - { mfr = "Bosch", model = "RFPR-ZB" }, - { mfr = "Bosch", model = "RFDL-ZB-MS" }, - { mfr = "Ecolink", model = "PIRZB1-ECO" }, - { mfr = "ADUROLIGHT", model = "VMS_ADUROLIGHT" }, - { mfr = "AduroSmart Eria", model = "VMS_ADUROLIGHT" } -} -local can_handle_battery_voltage = function(opts, driver, device, ...) - for _, fingerprint in ipairs(DEVICES_REPORTING_BATTERY_VOLTAGE) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end +local battery_defaults = require "st.zigbee.defaults.battery_defaults" local battery_voltage_motion = { @@ -37,7 +10,7 @@ local battery_voltage_motion = { lifecycle_handlers = { init = battery_defaults.build_linear_voltage_init(2.1, 3.0) }, - can_handle = can_handle_battery_voltage + can_handle = require("battery-voltage.can_handle"), } return battery_voltage_motion diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/centralite/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/centralite/can_handle.lua new file mode 100644 index 0000000000..750bcab752 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/centralite/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function centralite_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "CentraLite" then + return true, require("centralite") + end + return false +end + +return centralite_can_handle diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/centralite/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/centralite/init.lua index 6a40360224..e2492e7c78 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/centralite/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/centralite/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local battery_defaults = require "st.zigbee.defaults.battery_defaults" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -46,9 +36,7 @@ local centralite_handler = { lifecycle_handlers = { init = init_handler }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "CentraLite" - end + can_handle = require("centralite.can_handle"), } return centralite_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/compacta/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/compacta/can_handle.lua new file mode 100644 index 0000000000..7946b15125 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/compacta/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function compacta_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Compacta" then + return true, require("compacta") + end + return false +end + +return compacta_can_handle diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/compacta/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/compacta/init.lua index b303f6f8fd..85295b6391 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/compacta/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/compacta/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- ZCL local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -43,9 +33,7 @@ local compacta_driver = { added = added_handler, doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Compacta" - end + can_handle = require("compacta.can_handle"), } return compacta_driver diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/frient/can_handle.lua new file mode 100644 index 0000000000..236ec1f3e5 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/frient/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_frient_motion_sensor(opts, driver, device) + local FINGERPRINTS = require("frient.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("frient") + end + end + return false +end + +return can_handle_frient_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/frient/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/frient/fingerprints.lua new file mode 100644 index 0000000000..ddf11bbdfe --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/frient/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FRIENT_DEVICE_FINGERPRINTS = { + { mfr = "frient A/S", model = "MOSZB-140"}, + { mfr = "frient A/S", model = "MOSZB-141"}, + { mfr = "frient A/S", model = "MOSZB-153"} +} + +return FRIENT_DEVICE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua index 0278350cbe..901dbf2cf2 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local battery_defaults = require "st.zigbee.defaults.battery_defaults" local capabilities = require "st.capabilities" @@ -35,19 +24,7 @@ local POWER_CONFIGURATION_ENDPOINT = 0x23 local TEMPERATURE_ENDPOINT = 0x26 local ILLUMINANCE_ENDPOINT = 0x27 -local FRIENT_DEVICE_FINGERPRINTS = { - { mfr = "frient A/S", model = "MOSZB-140"}, - { mfr = "frient A/S", model = "MOSZB-141"} -} -local function can_handle_frient_motion_sensor(opts, driver, device) - for _, fingerprint in ipairs(FRIENT_DEVICE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function occupancy_attr_handler(driver, device, occupancy, zb_rx) device:emit_event(occupancy.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) @@ -108,24 +85,17 @@ local function device_init(driver, device) battery_defaults.build_linear_voltage_init(BATTERY_MIN_VOLTAGE, BATTERY_MAX_VOLTAGE)(driver, device) local attribute - attribute = CONFIGURATIONS[OCCUPANCY_ENDPOINT] - -- binding is directly triggered for specific endpoint in do_configure - device:add_monitored_attribute(attribute) - if device:supports_capability_by_id(capabilities.temperatureMeasurement.ID) then attribute = CONFIGURATIONS[TEMPERATURE_ENDPOINT] device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end if device:supports_capability_by_id(capabilities.illuminanceMeasurement.ID) then attribute = CONFIGURATIONS[ILLUMINANCE_ENDPOINT] device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end if device:supports_capability_by_id(capabilities.tamperAlert.ID) then attribute = CONFIGURATIONS[TAMPER_ENDPOINT] device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end @@ -220,6 +190,6 @@ local frient_motion_driver = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } }, - can_handle = can_handle_frient_motion_sensor + can_handle = require("frient.can_handle"), } -return frient_motion_driver \ No newline at end of file +return frient_motion_driver diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/gatorsystem/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/gatorsystem/can_handle.lua new file mode 100644 index 0000000000..4257bd7ff2 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/gatorsystem/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function gatorsystem_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "GatorSystem" and device:get_model() == "GSHW01" then + return true, require("gatorsystem") + end + return false +end + +return gatorsystem_can_handle diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/gatorsystem/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/gatorsystem/init.lua index 1e90a65e94..ee88254233 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/gatorsystem/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/gatorsystem/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -84,9 +74,7 @@ local gator_handler = { added = device_added, doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "GatorSystem" and device:get_model() == "GSHW01" - end + can_handle = require("gatorsystem.can_handle"), } return gator_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/ikea/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/ikea/can_handle.lua new file mode 100644 index 0000000000..7060fc8630 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/ikea/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_ikea_motion = function(opts, driver, device) + local FINGERPRINTS = require("ikea.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("ikea") + end + end + return false +end + +return is_ikea_motion diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/ikea/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/ikea/fingerprints.lua new file mode 100644 index 0000000000..27162e73cf --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/ikea/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local IKEA_MOTION_SENSOR_FINGERPRINTS = { + { mfr = "IKEA of Sweden", model = "TRADFRI motion sensor" } +} + +return IKEA_MOTION_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/ikea/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/ikea/init.lua index b2af5e1c88..b7cc733959 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/ikea/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/ikea/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local constants = require "st.zigbee.constants" local clusters = require "st.zigbee.zcl.clusters" @@ -26,9 +16,6 @@ local OnOff = clusters.OnOff local PowerConfiguration = clusters.PowerConfiguration local Groups = clusters.Groups -local IKEA_MOTION_SENSOR_FINGERPRINTS = { - { mfr = "IKEA of Sweden", model = "TRADFRI motion sensor" } -} local MOTION_RESET_TIMER = "motionResetTimer" local ENTRIES_READ = "ENTRIES_READ" @@ -48,14 +35,6 @@ local function on_with_timed_off_command_handler(driver, device, zb_rx) device:set_field(MOTION_RESET_TIMER, motion_reset_timer) end -local is_ikea_motion = function(opts, driver, device) - for _, fingerprint in ipairs(IKEA_MOTION_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function zdo_binding_table_handler(driver, device, zb_rx) for _, binding_table in pairs(zb_rx.body.zdo_body.binding_table_entries) do @@ -141,7 +120,7 @@ local ikea_motion_sensor = { added = device_added, doConfigure = do_configure }, - can_handle = is_ikea_motion + can_handle = require("ikea.can_handle"), } return ikea_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/init.lua index 69b069bf7b..11e9d94a85 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/init.lua @@ -1,22 +1,13 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" local constants = require "st.zigbee.constants" local zcl_clusters = require "st.zigbee.zcl.clusters" +local lazy_load_if_possible = require "lazy_load_subdriver" local temperature_measurement_defaults = { MIN_TEMP = "MIN_TEMP", @@ -110,23 +101,23 @@ local zigbee_motion_driver = { added = added_handler }, sub_drivers = { - require("aqara"), - require("aurora"), - require("ikea"), - require("iris"), - require("gatorsystem"), - require("motion_timeout"), - require("nyce"), - require("zigbee-plugin-motion-sensor"), - require("compacta"), - require("frient"), - require("samjin"), - require("battery-voltage"), - require("centralite"), - require("smartthings"), - require("smartsense"), - require("thirdreality"), - require("sengled") + lazy_load_if_possible("aqara"), + lazy_load_if_possible("aurora"), + lazy_load_if_possible("ikea"), + lazy_load_if_possible("iris"), + lazy_load_if_possible("gatorsystem"), + lazy_load_if_possible("motion_timeout"), + lazy_load_if_possible("nyce"), + lazy_load_if_possible("zigbee-plugin-motion-sensor"), + lazy_load_if_possible("compacta"), + lazy_load_if_possible("frient"), + lazy_load_if_possible("samjin"), + lazy_load_if_possible("battery-voltage"), + lazy_load_if_possible("centralite"), + lazy_load_if_possible("smartthings"), + lazy_load_if_possible("smartsense"), + lazy_load_if_possible("thirdreality"), + lazy_load_if_possible("sengled"), }, additional_zcl_profiles = { [0xFC01] = true diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/iris/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/iris/can_handle.lua new file mode 100644 index 0000000000..6d86812e50 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/iris/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_iris_motion_sensor = function(opts, driver, device) + local FINGERPRINTS = require("iris.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("iris") + end + end + return false +end + +return is_zigbee_iris_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/iris/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/iris/fingerprints.lua new file mode 100644 index 0000000000..2d4ebfc516 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/iris/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_IRIS_MOTION_SENSOR_FINGERPRINTS = { + { mfr = "iMagic by GreatStar", model = "1117-S" } +} + +return ZIGBEE_IRIS_MOTION_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/iris/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/iris/init.lua index 5a553b4325..f29c2772d3 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/iris/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/iris/init.lua @@ -1,23 +1,10 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" -local ZIGBEE_IRIS_MOTION_SENSOR_FINGERPRINTS = { - { mfr = "iMagic by GreatStar", model = "1117-S" } -} -- TODO: the IAS Zone changes should be replaced after supporting functions are included in the lua libs local do_init = function(driver, device) @@ -26,21 +13,13 @@ local do_init = function(driver, device) device:remove_configured_attribute(zcl_clusters.IASZone.ID, zcl_clusters.IASZone.attributes.ZoneStatus.ID) end -local is_zigbee_iris_motion_sensor = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_IRIS_MOTION_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local iris_motion_handler = { NAME = "Iris Motion Handler", lifecycle_handlers = { init = do_init }, - can_handle = is_zigbee_iris_motion_sensor + can_handle = require("iris.can_handle"), } return iris_motion_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-motion-sensor/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/can_handle.lua new file mode 100644 index 0000000000..679675b85e --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_motion_sensor = function(opts, driver, device) + local FINGERPRINTS = require("motion_timeout.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("motion_timeout") + end + end + return false +end + +return is_zigbee_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/fingerprints.lua new file mode 100644 index 0000000000..a3ff0ec226 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_MOTION_SENSOR_FINGERPRINTS = { + { mfr = "ORVIBO", model = "895a2d80097f4ae2b2d40500d5e03dcc", timeout = 20 }, + { mfr = "Megaman", model = "PS601/z1", timeout = 20 }, + { mfr = "HEIMAN", model = "PIRSensor-N", timeout = 20 } +} + +return ZIGBEE_MOTION_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/init.lua index 0fd0e7c070..fb6f34ece4 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/init.lua @@ -1,40 +1,18 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" local IASZone = zcl_clusters.IASZone -local ZIGBEE_MOTION_SENSOR_FINGERPRINTS = { - { mfr = "ORVIBO", model = "895a2d80097f4ae2b2d40500d5e03dcc", timeout = 20 }, - { mfr = "Megaman", model = "PS601/z1", timeout = 20 }, - { mfr = "HEIMAN", model = "PIRSensor-N", timeout = 20 } -} -local is_zigbee_motion_sensor = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_MOTION_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local generate_event_from_zone_status = function(driver, device, zone_status, zigbee_message) + local FINGERPRINTS = require("motion_timeout.fingerprints") device:emit_event( (zone_status:is_alarm1_set() or zone_status:is_alarm2_set()) and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) - for _, fingerprint in ipairs(ZIGBEE_MOTION_SENSOR_FINGERPRINTS) do + for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then device.thread:call_with_delay(fingerprint.timeout, function(d) device:emit_event(capabilities.motionSensor.motion.inactive()) @@ -66,7 +44,7 @@ local motion_timeout_handler = { } } }, - can_handle = is_zigbee_motion_sensor + can_handle = require("motion_timeout.can_handle"), } return motion_timeout_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/nyce/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/nyce/can_handle.lua new file mode 100644 index 0000000000..5c603cfef2 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/nyce/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_nyce_motion_sensor = function(opts, driver, device) + local FINGERPRINTS = require("nyce.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("nyce") + end + end + return false +end + +return is_zigbee_nyce_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/nyce/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/nyce/fingerprints.lua new file mode 100644 index 0000000000..ecb5193f99 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/nyce/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_NYCE_MOTION_SENSOR_FINGERPRINTS = { + { mfr = "NYCE", model = "3041" }, + { mfr = "NYCE", model = "3043" }, + { mfr = "NYCE", model = "3045" } +} + +return ZIGBEE_NYCE_MOTION_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/nyce/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/nyce/init.lua index 8a8196afa6..71efbf3e23 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/nyce/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/nyce/init.lua @@ -1,36 +1,13 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local OccupancySensing = zcl_clusters.OccupancySensing -local ZIGBEE_NYCE_MOTION_SENSOR_FINGERPRINTS = { - { mfr = "NYCE", model = "3041" }, - { mfr = "NYCE", model = "3043" }, - { mfr = "NYCE", model = "3045" } -} -local is_zigbee_nyce_motion_sensor = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_NYCE_MOTION_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function occupancy_attr_handler(driver, device, occupancy, zb_rx) device:emit_event( @@ -49,7 +26,7 @@ local nyce_motion_handler = { } } }, - can_handle = is_zigbee_nyce_motion_sensor + can_handle = require("nyce.can_handle"), } return nyce_motion_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/samjin/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/samjin/can_handle.lua new file mode 100644 index 0000000000..1ad7fe5f7a --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/samjin/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function samjin_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Samjin" then + return true, require("samjin") + end + return false +end + +return samjin_can_handle diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/samjin/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/samjin/init.lua index 66fe8b4491..5d3ff95fd0 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/samjin/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/samjin/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- ZCL local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -44,9 +34,7 @@ local samjin_driver = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Samjin" - end + can_handle = require("samjin.can_handle"), } return samjin_driver diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/sengled/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/sengled/can_handle.lua new file mode 100644 index 0000000000..68d774fb8f --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/sengled/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_sengled_products = function(opts, driver, device, ...) + local FINGERPRINTS = require("sengled.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("sengled") + end + end + return false +end + +return is_sengled_products diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/sengled/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/sengled/fingerprints.lua new file mode 100644 index 0000000000..bae9a89938 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/sengled/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "sengled", model = "E1M-G7H" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/sengled/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/sengled/init.lua index 6ae1de27df..5c2402f32e 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/sengled/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/sengled/init.lua @@ -1,12 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local IASZone = clusters.IASZone local PowerConfiguration = clusters.PowerConfiguration -local FINGERPRINTS = { - { mfr = "sengled", model = "E1M-G7H" } -} local CONFIGURATIONS = { { @@ -27,21 +27,12 @@ local CONFIGURATIONS = { } } -local is_sengled_products = function(opts, driver, device, ...) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function device_init(driver, device) battery_defaults.build_linear_voltage_init(2.6, 3.0)(driver, device) for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end @@ -50,7 +41,7 @@ local sengled_motion_sensor_handler = { lifecycle_handlers = { init = device_init }, - can_handle = is_sengled_products + can_handle = require("sengled.can_handle"), } return sengled_motion_sensor_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/can_handle.lua new file mode 100644 index 0000000000..e85561680b --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/can_handle.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, ...) + + local SMARTSENSE_MFR = "SmartThings" + local SMARTSENSE_MODEL = "PGC314" + local SMARTSENSE_PROFILE_ID = 0xFC01 + + local endpoint = device.zigbee_endpoints[1] or device.zigbee_endpoints["1"] + if (device:get_manufacturer() == SMARTSENSE_MFR and device:get_model() == SMARTSENSE_MODEL) or + endpoint.profile_id == SMARTSENSE_PROFILE_ID then + return true, require("smartsense") + end + return false +end + +return can_handle diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/init.lua index 78f570872f..df48f6af95 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local utils = require "st.utils" @@ -18,9 +8,6 @@ local utils = require "st.utils" local motion = capabilities.motionSensor.motion local signalStrength = capabilities.signalStrength -local SMARTSENSE_MFR = "SmartThings" -local SMARTSENSE_MODEL = "PGC314" -local SMARTSENSE_PROFILE_ID = 0xFC01 local SMARTSENSE_MOTION_CLUSTER = 0xFC04 local SMARTSENSE_MOTION_STATUS_CMD = 0x00 local MOTION_MASK = 0x02 @@ -43,14 +30,6 @@ local battery_table = { [0] = 0 } -local function can_handle(opts, driver, device, ...) - local endpoint = device.zigbee_endpoints[1] or device.zigbee_endpoints["1"] - if (device:get_manufacturer() == SMARTSENSE_MFR and device:get_model() == SMARTSENSE_MODEL) or - endpoint.profile_id == SMARTSENSE_PROFILE_ID then - return true - end - return false -end local function device_added(driver, device) device:emit_event(motion.inactive()) @@ -96,7 +75,7 @@ local smartsense_motion = { lifecycle_handlers = { added = device_added }, - can_handle = can_handle + can_handle = require("smartsense.can_handle"), } return smartsense_motion diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/smartthings/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/smartthings/can_handle.lua new file mode 100644 index 0000000000..747f859062 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/smartthings/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function smartthings_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "SmartThings" then + return true, require("smartthings") + end + return false + end + +return smartthings_can_handle diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/smartthings/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/smartthings/init.lua index 586378edc5..0907e7de16 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/smartthings/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/smartthings/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" @@ -44,9 +34,7 @@ local smartthings_motion = { lifecycle_handlers = { init = init_handler }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "SmartThings" - end + can_handle = require("smartthings.can_handle"), } return smartthings_motion diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua index 9d009d47cb..eb551c84fa 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua @@ -1,16 +1,6 @@ --- Copyright 2021 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. +-- Copyright 2021 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.zigbee.data_types" local utils = require "st.zigbee.utils" local zdo_commands = require "st.zigbee.zdo.commands" @@ -101,7 +91,7 @@ function ZdoMessageBody.deserialize(parent, buf) -- binding table entry endpoint_id local version = require "version" if version.rpc == 8 then - buf.buf = buf.buf .. "\01" + buf.buf = buf.buf .. "" buf:seek(1) end s.zdo_body = zdo_commands.parse_zdo_command(parent.address_header.cluster.value, buf) @@ -152,4 +142,4 @@ end setmetatable(zdo_messages.ZdoMessageBody, { __call = zdo_messages.ZdoMessageBody.from_values }) -return zdo_messages \ No newline at end of file +return zdo_messages diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_all_capabilities_zigbee_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_all_capabilities_zigbee_motion.lua index 003c7d95bf..948d433c9f 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_all_capabilities_zigbee_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_all_capabilities_zigbee_motion.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -143,6 +133,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -407,6 +405,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() end ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_high_precision.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_high_precision.lua index 4ec5b9a3f3..e7bfcbf642 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_high_precision.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_high_precision.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_motion_illuminance.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_motion_illuminance.lua index 52b437f86e..c8bb2c20f0 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_motion_illuminance.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_motion_illuminance.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -20,6 +10,10 @@ local data_types = require "st.zigbee.data_types" local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" +local messages = require "st.zigbee.messages" +local zb_const = require "st.zigbee.constants" +local write_attribute_response = require "st.zigbee.zcl.global_commands.write_attribute_response" +local zcl_messages = require "st.zigbee.zcl" test.add_package_capability("sensitivityAdjustment.yaml") test.add_package_capability("detectionFrequency.yaml") @@ -56,6 +50,29 @@ local function test_init() test.set_test_init_function(test_init) +local function build_write_attr_res(cluster, status) + local addr_header = messages.AddressHeader( + mock_device:get_short_address(), + mock_device.fingerprinted_endpoint_id, + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + cluster + ) + local write_attribute_body = write_attribute_response.WriteAttributeResponse(status, {}) + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(write_attribute_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = write_attribute_body + }) + return messages.ZigbeeMessageRx({ + address_header = addr_header, + body = message_body + }) +end + test.register_coroutine_test( "Handle added lifecycle", function() @@ -133,4 +150,59 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Motion detected twice cancels existing timer and creates a new one", + function() + local detect_duration = PREF_FREQUENCY_VALUE_DEFAULT + -- Pre-register two timers: first will be cancelled, second will fire + test.timer.__create_and_queue_test_time_advance_timer(detect_duration, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(detect_duration, "oneshot") + local attr_report_data = { + { MOTION_ILLUMINANCE_ATTRIBUTE_ID, data_types.Int32.ID, 0x0001006E } -- 65646 + } + -- First motion event + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.motionSensor.motion.active()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance(110)) + ) + test.wait_for_events() + -- Second motion event before first timer fires - cancels first timer + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.motionSensor.motion.active()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance(110)) + ) + test.wait_for_events() + -- Only the second timer fires + test.mock_time.advance_time(detect_duration) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.motionSensor.motion.inactive())) + end +) + +test.register_coroutine_test( + "WriteAttributeResponse with PREF_FREQUENCY_KEY updates detection frequency", + function() + mock_device:set_field(PREF_CHANGED_KEY, PREF_FREQUENCY_KEY) + mock_device:set_field(PREF_CHANGED_VALUE, PREF_FREQUENCY_VALUE_DEFAULT) + test.socket.zigbee:__queue_receive({ + mock_device.id, + build_write_attr_res(PRIVATE_CLUSTER_ID, 0x00) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + detectionFrequency.detectionFrequency(PREF_FREQUENCY_VALUE_DEFAULT, {visibility = {displayed = false}}))) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aurora_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aurora_motion.lua index 200d8e544b..1a5a292fc3 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aurora_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aurora_motion.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_battery_voltage_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_battery_voltage_motion.lua index 5878d9e687..737a594406 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_battery_voltage_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_battery_voltage_motion.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_centralite_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_centralite_motion.lua index 2fafad786b..1e35976fac 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_centralite_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_centralite_motion.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_compacta_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_compacta_motion.lua index 2e50c1a615..0660661d80 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_compacta_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_compacta_motion.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor.lua index 9b973b1724..3e3acf69e2 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor.lua @@ -1,16 +1,6 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local base64 = require "base64" @@ -82,39 +72,6 @@ test.register_message_test( } ) --- test.register_coroutine_test( --- "Health check should check all relevant attributes", --- function() --- test.wait_for_events() --- test.mock_time.advance_time(50000) --- test.socket.zigbee:__set_channel_ordering("relaxed") --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- IASZone.attributes.ZoneStatus:read(mock_device) --- } --- ) --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- OccupancySensing.attributes.Occupancy:read(mock_device):to_endpoint(OCCUPANCY_ENDPOINT) --- } --- ) --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT) --- } --- ) --- end, --- { --- test_init = function() --- test.mock_device.add_test_device(mock_device) --- test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") --- end --- } --- ) - test.register_coroutine_test( "Refresh should read all necessary attributes", function() @@ -140,6 +97,7 @@ test.register_coroutine_test( ) test.wait_for_events() + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.zigbee:__expect_send({ @@ -237,9 +195,52 @@ test.register_coroutine_test( }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + -- Advance time to trigger the do_refresh call scheduled in do_configure + test.wait_for_events() + test.mock_time.advance_time(5) + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.Occupancy:read(mock_device):to_endpoint(OCCUPANCY_ENDPOINT) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) end ) +test.register_message_test( + "Occupancy attribute handler emits motion active", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OccupancySensing.attributes.Occupancy:build_test_attr_report(mock_device, 0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.active()) + } + } +) + +test.register_message_test( + "Occupancy attribute handler emits motion inactive", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OccupancySensing.attributes.Occupancy:build_test_attr_report(mock_device, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive()) + } + } +) + test.register_coroutine_test( "infochanged to check for necessary preferences settings: Motion Turn-Off Delay, Motion Turn-On Delay, Movement Threshold in Turn-On Delay", function() diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor2_pet.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor2_pet.lua new file mode 100644 index 0000000000..6869412d49 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor2_pet.lua @@ -0,0 +1,388 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- Mock out globals +local base64 = require "base64" +local test = require "integration_test" +local t_utils = require "integration_test.utils" + +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local zcl_clusters = require "st.zigbee.zcl.clusters" + +local IASZone = zcl_clusters.IASZone +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" +local IlluminanceMeasurement = zcl_clusters.IlluminanceMeasurement +local OccupancySensing = zcl_clusters.OccupancySensing +local PowerConfiguration = zcl_clusters.PowerConfiguration +local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement + +local capabilities = require "st.capabilities" + +local IASZONE_ENDPOINT = 0x23 +local ILLUMINANCE_ENDPOINT = 0x27 +local OCCUPANCY_ENDPOINT = 0x22 +local POWER_CONFIGURATION_ENDPOINT = 0x23 +local TEMPERATURE_MEASUREMENT_ENDPOINT = 0x26 + +local DEFAULT_OCCUPIED_TO_UNOCCUPIED_DELAY = 240 +local DEFAULT_UNOCCUPIED_TO_OCCUPIED_DELAY = 0 +local DEFAULT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD = 0 + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("frient-motion-temp-illuminance-battery.yml"), + zigbee_endpoints = { + [0x22] = { + id = 0x22, + manufacturer = "frient A/S", + model = "MOSZB-153", + server_clusters = { 0x0000, 0x0003, 0x0406 } + }, + [0x23] = { + id = 0x23, + server_clusters = { 0x0000, 0x0001, 0x000f, 0x0020, 0x0500 } + }, + [0x26] = { + id = 0x26, + server_clusters = { 0x0000, 0x0003, 0x0402 } + }, + [0x27] = { + id = 0x27, + server_clusters = { 0x0000, 0x0003, 0x0400 } + }, + [0x28] = { + id = 0x28, + server_clusters = { 0x0000, 0x0003, 0x0406 } + }, + [0x29] = { + id = 0x29, + server_clusters = { 0x0000, 0x0003, 0x0406 } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device)end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Motion inactive clear states when the device is added", function() + test.socket.matter:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive()) + ) + test.wait_for_events() + end +) + +test.register_message_test( + "Battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 24) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(14)) + } + } +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } + } + } +) + +test.register_message_test( + "Illuminance report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + IlluminanceMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 21370) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance({ value = 137 })) + } + } +) + +-- test.register_coroutine_test( +-- "Health check should check all relevant attributes", +-- function() +-- test.wait_for_events() +-- test.mock_time.advance_time(50000) +-- test.socket.zigbee:__set_channel_ordering("relaxed") +-- test.socket.zigbee:__expect_send( +-- { +-- mock_device.id, +-- IlluminanceMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(ILLUMINANCE_ENDPOINT) +-- } +-- ) +-- test.socket.zigbee:__expect_send( +-- { +-- mock_device.id, +-- OccupancySensing.attributes.Occupancy:read(mock_device):to_endpoint(OCCUPANCY_ENDPOINT) +-- } +-- ) +-- test.socket.zigbee:__expect_send( +-- { +-- mock_device.id, +-- PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT) +-- } +-- ) +-- test.socket.zigbee:__expect_send( +-- { +-- mock_device.id, +-- TemperatureMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) +-- } +-- ) +-- end, +-- { +-- test_init = function() +-- test.mock_device.add_test_device(mock_device) +-- test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") +-- end +-- } +-- ) + +test.register_coroutine_test( + "Refresh should read all necessary attributes", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} }}) + test.socket.zigbee:__expect_send({mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT)}) + test.socket.zigbee:__expect_send({mock_device.id, OccupancySensing.attributes.Occupancy:read(mock_device):to_endpoint(OCCUPANCY_ENDPOINT)}) + test.socket.zigbee:__expect_send({mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT)}) + test.socket.zigbee:__expect_send({mock_device.id, IlluminanceMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(ILLUMINANCE_ENDPOINT)}) + end +) + +test.register_coroutine_test( + "init and doConfigure lifecycles should be handled properly", + function() + test.socket.environment_update:__queue_receive({ "zigbee", { hub_zigbee_id = base64.encode(zigbee_test_utils.mock_hub_eui) } }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive()) + ) + + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID, + POWER_CONFIGURATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device, + 30, + 21600, + 1 + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + IASZONE_ENDPOINT + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device, + 30, + 300, + 1 + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + OccupancySensing.ID, + OCCUPANCY_ENDPOINT + ):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.Occupancy:configure_reporting( + mock_device, + 0, + 3600 + ):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write( + mock_device, + zigbee_test_utils.mock_hub_eui + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + TemperatureMeasurement.ID, + TEMPERATURE_MEASUREMENT_ENDPOINT + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 3600, + 10 + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IlluminanceMeasurement.ID, + ILLUMINANCE_ENDPOINT + ):to_endpoint(ILLUMINANCE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 10, + 3600, + 0x2711 + ):to_endpoint(ILLUMINANCE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.PIROccupiedToUnoccupiedDelay:write(mock_device, DEFAULT_OCCUPIED_TO_UNOCCUPIED_DELAY) + :to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedDelay:write(mock_device, DEFAULT_UNOCCUPIED_TO_OCCUPIED_DELAY) + :to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedThreshold:write(mock_device, DEFAULT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD) + :to_endpoint(OCCUPANCY_ENDPOINT) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_coroutine_test( + "infochanged to check for necessary preferences settings: Temperature Sensitivity, Motion Turn-Off Delay, Motion Turn-On Delay, Movement Threshold in Turn-On Delay", + function() + local updates = { + preferences = { + temperatureSensitivity = 0.9, + occupiedToUnoccupiedD = 200, + unoccupiedToOccupiedD = 1, + unoccupiedToOccupiedT = 2 + } + } + test.socket.zigbee:__set_channel_ordering("relaxed") + test.wait_for_events() + + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + + test.socket.zigbee:__expect_send({ mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedDelay:write(mock_device, 1):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedThreshold:write(mock_device, 2):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ mock_device.id, + OccupancySensing.attributes.PIROccupiedToUnoccupiedDelay:write(mock_device, 200):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + local temperatureSensitivity = math.floor(0.9 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 3600, + temperatureSensitivity + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor_pro.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor_pro.lua index 753850e38e..1f72d365e6 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor_pro.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor_pro.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "base64" @@ -156,6 +145,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -179,51 +176,6 @@ test.register_message_test( } ) --- test.register_coroutine_test( --- "Health check should check all relevant attributes", --- function() --- test.wait_for_events() --- test.mock_time.advance_time(50000) --- test.socket.zigbee:__set_channel_ordering("relaxed") --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- IASZone.attributes.ZoneStatus:read(mock_device):to_endpoint(TAMPER_ENDPOINT) --- } --- ) --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- IlluminanceMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(ILLUMINANCE_ENDPOINT) --- } --- ) --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- OccupancySensing.attributes.Occupancy:read(mock_device):to_endpoint(OCCUPANCY_ENDPOINT) --- } --- ) --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT) --- } --- ) --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- TemperatureMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) --- } --- ) --- end, --- { --- test_init = function() --- test.mock_device.add_test_device(mock_device) --- test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") --- end --- } --- ) - test.register_coroutine_test( "Refresh should read all necessary attributes", function() @@ -435,4 +387,38 @@ test.register_coroutine_test( end ) +test.register_message_test( + "IASZone attribute status handler: tamper detected", + { + { + channel = "zigbee", + direction = "receive", + -- ZoneStatus | Bit2: Tamper set to 1 + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0004) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected()) + } + } +) + +test.register_message_test( + "IASZone attribute status handler: tamper clear", + { + { + channel = "zigbee", + direction = "receive", + -- ZoneStatus | Bit2: Tamper set to 0 + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0000) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) + } + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_gator_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_gator_motion.lua index 2ae1fe0952..3afdbbcaa2 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_gator_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_gator_motion.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -153,4 +143,20 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "ZoneStatusChangeNotification with alarm1 triggers motion active and inactive", + function() + test.timer.__create_and_queue_test_time_advance_timer(120, "oneshot") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0001, 0x00) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.active())) + test.mock_time.advance_time(120) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_ikea_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_ikea_motion.lua index 133fe63dbb..36312c8fdb 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_ikea_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_ikea_motion.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -233,4 +223,29 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Second OnWithTimedOff cancels existing timer and resets motion", + function() + local frm_ctrl = FrameCtrl(0x01) + -- Pre-register two timers: first will be cancelled, second will fire + test.timer.__create_and_queue_test_time_advance_timer(0x0708/10, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(0x0708/10, "oneshot") + -- First motion event + local first_cmd = OnOff.server.commands.OnWithTimedOff.build_test_rx(mock_device, 0x00, 0x0708, 0x0000) + first_cmd.body.zcl_header.frame_ctrl = frm_ctrl + test.socket.zigbee:__queue_receive({mock_device.id, first_cmd}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.active())) + test.wait_for_events() + -- Second motion event before first timer fires - cancels first timer + local second_cmd = OnOff.server.commands.OnWithTimedOff.build_test_rx(mock_device, 0x00, 0x0708, 0x0000) + second_cmd.body.zcl_header.frame_ctrl = frm_ctrl + test.socket.zigbee:__queue_receive({mock_device.id, second_cmd}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.active())) + test.wait_for_events() + -- Only the second timer fires + test.mock_time.advance_time(180) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_samjin_sensor.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_samjin_sensor.lua index d42685e704..d966d301f3 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_samjin_sensor.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_samjin_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_sengled_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_sengled_motion.lua index da645dcc9f..4450761c5d 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_sengled_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_sengled_motion.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartsense_motion_sensor.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartsense_motion_sensor.lua index b61abdec38..f96322467f 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartsense_motion_sensor.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartsense_motion_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zigbee_test_utils = require "integration_test.zigbee_test_utils" diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartthings_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartthings_motion.lua index 5997a2f75c..5211ce1982 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartthings_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartthings_motion.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_thirdreality_sensor.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_thirdreality_sensor.lua index 1113f34208..7a1655fc41 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_thirdreality_sensor.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_thirdreality_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -150,4 +140,23 @@ test.register_coroutine_test( end ) +test.register_message_test( + "Handle added lifecycle - reads ApplicationVersion", + { + { + channel = "device_lifecycle", + direction = "receive", + message = {mock_device1.id, "added"} + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device1.id, + Basic.attributes.ApplicationVersion:read(mock_device1) + } + } + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_iris.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_iris.lua index 0e91d76d49..f0cd1053dc 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_iris.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_iris.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_nyce.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_nyce.lua index 614e971671..e368e852c8 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_nyce.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_nyce.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_orvibo.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_orvibo.lua index 25db204646..1f586bce88 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_orvibo.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_orvibo.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_plugin_motion_sensor.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_plugin_motion_sensor.lua index f957f8ce2f..78634bbaa2 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_plugin_motion_sensor.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_plugin_motion_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/can_handle.lua new file mode 100644 index 0000000000..4f087c6e5d --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_third_reality_motion_sensor = function(opts, driver, device) + local FINGERPRINTS = require("thirdreality.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("thirdreality") + end + end + return false +end + +return is_third_reality_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/fingerprints.lua new file mode 100644 index 0000000000..a5e92ef0b9 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_MOTION_SENSOR_FINGERPRINTS = { + { mfr = "Third Reality, Inc", model = "3RMS16BZ"}, + { mfr = "THIRDREALITY", model = "3RMS16BZ"} +} + +return ZIGBEE_MOTION_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/init.lua index c93919f16b..37c6e5b8d6 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -20,19 +10,7 @@ local utils = require "st.utils" local APPLICATION_VERSION = "application_version" -local ZIGBEE_MOTION_SENSOR_FINGERPRINTS = { - { mfr = "Third Reality, Inc", model = "3RMS16BZ"}, - { mfr = "THIRDREALITY", model = "3RMS16BZ"} -} -local is_third_reality_motion_sensor = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_MOTION_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local device_added = function(self, device) device:set_field(APPLICATION_VERSION, 0) @@ -73,7 +51,7 @@ local third_reality_motion_sensor = { lifecycle_handlers = { added = device_added, }, - can_handle = is_third_reality_motion_sensor + can_handle = require("thirdreality.can_handle"), } return third_reality_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/can_handle.lua new file mode 100644 index 0000000000..9b926e0845 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_plugin_motion_sensor = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-plugin-motion-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return true, require("zigbee-plugin-motion-sensor") + end + end + return false +end + +return is_zigbee_plugin_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/fingerprints.lua new file mode 100644 index 0000000000..459c3e7702 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_PLUGIN_MOTION_SENSOR_FINGERPRINTS = { + { model = "E280-KR0A0Z0-HA" } +} + +return ZIGBEE_PLUGIN_MOTION_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/init.lua index eb96b32a94..b387a35fbb 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/init.lua @@ -1,34 +1,13 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" local device_management = require "st.zigbee.device_management" local OccupancySensing = zcl_clusters.OccupancySensing -local ZIGBEE_PLUGIN_MOTION_SENSOR_FINGERPRINTS = { - { model = "E280-KR0A0Z0-HA" } -} -local is_zigbee_plugin_motion_sensor = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_PLUGIN_MOTION_SENSOR_FINGERPRINTS) do - if device:get_model() == fingerprint.model then - return true - end - end - return false -end local function occupancy_attr_handler(driver, device, occupancy, zb_rx) device:emit_event(occupancy.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) @@ -59,7 +38,7 @@ local zigbee_plugin_motion_sensor = { [capabilities.refresh.commands.refresh.NAME] = do_refresh, } }, - can_handle = is_zigbee_plugin_motion_sensor + can_handle = require("zigbee-plugin-motion-sensor.can_handle"), } return zigbee_plugin_motion_sensor diff --git a/drivers/SmartThings/zigbee-power-meter/fingerprints.yml b/drivers/SmartThings/zigbee-power-meter/fingerprints.yml index 37cf9df678..ec58259dd5 100644 --- a/drivers/SmartThings/zigbee-power-meter/fingerprints.yml +++ b/drivers/SmartThings/zigbee-power-meter/fingerprints.yml @@ -28,6 +28,46 @@ zigbeeManufacturer: manufacturer: ShinaSystem model: "PMM-300Z3" deviceProfileName: power-meter-consumption-report-sihas + - id: "BITUO TECHNIK/SPM01-E0" + deviceLabel: Energy Monitor 1PN + manufacturer: BITUO TECHNIK + model: "SPM01-E0" + deviceProfileName: power-meter-1p + - id: "BITUO TECHNIK/SPM01X" + deviceLabel: Energy Monitor 1PN + manufacturer: BITUO TECHNIK + model: "SPM01X" + deviceProfileName: power-meter-1p + - id: "BITUO TECHNIK/SDM02-E0" + deviceLabel: Energy Monitor 2PN + manufacturer: BITUO TECHNIK + model: "SDM02-E0" + deviceProfileName: power-meter-2p + - id: "BITUO TECHNIK/SDM02X" + deviceLabel: Energy Monitor 2PN + manufacturer: BITUO TECHNIK + model: "SDM02X" + deviceProfileName: power-meter-2p + - id: "BITUO TECHNIK/SPM02-E0" + deviceLabel: Energy Monitor 3PN + manufacturer: BITUO TECHNIK + model: "SPM02-E0" + deviceProfileName: power-meter-3p + - id: "BITUO TECHNIK/SPM02X" + deviceLabel: Energy Monitor 3PN + manufacturer: BITUO TECHNIK + model: "SPM02X" + deviceProfileName: power-meter-3p + - id: "BITUO TECHNIK/SDM01W" + deviceLabel: Energy Monitor 3PN + manufacturer: BITUO TECHNIK + model: "SDM01W" + deviceProfileName: power-meter-3p + - id: "BITUO TECHNIK/SDM01B" + deviceLabel: Energy Monitor 1PN + manufacturer: BITUO TECHNIK + model: "SDM01B" + deviceProfileName: power-meter-1p zigbeeGeneric: - id: "genericMeter" deviceLabel: Zigbee Meter diff --git a/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-1p.yml b/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-1p.yml new file mode 100644 index 0000000000..7290794ffe --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-1p.yml @@ -0,0 +1,31 @@ +name: power-meter-1p +components: +- id: main + label: Total Forward Energy + capabilities: + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter +- id: TotalReverseEnergy + label: Total Reverse Energy + capabilities: + - id: energyMeter + version: 1 + categories: + - name: CurbPowerMeter +- id: PhaseA + label: Phase A + capabilities: + - id: powerMeter + version: 1 + - id: currentMeasurement + version: 1 + - id: voltageMeasurement + version: 1 + categories: + - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-2p.yml b/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-2p.yml new file mode 100644 index 0000000000..11f2dd412b --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-2p.yml @@ -0,0 +1,42 @@ +name: power-meter-2p +components: +- id: main + label: Total Forward Energy + capabilities: + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter +- id: TotalReverseEnergy + label: Total Reverse Energy + capabilities: + - id: energyMeter + version: 1 + categories: + - name: CurbPowerMeter +- id: PhaseA + label: Phase A + capabilities: + - id: powerMeter + version: 1 + - id: currentMeasurement + version: 1 + - id: voltageMeasurement + version: 1 + categories: + - name: CurbPowerMeter +- id: PhaseB + label: Phase B + capabilities: + - id: powerMeter + version: 1 + - id: currentMeasurement + version: 1 + - id: voltageMeasurement + version: 1 + categories: + - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-3p.yml b/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-3p.yml new file mode 100644 index 0000000000..20c824d092 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-3p.yml @@ -0,0 +1,53 @@ +name: power-meter-3p +components: +- id: main + label: Total Forward Energy + capabilities: + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter +- id: TotalReverseEnergy + label: Total Reverse Energy + capabilities: + - id: energyMeter + version: 1 + categories: + - name: CurbPowerMeter +- id: PhaseA + label: Phase A + capabilities: + - id: powerMeter + version: 1 + - id: currentMeasurement + version: 1 + - id: voltageMeasurement + version: 1 + categories: + - name: CurbPowerMeter +- id: PhaseB + label: Phase B + capabilities: + - id: powerMeter + version: 1 + - id: currentMeasurement + version: 1 + - id: voltageMeasurement + version: 1 + categories: + - name: CurbPowerMeter +- id: PhaseC + label: Phase C + capabilities: + - id: powerMeter + version: 1 + - id: currentMeasurement + version: 1 + - id: voltageMeasurement + version: 1 + categories: + - name: CurbPowerMeter diff --git a/drivers/SmartThings/zigbee-power-meter/src/bituo/can_handle.lua b/drivers/SmartThings/zigbee-power-meter/src/bituo/can_handle.lua new file mode 100644 index 0000000000..a2124afdd6 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/bituo/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_bituo_power_meter = function(opts, driver, device) + local FINGERPRINTS = require("bituo.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return true, require("bituo") + end + end + + return false +end + +return is_bituo_power_meter diff --git a/drivers/SmartThings/zigbee-power-meter/src/bituo/fingerprints.lua b/drivers/SmartThings/zigbee-power-meter/src/bituo/fingerprints.lua new file mode 100644 index 0000000000..3909881854 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/bituo/fingerprints.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_POWER_METER_FINGERPRINTS = { + { mfr = "BITUO TECHNIK", model = "SPM01-E0" }, + { mfr = "BITUO TECHNIK", model = "SPM01X" }, + { mfr = "BITUO TECHNIK", model = "SDM02-E0" }, + { mfr = "BITUO TECHNIK", model = "SDM02X" }, + { mfr = "BITUO TECHNIK", model = "SPM02-E0" }, + { mfr = "BITUO TECHNIK", model = "SPM02X" }, + { mfr = "BITUO TECHNIK", model = "SDM01W" }, + { mfr = "BITUO TECHNIK", model = "SDM01B" } +} + +return ZIGBEE_POWER_METER_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-power-meter/src/bituo/init.lua b/drivers/SmartThings/zigbee-power-meter/src/bituo/init.lua new file mode 100644 index 0000000000..4bf5dbd359 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/bituo/init.lua @@ -0,0 +1,231 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local capabilities = require "st.capabilities" +local constants = require "st.zigbee.constants" +local clusters = require "st.zigbee.zcl.clusters" +local SimpleMetering = clusters.SimpleMetering +local ElectricalMeasurement = clusters.ElectricalMeasurement +local configurations = require "configurations" + + +local PHASE_A_CONFIGURATION = { + { + cluster = SimpleMetering.ID, + attribute = SimpleMetering.attributes.CurrentSummationDelivered.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = SimpleMetering.attributes.CurrentSummationDelivered.base_type, + reportable_change = 0 + }, + { + cluster = SimpleMetering.ID, + attribute = 0x0001, + minimum_interval = 30, + maximum_interval = 120, + data_type = SimpleMetering.attributes.CurrentSummationDelivered.base_type, + reportable_change = 0 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.ActivePower.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.ActivePower.base_type, + reportable_change = 0 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSVoltage.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.RMSVoltage.base_type, + reportable_change = 0 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSCurrent.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.RMSCurrent.base_type, + reportable_change = 0 + } +} +local PHASE_B_CONFIGURATION = { + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.ActivePowerPhB.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.ActivePowerPhB.base_type, + reportable_change = 0 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSVoltagePhB.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.RMSVoltagePhB.base_type, + reportable_change = 0 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSCurrentPhB.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.RMSCurrentPhB.base_type, + reportable_change = 0 + }, +} +local PHASE_C_CONFIGURATION = { + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.ActivePowerPhC.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.ActivePowerPhC.base_type, + reportable_change = 0 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSVoltagePhC.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.RMSVoltagePhC.base_type, + reportable_change = 0 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSCurrentPhC.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.RMSCurrentPhC.base_type, + reportable_change = 0 + } +} + +local function energy_handler(driver, device, value, zb_rx) + local multiplier = 1 + local divisor = 100 + local raw_value = value.value + local raw_value_kilowatts = raw_value * multiplier/divisor + + local offset = device:get_field(constants.ENERGY_METER_OFFSET) or 0 + if raw_value_kilowatts < offset then + --- somehow our value has gone below the offset, so we'll reset the offset, since the device seems to have + offset = 0 + device:set_field(constants.ENERGY_METER_OFFSET, offset, {persist = true}) + end + raw_value_kilowatts = raw_value_kilowatts - offset + + local raw_value_watts = raw_value_kilowatts*1000 + local delta_tick + local last_save_ticks = device:get_field("LAST_SAVE_TICK") + + if last_save_ticks == nil then last_save_ticks = 0 end + delta_tick = os.time() - last_save_ticks + + -- wwst energy certification : powerConsumptionReport capability values should be updated every 15 minutes. + -- Check if 15 minutes have passed since the reporting time of current_power_consumption. + if delta_tick >= 15*60 then + local delta_energy = 0.0 + local current_power_consumption = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, capabilities.powerConsumptionReport.powerConsumption.NAME) + if current_power_consumption ~= nil then + delta_energy = math.max(raw_value_watts - current_power_consumption.energy, 0.0) + end + device:emit_event(capabilities.powerConsumptionReport.powerConsumption({energy = raw_value_watts, deltaEnergy = delta_energy })) -- the unit of these values should be 'Wh' + + local curr_save_tick = last_save_ticks + 15*60 -- Set the time to a regular interval by adding 15 minutes to the existing last_save_ticks. + -- If the time 15 minutes from now is less than the current time, set the current time as the last time. + if curr_save_tick + 15*60 < os.time() then + curr_save_tick = os.time() + end + device:set_field("LAST_SAVE_TICK", curr_save_tick, {persist = false}) + end + device:emit_event(capabilities.energyMeter.energy({value = raw_value_kilowatts, unit = "kWh"})) +end + +local function generic_handler_factory(component_name, capability, multiplier, divisor, unit) + return function(driver, device, value, zb_rx) + local component = device.profile.components[component_name] + if component ~= nil then + local raw_value = value.value * multiplier / divisor + device:emit_component_event(component, capability({value = raw_value, unit = unit})) + end + end +end + +local refresh = function(driver, device, cmd) + device:refresh() +end + +local function resetEnergyMeter(self, device) + device:send(clusters.OnOff.server.commands.On(device)) + -- Reset Power consumption + device:set_field(constants.ENERGY_METER_OFFSET, 0, {persist = true}) + device:set_field("LAST_SAVE_TICK", os.time(), {persist = false}) +end +local function do_configure(driver, device) + device:configure() + --device:send(device_management.build_bind_request(device, clusters.SimpleMetering.ID, driver.environment_info.hub_zigbee_eui)) + --device:send(device_management.build_bind_request(device, clusters.ElectricalMeasurement.ID, driver.environment_info.hub_zigbee_eui)) + device:refresh() +end + +local device_init = function(self, device) + for _, attribute in ipairs(PHASE_A_CONFIGURATION) do + device:add_configured_attribute(attribute) + device:add_monitored_attribute(attribute) + end + if string.find(device:get_model(), "SDM02") or string.find(device:get_model(), "SPM02") or string.find(device:get_model(), "SDM01W") then + for _, attribute in ipairs(PHASE_B_CONFIGURATION) do + device:add_configured_attribute(attribute) + device:add_monitored_attribute(attribute) + end + end + if string.find(device:get_model(), "SPM02") or string.find(device:get_model(), "SDM01W") then + for _, attribute in ipairs(PHASE_C_CONFIGURATION) do + device:add_configured_attribute(attribute) + device:add_monitored_attribute(attribute) + end + end +end + +local bituo_power_meter_handler = { + NAME = "bituo power meter handler", + lifecycle_handlers = { + init = configurations.power_reconfig_wrapper(device_init), + doConfigure = do_configure, + }, + zigbee_handlers = { + attr = { + [clusters.SimpleMetering.ID] = { + [clusters.SimpleMetering.attributes.CurrentSummationDelivered.ID] = energy_handler, + [0x0001] = generic_handler_factory("TotalReverseEnergy", capabilities.energyMeter.energy, 1, 100, "kWh"), + }, + [clusters.ElectricalMeasurement.ID] = { + [ElectricalMeasurement.attributes.ActivePower.ID] = generic_handler_factory("PhaseA", capabilities.powerMeter.power, 1, 1, "W"), + [ElectricalMeasurement.attributes.ActivePowerPhB.ID] = generic_handler_factory("PhaseB", capabilities.powerMeter.power, 1, 1, "W"), + [ElectricalMeasurement.attributes.ActivePowerPhC.ID] = generic_handler_factory("PhaseC", capabilities.powerMeter.power, 1, 1, "W"), + [ElectricalMeasurement.attributes.RMSVoltage.ID] = generic_handler_factory("PhaseA", capabilities.voltageMeasurement.voltage, 1, 100, "V"), + [ElectricalMeasurement.attributes.RMSVoltagePhB.ID] = generic_handler_factory("PhaseB", capabilities.voltageMeasurement.voltage, 1, 100, "V"), + [ElectricalMeasurement.attributes.RMSVoltagePhC.ID] = generic_handler_factory("PhaseC", capabilities.voltageMeasurement.voltage, 1, 100, "V"), + [ElectricalMeasurement.attributes.RMSCurrent.ID] = generic_handler_factory("PhaseA", capabilities.currentMeasurement.current, 1, 100, "A"), + [ElectricalMeasurement.attributes.RMSCurrentPhB.ID] = generic_handler_factory("PhaseB", capabilities.currentMeasurement.current, 1, 100, "A"), + [ElectricalMeasurement.attributes.RMSCurrentPhC.ID] = generic_handler_factory("PhaseC", capabilities.currentMeasurement.current, 1, 100, "A") + } + } + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh + }, + [capabilities.energyMeter.ID] = { + [capabilities.energyMeter.commands.resetEnergyMeter.NAME] = resetEnergyMeter, + }, + }, + can_handle = require("bituo.can_handle"), +} + +return bituo_power_meter_handler diff --git a/drivers/SmartThings/zigbee-power-meter/src/configurations.lua b/drivers/SmartThings/zigbee-power-meter/src/configurations.lua new file mode 100644 index 0000000000..557e790f76 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/configurations.lua @@ -0,0 +1,148 @@ +-- 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 capabilities = require "st.capabilities" +local device_def = require "st.device" +local SimpleMetering = clusters.SimpleMetering +local ElectricalMeasurement = clusters.ElectricalMeasurement +local device_management = require "st.zigbee.device_management" + + +local Status = require "st.zigbee.generated.types.ZclStatus" + +local CONFIGURATION_VERSION_KEY = "_configuration_version" +local CONFIGURATION_ATTEMPTED = "_reconfiguration_attempted" + + +local configurations = {} + +local active_power_configuration = { + cluster = clusters.ElectricalMeasurement.ID, + attribute = clusters.ElectricalMeasurement.attributes.ActivePower.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = clusters.ElectricalMeasurement.attributes.ActivePower.base_type, + reportable_change = 5 +} + +local instantaneous_demand_configuration = { + cluster = clusters.SimpleMetering.ID, + attribute = clusters.SimpleMetering.attributes.InstantaneousDemand.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = clusters.SimpleMetering.attributes.InstantaneousDemand.base_type, + reportable_change = 5 +} + +configurations.find_cluster_config = function(device, cluster, attribute) + -- This is an internal field, but this is the easiest way to allow the custom configuraitons without + -- larger driver changes + local configured_attrs = device:get_field("__configured_attributes") or {} + for clus, attrs in pairs(configured_attrs) do + if cluster == clus then + for _, attr_config in pairs(attrs) do + if attr_config.attribute == attribute then + local u = require "st.utils" + print(u.stringify_table(attr_config)) + return attr_config + end + end + end + end + return nil +end + + +configurations.check_and_reconfig_devices = function(driver) + for device_id, device in pairs(driver.device_cache) do + local config_version = device:get_field(CONFIGURATION_VERSION_KEY) + if config_version == nil or config_version < driver.current_config_version then + if device:supports_capability(capabilities.powerMeter) then + if device:supports_server_cluster(clusters.ElectricalMeasurement.ID) then + -- Allow for custom configurations as long as the minimum reporting interval is at least 5 + local config = configurations.find_cluster_config(device, clusters.ElectricalMeasurement.ID, ElectricalMeasurement.attributes.ActivePower.ID) + if config == nil or config.minimum_interval < 5 then + config = active_power_configuration + end + device:send(device_management.attr_config(device, config)) + device:add_configured_attribute(config) + end + if device:supports_server_cluster(clusters.SimpleMetering.ID) then + -- Allow for custom configurations as long as the minimum reporting interval is at least 5 + local config = configurations.find_cluster_config(device, clusters.SimpleMetering.ID, SimpleMetering.attributes.InstantaneousDemand.ID) + if config == nil or config.minimum_interval < 5 then + config = instantaneous_demand_configuration + end + device:send(device_management.attr_config(device, config)) + device:add_configured_attribute(config) + + -- perform reconfiguration of summation attribute if it's configured + config = configurations.find_cluster_config(device, clusters.SimpleMetering.ID, SimpleMetering.attributes.CurrentSummationDelivered.ID) + if config ~= nil then + device:send(device_management.attr_config(device, config)) + end + end + end + device:set_field(CONFIGURATION_ATTEMPTED, true, {persist = true}) + end + end + driver._reconfig_timer = nil +end + +configurations.handle_reporting_config_response = function(driver, device, zb_mess) + local dev = device + local find_child_fn = device:get_field(device_def.FIND_CHILD_KEY) + if find_child_fn ~= nil then + local child = find_child_fn(device, zb_mess.address_header.src_endpoint.value) + if child ~= nil then + dev = child + end + end + if dev:get_field(CONFIGURATION_ATTEMPTED) == true then + if zb_mess.body.zcl_body.global_status ~= nil and zb_mess.body.zcl_body.global_status.value == Status.SUCCESS then + dev:set_field(CONFIGURATION_VERSION_KEY, driver.current_config_version, {persist = true}) + elseif zb_mess.body.zcl_body.config_records ~= nil then + local config_records = zb_mess.body.zcl_body.config_records + for _, record in ipairs(config_records) do + if zb_mess.address_header.cluster.value == clusters.SimpleMetering.ID then + if record.attr_id.value == clusters.SimpleMetering.attributes.InstantaneousDemand.ID + and record.status.value == Status.SUCCESS then + dev:set_field(CONFIGURATION_VERSION_KEY, driver.current_config_version, {persist = true}) + end + elseif zb_mess.address_header.cluster.value == clusters.ElectricalMeasurement.ID then + if record.attr_id.value == clusters.ElectricalMeasurement.attributes.ActivePower.ID + and record.status.value == Status.SUCCESS then + dev:set_field(CONFIGURATION_VERSION_KEY, driver.current_config_version, {persist = true}) + end + end + end + end + end +end + +configurations.power_reconfig_wrapper = function(orig_function) + local new_init = function(driver, device) + local config_version = device:get_field(CONFIGURATION_VERSION_KEY) + if config_version == nil or config_version < driver.current_config_version then + if driver._reconfig_timer == nil then + driver._reconfig_timer = driver:call_with_delay(5*60, configurations.check_and_reconfig_devices, "reconfig_power_devices") + end + end + orig_function(driver, device) + end + return new_init +end + +return configurations \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-power-meter/src/ezex/can_handle.lua b/drivers/SmartThings/zigbee-power-meter/src/ezex/can_handle.lua new file mode 100644 index 0000000000..91569fd4b4 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/ezex/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_ezex_power_meter = function(opts, driver, device) + local FINGERPRINTS = require("ezex.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return true, require("ezex") + end + end + + return false +end + +return is_ezex_power_meter diff --git a/drivers/SmartThings/zigbee-power-meter/src/ezex/fingerprints.lua b/drivers/SmartThings/zigbee-power-meter/src/ezex/fingerprints.lua new file mode 100644 index 0000000000..1495f82c56 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/ezex/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_POWER_METER_FINGERPRINTS = { + { model = "E240-KR080Z0-HA" } +} + +return ZIGBEE_POWER_METER_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua b/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua index 0b85da97c7..15ca345f31 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua @@ -1,43 +1,23 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local constants = require "st.zigbee.constants" local clusters = require "st.zigbee.zcl.clusters" local SimpleMetering = clusters.SimpleMetering +local ElectricalMeasurement = clusters.ElectricalMeasurement local energy_meter_defaults = require "st.zigbee.defaults.energyMeter_defaults" +local configurations = require "configurations" -local ZIGBEE_POWER_METER_FINGERPRINTS = { - { model = "E240-KR080Z0-HA" } -} -local is_ezex_power_meter = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_POWER_METER_FINGERPRINTS) do - if device:get_model() == fingerprint.model then - return true - end - end - - return false -end local instantaneous_demand_configuration = { - cluster = clusters.SimpleMetering.ID, - attribute = clusters.SimpleMetering.attributes.InstantaneousDemand.ID, - minimum_interval = 1, + cluster = SimpleMetering.ID, + attribute = SimpleMetering.attributes.InstantaneousDemand.ID, + minimum_interval = 5, maximum_interval = 3600, - data_type = clusters.SimpleMetering.attributes.InstantaneousDemand.base_type, + data_type = SimpleMetering.attributes.InstantaneousDemand.base_type, reportable_change = 500 } @@ -49,11 +29,14 @@ end local device_init = function(self, device) device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, 1000000, {persist = true}) device:set_field(constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10, {persist = true}) - - device:add_monitored_attribute(instantaneous_demand_configuration) + device:remove_configured_attribute(ElectricalMeasurement.ID, ElectricalMeasurement.attributes.ActivePower.ID) + device:remove_configured_attribute(ElectricalMeasurement.ID, ElectricalMeasurement.attributes.ACPowerDivisor.ID) + device:remove_configured_attribute(ElectricalMeasurement.ID, ElectricalMeasurement.attributes.ACPowerMultiplier.ID) device:add_configured_attribute(instantaneous_demand_configuration) end +local function noop_active_power(driver, device, value, zb_rx) end + local function energy_meter_handler(driver, device, value, zb_rx) local raw_value_miliwatts = value.value local raw_value_watts = raw_value_miliwatts / 1000 @@ -73,14 +56,17 @@ local ezex_power_meter_handler = { attr = { [SimpleMetering.ID] = { [SimpleMetering.attributes.CurrentSummationDelivered.ID] = energy_meter_handler + }, + [ElectricalMeasurement.ID] = { + [ElectricalMeasurement.attributes.ActivePower.ID] = noop_active_power } } }, lifecycle_handlers = { - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure, }, - can_handle = is_ezex_power_meter + can_handle = require("ezex.can_handle"), } return ezex_power_meter_handler diff --git a/drivers/SmartThings/zigbee-power-meter/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-power-meter/src/frient/can_handle.lua new file mode 100644 index 0000000000..3cf7077ddd --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_frient_power_meter = function(opts, driver, device) + local FINGERPRINTS = require("frient.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return true, require("frient") + end + end + + return false +end + +return is_frient_power_meter diff --git a/drivers/SmartThings/zigbee-power-meter/src/frient/fingerprints.lua b/drivers/SmartThings/zigbee-power-meter/src/frient/fingerprints.lua new file mode 100644 index 0000000000..5bc09f600d --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_POWER_METER_FINGERPRINTS = { + { model = "ZHEMI101" }, + { model = "EMIZB-132" }, +} + +return ZIGBEE_POWER_METER_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua b/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua index 1c9028f853..5933faf5cb 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua @@ -1,33 +1,11 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -local constants = require "st.zigbee.constants" -local ZIGBEE_POWER_METER_FINGERPRINTS = { - { model = "ZHEMI101" }, - { model = "EMIZB-132" }, -} +local constants = require "st.zigbee.constants" +local configurations = require "configurations" -local is_frient_power_meter = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_POWER_METER_FINGERPRINTS) do - if device:get_model() == fingerprint.model then - return true - end - end - return false -end local do_configure = function(self, device) device:refresh() @@ -42,10 +20,10 @@ end local frient_power_meter_handler = { NAME = "frient power meter handler", lifecycle_handlers = { - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure, }, - can_handle = is_frient_power_meter + can_handle = require("frient.can_handle"), } return frient_power_meter_handler \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-power-meter/src/init.lua b/drivers/SmartThings/zigbee-power-meter/src/init.lua index a12272aada..f15fae7905 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -18,6 +8,9 @@ local defaults = require "st.zigbee.defaults" local zigbee_constants = require "st.zigbee.constants" local clusters = require "st.zigbee.zcl.clusters" local SimpleMetering = clusters.SimpleMetering +local ElectricalMeasurement = clusters.ElectricalMeasurement +local zcl_global_commands = require "st.zigbee.zcl.global_commands" +local configurations = require "configurations" local do_configure = function(self, device) device:refresh() @@ -49,13 +42,20 @@ local zigbee_power_meter_driver_template = { capabilities.energyMeter, capabilities.powerConsumptionReport, }, - sub_drivers = { - require("ezex"), - require("frient"), - require("shinasystems"), + zigbee_handlers = { + global = { + [SimpleMetering.ID] = { + [zcl_global_commands.CONFIGURE_REPORTING_RESPONSE_ID] = configurations.handle_reporting_config_response + }, + [ElectricalMeasurement.ID] = { + [zcl_global_commands.CONFIGURE_REPORTING_RESPONSE_ID] = configurations.handle_reporting_config_response + } + } }, + current_config_version = 1, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure, }, health_check = false, diff --git a/drivers/SmartThings/zigbee-power-meter/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-power-meter/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-power-meter/src/shinasystems/can_handle.lua b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/can_handle.lua new file mode 100644 index 0000000000..94808a8486 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_shinasystems_power_meter = function(opts, driver, device) + local FINGERPRINTS = require("shinasystems.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return true, require("shinasystems") + end + end + + return false +end + +return is_shinasystems_power_meter diff --git a/drivers/SmartThings/zigbee-power-meter/src/shinasystems/fingerprints.lua b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/fingerprints.lua new file mode 100644 index 0000000000..5364835514 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_POWER_METER_FINGERPRINTS = { + { model = "PMM-300Z1" }, + { model = "PMM-300Z2" }, + { model = "PMM-300Z3" } +} + +return ZIGBEE_POWER_METER_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua index 4ba1c6467e..54866e2f82 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua @@ -1,35 +1,21 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local constants = require "st.zigbee.constants" local clusters = require "st.zigbee.zcl.clusters" local SimpleMetering = clusters.SimpleMetering local ElectricalMeasurement = clusters.ElectricalMeasurement +local configurations = require "configurations" -local ZIGBEE_POWER_METER_FINGERPRINTS = { - { model = "PMM-300Z1" }, - { model = "PMM-300Z2" }, - { model = "PMM-300Z3" } -} local POWERMETER_CONFIGURATION_V2 = { { cluster = SimpleMetering.ID, attribute = SimpleMetering.attributes.CurrentSummationDelivered.ID, minimum_interval = 5, - maximum_interval = 300, + maximum_interval = 450, -- Since the 15 minute report below depends on this report we make this 7.5 minutes data_type = SimpleMetering.attributes.CurrentSummationDelivered.base_type, reportable_change = 1 }, @@ -37,29 +23,20 @@ local POWERMETER_CONFIGURATION_V2 = { cluster = SimpleMetering.ID, attribute = SimpleMetering.attributes.InstantaneousDemand.ID, minimum_interval = 5, - maximum_interval = 300, + maximum_interval = 3600, data_type = SimpleMetering.attributes.InstantaneousDemand.base_type, - reportable_change = 1 + reportable_change = 5 }, { -- reporting : no cluster = ElectricalMeasurement.ID, attribute = ElectricalMeasurement.attributes.ActivePower.ID, - minimum_interval = 0, + minimum_interval = 5, maximum_interval = 65535, data_type = ElectricalMeasurement.attributes.ActivePower.base_type, - reportable_change = 1 + reportable_change = 5 } } -local is_shinasystems_power_meter = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_POWER_METER_FINGERPRINTS) do - if device:get_model() == fingerprint.model then - return true - end - end - - return false -end local function energy_meter_handler(driver, device, value, zb_rx) local multiplier = device:get_field(constants.SIMPLE_METERING_MULTIPLIER_KEY) or 1 @@ -111,7 +88,6 @@ local device_init = function(self, device) device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, 1000, {persist = true}) for _, attribute in ipairs(POWERMETER_CONFIGURATION_V2) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end @@ -125,10 +101,10 @@ local shinasystems_power_meter_handler = { } }, lifecycle_handlers = { - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure, }, - can_handle = is_shinasystems_power_meter + can_handle = require("shinasystems.can_handle"), } return shinasystems_power_meter_handler diff --git a/drivers/SmartThings/zigbee-power-meter/src/sub_drivers.lua b/drivers/SmartThings/zigbee-power-meter/src/sub_drivers.lua new file mode 100644 index 0000000000..51b24aca32 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/sub_drivers.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("ezex"), + lazy_load_if_possible("frient"), + lazy_load_if_possible("shinasystems"), + lazy_load_if_possible("bituo"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter.lua index afe97fb0f9..5f96805360 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter.lua @@ -20,6 +20,12 @@ local SimpleMetering = clusters.SimpleMetering local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" +local messages = require "st.zigbee.messages" +local config_reporting_response = require "st.zigbee.zcl.global_commands.configure_reporting_response" +local zb_const = require "st.zigbee.constants" +local zcl_messages = require "st.zigbee.zcl" +local data_types = require "st.zigbee.data_types" +local Status = require "st.zigbee.generated.types.ZclStatus" local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("power-meter.yml") } @@ -27,10 +33,143 @@ local mock_device = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() - test.mock_device.add_test_device(mock_device)end + mock_device:set_field("_configuration_version", 1, {persist = true}) + test.mock_device.add_test_device(mock_device) +end test.set_test_init_function(test_init) +local function build_config_response_msg(device, cluster, global_status, attribute, attr_status) + local addr_header = messages.AddressHeader( + device:get_short_address(), + device.fingerprinted_endpoint_id, + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + cluster + ) + local config_response_body + if global_status ~= nil then + config_response_body = config_reporting_response.ConfigureReportingResponse({}, global_status) + else + local individual_record = config_reporting_response.ConfigureReportingResponseRecord(attr_status, 0x01, attribute) + config_response_body = config_reporting_response.ConfigureReportingResponse({individual_record}, nil) + end + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(config_response_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = config_response_body + }) + return messages.ZigbeeMessageRx({ + address_header = addr_header, + body = message_body + }) +end + +test.register_coroutine_test( + "configuration version below 1", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, Status.SUCCESS)}) + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, SimpleMetering.ID, Status.SUCCESS)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == 1) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) + +test.register_coroutine_test( + "configuration version below 1 config response not success", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, Status.UNSUPPORTED_ATTRIBUTE)}) + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, SimpleMetering.ID, Status.UNSUPPORTED_ATTRIBUTE)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == nil) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) + +test.register_coroutine_test( + "configuration version below 1 individual config response records ElectricalMeasurement", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, nil, ElectricalMeasurement.attributes.ActivePower.ID, Status.SUCCESS)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == 1) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) + +test.register_coroutine_test( + "configuration version below 1 individual config response records SimpleMetering", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, SimpleMetering.ID, nil, SimpleMetering.attributes.InstantaneousDemand.ID, Status.SUCCESS)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == 1) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) + test.register_message_test( "ActivePower Report should be handled. Sensor value is in W, capability attribute value is in hectowatts", { @@ -99,7 +238,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 1, 3600, 5) + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -113,7 +252,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 1, 3600, 5) + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_1p.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_1p.lua new file mode 100644 index 0000000000..a148df5b1e --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_1p.lua @@ -0,0 +1,321 @@ +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local ElectricalMeasurement = clusters.ElectricalMeasurement +local SimpleMetering = clusters.SimpleMetering +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" +local constants = require "st.zigbee.constants" + + +-- Mock out globals +local mock_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("power-meter-1p.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "BITUO TECHNIK", + model = "SPM01X", + server_clusters = {SimpleMetering.ID, ElectricalMeasurement.ID} + } + } +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_device) + zigbee_test_utils.init_noop_health_check_timer() +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "SimpleMetering event should be handled by powerConsumptionReport capability", + function() + test.timer.__create_and_queue_test_time_advance_timer(15*60, "oneshot") + -- #1 : 15 minutes have passed + test.mock_time.advance_time(15*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,150) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 1500.0, deltaEnergy = 0.0 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.5, unit = "kWh"})) + ) + -- #2 : Not even 15 minutes passed + test.wait_for_events() + test.mock_time.advance_time(1*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,170) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.7, unit = "kWh"})) + ) + -- #3 : 15 minutes have passed + test.wait_for_events() + test.mock_time.advance_time(14*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,200) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 2000.0, deltaEnergy = 500.0 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 2.0, unit = "kWh"})) + ) + end +) + +test.register_message_test( + "ActivePower Report should be handled. Sensor value is in W, capability attribute value is in hectowatts", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_device, + 27) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + } + } +) + +test.register_message_test( + "ActivePower Report should be handled. Sensor value is in W, capability attribute value is in hectowatts", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_device, + 27) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + } + } +) + +test.register_message_test( + "RMSCurrent Report for PhaseA should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSCurrent:build_test_attr_report(mock_device, + 34) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.currentMeasurement.current({ value = 0.34, unit = "A" })) + } + } +) + +test.register_message_test( + "RMSVoltage Report for PhaseA should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSVoltage:build_test_attr_report(mock_device, + 22000) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.voltageMeasurement.voltage({ value = 220.0, unit = "V" })) + } + } +) + +test.register_coroutine_test( + "Device configure lifecycle event should configure device properly", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, SimpleMetering.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltage:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.configure_reporting(mock_device, data_types.ClusterId(SimpleMetering.ID), data_types.AttributeId(0x0001), data_types.ZigbeeDataType(SimpleMetering.attributes.CurrentSummationDelivered.base_type.ID), 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_attribute(mock_device, data_types.ClusterId(SimpleMetering.ID), data_types.AttributeId(0x0001)) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "resetEnergyMeter command should send OnOff On to reset device", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "energyMeter", component = "main", command = "resetEnergyMeter", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, clusters.OnOff.server.commands.On(mock_device) } + } + } +) + +test.register_coroutine_test( + "refresh capability command should read device attributes", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_attribute(mock_device, data_types.ClusterId(SimpleMetering.ID), data_types.AttributeId(0x0001)) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + }) + end +) + +test.register_coroutine_test( + "energy handler resets offset when reading is below stored offset", + function() + -- Set an offset larger than the incoming value (100 raw / 100 = 1.0 kWh, offset = 5.0) + mock_device:set_field(constants.ENERGY_METER_OFFSET, 5.0, {persist = true}) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 100) + }) + -- Offset resets to 0; raw_value_kilowatts = 1.0 - 0 = 1.0; no powerConsumptionReport (delta_tick < 15 min) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.0, unit = "kWh"})) + ) + end +) + +test.register_coroutine_test( + "energy handler resets save tick when timer has slipped beyond 30 minutes", + function() + -- Advance time > 30 min so that curr_save_tick + 15*60 < os.time() is true + test.timer.__create_and_queue_test_time_advance_timer(40*60, "oneshot") + test.mock_time.advance_time(40*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 100) + }) + -- raw_value = 100, divisor = 100, kWh = 1.0, watts = 1000.0; first report: deltaEnergy = 0.0 + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 1000.0, deltaEnergy = 0.0})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.0, unit = "kWh"})) + ) + end +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_2p.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_2p.lua new file mode 100644 index 0000000000..b85c955005 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_2p.lua @@ -0,0 +1,281 @@ +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local ElectricalMeasurement = clusters.ElectricalMeasurement +local SimpleMetering = clusters.SimpleMetering +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" + +-- 使用两相电能表配置文件 +local mock_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("power-meter-2p.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "BITUO TECHNIK", + model = "SDM02X", + server_clusters = {SimpleMetering.ID, ElectricalMeasurement.ID} + } + } +}) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) + zigbee_test_utils.init_noop_health_check_timer() +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "SimpleMetering event should be handled by powerConsumptionReport capability", + function() + test.timer.__create_and_queue_test_time_advance_timer(15*60, "oneshot") + -- #1 : 15 minutes have passed + test.mock_time.advance_time(15*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,150) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 1500.0, deltaEnergy = 0.0 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.5, unit = "kWh"})) + ) + -- #2 : Not even 15 minutes passed + test.wait_for_events() + test.mock_time.advance_time(1*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,170) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.7, unit = "kWh"})) + ) + -- #3 : 15 minutes have passed + test.wait_for_events() + test.mock_time.advance_time(14*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,200) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 2000.0, deltaEnergy = 500.0 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 2.0, unit = "kWh"})) + ) + end +) + +test.register_message_test( + "ActivePower Report should be handled. Sensor value is in W, capability attribute value is in hectowatts", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_device, + 27) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + } + } +) + +test.register_message_test( + "RMSCurrent Report for PhaseA should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSCurrent:build_test_attr_report(mock_device, + 34) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.currentMeasurement.current({ value = 0.34, unit = "A" })) + } + } +) + +test.register_message_test( + "RMSVoltage Report for PhaseB should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSVoltage:build_test_attr_report(mock_device, + 22000) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.voltageMeasurement.voltage({ value = 220.0, unit = "V" })) + } + } +) + +test.register_message_test( + "ActivePower Report should be handled. Sensor value is in W, capability attribute value is in hectowatts", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ActivePowerPhB:build_test_attr_report(mock_device, + 27) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseB", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + } + } +) + +test.register_message_test( + "RMSCurrent Report for PhaseB should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSCurrentPhB:build_test_attr_report(mock_device, + 34) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseB", capabilities.currentMeasurement.current({ value = 0.34, unit = "A" })) + } + } +) + +test.register_message_test( + "RMSVoltage Report for PhaseB should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSVoltagePhB:build_test_attr_report(mock_device, + 22000) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseB", capabilities.voltageMeasurement.voltage({ value = 220.0, unit = "V" })) + } + } +) + +test.register_coroutine_test( + "Device configure lifecycle event should configure device properly", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, SimpleMetering.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltage:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhB:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhB:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhB:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.configure_reporting(mock_device, data_types.ClusterId(SimpleMetering.ID), data_types.AttributeId(0x0001), data_types.ZigbeeDataType(SimpleMetering.attributes.CurrentSummationDelivered.base_type.ID), 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_attribute(mock_device, data_types.ClusterId(SimpleMetering.ID), data_types.AttributeId(0x0001)) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhB:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhB:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhB:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:read(mock_device) + }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_3p.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_3p.lua new file mode 100644 index 0000000000..fad26aad40 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_3p.lua @@ -0,0 +1,355 @@ +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local ElectricalMeasurement = clusters.ElectricalMeasurement +local SimpleMetering = clusters.SimpleMetering +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" + +local mock_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("power-meter-3p.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "BITUO TECHNIK", + model = "SDM01W", + server_clusters = {SimpleMetering.ID, ElectricalMeasurement.ID} + } + } +}) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) + zigbee_test_utils.init_noop_health_check_timer() +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "SimpleMetering event should be handled by powerConsumptionReport capability", + function() + test.timer.__create_and_queue_test_time_advance_timer(15*60, "oneshot") + -- #1 : 15 minutes have passed + test.mock_time.advance_time(15*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,150) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 1500.0, deltaEnergy = 0.0 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.5, unit = "kWh"})) + ) + -- #2 : Not even 15 minutes passed + test.wait_for_events() + test.mock_time.advance_time(1*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,170) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.7, unit = "kWh"})) + ) + -- #3 : 15 minutes have passed + test.wait_for_events() + test.mock_time.advance_time(14*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,200) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 2000.0, deltaEnergy = 500.0 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 2.0, unit = "kWh"})) + ) + end +) + +test.register_message_test( + "ActivePower Report should be handled. Sensor value is in W, capability attribute value is in hectowatts", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_device, + 27) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + } + } +) + +test.register_message_test( + "RMSCurrent Report for PhaseA should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSCurrent:build_test_attr_report(mock_device, + 34) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.currentMeasurement.current({ value = 0.34, unit = "A" })) + } + } +) + +test.register_message_test( + "RMSVoltage Report for PhaseA should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSVoltage:build_test_attr_report(mock_device, + 22000) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.voltageMeasurement.voltage({ value = 220.0, unit = "V" })) + } + } +) + +test.register_message_test( + "ActivePower Report for PhaseB should be handled. Sensor value is in W, capability attribute value is in hectowatts", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ActivePowerPhB:build_test_attr_report(mock_device, + 27) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseB", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + } + } +) + +test.register_message_test( + "RMSCurrent Report for PhaseB should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSCurrentPhB:build_test_attr_report(mock_device, + 34) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseB", capabilities.currentMeasurement.current({ value = 0.34, unit = "A" })) + } + } +) + +test.register_message_test( + "RMSVoltage Report for PhaseB should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSVoltagePhB:build_test_attr_report(mock_device, + 22000) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseB", capabilities.voltageMeasurement.voltage({ value = 220.0, unit = "V" })) + } + } +) + +test.register_message_test( + "ActivePower Report for PhaseC should be handled. Sensor value is in W, capability attribute value is in hectowatts", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ActivePowerPhC:build_test_attr_report(mock_device, + 27) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseC", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + } + } +) + +test.register_message_test( + "RMSCurrent Report for PhaseC should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSCurrentPhC:build_test_attr_report(mock_device, + 34) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseC", capabilities.currentMeasurement.current({ value = 0.34, unit = "A" })) + } + } +) + +test.register_message_test( + "RMSVoltage Report for PhaseC should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSVoltagePhC:build_test_attr_report(mock_device, + 22000) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseC", capabilities.voltageMeasurement.voltage({ value = 220.0, unit = "V" })) + } + } +) + +test.register_coroutine_test( + "Device configure lifecycle event should configure device properly", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, SimpleMetering.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltage:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhB:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhB:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhB:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhC:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhC:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhC:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.configure_reporting(mock_device, data_types.ClusterId(SimpleMetering.ID), data_types.AttributeId(0x0001), data_types.ZigbeeDataType(SimpleMetering.attributes.CurrentSummationDelivered.base_type.ID), 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_attribute(mock_device, data_types.ClusterId(SimpleMetering.ID), data_types.AttributeId(0x0001)) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhB:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhB:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhB:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhC:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhC:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhC:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:read(mock_device) + }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua deleted file mode 100644 index dcbb1b0b00..0000000000 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua +++ /dev/null @@ -1,162 +0,0 @@ --- Copyright 2022 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. - --- Mock out globals -local test = require "integration_test" -local clusters = require "st.zigbee.zcl.clusters" -local ElectricalMeasurement = clusters.ElectricalMeasurement -local SimpleMetering = clusters.SimpleMetering -local capabilities = require "st.capabilities" -local zigbee_test_utils = require "integration_test.zigbee_test_utils" -local t_utils = require "integration_test.utils" - -local mock_device = test.mock_device.build_test_zigbee_device( - { - profile = t_utils.get_profile_definition("power-meter-consumption-report.yml"), - zigbee_endpoints = { - [1] = { - id = 1, - model = "E240-KR080Z0-HA", - server_clusters = {SimpleMetering.ID, ElectricalMeasurement.ID} - } - } - } -) - -zigbee_test_utils.prepare_zigbee_env_info() -local function test_init() - test.mock_device.add_test_device(mock_device)end - -test.set_test_init_function(test_init) - -test.register_message_test( - "ActivePower Report should be handled. Sensor value is in W, capability attribute value is in hectowatts", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, ElectricalMeasurement.attributes.ACPowerDivisor:build_test_attr_report(mock_device, 0x0A) } - }, - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_device, - 27) }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 2.7, unit = "W" })) - } - } -) - -test.register_message_test( - "SimpleMetering event should be handled by powerConsumptionReport capability", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 1000) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 1.0, deltaEnergy = 0.0 })) - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 0.001, unit = "kWh"})) - }, - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 1500) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 1.5, deltaEnergy = 0.5 })) - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 0.0015, unit = "kWh"})) - } - } -) - -test.register_coroutine_test( - "lifecycle configure event should configure device", - function () - test.socket.zigbee:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMetering.attributes.InstantaneousDemand:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurement.attributes.ActivePower:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - SimpleMetering.ID) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 1, 3600, 500) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - ElectricalMeasurement.ID) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 1, 3600, 5) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) - }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report_sihas.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report_sihas.lua index 536863b409..2949961fb1 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report_sihas.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report_sihas.lua @@ -20,6 +20,14 @@ local SimpleMetering = clusters.SimpleMetering local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" +local messages = require "st.zigbee.messages" +local config_reporting_response = require "st.zigbee.zcl.global_commands.configure_reporting_response" +local zb_const = require "st.zigbee.constants" +local zcl_messages = require "st.zigbee.zcl" +local data_types = require "st.zigbee.data_types" +local Status = require "st.zigbee.generated.types.ZclStatus" +local constants = require "st.zigbee.constants" + local mock_device = test.mock_device.build_test_zigbee_device( { @@ -36,7 +44,38 @@ local mock_device = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() - test.mock_device.add_test_device(mock_device)end + mock_device:set_field("_configuration_version", 1, {persist = true}) + test.mock_device.add_test_device(mock_device) +end + +local function build_config_response_msg(device, cluster, global_status, attribute, attr_status) + local addr_header = messages.AddressHeader( + device:get_short_address(), + device.fingerprinted_endpoint_id, + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + cluster + ) + local config_response_body + if global_status ~= nil then + config_response_body = config_reporting_response.ConfigureReportingResponse({}, global_status) + else + local individual_record = config_reporting_response.ConfigureReportingResponseRecord(attr_status, 0x01, attribute) + config_response_body = config_reporting_response.ConfigureReportingResponse({individual_record}, nil) + end + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(config_response_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = config_response_body + }) + return messages.ZigbeeMessageRx({ + address_header = addr_header, + body = message_body + }) +end test.set_test_init_function(test_init) @@ -137,11 +176,11 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 300, 1) + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, - SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 300, 1) + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 450, 1) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -151,7 +190,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 0, 65535, 1) + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 65535, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -165,4 +204,66 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "configuration version below 1 use override configs", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 65535, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 450, 1)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, Status.SUCCESS)}) + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, SimpleMetering.ID, Status.SUCCESS)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == 1) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) +test.register_coroutine_test( + "energy handler resets shinasystems offset when reading is below stored offset", + function() + -- divisor=1000; raw_value=100 -> 0.1 kWh; offset=0.5 -> 0.1 < 0.5 triggers reset + mock_device:set_field(constants.ENERGY_METER_OFFSET, 0.5, {persist = true}) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 100) + }) + -- offset resets to 0; raw_value_kilowatts = 0.1; no powerConsumptionReport (delta_tick < 15 min) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 0.1, unit = "kWh"})) + ) + end +) + +test.register_coroutine_test( + "shinasystems energy handler resets save tick when timer has slipped beyond 30 minutes", + function() + -- Advance time > 30 min so that curr_save_tick + 15*60 < os.time() is true + test.timer.__create_and_queue_test_time_advance_timer(40*60, "oneshot") + test.mock_time.advance_time(40*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 1500) + }) + -- raw_value=1500, divisor=1000, kWh=1.5, watts=1500.0; first report: deltaEnergy=0.0 + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 1500.0, deltaEnergy = 0.0})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.5, unit = "kWh"})) + ) + end +) + + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_ezex.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_ezex.lua new file mode 100644 index 0000000000..7f582d55b7 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_ezex.lua @@ -0,0 +1,191 @@ +-- Copyright 2022 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. + +-- Mock out globals +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local ElectricalMeasurement = clusters.ElectricalMeasurement +local SimpleMetering = clusters.SimpleMetering +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local messages = require "st.zigbee.messages" +local config_reporting_response = require "st.zigbee.zcl.global_commands.configure_reporting_response" +local zb_const = require "st.zigbee.constants" +local zcl_messages = require "st.zigbee.zcl" +local data_types = require "st.zigbee.data_types" +local Status = require "st.zigbee.generated.types.ZclStatus" + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("power-meter-consumption-report.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + model = "E240-KR080Z0-HA", + server_clusters = {SimpleMetering.ID, ElectricalMeasurement.ID} + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + mock_device:set_field("_configuration_version", 1, {persist = true}) + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +local function build_config_response_msg(device, cluster, global_status, attribute, attr_status) + local addr_header = messages.AddressHeader( + device:get_short_address(), + device.fingerprinted_endpoint_id, + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + cluster + ) + local config_response_body + if global_status ~= nil then + config_response_body = config_reporting_response.ConfigureReportingResponse({}, global_status) + else + local individual_record = config_reporting_response.ConfigureReportingResponseRecord(attr_status, 0x01, attribute) + config_response_body = config_reporting_response.ConfigureReportingResponse({individual_record}, nil) + end + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(config_response_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = config_response_body + }) + return messages.ZigbeeMessageRx({ + address_header = addr_header, + body = message_body + }) +end + +test.register_message_test( + "ActivePower Report should not generate an event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ACPowerDivisor:build_test_attr_report(mock_device, 0x0A) } + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_device, + 27) }, + } + } +) + +test.register_message_test( + "SimpleMetering event should be handled by powerConsumptionReport capability", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 1000) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 1.0, deltaEnergy = 0.0 })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 0.001, unit = "kWh"})) + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 1500) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 1.5, deltaEnergy = 0.5 })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 0.0015, unit = "kWh"})) + } + } +) + +test.register_coroutine_test( + "lifecycle configure event should configure device", + function () + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + SimpleMetering.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 500) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1) + }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_coroutine_test( + "configuration version below 1 use override configs", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 500)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, Status.SUCCESS)}) + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, SimpleMetering.ID, Status.SUCCESS)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == 1) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) + + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_frient.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_frient.lua new file mode 100644 index 0000000000..81caddae56 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_frient.lua @@ -0,0 +1,102 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local ElectricalMeasurement = clusters.ElectricalMeasurement +local SimpleMetering = clusters.SimpleMetering +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local constants = require "st.zigbee.constants" + +local mock_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("power-meter.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "Develco Products A/S", + model = "EMIZB-132", + server_clusters = {SimpleMetering.ID, ElectricalMeasurement.ID} + } + } +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + mock_device:set_field("_configuration_version", 1, {persist = true}) + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "frient device_init sets divisor fields", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + assert(mock_device:get_field(constants.SIMPLE_METERING_DIVISOR_KEY) == 1000, + "SIMPLE_METERING_DIVISOR_KEY should be 1000") + assert(mock_device:get_field(constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY) == 10000, + "ELECTRICAL_MEASUREMENT_DIVISOR_KEY should be 10000") + end +) + +test.register_coroutine_test( + "frient lifecycle configure event should configure device", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, SimpleMetering.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) + }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-presence-sensor/src/aqara/can_handle.lua new file mode 100644 index 0000000000..33a0bde838 --- /dev/null +++ b/drivers/SmartThings/zigbee-presence-sensor/src/aqara/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_aqara_products = function(opts, driver, device, ...) + local FINGERPRINTS = { mfr = "aqara", model = "lumi.motion.ac01" } + + if device:get_manufacturer() == FINGERPRINTS.mfr and device:get_model() == FINGERPRINTS.model then + return true, require("aqara") + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/aqara/init.lua b/drivers/SmartThings/zigbee-presence-sensor/src/aqara/init.lua index b9a0bfb001..1fd79e3875 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/aqara/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" @@ -19,12 +22,6 @@ local SENSITIVITY = "stse.sensitivity" local RESET_PRESENCE = "stse.resetPresence" local APP_DISTANCE = "stse.approachDistance" -local FINGERPRINTS = { mfr = "aqara", model = "lumi.motion.ac01" } - -local is_aqara_products = function(opts, driver, device, ...) - return device:get_manufacturer() == FINGERPRINTS.mfr and device:get_model() == FINGERPRINTS.model -end - local function device_init(driver, device) -- no action end @@ -104,7 +101,7 @@ local aqara_fp1_handler = { doConfigure = do_configure, infoChanged = device_info_changed }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } return aqara_fp1_handler diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/can_handle.lua b/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/can_handle.lua new file mode 100644 index 0000000000..d848d46caa --- /dev/null +++ b/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function arrival_sensor_v1_can_handle(opts, driver, device, ...) + -- excluding Aqara device and tagv4 + if device:get_manufacturer() ~= "aqara" and device:get_model() ~= "tagv4" then + return true, require("arrival-sensor-v1") + end + return false +end + +return arrival_sensor_v1_can_handle diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/init.lua b/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/init.lua index dd0566a3a0..81d9461a38 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/init.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Zigbee Spec Utils local zcl_messages = require "st.zigbee.zcl" @@ -41,10 +31,6 @@ local presence_utils = require "presence_utils" local CHECKIN_INTERVAL = 20 -- seconds -local function arrival_sensor_v1_can_handle(opts, driver, device, ...) - -- excluding Aqara device and tagv4 - return device:get_manufacturer() ~= "aqara" and device:get_model() ~= "tagv4" -end local function legacy_battery_handler(self, device, zb_rx) local battery_value = string.byte(zb_rx.body.zcl_body.body_bytes) @@ -100,8 +86,14 @@ local function beep_handler(self, device, command) end end +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + local function added_handler(self, device) - device:emit_event(PresenceSensor.presence("present")) + emit_event_if_latest_state_missing(device, "main", PresenceSensor, PresenceSensor.presence.NAME, PresenceSensor.presence("present")) end local function init_handler(self, device, event, args) @@ -138,7 +130,7 @@ local arrival_sensor_v1 = { added = added_handler, init = init_handler }, - can_handle = arrival_sensor_v1_can_handle + can_handle = require("arrival-sensor-v1.can_handle"), } return arrival_sensor_v1 diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/init.lua b/drivers/SmartThings/zigbee-presence-sensor/src/init.lua index 1ab62e821d..261bc90922 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" @@ -111,7 +101,6 @@ end local function init_handler(self, device, event, args) device:set_field(battery_defaults.DEVICE_VOLTAGE_TABLE_KEY, battery_table) device:add_configured_attribute(battery_voltage_attr_configuration) - device:add_monitored_attribute(battery_voltage_attr_configuration) device:remove_monitored_attribute(PowerConfiguration.ID, PowerConfiguration.attributes.BatteryPercentageRemaining.ID) device:remove_configured_attribute(PowerConfiguration.ID, PowerConfiguration.attributes.BatteryPercentageRemaining.ID) @@ -139,8 +128,14 @@ local function beep_handler(self, device, command) device:send(IdentifyCluster.server.commands.Identify(device, BEEP_IDENTIFY_TIME)) end +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + local function added_handler(self, device) - device:emit_event(PresenceSensor.presence("present")) + emit_event_if_latest_state_missing(device, "main", PresenceSensor, PresenceSensor.presence.NAME, PresenceSensor.presence("present")) device:set_field(IS_PRESENCE_BASED_ON_BATTERY_REPORTS, false, {persist = true}) device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) end @@ -199,10 +194,7 @@ local zigbee_presence_driver = { }, -- Custom handler for every Zigbee message zigbee_message_handler = all_zigbee_message_handler, - sub_drivers = { - require("aqara"), - require("arrival-sensor-v1") - }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-presence-sensor/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-presence-sensor/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/presence_utils.lua b/drivers/SmartThings/zigbee-presence-sensor/src/presence_utils.lua index a68de0f95e..968d75ea54 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/presence_utils.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/presence_utils.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local presence_utils = {} diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/sub_drivers.lua b/drivers/SmartThings/zigbee-presence-sensor/src/sub_drivers.lua new file mode 100644 index 0000000000..1340826663 --- /dev/null +++ b/drivers/SmartThings/zigbee-presence-sensor/src/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("aqara"), + lazy_load_if_possible("arrival-sensor-v1"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/test/test_st_arrival_sensor_v1.lua b/drivers/SmartThings/zigbee-presence-sensor/src/test/test_st_arrival_sensor_v1.lua index 0ddffcedab..123edc9de1 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/test/test_st_arrival_sensor_v1.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/test/test_st_arrival_sensor_v1.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -68,6 +57,7 @@ end zigbee_test_utils.prepare_zigbee_env_info() local add_device = function() + -- The initial presenceSensor event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added"}) test.socket.capability:__expect_send(mock_simple_device:generate_test_message("main", capabilities.presenceSensor.presence("present") @@ -75,6 +65,12 @@ local add_device = function() test.wait_for_events() end +local add_device_after_switch_over = function() + -- Avoid sending the initial presenceSensor event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added"}) + test.wait_for_events() +end + local function test_init() test.mock_device.add_test_device(mock_simple_device)end @@ -126,6 +122,7 @@ test.register_coroutine_test( "Added lifecycle should be handlded", function () add_device() + add_device_after_switch_over() end ) diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/test/test_zigbee_presence_sensor.lua b/drivers/SmartThings/zigbee-presence-sensor/src/test/test_zigbee_presence_sensor.lua index 30fec61f6d..ca98e8a1a7 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/test/test_zigbee_presence_sensor.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/test/test_zigbee_presence_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -21,6 +10,7 @@ local PowerConfiguration = clusters.PowerConfiguration local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" +local presence_utils = require "presence_utils" -- Needed for building ConfigureReportingResponse msg local messages = require "st.zigbee.messages" @@ -109,6 +99,7 @@ test.register_message_test( ) local add_device = function() + -- The initial presenceSensor event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added"}) test.socket.capability:__expect_send(mock_simple_device:generate_test_message("main", capabilities.presenceSensor.presence("present") @@ -120,6 +111,16 @@ local add_device = function() test.wait_for_events() end +local add_device_after_switch_over = function() + -- Avoid sending the initial presenceSensor event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added"}) + test.socket.zigbee:__expect_send({ + mock_simple_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_simple_device) + }) + test.wait_for_events() +end + test.register_coroutine_test( "Battery Voltage test cases when polling from hub", function() @@ -185,6 +186,7 @@ test.register_coroutine_test( "Added lifecycle should be handlded", function () add_device() + add_device_after_switch_over() end ) @@ -293,4 +295,96 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "battery_config_response_handler cancels pre-existing recurring poll timer", + function() + -- Place a live timer in the field so the nil-check branch is taken. + local pre_timer = test.timer.__create_test_time_advance_timer(60, "interval") + mock_simple_device:set_field(presence_utils.RECURRING_POLL_TIMER, pre_timer) + test.socket.zigbee:__queue_receive({ + mock_simple_device.id, + build_config_response_msg(PowerConfiguration.ID, 0x00) + }) + -- poke() emits "present" for every inbound zigbee message + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.presenceSensor.presence("present")) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "info_changed with changed check_interval cancels existing recurring poll timer", + function() + local pre_timer = test.timer.__create_test_time_advance_timer(60, "interval") + mock_simple_device:set_field(presence_utils.RECURRING_POLL_TIMER, pre_timer) + test.socket.device_lifecycle():__queue_receive( + mock_simple_device:generate_info_changed({ preferences = { check_interval = 100 } }) + ) + test.wait_for_events() + end +) + +-- Build two additional mock devices (module-level) for checkInterval type variants. +-- The profile default sets checkInterval = 120 (number); we override after building. +local mock_device_str_interval = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("smartthings-arrival-sensor.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "SmartThings", + model = "tagv4", + server_clusters = {0x0000, 0x0001, 0x0003} + } + } + } +) +mock_device_str_interval.preferences.checkInterval = "120" -- string → triggers elseif branch + +local mock_device_nil_interval = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("smartthings-arrival-sensor.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "SmartThings", + model = "tagv4", + server_clusters = {0x0000, 0x0001, 0x0003} + } + } + } +) +mock_device_nil_interval.preferences.checkInterval = nil -- nil → triggers default-return branch + +test.register_coroutine_test( + "init with string checkInterval uses parsed value for presence timeout", + function() + test.mock_device.add_test_device(mock_device_str_interval) + test.timer.__create_and_queue_test_time_advance_timer(120, "oneshot") + test.socket.device_lifecycle:__queue_receive({ mock_device_str_interval.id, "init" }) + test.wait_for_events() + test.mock_time.advance_time(121) + test.socket.capability:__expect_send( + mock_device_str_interval:generate_test_message("main", capabilities.presenceSensor.presence("not present")) + ) + end, + { test_init = function() end } +) + +test.register_coroutine_test( + "init with nil checkInterval uses default presence timeout", + function() + test.mock_device.add_test_device(mock_device_nil_interval) + test.timer.__create_and_queue_test_time_advance_timer(120, "oneshot") + test.socket.device_lifecycle:__queue_receive({ mock_device_nil_interval.id, "init" }) + test.wait_for_events() + test.mock_time.advance_time(121) + test.socket.capability:__expect_send( + mock_device_nil_interval:generate_test_message("main", capabilities.presenceSensor.presence("not present")) + ) + end, + { test_init = function() end } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-range-extender/fingerprints.yml b/drivers/SmartThings/zigbee-range-extender/fingerprints.yml index e889fc1839..d8d4bcd5cf 100644 --- a/drivers/SmartThings/zigbee-range-extender/fingerprints.yml +++ b/drivers/SmartThings/zigbee-range-extender/fingerprints.yml @@ -29,3 +29,8 @@ zigbeeManufacturer: manufacturer: Insta GmbH model: NEXENTRO Pushbutton Interface deviceProfileName: range-extender + - id: "frientA/S/111" + deviceLabel: frient Zigbee Range Extender + manufacturer: frient A/S + model: REXZB-111 + deviceProfileName: range-extender-battery-source \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-range-extender/profiles/range-extender-battery-source.yml b/drivers/SmartThings/zigbee-range-extender/profiles/range-extender-battery-source.yml new file mode 100644 index 0000000000..f8958a3901 --- /dev/null +++ b/drivers/SmartThings/zigbee-range-extender/profiles/range-extender-battery-source.yml @@ -0,0 +1,20 @@ +name: range-extender-battery-source +components: + - id: main + capabilities: + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + - id: battery + version: 1 + - id: powerSource + version: 1 + config: + values: + - key: "powerSource.value" + enabledValues: + - battery + - mains + categories: + - name: Networking diff --git a/drivers/SmartThings/zigbee-range-extender/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-range-extender/src/frient/can_handle.lua new file mode 100644 index 0000000000..9967c0edf8 --- /dev/null +++ b/drivers/SmartThings/zigbee-range-extender/src/frient/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function frient_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "frient A/S" and (device:get_model() == "REXZB-111") then + return true, require("frient") + end + return false +end + +return frient_can_handle diff --git a/drivers/SmartThings/zigbee-range-extender/src/frient/init.lua b/drivers/SmartThings/zigbee-range-extender/src/frient/init.lua new file mode 100644 index 0000000000..fef5d8470a --- /dev/null +++ b/drivers/SmartThings/zigbee-range-extender/src/frient/init.lua @@ -0,0 +1,66 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local battery_defaults = require "st.zigbee.defaults.battery_defaults" + +local IASZone = clusters.IASZone +local PowerConfiguration = clusters.PowerConfiguration + +local function generate_event_from_zone_status(driver, device, zone_status, zigbee_message) + device:emit_event_for_endpoint( + zigbee_message.address_header.src_endpoint.value, + zone_status:is_ac_mains_fault_set() and capabilities.powerSource.powerSource.battery() or capabilities.powerSource.powerSource.mains() + ) +end + +local function ias_zone_status_attr_handler(driver, device, zone_status, zb_rx) + generate_event_from_zone_status(driver, device, zone_status, zb_rx) +end + +local function ias_zone_status_change_handler(driver, device, zb_rx) + generate_event_from_zone_status(driver, device, zb_rx.body.zcl_body.zone_status, zb_rx) +end + +local function device_added(driver, device) + device:emit_event(capabilities.powerSource.powerSource.mains()) +end + +local function device_init(driver, device) + battery_defaults.build_linear_voltage_init(3.3, 4.1)(driver, device) +end + +local function do_refresh(driver, device) + device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) + device:send(IASZone.attributes.ZoneStatus:read(device)) +end + +local frient_range_extender = { + NAME = "frient Range Extender", + lifecycle_handlers = { + added = device_added, + init = device_init + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh, + } + }, + zigbee_handlers = { + attr = { + [IASZone.ID] = { + [IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler + } + }, + cluster = { + [IASZone.ID] = { + [IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler + } + } + }, + can_handle = require("frient.can_handle"), +} + +return frient_range_extender diff --git a/drivers/SmartThings/zigbee-range-extender/src/init.lua b/drivers/SmartThings/zigbee-range-extender/src/init.lua index a8c4a2796a..565aa74a28 100644 --- a/drivers/SmartThings/zigbee-range-extender/src/init.lua +++ b/drivers/SmartThings/zigbee-range-extender/src/init.lua @@ -1,19 +1,9 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -local capabilities = require "st.capabilities" +local capabilities = require "st.capabilities" +local defaults = require "st.zigbee.defaults" local Basic = (require "st.zigbee.zcl.clusters").Basic local ZigbeeDriver = require "st.zigbee" @@ -23,7 +13,8 @@ end local zigbee_range_driver_template = { supported_capabilities = { - capabilities.refresh + capabilities.refresh, + capabilities.battery }, capability_handlers = { [capabilities.refresh.ID] = { @@ -31,8 +22,11 @@ local zigbee_range_driver_template = { } }, health_check = false, + sub_drivers = require("sub_drivers"), } +defaults.register_for_default_handlers(zigbee_range_driver_template, zigbee_range_driver_template.supported_capabilities) + local zigbee_range_extender_driver = ZigbeeDriver("zigbee-range-extender", zigbee_range_driver_template) function zigbee_range_extender_driver:device_health_check() @@ -42,6 +36,7 @@ function zigbee_range_extender_driver:device_health_check() device:send(Basic.attributes.ZCLVersion:read(device)) end end + zigbee_range_extender_driver.device_health_timer = zigbee_range_extender_driver.call_on_schedule(zigbee_range_extender_driver, 300, zigbee_range_extender_driver.device_health_check) zigbee_range_extender_driver:run() diff --git a/drivers/SmartThings/zigbee-range-extender/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-range-extender/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-range-extender/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-range-extender/src/sub_drivers.lua b/drivers/SmartThings/zigbee-range-extender/src/sub_drivers.lua new file mode 100644 index 0000000000..2f30e461ee --- /dev/null +++ b/drivers/SmartThings/zigbee-range-extender/src/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("frient"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-range-extender/src/test/test_frient_zigbee_range_extender.lua b/drivers/SmartThings/zigbee-range-extender/src/test/test_frient_zigbee_range_extender.lua new file mode 100644 index 0000000000..bdd79dd59f --- /dev/null +++ b/drivers/SmartThings/zigbee-range-extender/src/test/test_frient_zigbee_range_extender.lua @@ -0,0 +1,209 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Mock out globals +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" + +local IASZone = clusters.IASZone +local PowerConfiguration = clusters.PowerConfiguration +local ZoneStatusAttribute = IASZone.attributes.ZoneStatus + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("range-extender-battery-source.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "REXZB-111", + server_clusters = {IASZone.ID, PowerConfiguration.ID } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Refresh necessary attributes", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} } }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + } + ) + end +) + +test.register_coroutine_test( + "lifecycles - init and doConfigure test", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read( mock_device ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:read( mock_device ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device, + 30, + 21600, + 1 + ) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + end +) + +test.register_message_test( + "Power source / mains should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0001) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.mains()) + } + } +) + +test.register_message_test( + "Power source / battery should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0081) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.battery()) + } + } +) + +test.register_message_test( + "Min battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 33) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +test.register_message_test( + "Medium battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 37) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(50)) + } + } +) + +test.register_message_test( + "Max battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 41) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + +test.register_message_test( + "ZoneStatusChangeNotification - mains should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0001, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.mains()) + } + } +) + +test.register_message_test( + "Device added lifecycle should emit mains powerSource", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_device.id, "added" } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.mains()) + } + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-range-extender/src/test/test_zigbee_extend.lua b/drivers/SmartThings/zigbee-range-extender/src/test/test_zigbee_extend.lua index 39404005fa..c76e228cef 100644 --- a/drivers/SmartThings/zigbee-range-extender/src/test/test_zigbee_extend.lua +++ b/drivers/SmartThings/zigbee-range-extender/src/test/test_zigbee_extend.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-siren/fingerprints.yml b/drivers/SmartThings/zigbee-siren/fingerprints.yml index d40b7efb9a..57b192b294 100644 --- a/drivers/SmartThings/zigbee-siren/fingerprints.yml +++ b/drivers/SmartThings/zigbee-siren/fingerprints.yml @@ -4,16 +4,26 @@ zigbeeManufacturer : manufacturer: ClimaxTechnology model: SRAC_00.00.00.16TC deviceProfileName: switch-alarm-generic-siren-7 + - id: "frient/SIRZB-110" + deviceLabel: frient Smart Siren + manufacturer: frient A/S + model: SIRZB-110 + deviceProfileName: frient-siren-battery-source-tamper + - id: "frient/SIRZB-111" + deviceLabel: frient Smart Siren + manufacturer: frient A/S + model: SIRZB-111 + deviceProfileName: frient-siren-battery-source + - id: "frient/SIRZB-112" + deviceLabel: frient Smart Siren + manufacturer: frient A/S + model: SIRZB-112 + deviceProfileName: frient-siren-battery-source-tamper - id : Heiman/WarningDevice deviceLabel : HEIMAN Siren manufacturer : Heiman model : WarningDevice deviceProfileName : switch-alarm-generic-siren-7 - - id : frient/SIRZB-110 - deviceLabel : frient Siren - manufacturer : frient A/S - model : SIRZB-110 - deviceProfileName : switch-alarm-generic-siren-7 - id: "Sercomm Corp./SZ-SRN12N" deviceLabel: SmartThings Siren manufacturer: Sercomm Corp. diff --git a/drivers/SmartThings/zigbee-siren/profiles/frient-siren-battery-source-tamper.yml b/drivers/SmartThings/zigbee-siren/profiles/frient-siren-battery-source-tamper.yml new file mode 100644 index 0000000000..6c7e991e45 --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/profiles/frient-siren-battery-source-tamper.yml @@ -0,0 +1,53 @@ +name: frient-siren-battery-source-tamper +components: + - id: main + capabilities: + - id: alarm + version: 1 + - id: tone + version: 1 + - id: battery + version: 1 + - id: powerSource + version: 1 + - id: tamperAlert + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Siren + - id: SirenVolume + capabilities: + - id: mode + version: 1 + - id: SirenVoice + capabilities: + - id: mode + version: 1 + - id: SquawkVolume + capabilities: + - id: mode + version: 1 + - id: SquawkVoice + capabilities: + - id: mode + version: 1 + - id: WarningDuration + capabilities: + - id: mode + version: 1 +preferences: + - title: "Max alarm duration (s)" + name: maxWarningDuration + description: "After how many seconds should the alarm turn off" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 240 +metadata: + mnmn: SmartThings + vid: SmartThings-smartthings-frient_Siren_Tamper \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-siren/profiles/frient-siren-battery-source.yml b/drivers/SmartThings/zigbee-siren/profiles/frient-siren-battery-source.yml new file mode 100644 index 0000000000..5248db2b54 --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/profiles/frient-siren-battery-source.yml @@ -0,0 +1,51 @@ +name: frient-siren-battery-source +components: + - id: main + capabilities: + - id: alarm + version: 1 + - id: tone + version: 1 + - id: battery + version: 1 + - id: powerSource + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Siren + - id: SirenVolume + capabilities: + - id: mode + version: 1 + - id: SirenVoice + capabilities: + - id: mode + version: 1 + - id: SquawkVolume + capabilities: + - id: mode + version: 1 + - id: SquawkVoice + capabilities: + - id: mode + version: 1 + - id: WarningDuration + capabilities: + - id: mode + version: 1 +preferences: + - title: "Max alarm duration (s)" + name: maxWarningDuration + description: "After how many seconds should the alarm turn off" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 240 +metadata: + mnmn: SmartThings + vid: SmartThings-smartthings-frient_Siren \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-siren/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-siren/src/frient/can_handle.lua new file mode 100644 index 0000000000..ed6acf5b42 --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/src/frient/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function frient_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "frient A/S" and + (device:get_model() == "SIRZB-110" or + device:get_model() == "SIRZB-111" or + device:get_model() == "SIRZB-112") + then + return true, require("frient") + end + return false +end + +return frient_can_handle diff --git a/drivers/SmartThings/zigbee-siren/src/frient/init.lua b/drivers/SmartThings/zigbee-siren/src/frient/init.lua index 085b8ecd79..d408579696 100644 --- a/drivers/SmartThings/zigbee-siren/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-siren/src/frient/init.lua @@ -1,97 +1,416 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.zigbee.data_types" +local battery_defaults = require "st.zigbee.defaults.battery_defaults" --ZCL local zcl_clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local Basic = zcl_clusters.Basic local IASWD = zcl_clusters.IASWD -local SirenConfiguration = IASWD.types.SirenConfiguration +local IASZone = zcl_clusters.IASZone local IaswdLevel = IASWD.types.IaswdLevel +local SirenConfiguration = IASWD.types.SirenConfiguration +local SquawkConfiguration = IASWD.types.SquawkConfiguration +local SquawkMode = IASWD.types.SquawkMode +local WarningMode = IASWD.types.WarningMode +local PowerConfiguration = zcl_clusters.PowerConfiguration --capability local capabilities = require "st.capabilities" local alarm = capabilities.alarm -local switch = capabilities.switch local ALARM_COMMAND = "alarmCommand" -local ALARM_LAST_DURATION = "lastDuration" -local ALARM_MAX_DURATION = "maxDuration" +local ALARM_DURATION = "warningDuration" +local SIREN_FIXED_ENDIAN_SW_VERSION = "010903" -local ALARM_DEFAULT_MAX_DURATION = 0x00B4 -local ALARM_STROBE_DUTY_CYCLE = 00 +local DEFAULT_MAX_WARNING_DURATION = 0x00F0 +local PRIMARY_SW_VERSION = "primary_sw_version" +local DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR = 0x8000 +local DEVELCO_MANUFACTURER_CODE = 0x1015 +local IASZONE_ENDPOINT = 0x2B local alarm_command = { OFF = 0, SIREN = 1, - STROBE = 2, - BOTH = 3 } -local send_siren_command = function(device) - local max_duration = device:get_field(ALARM_MAX_DURATION) - local warning_duration = max_duration and max_duration or ALARM_DEFAULT_MAX_DURATION - local duty_cycle = ALARM_STROBE_DUTY_CYCLE +local IASZone_configuration = { + { + cluster = IASZone.ID, + attribute = IASZone.attributes.ZoneStatus.ID, + minimum_interval = 0, + maximum_interval = 6*60*60, + data_type = IASZone.attributes.ZoneStatus.base_type, + reportable_change = 1 + } +} + +local SQUAWK_VOICE_MAP = { + ["Armed"] = 0, + ["Disarmed"] = 1 +} + +local VOLUME_MAP = { + ["Low"] = 0, + ["Medium"] = 1, + ["High"] = 2, + ["Very High"] = 3 +} +local SIREN_VOICE_MAP = { + ["Burglar"] = 1, + ["Fire"] = 2, + ["Emergency"] = 3, + ["Panic"] = 4, + ["Panic Fire"] = 5, + ["Panic Emergency"] = 6 +} - device:set_field(ALARM_LAST_DURATION, warning_duration, {persist = true}) +local WARNING_DURATION_MAP = { + ["5 seconds"] = 5, + ["10 seconds"] = 10, + ["15 seconds"] = 15, + ["20 seconds"] = 20, + ["25 seconds"] = 25, + ["30 seconds"] = 30, + ["40 seconds"] = 40, + ["50 seconds"] = 50, + ["1 minute"] = 60, + ["2 minutes"] = 120, + ["3 minutes"] = 180, + ["4 minutes"] = 240, + ["5 minutes"] = 300, + ["10 minutes"] = 600 +} - local siren_configuration = SirenConfiguration(0xC1) +local MODEL_DEVICE_PROFILE_MAP = { + ["SIRZB-110"] = "frient-siren-battery-source-tamper", + ["SIRZB-111"] = "frient-siren-battery-source" +} - device:send( - IASWD.server.commands.StartWarning( - device, - siren_configuration, - data_types.Uint16(warning_duration), - data_types.Uint8(duty_cycle), - data_types.Enum8(IaswdLevel.LOW_LEVEL) +local BATTERY_CONFIG_APPLIED_KEY = "_frient_battery_config_applied" + +local function get_current_max_warning_duration(device) + return device.preferences.maxWarningDuration == nil and DEFAULT_MAX_WARNING_DURATION or device.preferences.maxWarningDuration +end + +local function get_warning_duration(device) + -- User may select one of predefine modes on the UI or input duration in preferences + -- every time ALARM_DURATION is updated + local selected_duration = device:get_field(ALARM_DURATION) + local current_max_warning_duration = get_current_max_warning_duration(device) + + local warning_duration + if selected_duration == nil or selected_duration > current_max_warning_duration then + warning_duration = current_max_warning_duration + else + warning_duration = selected_duration + end + return warning_duration +end + +local function configure_battery_handling_based_on_fw(driver, device) + local sw_version = device:get_field(PRIMARY_SW_VERSION) + local applied_state = device:get_field(BATTERY_CONFIG_APPLIED_KEY) + + if sw_version and sw_version < SIREN_FIXED_ENDIAN_SW_VERSION then + if applied_state ~= "voltage" then + -- Old firmware - does not support BatteryPercentageRemaining attribute, use battery defaults (voltage-based) + battery_defaults.build_linear_voltage_init(3.3, 4.1)(driver, device) + device:set_field(BATTERY_CONFIG_APPLIED_KEY, "voltage", { persist = true }) + return true + end + else + if applied_state ~= "percentage" then + -- New firmware - supports BatteryPercentageRemaining, remove voltage monitoring + device:remove_configured_attribute(PowerConfiguration.ID, PowerConfiguration.attributes.BatteryVoltage.ID) + device:remove_monitored_attribute(PowerConfiguration.ID, PowerConfiguration.attributes.BatteryVoltage.ID) + device:set_field(BATTERY_CONFIG_APPLIED_KEY, "percentage", { persist = true }) + return true + end + end + + return false +end + +local function device_init(driver, device) + for _, attribute in ipairs(IASZone_configuration) do + device:add_configured_attribute(attribute) + device:add_monitored_attribute(attribute) + end +end + +local function device_added (driver, device) + for comp_name, comp in pairs(device.profile.components) do + if comp_name ~= "main" then + if comp_name == "SirenVoice" then + device:emit_component_event(comp, capabilities.mode.supportedModes({"Burglar", "Fire", "Emergency", "Panic","Panic Fire","Panic Emergency" }, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.supportedArguments({"Burglar", "Fire", "Emergency", "Panic","Panic Fire","Panic Emergency" }, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.mode("Burglar")) + elseif comp_name == "SquawkVoice" then + device:emit_component_event(comp, capabilities.mode.supportedModes({"Armed", "Disarmed"}, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.supportedArguments({"Armed", "Disarmed"}, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.mode("Armed")) + elseif comp_name == "WarningDuration" then + device:emit_component_event(comp, capabilities.mode.supportedModes({ + "5 seconds", "10 seconds", "15 seconds", "20 seconds", "25 seconds", "30 seconds", "40 seconds", "50 seconds", + "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "10 minutes" + }, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.supportedArguments({ + "5 seconds", "10 seconds", "15 seconds", "20 seconds", "25 seconds", "30 seconds", "40 seconds", "50 seconds", + "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "10 minutes" + }, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.mode("4 minutes")) + else + device:emit_component_event(comp, capabilities.mode.supportedModes({"Low", "Medium", "High", "Very High"}, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.supportedArguments({"Low", "Medium", "High", "Very High"}, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.mode("Very High")) + end + end + end + + device:emit_event(capabilities.alarm.alarm.off()) + + if(device:supports_capability(capabilities.tamperAlert)) then + device:emit_event(capabilities.tamperAlert.tamper.clear()) + end +end + +local function do_refresh(driver, device) + device:refresh() + device:send(IASZone.attributes.ZoneStatus:read(device):to_endpoint(IASZONE_ENDPOINT)) + + -- Check if we have the software version + local sw_version = device:get_field(PRIMARY_SW_VERSION) + if ((sw_version == nil) or (sw_version == "")) then + device:send(cluster_base.read_manufacturer_specific_attribute(device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE)) + end +end + +local function do_configure(driver, device) + local maxWarningDuration = get_current_max_warning_duration(device) + device:set_field(ALARM_DURATION, maxWarningDuration , { persist = true}) + device:send(IASWD.attributes.MaxDuration:write(device, maxWarningDuration):to_endpoint(0x2B)) + + -- Check if we have the software version + local sw_version = device:get_field(PRIMARY_SW_VERSION) + if ((sw_version == nil) or (sw_version == "")) then + device:send(cluster_base.read_manufacturer_specific_attribute(device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE)) + else + configure_battery_handling_based_on_fw(driver, device) + end + + -- handle frient sirens that were already connected and using old device profile + if (not device:supports_capability_by_id(capabilities.mode.ID)) then + device:try_update_metadata({profile = MODEL_DEVICE_PROFILE_MAP[device:get_model()]}) + end + + device.thread:call_with_delay(5, function() + do_refresh(driver, device) + end) + device:configure() +end + +local function primary_sw_version_attr_handler(driver, device, value, zb_rx) + local primary_sw_version = value.value:gsub('.', function (c) return string.format('%02x', string.byte(c)) end) + device:set_field(PRIMARY_SW_VERSION, primary_sw_version, {persist = true}) + local config_changed = configure_battery_handling_based_on_fw(driver, device) + if config_changed then + device.thread:call_with_delay(1, function() + device:configure() + end) + end + device.thread:call_with_delay(config_changed and 2 or 1, function() + do_refresh(driver, device) + end) +end + +local function generate_event_from_zone_status(driver, device, zone_status, zb_rx) + if device:supports_capability(capabilities.tamperAlert) then + device:emit_event_for_endpoint( + zb_rx.address_header.src_endpoint.value, + zone_status:is_tamper_set() and capabilities.tamperAlert.tamper.detected() or capabilities.tamperAlert.tamper.clear() ) + end + device:emit_event_for_endpoint( + zb_rx.address_header.src_endpoint.value, + zone_status:is_ac_mains_fault_set() and capabilities.powerSource.powerSource.battery() or capabilities.powerSource.powerSource.mains() + ) +end + +local function ias_zone_status_attr_handler(driver, device, zone_status, zb_rx) + generate_event_from_zone_status(driver, device, zone_status, zb_rx) +end + +local function ias_zone_status_change_handler(driver, device, zb_rx) + local zone_status = zb_rx.body.zcl_body.zone_status + generate_event_from_zone_status(driver, device, zone_status, zb_rx) +end + +local function send_siren_command(device, warning_mode, warning_siren_level) + -- Check if we have the software version first + local sw_version = device:get_field(PRIMARY_SW_VERSION) + if ((sw_version == nil) or (sw_version == "")) then + device:send(cluster_base.read_manufacturer_specific_attribute(device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE)) + end + + local warning_duration = get_warning_duration(device) + + local siren_configuration + + if (sw_version and sw_version < SIREN_FIXED_ENDIAN_SW_VERSION) then + -- Old frient firmware, the endian format is reversed + local siren_config_value = (warning_siren_level << 6) | warning_mode + siren_configuration = SirenConfiguration(siren_config_value) + else + siren_configuration = SirenConfiguration(0x00) + siren_configuration:set_warning_mode(warning_mode) + siren_configuration:set_siren_level(warning_siren_level) + end + + device:send( + IASWD.server.commands.StartWarning( + device, + siren_configuration, + data_types.Uint16(warning_duration), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) ) end -local siren_switch_both_handler = function(driver, device, command) - device:set_field(ALARM_COMMAND, alarm_command.BOTH, {persist = true}) - send_siren_command(device) +local function siren_switch_off_handler(driver, device, command) + device:set_field(ALARM_COMMAND, alarm_command.OFF, {persist = true}) + send_siren_command(device, WarningMode.STOP, IaswdLevel.LOW_LEVEL) end -local siren_alarm_siren_handler = function(driver, device, command) +local function siren_alarm_siren_handler(driver, device, command) device:set_field(ALARM_COMMAND, alarm_command.SIREN, {persist = true}) - send_siren_command(device) + + -- delay is needed to allow st automations get updated fields when mode, volume, voice is set sequentially + device.thread:call_with_delay(1, function() + local sirenVoice_msg = device:get_latest_state("SirenVoice", capabilities.mode.ID, capabilities.mode.mode.NAME) + local sirenVolume_msg = device:get_latest_state("SirenVolume", capabilities.mode.ID, capabilities.mode.mode.NAME) + send_siren_command(device,sirenVoice_msg == nil and WarningMode.BURGLAR or SIREN_VOICE_MAP[sirenVoice_msg] , sirenVolume_msg == nil and IaswdLevel.VERY_HIGH_LEVEL or VOLUME_MAP[sirenVolume_msg]) + end) + + local warningDurationDelay = get_warning_duration(device) + + device.thread:call_with_delay(warningDurationDelay, function() -- Send command to switch from siren to off in the app when the siren is done + if(device:get_field(ALARM_COMMAND) == alarm_command.SIREN) then + siren_switch_off_handler(driver, device, command) + end + end) +end + +local function send_squawk_command(device, squawk_mode, squawk_siren_level) + -- Check if we have the software version first + local sw_version = device:get_field(PRIMARY_SW_VERSION) + + if ((sw_version == nil) or (sw_version == "")) then + device:send(cluster_base.read_manufacturer_specific_attribute(device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE)) + end + + local squawk_configuration + + if (sw_version and sw_version < SIREN_FIXED_ENDIAN_SW_VERSION) then + -- Old frient firmware, the endian format is reversed + local squawk_config_value = (squawk_siren_level << 6) | squawk_mode + squawk_configuration = SquawkConfiguration(squawk_config_value) + else + squawk_configuration = SquawkConfiguration(0x00) + squawk_configuration:set_squawk_mode(squawk_mode) + squawk_configuration:set_squawk_level(squawk_siren_level) + end + + device:send( + IASWD.server.commands.Squawk( + device, + squawk_configuration + ) + ) +end + +local function siren_tone_beep_handler(driver, device, command) + device.thread:call_with_delay(1, function () + local squawkVolume_msg = device:get_latest_state("SquawkVolume", capabilities.mode.ID, capabilities.mode.mode.NAME) + local squawkVoice_msg = device:get_latest_state("SquawkVoice", capabilities.mode.ID, capabilities.mode.mode.NAME) + + send_squawk_command(device, SQUAWK_VOICE_MAP[squawkVoice_msg] or SquawkMode.SOUND_FOR_SYSTEM_IS_ARMED,VOLUME_MAP[squawkVolume_msg] or IaswdLevel.VERY_HIGH_LEVEL) + end ) end -local siren_alarm_strobe_handler = function(driver, device, command) - device:set_field(ALARM_COMMAND, alarm_command.STROBE, {persist = true}) - send_siren_command(device) +local function info_changed(driver, device, event, args) + for name, info in pairs(device.preferences) do + if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then + local input = device.preferences[name] + if (name == "maxWarningDuration") then + device:send(IASWD.attributes.MaxDuration:write(device, tonumber(input))) + end + end + end end -local siren_switch_on_handler = function(driver, device, command) - siren_switch_both_handler(driver, device, command) +local function siren_mode_handler(driver, device, command) + local mode_set = command.args.mode + local component = command.component + local compObj = device.profile.components[component] + + if compObj then + if component == "WarningDuration" then + local warning_duration = WARNING_DURATION_MAP[mode_set] or DEFAULT_MAX_WARNING_DURATION + device:set_field(ALARM_DURATION, warning_duration, {persist = true}) + end + end + + device.thread:call_with_delay(2,function() + device:emit_component_event( + compObj, + capabilities.mode.mode(mode_set)) + end) end local frient_siren_driver = { NAME = "frient A/S", + lifecycle_handlers = { + added = device_added, + init = device_init, + doConfigure = do_configure, + infoChanged = info_changed, + }, capability_handlers = { [alarm.ID] = { - [alarm.commands.both.NAME] = siren_switch_both_handler, + [alarm.commands.off.NAME] = siren_switch_off_handler, [alarm.commands.siren.NAME] = siren_alarm_siren_handler, - [alarm.commands.strobe.NAME] = siren_alarm_strobe_handler + [alarm.commands.both.NAME] = siren_alarm_siren_handler + }, + [capabilities.tone.ID] = { + [capabilities.tone.commands.beep.NAME] = siren_tone_beep_handler }, - [switch.ID] = { - [switch.commands.on.NAME] = siren_switch_on_handler, + [capabilities.mode.ID] = { + [capabilities.mode.commands.setMode.NAME] = siren_mode_handler + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "frient A/S" - end + zigbee_handlers = { + cluster = { + [IASZone.ID] = { + [IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler + } + }, + attr = { + [IASZone.ID] = { + [IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler + }, + [Basic.ID] = { + [DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR] = primary_sw_version_attr_handler + } + } + }, + can_handle = require("frient.can_handle"), } -return frient_siren_driver +return frient_siren_driver \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-siren/src/init.lua b/drivers/SmartThings/zigbee-siren/src/init.lua index 0f08b067fb..b1ad81c9c6 100644 --- a/drivers/SmartThings/zigbee-siren/src/init.lua +++ b/drivers/SmartThings/zigbee-siren/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" @@ -32,7 +22,9 @@ local IaswdLevel = IASWD.types.IaswdLevel local capabilities = require "st.capabilities" local alarm = capabilities.alarm local switch = capabilities.switch - +local mode = capabilities.mode +local battery = capabilities.battery +local refresh = capabilities.refresh -- Constants local ALARM_COMMAND = "alarmCommand" local ALARM_LAST_DURATION = "lastDuration" @@ -81,13 +73,13 @@ local send_siren_command = function(device, warning_mode, warning_siren_level, s siren_configuration:set_siren_level(warning_siren_level) device:send( - IASWD.server.commands.StartWarning( - device, - siren_configuration, - data_types.Uint16(warning_duration), - data_types.Uint8(duty_cycle), - data_types.Enum8(strobe_level) - ) + IASWD.server.commands.StartWarning( + device, + siren_configuration, + data_types.Uint16(warning_duration), + data_types.Uint8(duty_cycle), + data_types.Enum8(strobe_level) + ) ) end @@ -157,7 +149,10 @@ end local zigbee_siren_driver_template = { supported_capabilities = { alarm, - switch + switch, + mode, + battery, + refresh }, ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, zigbee_handlers = { @@ -189,7 +184,7 @@ local zigbee_siren_driver_template = { added = device_added, doConfigure = do_configure }, - sub_drivers = { require("ozom"), require("frient") }, + sub_drivers = require("sub_drivers"), cluster_configurations = { [alarm.ID] = { { @@ -206,4 +201,4 @@ local zigbee_siren_driver_template = { defaults.register_for_default_handlers(zigbee_siren_driver_template, zigbee_siren_driver_template.supported_capabilities) local zigbee_siren = ZigbeeDriver("zigbee-siren", zigbee_siren_driver_template) -zigbee_siren:run() +zigbee_siren:run() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-siren/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-siren/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-siren/src/ozom/can_handle.lua b/drivers/SmartThings/zigbee-siren/src/ozom/can_handle.lua new file mode 100644 index 0000000000..bec6069a7f --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/src/ozom/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function ozom_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "ClimaxTechnology" then + return true, require("ozom") + end + return false +end + +return ozom_can_handle diff --git a/drivers/SmartThings/zigbee-siren/src/ozom/init.lua b/drivers/SmartThings/zigbee-siren/src/ozom/init.lua index e4cecc28ab..c17a1c4fb7 100644 --- a/drivers/SmartThings/zigbee-siren/src/ozom/init.lua +++ b/drivers/SmartThings/zigbee-siren/src/ozom/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.zigbee.data_types" --ZCL @@ -75,9 +65,7 @@ local ozom_siren_driver = { [switch.commands.on.NAME] = siren_switch_on_handler } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "ClimaxTechnology" - end + can_handle = require("ozom.can_handle"), } return ozom_siren_driver diff --git a/drivers/SmartThings/zigbee-siren/src/sub_drivers.lua b/drivers/SmartThings/zigbee-siren/src/sub_drivers.lua new file mode 100644 index 0000000000..05d186b87e --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/src/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" + +return { + lazy_load_if_possible("frient"), + lazy_load_if_possible("ozom"), +} diff --git a/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua b/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua index 098b2ab1bd..c88d272807 100644 --- a/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua +++ b/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2022 SmartThings, Inc. -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -15,110 +15,1106 @@ -- Mock out globals local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" +local OnOff = clusters.OnOff +local Scenes = clusters.Scenes +local Basic = clusters.Basic +local Identify = clusters.Identify +local PowerConfiguration = clusters.PowerConfiguration +local Groups = clusters.Groups +local IASZone = clusters.IASZone local IASWD = clusters.IASWD +local IaswdLevel = IASWD.types.IaswdLevel +local WarningMode = IASWD.types.WarningMode +local SquawkMode = IASWD.types.SquawkMode +local SirenConfiguration = IASWD.types.SirenConfiguration +local SquawkConfiguration = IASWD.types.SquawkConfiguration +local ZoneStatusAttribute = IASZone.attributes.ZoneStatus + +local PRIMARY_SW_VERSION = "primary_sw_version" +local SIREN_ENDIAN = "siren_endian" +local ALARM_DURATION = "warningDuration" +local ALARM_DEFAULT_MAX_DURATION = 0x00F0 +local ALARM_DURATION_TEST_VALUE = 5 +local DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR = 0x8000 +local DEVELCO_MANUFACTURER_CODE = 0x1015 + +local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local data_types = require "st.zigbee.data_types" -local SirenConfiguration = require "st.zigbee.generated.zcl_clusters.IASWD.types.SirenConfiguration" local t_utils = require "integration_test.utils" + local mock_device = test.mock_device.build_test_zigbee_device( - { - profile = t_utils.get_profile_definition("switch-alarm.yml"), - zigbee_endpoints = { - [1] = { - id = 1, - manufacturer = "frient A/S", - model = "SIRZB-110", - server_clusters = {0x0502} + { + profile = t_utils.get_profile_definition("frient-siren-battery-source.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "SIRZB-111", + server_clusters = { Scenes.ID, OnOff.ID} + }, + [0x2B] = { + id = 0x2B, + server_clusters = { Basic.ID, Identify.ID, PowerConfiguration.ID, Groups.ID, IASZone.ID, IASWD.ID } + } + } } - } - } ) zigbee_test_utils.prepare_zigbee_env_info() local function test_init() - test.mock_device.add_test_device(mock_device)end + test.mock_device.add_test_device(mock_device) +end test.set_test_init_function(test_init) +local function set_new_firmware_and_defaults() + -- set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "010903", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + -- set test durations and parameters + mock_device:set_field(ALARM_DURATION, ALARM_DURATION_TEST_VALUE, {persist = true}) +end + +local function set_older_firmware_and_defaults() + -- set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "010901", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + -- set test durations and parameters + mock_device:set_field(ALARM_DURATION, ALARM_DURATION_TEST_VALUE, {persist = true}) +end + +local function set_no_firmware_and_defaults() + -- set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, nil, {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + -- set test durations and parameters + mock_device:set_field(ALARM_DURATION, ALARM_DURATION_TEST_VALUE, {persist = true}) +end + +local function get_siren_commands_new_fw( warningMode, sirenLevel, duration ) + local expectedSirenONConfiguration = SirenConfiguration(0x00) + expectedSirenONConfiguration:set_warning_mode(warningMode) --WarningMode.BURGLAR + expectedSirenONConfiguration:set_siren_level(sirenLevel) --IaswdLevel.VERY_HIGH_LEVEL + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning( + mock_device, + expectedSirenONConfiguration, + data_types.Uint16(duration), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) + }) +end + +local function get_siren_commands_old_fw(warningMode, sirenLevel) + local expectedSirenONConfiguration + local siren_config_value = (sirenLevel << 6) | warningMode + expectedSirenONConfiguration = SirenConfiguration(siren_config_value) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning( + mock_device, + expectedSirenONConfiguration, + data_types.Uint16(ALARM_DURATION_TEST_VALUE), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) + }) +end + +local function get_siren_OFF_commands(duration) + local expectedSirenOFFConfiguration = SirenConfiguration(0x00) + expectedSirenOFFConfiguration:set_warning_mode(WarningMode.STOP) + expectedSirenOFFConfiguration:set_siren_level(IaswdLevel.LOW_LEVEL) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning( + mock_device, + expectedSirenOFFConfiguration, + data_types.Uint16(duration and duration or ALARM_DURATION_TEST_VALUE), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) + }) +end + +local function get_squawk_command_new_fw(squawk_mode, squawk_siren_level) + local expected_squawk_configuration = SquawkConfiguration(0x00) + expected_squawk_configuration:set_squawk_mode(squawk_mode) + expected_squawk_configuration:set_squawk_level(squawk_siren_level) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.Squawk( + mock_device, + expected_squawk_configuration + ) + }) +end + +local function get_squawk_command_older_fw(squawk_mode, squawk_siren_level) + local expected_squawk_configuration + local squawk_config_value = (squawk_siren_level << 6) | squawk_mode + expected_squawk_configuration = SquawkConfiguration(squawk_config_value) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.Squawk( + mock_device, + expected_squawk_configuration + ) + }) +end + +test.register_coroutine_test( + "lifecycles - init and doConfigure test", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write( + mock_device, + ALARM_DEFAULT_MAX_DURATION + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID, + 0x2B + ):to_endpoint(0x2B) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting( + mock_device, + 30, + 21600, + 1 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + 0x2B + ):to_endpoint(0x2B) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device, + 0, + 21600, + 1 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write( + mock_device, + zigbee_test_utils.mock_hub_eui + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + end +) + +test.register_coroutine_test( + "lifecycle - added test", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVoice", + capabilities.mode.supportedModes({ "Burglar", "Fire", "Emergency", "Panic", "Panic Fire", "Panic Emergency" }, { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVoice", + capabilities.mode.supportedArguments({ "Burglar", "Fire", "Emergency", "Panic", "Panic Fire", "Panic Emergency" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVoice", + capabilities.mode.mode("Burglar") + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVolume", + capabilities.mode.supportedModes({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVolume", + capabilities.mode.supportedArguments({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVolume", + capabilities.mode.mode({value = "Very High"}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVoice", + capabilities.mode.supportedModes({ "Armed", "Disarmed" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVoice", + capabilities.mode.supportedArguments({ "Armed", "Disarmed" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVoice", + capabilities.mode.mode("Armed") + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVolume", + capabilities.mode.supportedModes({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVolume", + capabilities.mode.supportedArguments({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVolume", + capabilities.mode.mode("Very High") + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "WarningDuration", + capabilities.mode.supportedModes( + { + "5 seconds", "10 seconds", "15 seconds", "20 seconds", "25 seconds", "30 seconds", "40 seconds", "50 seconds", + "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "10 minutes" + }, + { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "WarningDuration", + capabilities.mode.supportedArguments( + { + "5 seconds", "10 seconds", "15 seconds", "20 seconds", "25 seconds", "30 seconds", "40 seconds", "50 seconds", + "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "10 minutes" + }, + { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "WarningDuration", + capabilities.mode.mode("4 minutes") + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.alarm.alarm.off() + ) + ) + end +) + +test.register_coroutine_test( + "Should detect newer firmware version and use correct endian format to turn on the siren (test with default settings)", + function() + set_new_firmware_and_defaults() + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) >= "010903", "PRIMARY_SW_VERSION should be greater than or equal to '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.BURGLAR,IaswdLevel.VERY_HIGH_LEVEL,ALARM_DURATION_TEST_VALUE) + test.mock_time.advance_time(ALARM_DURATION_TEST_VALUE+1) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands() + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Should detect older firmware version and use correct endian format to turn on the siren (test with default settings)", + function() + set_older_firmware_and_defaults() + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) < "010903", "PRIMARY_SW_VERSION should be lower than '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_old_fw(WarningMode.BURGLAR,IaswdLevel.VERY_HIGH_LEVEL) + test.mock_time.advance_time(ALARM_DURATION_TEST_VALUE) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands() + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Alarm OFF should be handled", + function() + set_new_firmware_and_defaults() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "off", args = {} } + }) + get_siren_OFF_commands() + test.wait_for_events() + end +) + +test.register_coroutine_test( + "SirenVoice mode 'Fire' and SirenVolume mode 'LOW' should be handled", + function() + set_new_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVoice", command = "setMode", args = {"Fire"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVoice", capabilities.mode.mode("Fire")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVolume", command = "setMode", args = {"Low"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVolume", capabilities.mode.mode("Low")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "WarningDuration", command = "setMode", args = {"5 seconds"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("WarningDuration", capabilities.mode.mode("5 seconds")) + ) + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.FIRE,IaswdLevel.LOW_LEVEL,ALARM_DURATION_TEST_VALUE) + test.mock_time.advance_time(5) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands() + end +) + +test.register_coroutine_test( + "SirenVoice mode 'Fire' and SirenVolume mode 'LOW' should be handled - in case of NO FW version was reported before", + function() + set_no_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVoice", command = "setMode", args = {"Fire"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVoice", capabilities.mode.mode("Fire")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVolume", command = "setMode", args = {"Low"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVolume", capabilities.mode.mode("Low")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "WarningDuration", command = "setMode", args = {"5 seconds"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("WarningDuration", capabilities.mode.mode("5 seconds")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + test.mock_time.advance_time(1) + -- Read the FW version first + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ):to_endpoint(0x2B) + } + ) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.FIRE,IaswdLevel.LOW_LEVEL,ALARM_DURATION_TEST_VALUE) + + test.mock_time.advance_time(5) + -- stop the siren + -- Read the FW version first + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ):to_endpoint(0x2B) + } + ) + -- Expect the OFF command + get_siren_OFF_commands() + end +) + +test.register_coroutine_test( + "SirenVoice mode 'Fire' and SirenVolume mode 'LOW' should be handled - when maxWarningDuration is shorted then selected mode", + function() + local expectedWarningDuration = 7 + set_new_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(expectedWarningDuration, "oneshot") + + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed( + { + preferences = { + maxWarningDuration = expectedWarningDuration + } + } + )) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write( + mock_device, + expectedWarningDuration + ) + }) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVoice", command = "setMode", args = {"Fire"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVoice", capabilities.mode.mode("Fire")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVolume", command = "setMode", args = {"Low"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVolume", capabilities.mode.mode("Low")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "WarningDuration", command = "setMode", args = {"50 seconds"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("WarningDuration", capabilities.mode.mode("50 seconds")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.FIRE,IaswdLevel.LOW_LEVEL, expectedWarningDuration) + + test.mock_time.advance_time(expectedWarningDuration) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands(expectedWarningDuration) + end +) + +test.register_coroutine_test( + "Should detect newer firmware version and use correct endian format to turn on squawk (test with default settings)", + function() + + set_new_firmware_and_defaults() + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) >= "010903", "PRIMARY_SW_VERSION should be greater than or equal to '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_new_fw( SquawkMode.SOUND_FOR_SYSTEM_IS_ARMED, IaswdLevel.VERY_HIGH_LEVEL ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Should detect older firmware version and use correct endian format to turn on squawk (test with default settings)", + function() + set_older_firmware_and_defaults() + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) < "010903", "PRIMARY_SW_VERSION should be lower than '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_older_fw( SquawkMode.SOUND_FOR_SYSTEM_IS_ARMED, IaswdLevel.VERY_HIGH_LEVEL ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "SquawkVoice mode 'Disarmed' and SquawkVolume mode 'Medium' should be handled", + function() + set_new_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVoice", command = "setMode", args = { "Disarmed" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVoice", capabilities.mode.mode("Disarmed")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVolume", command = "setMode", args = { "Medium" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVolume", capabilities.mode.mode("Medium")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_new_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) + end +) + +test.register_coroutine_test( + "SquawkVoice mode 'Disarmed' and SquawkVolume mode 'Medium' should be handled - in case of OLD FW version was reported before", + function() + set_older_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVoice", command = "setMode", args = { "Disarmed" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVoice", capabilities.mode.mode("Disarmed")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVolume", command = "setMode", args = { "Medium" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVolume", capabilities.mode.mode("Medium")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_older_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) + end +) + +test.register_coroutine_test( + "SquawkVoice mode 'Disarmed' and SquawkVolume mode 'Medium' should be handled - in case of NO FW version was reported before", + function() + set_no_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVoice", command = "setMode", args = { "Disarmed" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVoice", capabilities.mode.mode("Disarmed")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVolume", command = "setMode", args = { "Medium" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVolume", capabilities.mode.mode("Medium")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + test.mock_time.advance_time(1) + -- Read the FW version first + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ):to_endpoint(0x2B) + } + ) + -- Expect the command with given configuration + get_squawk_command_new_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) + end +) + +test.register_coroutine_test( + "Refresh should be handled - new FW", + function() + set_new_firmware_and_defaults() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + }) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Refresh should be handled - FW not known", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + }) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + } + ) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ) + } + ) + test.wait_for_events() + end +) + +test.register_message_test( + "Power source / mains should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0000) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.mains()) + } + } +) + test.register_message_test( - "Capability(switch) command(on) on should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "switch", command = "on", args = { } } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, - SirenConfiguration(0xC1), - data_types.Uint16(0x00B4), - data_types.Uint8(00), - data_types.Enum8(00)) } - } - } + "Power source / battery and tamper clear should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0081) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.battery()) + } + } ) test.register_message_test( - "Capability(alarm) command(both) on should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "alarm", component = "main", command = "both", args = { } } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, - SirenConfiguration(0xC1), - data_types.Uint16(0x00B4), - data_types.Uint8(00), - data_types.Enum8(00)) } - } - } + "Min battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 0xC8) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } ) test.register_message_test( - "Capability(alarm) command(siren) on should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "alarm", component = "main", command = "siren", args = { } } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, - SirenConfiguration(0xC1), - data_types.Uint16(0x00B4), - data_types.Uint8(00), - data_types.Enum8(00)) } - } - } + "Medium battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 0x64) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(50)) + } + } ) test.register_message_test( - "Capability(alarm) command(strobe) on should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "alarm", component = "main", command = "strobe", args = { } } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, - SirenConfiguration(0xC1), - data_types.Uint16(0x00B4), - data_types.Uint8(00), - data_types.Enum8(00)) } - } - } -) - -test.run_registered_tests() + "Max battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 0) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +local function build_sw_version_attr_report(device, fw_bytes) + local zcl_messages_mod = require "st.zigbee.zcl" + local messages_mod = require "st.zigbee.messages" + local zb_const_mod = require "st.zigbee.constants" + local report_attr = require "st.zigbee.zcl.global_commands.report_attribute" + local zcl_cmds_mod = require "st.zigbee.zcl.global_commands" + + local attr_record = report_attr.ReportAttributeAttributeRecord( + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + data_types.CharString.ID, + fw_bytes + ) + local report_body = report_attr.ReportAttribute({ attr_record }) + local zclh = zcl_messages_mod.ZclHeader({ + cmd = data_types.ZCLCommandId(zcl_cmds_mod.REPORT_ATTRIBUTE_ID) + }) + local addrh = messages_mod.AddressHeader( + device:get_short_address(), + device:get_endpoint(Basic.ID), + zb_const_mod.HUB.ADDR, + zb_const_mod.HUB.ENDPOINT, + zb_const_mod.HA_PROFILE_ID, + Basic.ID + ) + local message_body = zcl_messages_mod.ZclMessageBody({ zcl_header = zclh, zcl_body = report_body }) + return messages_mod.ZigbeeMessageRx({ address_header = addrh, body = message_body }) +end + +test.register_coroutine_test( + "SW version attr handler with new firmware should configure battery percentage", + function() + mock_device:set_field(PRIMARY_SW_VERSION, nil, {persist = true}) + mock_device:set_field("_frient_battery_config_applied", nil, {persist = true}) + + -- Binary "\x01\x09\x03" -> hex "010903" (new firmware >= SIREN_FIXED_ENDIAN_SW_VERSION) + test.socket.zigbee:__queue_receive({ + mock_device.id, + build_sw_version_attr_report(mock_device, "\x01\x09\x03") + }) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "SW version attr handler with old firmware should configure battery voltage", + function() + mock_device:set_field(PRIMARY_SW_VERSION, nil, {persist = true}) + mock_device:set_field("_frient_battery_config_applied", nil, {persist = true}) + + -- Binary "\x01\x09\x01" -> hex "010901" (old firmware < SIREN_FIXED_ENDIAN_SW_VERSION) + test.socket.zigbee:__queue_receive({ + mock_device.id, + build_sw_version_attr_report(mock_device, "\x01\x09\x01") + }) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "doConfigure with existing sw_version should call configure_battery_handling", + function() + mock_device:set_field(PRIMARY_SW_VERSION, "010903", {persist = true}) + mock_device:set_field("_frient_battery_config_applied", nil, {persist = true}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write(mock_device, ALARM_DEFAULT_MAX_DURATION) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID, + 0x2B + ):to_endpoint(0x2B) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting( + mock_device, 30, 21600, 1) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + 0x2B + ):to_endpoint(0x2B) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting(mock_device, 0, 21600, 1) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write(mock_device, zigbee_test_utils.mock_hub_eui) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse(mock_device, IasEnrollResponseCode.SUCCESS, 0x00) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren_tamper.lua b/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren_tamper.lua new file mode 100644 index 0000000000..aed318f49a --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren_tamper.lua @@ -0,0 +1,1083 @@ +-- 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. + +-- Mock out globals +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" +local OnOff = clusters.OnOff +local Scenes = clusters.Scenes +local Basic = clusters.Basic +local Identify = clusters.Identify +local PowerConfiguration = clusters.PowerConfiguration +local Groups = clusters.Groups +local IASZone = clusters.IASZone +local IASWD = clusters.IASWD +local IaswdLevel = IASWD.types.IaswdLevel +local WarningMode = IASWD.types.WarningMode +local SquawkMode = IASWD.types.SquawkMode +local SirenConfiguration = IASWD.types.SirenConfiguration +local SquawkConfiguration = IASWD.types.SquawkConfiguration +local ZoneStatusAttribute = IASZone.attributes.ZoneStatus + +local PRIMARY_SW_VERSION = "primary_sw_version" +local SIREN_ENDIAN = "siren_endian" +local ALARM_DURATION = "warningDuration" +local ALARM_DEFAULT_MAX_DURATION = 0x00F0 +local ALARM_DURATION_TEST_VALUE = 5 +local DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR = 0x8000 +local DEVELCO_MANUFACTURER_CODE = 0x1015 + +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local data_types = require "st.zigbee.data_types" +local t_utils = require "integration_test.utils" + + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("frient-siren-battery-source-tamper.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "SIRZB-111", + server_clusters = { Scenes.ID, OnOff.ID} + }, + [0x2B] = { + id = 0x2B, + server_clusters = { Basic.ID, Identify.ID, PowerConfiguration.ID, Groups.ID, IASZone.ID, IASWD.ID } + } + } + } +) + +local mock_device_112 = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("frient-siren-battery-source-tamper.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "SIRZB-112", + server_clusters = { Scenes.ID, OnOff.ID} + }, + [0x2B] = { + id = 0x2B, + server_clusters = { Basic.ID, Identify.ID, PowerConfiguration.ID, Groups.ID, IASZone.ID, IASWD.ID } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +local function set_new_firmware_and_defaults() + -- set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "010903", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + -- set test durations and parameters + mock_device:set_field(ALARM_DURATION, ALARM_DURATION_TEST_VALUE, {persist = true}) +end + +local function set_older_firmware_and_defaults() + -- set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "010901", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + -- set test durations and parameters + mock_device:set_field(ALARM_DURATION, ALARM_DURATION_TEST_VALUE, {persist = true}) +end + +local function set_no_firmware_and_defaults() + -- set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, nil, {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + -- set test durations and parameters + mock_device:set_field(ALARM_DURATION, ALARM_DURATION_TEST_VALUE, {persist = true}) +end + +local function get_siren_commands_new_fw(warningMode, sirenLevel, duration) + local expectedSirenONConfiguration = SirenConfiguration(0x00) + expectedSirenONConfiguration:set_warning_mode(warningMode) --WarningMode.BURGLAR + expectedSirenONConfiguration:set_siren_level(sirenLevel) --IaswdLevel.VERY_HIGH_LEVEL + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning( + mock_device, + expectedSirenONConfiguration, + data_types.Uint16(duration), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) + }) +end + +local function get_siren_commands_old_fw(warningMode, sirenLevel) + local expectedSirenONConfiguration + local siren_config_value = (sirenLevel << 6) | warningMode + expectedSirenONConfiguration = SirenConfiguration(siren_config_value) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning( + mock_device, + expectedSirenONConfiguration, + data_types.Uint16(ALARM_DURATION_TEST_VALUE), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) + }) +end + +local function get_siren_OFF_commands(duration) + local expectedSirenOFFConfiguration = SirenConfiguration(0x00) + expectedSirenOFFConfiguration:set_warning_mode(WarningMode.STOP) + expectedSirenOFFConfiguration:set_siren_level(IaswdLevel.LOW_LEVEL) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning( + mock_device, + expectedSirenOFFConfiguration, + data_types.Uint16(duration and duration or ALARM_DURATION_TEST_VALUE), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) + }) +end + +local function get_squawk_command_new_fw(squawk_mode, squawk_siren_level) + local expected_squawk_configuration = SquawkConfiguration(0x00) + expected_squawk_configuration:set_squawk_mode(squawk_mode) + expected_squawk_configuration:set_squawk_level(squawk_siren_level) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.Squawk( + mock_device, + expected_squawk_configuration + ) + }) +end + +local function get_squawk_command_older_fw(squawk_mode, squawk_siren_level) + local expected_squawk_configuration + local squawk_config_value = (squawk_siren_level << 6) | squawk_mode + expected_squawk_configuration = SquawkConfiguration(squawk_config_value) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.Squawk( + mock_device, + expected_squawk_configuration + ) + }) +end + +test.register_coroutine_test( + "lifecycles - init and doConfigure test", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write( + mock_device, + ALARM_DEFAULT_MAX_DURATION + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID, + 0x2B + ):to_endpoint(0x2B) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting( + mock_device, + 30, + 21600, + 1 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + 0x2B + ):to_endpoint(0x2B) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device, + 0, + 21600, + 1 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write( + mock_device, + zigbee_test_utils.mock_hub_eui + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + end +) + +test.register_coroutine_test( + "lifecycle - added test", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVoice", + capabilities.mode.supportedModes({ "Burglar", "Fire", "Emergency", "Panic", "Panic Fire", "Panic Emergency" }, { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVoice", + capabilities.mode.supportedArguments({ "Burglar", "Fire", "Emergency", "Panic", "Panic Fire", "Panic Emergency" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVoice", + capabilities.mode.mode("Burglar") + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVolume", + capabilities.mode.supportedModes({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVolume", + capabilities.mode.supportedArguments({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVolume", + capabilities.mode.mode({value = "Very High"}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVoice", + capabilities.mode.supportedModes({ "Armed", "Disarmed" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVoice", + capabilities.mode.supportedArguments({ "Armed", "Disarmed" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVoice", + capabilities.mode.mode("Armed") + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVolume", + capabilities.mode.supportedModes({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVolume", + capabilities.mode.supportedArguments({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVolume", + capabilities.mode.mode("Very High") + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "WarningDuration", + capabilities.mode.supportedModes( + { + "5 seconds", "10 seconds", "15 seconds", "20 seconds", "25 seconds", "30 seconds", "40 seconds", "50 seconds", + "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "10 minutes" + }, + { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "WarningDuration", + capabilities.mode.supportedArguments( + { + "5 seconds", "10 seconds", "15 seconds", "20 seconds", "25 seconds", "30 seconds", "40 seconds", "50 seconds", + "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "10 minutes" + }, + { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "WarningDuration", + capabilities.mode.mode("4 minutes") + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.alarm.alarm.off() + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.tamperAlert.tamper.clear() + ) + ) + end +) + +test.register_coroutine_test( + "Should detect newer firmware version and use correct endian format to turn on the siren (test with default settings)", + function() + set_new_firmware_and_defaults() + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) >= "010903", "PRIMARY_SW_VERSION should be greater than or equal to '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.BURGLAR,IaswdLevel.VERY_HIGH_LEVEL,ALARM_DURATION_TEST_VALUE) + test.mock_time.advance_time(ALARM_DURATION_TEST_VALUE) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands() + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Should detect older firmware version and use correct endian format to turn on the siren (test with default settings)", + function() + set_older_firmware_and_defaults() + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) < "010903", "PRIMARY_SW_VERSION should be lower than '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_old_fw(WarningMode.BURGLAR,IaswdLevel.VERY_HIGH_LEVEL) + test.mock_time.advance_time(ALARM_DURATION_TEST_VALUE) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands() + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Alarm OFF should be handled", + function() + set_new_firmware_and_defaults() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "off", args = {} } + }) + get_siren_OFF_commands() + test.wait_for_events() + end +) + +test.register_coroutine_test( + "SirenVoice mode 'Fire' and SirenVolume mode 'LOW' should be handled", + function() + set_new_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVoice", command = "setMode", args = {"Fire"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVoice", capabilities.mode.mode("Fire")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVolume", command = "setMode", args = {"Low"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVolume", capabilities.mode.mode("Low")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "WarningDuration", command = "setMode", args = {"5 seconds"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("WarningDuration", capabilities.mode.mode("5 seconds")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.FIRE,IaswdLevel.LOW_LEVEL,ALARM_DURATION_TEST_VALUE) + test.mock_time.advance_time(5) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands() + end +) + +test.register_coroutine_test( + "SirenVoice mode 'Fire' and SirenVolume mode 'LOW' should be handled - in case of NO FW version was reported before", + function() + set_no_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVoice", command = "setMode", args = {"Fire"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVoice", capabilities.mode.mode("Fire")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVolume", command = "setMode", args = {"Low"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVolume", capabilities.mode.mode("Low")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "WarningDuration", command = "setMode", args = {"5 seconds"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("WarningDuration", capabilities.mode.mode("5 seconds")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + test.mock_time.advance_time(1) + -- Read the FW version first + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ):to_endpoint(0x2B) + } + ) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.FIRE,IaswdLevel.LOW_LEVEL,ALARM_DURATION_TEST_VALUE) + + test.mock_time.advance_time(5) + -- stop the siren + -- Read the FW version first + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ):to_endpoint(0x2B) + } + ) + -- Expect the OFF command + get_siren_OFF_commands() + end +) + +test.register_coroutine_test( + "SirenVoice mode 'Fire' and SirenVolume mode 'LOW' should be handled - when maxWarningDuration is shorted then selected mode", + function() + local expectedWarningDuration = 7 + set_new_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(expectedWarningDuration, "oneshot") + + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed( + { + preferences = { + maxWarningDuration = expectedWarningDuration + } + } + )) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write( + mock_device, + expectedWarningDuration + ) + }) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVoice", command = "setMode", args = {"Fire"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVoice", capabilities.mode.mode("Fire")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVolume", command = "setMode", args = {"Low"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVolume", capabilities.mode.mode("Low")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "WarningDuration", command = "setMode", args = {"50 seconds"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("WarningDuration", capabilities.mode.mode("50 seconds")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.FIRE,IaswdLevel.LOW_LEVEL, expectedWarningDuration) + + test.mock_time.advance_time(expectedWarningDuration) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands(expectedWarningDuration) + end +) + +test.register_coroutine_test( + "Should detect newer firmware version and use correct endian format to turn on squawk (test with default settings)", + function() + set_new_firmware_and_defaults() + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) >= "010903", "PRIMARY_SW_VERSION should be greater than or equal to '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_new_fw( SquawkMode.SOUND_FOR_SYSTEM_IS_ARMED, IaswdLevel.VERY_HIGH_LEVEL ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Should detect older firmware version and use correct endian format to turn on squawk (test with default settings)", + function() + set_older_firmware_and_defaults() + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) < "010903", "PRIMARY_SW_VERSION should be lower than '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_older_fw( SquawkMode.SOUND_FOR_SYSTEM_IS_ARMED, IaswdLevel.VERY_HIGH_LEVEL ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "SquawkVoice mode 'Disarmed' and SquawkVolume mode 'Medium' should be handled", + function() + set_new_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVoice", command = "setMode", args = { "Disarmed" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVoice", capabilities.mode.mode("Disarmed")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVolume", command = "setMode", args = { "Medium" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVolume", capabilities.mode.mode("Medium")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_new_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) + end +) + +test.register_coroutine_test( + "SquawkVoice mode 'Disarmed' and SquawkVolume mode 'Medium' should be handled - in case of OLD FW version was reported before", + function() + set_older_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVoice", command = "setMode", args = { "Disarmed" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVoice", capabilities.mode.mode("Disarmed")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVolume", command = "setMode", args = { "Medium" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVolume", capabilities.mode.mode("Medium")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_older_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) + end +) + +test.register_coroutine_test( + "SquawkVoice mode 'Disarmed' and SquawkVolume mode 'Medium' should be handled - in case of NO FW version was reported before", + function() + set_no_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVoice", command = "setMode", args = { "Disarmed" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVoice", capabilities.mode.mode("Disarmed")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVolume", command = "setMode", args = { "Medium" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVolume", capabilities.mode.mode("Medium")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + test.mock_time.advance_time(1) + -- Read the FW version first + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ):to_endpoint(0x2B) + } + ) + -- Expect the command with given configuration + get_squawk_command_new_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) + end +) + +test.register_coroutine_test( + "Refresh should be handled - new FW", + function() + set_new_firmware_and_defaults() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + }) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Refresh should be handled - FW not known", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + }) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + } + ) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ) + } + ) + + test.wait_for_events() + end +) + +test.register_message_test( + "Power source / mains should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0005) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.mains()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected()) + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_message_test( + "Power source / battery should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0081) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.battery()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_message_test( + "Min battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 0xC8) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + +test.register_message_test( + "Medium battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 0x64) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(50)) + } + } +) + +test.register_message_test( + "Max battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 0) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +local function get_siren_OFF_commands_112(duration) + local expectedSirenOFFConfiguration = SirenConfiguration(0x00) + expectedSirenOFFConfiguration:set_warning_mode(WarningMode.STOP) + expectedSirenOFFConfiguration:set_siren_level(IaswdLevel.LOW_LEVEL) + + test.socket.zigbee:__expect_send({ + mock_device_112.id, + IASWD.server.commands.StartWarning( + mock_device_112, + expectedSirenOFFConfiguration, + data_types.Uint16(duration and duration or ALARM_DURATION_TEST_VALUE), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) + }) +end + +test.register_coroutine_test( + "SIRZB-112 alarm off command should be handled via frient sub-driver", + function() + mock_device_112:set_field(PRIMARY_SW_VERSION, "010903", {persist = true}) + mock_device_112:set_field(ALARM_DURATION, ALARM_DURATION_TEST_VALUE, {persist = true}) + + test.socket.capability:__queue_receive({ + mock_device_112.id, + { capability = "alarm", component = "main", command = "off", args = {} } + }) + + get_siren_OFF_commands_112() + test.wait_for_events() + end, + { test_init = function() + test.mock_device.add_test_device(mock_device_112) + end } +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-siren/src/test/test_ozom_siren.lua b/drivers/SmartThings/zigbee-siren/src/test/test_ozom_siren.lua index 5b9a6a2bea..f05c58468f 100644 --- a/drivers/SmartThings/zigbee-siren/src/test/test_ozom_siren.lua +++ b/drivers/SmartThings/zigbee-siren/src/test/test_ozom_siren.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2022 SmartThings, Inc. -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/drivers/SmartThings/zigbee-siren/src/test/test_zigbee_siren.lua b/drivers/SmartThings/zigbee-siren/src/test/test_zigbee_siren.lua index e8de7eb958..52db9f96f4 100644 --- a/drivers/SmartThings/zigbee-siren/src/test/test_zigbee_siren.lua +++ b/drivers/SmartThings/zigbee-siren/src/test/test_zigbee_siren.lua @@ -19,6 +19,7 @@ local zcl_cmds = require "st.zigbee.zcl.global_commands" local IASZone = clusters.IASZone local IASWD = clusters.IASWD local OnOff = clusters.OnOff +local PowerConfiguration = clusters.PowerConfiguration local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local data_types = require "st.zigbee.data_types" @@ -212,6 +213,20 @@ test.register_coroutine_test( mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(mock_device, 30, 21600, 1) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -265,6 +280,14 @@ test.register_message_test( OnOff.attributes.OnOff:read(mock_device) } }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + } + } }, { inner_block_ordering = "relaxed" @@ -317,6 +340,63 @@ test.register_coroutine_test( end ) +local function build_default_response_zigbee_msg() + local zcl_messages = require "st.zigbee.zcl" + local messages = require "st.zigbee.messages" + local zb_const = require "st.zigbee.constants" + local buf_lib = require "st.buf" + local buf_from_str = function(str) return buf_lib.Reader(str) end + local frame = "\x00\x00" + local default_response = zcl_cmds.DefaultResponse.deserialize(buf_from_str(frame)) + local zclh = zcl_messages.ZclHeader({ cmd = data_types.ZCLCommandId(zcl_cmds.DefaultResponse.ID) }) + local addrh = messages.AddressHeader( + mock_device:get_short_address(), + mock_device:get_endpoint(data_types.ClusterId(IASWD.ID)), + zb_const.HUB.ADDR, zb_const.HUB.ENDPOINT, zb_const.HA_PROFILE_ID, IASWD.ID) + local message_body = zcl_messages.ZclMessageBody({ zcl_header = zclh, zcl_body = default_response }) + return messages.ZigbeeMessageRx({ address_header = addrh, body = message_body }) +end + +test.register_coroutine_test( + "Default response with OFF alarm command should emit off events", + function() + mock_device:set_field("alarmCommand", 0, {persist = true}) + test.socket.zigbee:__queue_receive({ mock_device.id, build_default_response_zigbee_msg() }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.alarm.alarm.off())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) + end +) + +test.register_coroutine_test( + "Default response with STROBE alarm command should emit strobe event and timer off", + function() + test.timer.__create_and_queue_test_time_advance_timer(180, "oneshot") + mock_device:set_field("alarmCommand", 2, {persist = true}) + test.socket.zigbee:__queue_receive({ mock_device.id, build_default_response_zigbee_msg() }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.alarm.alarm.strobe())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.on())) + test.mock_time.advance_time(180) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.alarm.alarm.off())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Default response with BOTH alarm command should emit both event", + function() + test.timer.__create_and_queue_test_time_advance_timer(180, "oneshot") + mock_device:set_field("alarmCommand", 3, {persist = true}) + test.socket.zigbee:__queue_receive({ mock_device.id, build_default_response_zigbee_msg() }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.alarm.alarm.both())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.on())) + test.mock_time.advance_time(180) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.alarm.alarm.off())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) + test.wait_for_events() + end +) + test.register_coroutine_test( "Setting a max duration should be handled", function() diff --git a/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml b/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml index 76b105fdb8..ddd1135cbd 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml +++ b/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml @@ -34,6 +34,11 @@ zigbeeManufacturer: manufacturer: frient A/S model: SMSZB-120 deviceProfileName: smoke-temp-battery-alarm + - id: "frient/HESZB-120" + deviceLabel: frient Heat Detector + manufacturer: frient A/S + model: HESZB-120 + deviceProfileName: heat-temp-battery-alarm - id: "Heiman/Orvibo/Gas3" deviceLabel: Orvibo Gas Detector manufacturer: Heiman diff --git a/drivers/SmartThings/zigbee-smoke-detector/profiles/heat-temp-battery-alarm.yml b/drivers/SmartThings/zigbee-smoke-detector/profiles/heat-temp-battery-alarm.yml new file mode 100644 index 0000000000..c7abdd4e99 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/profiles/heat-temp-battery-alarm.yml @@ -0,0 +1,55 @@ +name: heat-temp-battery-alarm +components: +- id: main + capabilities: + - id: temperatureAlarm + version: 1 + config: + values: + - key: "temperatureAlarm.value" + enabledValues: + - heat + - cleared + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + - id: alarm + version: 1 + config: + values: + - key: "alarm.value" + enabledValues: + - off + - siren + - key: "{{enumCommands}}" + enabledValues: + - off + - siren + categories: + - name: TempSensor +preferences: + - preferenceId: tempOffset + explicit: true + - name: "tempSensitivity" + title: "Temperature Sensitivity (°C)" + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 + - name: "warningDuration" + title: "Alarm duration (s)" + description: "After how many seconds should the alarm turn off" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 240 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/can_handle.lua b/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/can_handle.lua new file mode 100644 index 0000000000..41ba568b80 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_aqara_products(opts, driver, device) + local FINGERPRINTS = require("aqara-gas.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara-gas") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/fingerprints.lua b/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/fingerprints.lua new file mode 100644 index 0000000000..61cc4d9730 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.sensor_gas.acn02" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/init.lua index 863385accc..e0474243ce 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.zigbee.data_types" local cluster_base = require "st.zigbee.cluster_base" local capabilities = require "st.capabilities" @@ -31,9 +21,6 @@ local PRIVATE_LIFE_TIME_ATTRIBUTE_ID = 0x0128 local PRIVATE_GAS_ZONE_STATUS_ATTRIBUTE_ID = 0x013A -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.sensor_gas.acn02" } -} local CONFIGURATIONS = { @@ -125,20 +112,11 @@ local function self_check_attr_handler(self, device, zone_status, zb_rx) PRIVATE_CLUSTER_ID, PRIVATE_SELF_CHECK_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true)) end -local function is_aqara_products(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function device_init(driver, device) if CONFIGURATIONS ~= nil then for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end @@ -187,8 +165,7 @@ local aqara_gas_detector_handler = { [startSelfCheckCommandName] = self_check_attr_handler }, }, - can_handle = is_aqara_products + can_handle = require("aqara-gas.can_handle"), } return aqara_gas_detector_handler - diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-smoke-detector/src/aqara/can_handle.lua new file mode 100644 index 0000000000..e4453597ed --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_aqara_products(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-smoke-detector/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..d296aa8518 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/aqara/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.sensor_smoke.acn03" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/aqara/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/aqara/init.lua index c9813a2b10..1950d1f923 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/aqara/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.zigbee.data_types" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" @@ -30,9 +20,6 @@ local PRIVATE_SMOKE_ZONE_STATUS_ATTRIBUTE_ID = 0x013A local PowerConfiguration = clusters.PowerConfiguration -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.sensor_smoke.acn03" } -} local CONFIGURATIONS = { @@ -98,21 +85,12 @@ local function self_check_attr_handler(self, device, zone_status, zb_rx) PRIVATE_CLUSTER_ID, PRIVATE_SELF_CHECK_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true)) end -local function is_aqara_products(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function device_init(driver, device) battery_defaults.build_linear_voltage_init(2.6, 3.0)(driver, device) if CONFIGURATIONS ~= nil then for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end @@ -155,8 +133,7 @@ local aqara_gas_detector_handler = { [startSelfCheckCommandName] = self_check_attr_handler }, }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } return aqara_gas_detector_handler - diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-smoke-detector/src/frient/can_handle.lua new file mode 100644 index 0000000000..5154ec5fe6 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/frient/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function frient_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "frient A/S" and (device:get_model() == "SMSZB-120" or device:get_model() == "HESZB-120") then + return true, require("frient") + end + return false +end + +return frient_can_handle diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua index 7bc2fe48b2..d3a3604c26 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local battery_defaults = require "st.zigbee.defaults.battery_defaults" local capabilities = require "st.capabilities" @@ -21,6 +11,7 @@ local cluster_base = require "st.zigbee.cluster_base" local Basic = zcl_clusters.Basic local alarm = capabilities.alarm local smokeDetector = capabilities.smokeDetector +local temperatureAlarm = capabilities.temperatureAlarm local IASWD = zcl_clusters.IASWD local IASZone = zcl_clusters.IASZone @@ -72,7 +63,14 @@ end local function device_added(driver, device) device:emit_event(alarm.alarm.off()) - device:emit_event(smokeDetector.smoke.clear()) + + if device:supports_capability(capabilities.temperatureAlarm) then + device:emit_event(temperatureAlarm.temperatureAlarm.cleared()) + end + + if device:supports_capability(capabilities.smokeDetector) then + device:emit_event(smokeDetector.smoke.clear()) + end device:send(cluster_base.read_manufacturer_specific_attribute(device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE)) end @@ -81,7 +79,6 @@ local function device_init(driver, device) if CONFIGURATIONS ~= nil then for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end @@ -112,14 +109,28 @@ local info_changed = function (driver, device, event, args) end local function generate_event_from_zone_status(driver, device, zone_status, zigbee_message) - if zone_status:is_test_set() then - device:emit_event(smokeDetector.smoke.tested()) + if device:supports_capability(capabilities.temperatureAlarm) then + device:emit_event(temperatureAlarm.temperatureAlarm.heat()) + end + if device:supports_capability(capabilities.smokeDetector) then + device:emit_event(smokeDetector.smoke.tested()) + end elseif zone_status:is_alarm1_set() then - device:emit_event(smokeDetector.smoke.detected()) + if device:supports_capability(capabilities.temperatureAlarm) then + device:emit_event(temperatureAlarm.temperatureAlarm.heat()) + end + if device:supports_capability(capabilities.smokeDetector) then + device:emit_event(smokeDetector.smoke.detected()) + end else device.thread:call_with_delay(6, function () - device:emit_event(smokeDetector.smoke.clear()) + if device:supports_capability(capabilities.temperatureAlarm) then + device:emit_event(temperatureAlarm.temperatureAlarm.cleared()) + end + if device:supports_capability(capabilities.smokeDetector) then + device:emit_event(smokeDetector.smoke.clear()) + end end) end end @@ -249,8 +260,6 @@ local frient_smoke_sensor = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "frient A/S" and device:get_model() == "SMSZB-120" - end + can_handle = require("frient.can_handle"), } return frient_smoke_sensor diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/init.lua index 2508061813..fe64260c11 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -22,13 +12,10 @@ local zigbee_smoke_driver_template = { capabilities.smokeDetector, capabilities.battery, capabilities.alarm, - capabilities.temperatureMeasurement - }, - sub_drivers = { - require("frient"), - require("aqara-gas"), - require("aqara") + capabilities.temperatureMeasurement, + capabilities.temperatureAlarm }, + sub_drivers = require("sub_drivers"), ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, } @@ -36,4 +23,4 @@ local zigbee_smoke_driver_template = { defaults.register_for_default_handlers(zigbee_smoke_driver_template, zigbee_smoke_driver_template.supported_capabilities, {native_capability_attrs_enabled = true}) local zigbee_smoke_driver = ZigbeeDriver("zigbee-smoke-detector", zigbee_smoke_driver_template) -zigbee_smoke_driver:run() \ No newline at end of file +zigbee_smoke_driver:run() diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-smoke-detector/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/sub_drivers.lua b/drivers/SmartThings/zigbee-smoke-detector/src/sub_drivers.lua new file mode 100644 index 0000000000..e6c5e004f3 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/sub_drivers.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("frient"), + lazy_load_if_possible("aqara-gas"), + lazy_load_if_possible("aqara"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_gas_detector.lua b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_gas_detector.lua index 6c194351a6..1203ffa00a 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_gas_detector.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_gas_detector.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" @@ -255,6 +244,66 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "selfCheck report should be handled, idle", + function() + local attr_report_data = { + { PRIVATE_SELF_CHECK_ATTRIBUTE_ID, data_types.Uint8.ID, 0x00 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + selfCheck.selfCheckState.idle())) + end +) + +test.register_coroutine_test( + "sensitivityAdjustment report should be handled, High", + function() + local attr_report_data = { + { PRIVATE_SENSITIVITY_ADJUSTMENT_ATTRIBUTE_ID, data_types.Uint8.ID, 0x02 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + sensitivityAdjustment.sensitivityAdjustment.High())) + end +) + +test.register_coroutine_test( + "Capability on command should be handled : setSensitivityAdjustment High", + function() + local attr_report_data = { + { PRIVATE_SENSITIVITY_ADJUSTMENT_ATTRIBUTE_ID, data_types.Uint8.ID, 0x02 } + } + test.socket.capability:__queue_receive({ mock_device.id, + { capability = sensitivityAdjustmentId, component = "main", command = "setSensitivityAdjustment", args = {"High"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + PRIVATE_SENSITIVITY_ADJUSTMENT_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0x02) + }) + test.wait_for_events() + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + sensitivityAdjustment.sensitivityAdjustment.High()) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ mock_device.id, + { capability = sensitivityAdjustmentId, component = "main", command = "setSensitivityAdjustment", args = {"High"}} + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + sensitivityAdjustment.sensitivityAdjustment.High()) + ) + end +) test.register_coroutine_test( diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_smoke_detector.lua b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_smoke_detector.lua index bd0fae534c..b0b6a4875a 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_smoke_detector.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_smoke_detector.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" @@ -18,13 +7,10 @@ local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" - - local PowerConfiguration = clusters.PowerConfiguration local selfCheck = capabilities["stse.selfCheck"] local selfCheckId = "stse.selfCheck" - test.add_package_capability("selfCheck.yaml") local PRIVATE_CLUSTER_ID = 0xFCC0 @@ -34,8 +20,6 @@ local PRIVATE_MUTE_ATTRIBUTE_ID = 0x0126 local PRIVATE_SELF_CHECK_ATTRIBUTE_ID = 0x0127 local PRIVATE_SMOKE_ZONE_STATUS_ATTRIBUTE_ID = 0x013A - - local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("smoke-battery-aqara.yml"), @@ -50,15 +34,11 @@ local mock_device = test.mock_device.build_test_zigbee_device( } ) - - zigbee_test_utils.prepare_zigbee_env_info() local function test_init() test.mock_device.add_test_device(mock_device) end - - test.set_test_init_function(test_init) test.register_coroutine_test( @@ -100,7 +80,6 @@ test.register_coroutine_test( end ) - test.register_coroutine_test( "smokeDetector report should be handled", function() @@ -116,8 +95,6 @@ test.register_coroutine_test( end ) - - test.register_coroutine_test( "audioMute report should be handled", function() @@ -133,8 +110,6 @@ test.register_coroutine_test( end ) - - test.register_coroutine_test( "Capability on command should be handled : device mute", function() @@ -146,8 +121,6 @@ test.register_coroutine_test( end ) - - test.register_coroutine_test( "selfCheck report should be handled", function() @@ -163,8 +136,6 @@ test.register_coroutine_test( end ) - - test.register_coroutine_test( "Capability on command should be handled : device selfCheck", function() @@ -177,7 +148,50 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "smokeDetector report should be handled, smoke clear", + function() + local attr_report_data = { + { PRIVATE_SMOKE_ZONE_STATUS_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0000 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.smokeDetector.smoke.clear())) + end +) + +test.register_coroutine_test( + "audioMute report should be handled, unmuted", + function() + local attr_report_data = { + { PRIVATE_MUTE_ATTRIBUTE_ID, data_types.Uint8.ID, 0x00 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.audioMute.mute.unmuted())) + end +) +test.register_coroutine_test( + "selfCheck report should be handled, idle", + function() + local attr_report_data = { + { PRIVATE_SELF_CHECK_ATTRIBUTE_ID, data_types.Uint8.ID, 0x00 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + selfCheck.selfCheckState.idle())) + end +) test.register_message_test( "Battery voltage report should be handled", @@ -195,5 +209,4 @@ test.register_message_test( } ) - test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_heat_detector.lua b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_heat_detector.lua new file mode 100644 index 0000000000..21b1660c1a --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_heat_detector.lua @@ -0,0 +1,604 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Mock out globals +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local IASWD = clusters.IASWD +local IASZone = clusters.IASZone +local PowerConfiguration = clusters.PowerConfiguration +local TemperatureMeasurement = clusters.TemperatureMeasurement +local Basic = clusters.Basic +local capabilities = require "st.capabilities" +local alarm = capabilities.alarm +local temperatureAlarm = capabilities.temperatureAlarm +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" +local t_utils = require "integration_test.utils" +local data_types = require "st.zigbee.data_types" +local SirenConfiguration = require "st.zigbee.generated.zcl_clusters.IASWD.types.SirenConfiguration" +local ALARM_DEFAULT_MAX_DURATION = 0x00F0 +local POWER_CONFIGURATION_ENDPOINT = 0x23 +local IASZONE_ENDPOINT = 0x23 +local TEMPERATURE_MEASUREMENT_ENDPOINT = 0x26 +local base64 = require "base64" +local PRIMARY_SW_VERSION = "primary_sw_version" +local SIREN_ENDIAN = "siren_endian" +local DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR = 0x8000 +local DEVELCO_MANUFACTURER_CODE = 0x1015 +local cluster_base = require "st.zigbee.cluster_base" +local defaultWarningDuration = 240 + +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("heat-temp-battery-alarm.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "HESZB-120", + server_clusters = { 0x0003, 0x0005, 0x0006 } + }, + [0x23] = { + id = 0x23, + server_clusters = { 0x0000, 0x0001, 0x0003, 0x000f, 0x0020, 0x0500, 0x0502 } + }, + [0x26] = { + id = 0x26, + server_clusters = { 0x0000, 0x0003, 0x0402 } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device)end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Clear alarm and temperatureAlarm states, and read firmware version when the device is added", function() + test.socket.matter:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", alarm.alarm.off()) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", temperatureAlarm.temperatureAlarm.cleared()) + ) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE) + }) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "init and doConfigure lifecycles should be handled properly", + function() + test.socket.environment_update:__queue_receive({ "zigbee", { hub_zigbee_id = base64.encode(zigbee_test_utils.mock_hub_eui) } }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + test.wait_for_events() + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", alarm.alarm.off()) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", temperatureAlarm.temperatureAlarm.cleared()) + ) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE) + }) + + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID, + POWER_CONFIGURATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device, + 30, + 21600, + 1 + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + TemperatureMeasurement.ID, + TEMPERATURE_MEASUREMENT_ENDPOINT + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 60, + 600, + 100 + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + IASZONE_ENDPOINT + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device, + 30, + 300, + 0 + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write( + mock_device, + zigbee_test_utils.mock_hub_eui + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write(mock_device, ALARM_DEFAULT_MAX_DURATION) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "Battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 24) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(14)) + } + } +) + +test.register_coroutine_test( + "ZoneStatusChangeNotification should be handled: detected", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0001, 0x00) + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.heat()) + ) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "ZoneStatusChangeNotification should be handled: tested", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0100, 0x01) + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.heat()) + ) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "ZoneStatusChangeNotification should be handled: cleared", + function() + test.timer.__create_and_queue_test_time_advance_timer(6, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0000, 0x00) + }) + + test.mock_time.advance_time(6) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared()) + ) + + test.wait_for_events() + end +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } + } + } +) + +test.register_message_test( + "Refresh should read all necessary attributes", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_coroutine_test( + "infochanged to check for necessary preferences settings: tempSensitivity, warningDuration", + function() + local updates = { + preferences = { + tempSensitivity = 1.3, + warningDuration = 100 + } + } + + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 60, + 600, + 130 + )--:to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write(mock_device, 0x0064)--:to_endpoint(IASZONE_ENDPOINT) + }) + + + test.socket.zigbee:__set_channel_ordering("relaxed") + + end +) + +test.register_coroutine_test( + "Should detect older firmware version and use correct endian format to turn on the siren", + function() + -- Manually set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "040002", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, "reverse", {persist = true}) + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) < "040005", "PRIMARY_SW_VERSION should be less than '040005'") + assert(mock_device:get_field(SIREN_ENDIAN) == "reverse", "SIREN_ENDIAN should be set to 'reverse'") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + + -- Expect the command with reverse endian format + local expectedConfiguration = SirenConfiguration(0x01) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning(mock_device, + expectedConfiguration, + data_types.Uint16(defaultWarningDuration), + data_types.Uint8(00), + data_types.Enum8(00)) + }) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Should detect newer firmware version and use correct endian format to turn on the siren", + function() + -- Manually set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "040005", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) >= "040005", "PRIMARY_SW_VERSION should be greater than or equal to '040005'") + assert(mock_device:get_field(SIREN_ENDIAN) == nil, "SIREN_ENDIAN should be set to 'nil'") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + + -- Expect the command with reverse endian format + local expectedConfiguration = SirenConfiguration(0x00) + expectedConfiguration:set_warning_mode(0x01) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning(mock_device, + expectedConfiguration, + data_types.Uint16(defaultWarningDuration), + data_types.Uint8(00), + data_types.Enum8(00)) + }) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Should detect older firmware version and use correct endian format to turn on the siren", + function() + -- Manually set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "040002", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, "reverse", {persist = true}) + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) < "040005", "PRIMARY_SW_VERSION should be less than '040005'") + assert(mock_device:get_field(SIREN_ENDIAN) == "reverse", "SIREN_ENDIAN should be set to 'reverse'") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "off", args = {} } + }) + + -- Expect the command with reverse endian format + local expectedConfiguration = SirenConfiguration(0x00) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning(mock_device, + expectedConfiguration, + data_types.Uint16(0x00), + data_types.Uint8(00), + data_types.Enum8(00)) + }) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Should detect newer firmware version and use correct endian format to turn off the siren", + function() + -- Manually set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "040005", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) >= "040005", "PRIMARY_SW_VERSION should be greater than or equal to '040005'") + assert(mock_device:get_field(SIREN_ENDIAN) == nil, "SIREN_ENDIAN should be set to 'nil'") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "off", args = {} } + }) + + -- Expect the command with reverse endian format + local expectedConfiguration = SirenConfiguration(0x00) + expectedConfiguration:set_warning_mode(0x00) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning(mock_device, + expectedConfiguration, + data_types.Uint16(0x00), + data_types.Uint8(00), + data_types.Enum8(00)) + }) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Test firmware version conversion using direct simulation", + function() + -- Binary firmware version test cases + local test_cases = { + { + binary = string.char(4, 0, 5), -- \004\000\005 + expected_hex = "040005" -- Expected output + }, + { + binary = string.char(4, 0, 12), -- \004\000\012 + expected_hex = "04000c" -- Expected output + }, + { + binary = string.char(5, 1, 3), -- \005\001\003 + expected_hex = "050103" -- Expected output + } + } + + for i, test_case in ipairs(test_cases) do + print("\n----- Test Case " .. i .. " -----") + local binary_fw = test_case.binary + local expected_hex = test_case.expected_hex + + -- Print the raw binary version and its byte values + print("Binary firmware version (raw):", binary_fw) + print("Binary firmware bytes:", string.format( + "\\%03d\\%03d\\%03d", + string.byte(binary_fw, 1), + string.byte(binary_fw, 2), + string.byte(binary_fw, 3) + )) + + -- Reset the field for clean test + mock_device:set_field(PRIMARY_SW_VERSION, nil, {persist = true}) + + -- Create a mock value object + local mock_value = { + value = binary_fw + } + + -- Simulate what happens in primary_sw_version_attr_handler + local primary_sw_version = mock_value.value:gsub('.', function (c) + return string.format('%02x', string.byte(c)) + end) + + -- Store the version in PRIMARY_SW_VERSION field + mock_device:set_field(PRIMARY_SW_VERSION, primary_sw_version, {persist = true}) + + -- What the conversion should do + print("\nConversion steps:") + local hex_result = "" + for i = 1, #binary_fw do + local char = binary_fw:sub(i, i) + local byte_val = string.byte(char) + local hex_val = string.format('%02x', byte_val) + hex_result = hex_result .. hex_val + print(string.format("Character at position %d: byte value = %d, hex = %02x", + i, byte_val, byte_val)) + end + + print("\nExpected hex result:", expected_hex) + print("Manual conversion result:", hex_result) + + -- Verify the stored version + local stored_version = mock_device:get_field(PRIMARY_SW_VERSION) + print("\nStored version in device field:", stored_version) + assert(stored_version == expected_hex, + string.format("Version mismatch! Expected '%s' but got '%s'", + expected_hex, stored_version or "nil")) + end + end +) + +test.register_message_test( + "IASZone attribute report should be handled: detected", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0001) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.heat()) + } + } +) + +test.register_coroutine_test( + "IASZone attribute report should be handled: cleared", + function() + test.timer.__create_and_queue_test_time_advance_timer(6, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0000) + }) + test.mock_time.advance_time(6) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared()) + ) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua index b8a685bd9a..fdb05cf22d 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -39,6 +28,12 @@ local DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR = 0x8000 local DEVELCO_MANUFACTURER_CODE = 0x1015 local cluster_base = require "st.zigbee.cluster_base" local defaultWarningDuration = 240 +local messages = require "st.zigbee.messages" +local default_response = require "st.zigbee.zcl.global_commands.default_response" +local zb_const = require "st.zigbee.constants" +local Status = require "st.zigbee.generated.types.ZclStatus" +local zcl_messages = require "st.zigbee.zcl" +local ALARM_COMMAND = "alarmCommand" local mock_device = test.mock_device.build_test_zigbee_device( @@ -258,6 +253,24 @@ test.register_message_test( } ) +test.register_coroutine_test( + "ZoneStatusChangeNotification should be handled: clear", + function() + test.timer.__create_and_queue_test_time_advance_timer(6, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0000, 0x00) + }) + + test.mock_time.advance_time(6) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.smokeDetector.smoke.clear()) + ) + + test.wait_for_events() + end +) + test.register_message_test( "Temperature report should be handled (C) for the temperature cluster", { @@ -270,6 +283,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -594,4 +615,88 @@ test.register_coroutine_test( end ) +local function build_default_response_msg(device, cluster, command, status) + local addr_header = messages.AddressHeader( + device:get_short_address(), + device.fingerprinted_endpoint_id, + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + cluster + ) + local default_response_body = default_response.DefaultResponse(command, status) + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(default_response_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = default_response_body + }) + return messages.ZigbeeMessageRx({ + address_header = addr_header, + body = message_body + }) +end + +test.register_message_test( + "IASZone attribute report should be handled: detected", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0001) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", smokeDetector.smoke.detected()) + } + } +) + +test.register_coroutine_test( + "IASZone attribute report should be handled: clear", + function() + test.timer.__create_and_queue_test_time_advance_timer(6, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0000) + }) + test.mock_time.advance_time(6) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", smokeDetector.smoke.clear()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "default_response_handler: siren active, emit siren then off after delay", + function() + mock_device:set_field(ALARM_COMMAND, 1) + test.timer.__create_and_queue_test_time_advance_timer(ALARM_DEFAULT_MAX_DURATION, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + build_default_response_msg(mock_device, IASWD.ID, IASWD.server.commands.StartWarning.ID, Status.SUCCESS) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", alarm.alarm.siren())) + test.mock_time.advance_time(ALARM_DEFAULT_MAX_DURATION) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", alarm.alarm.off())) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "default_response_handler: alarm off, emit off", + function() + mock_device:set_field(ALARM_COMMAND, 0) + test.socket.zigbee:__queue_receive({ + mock_device.id, + build_default_response_msg(mock_device, IASWD.ID, IASWD.server.commands.StartWarning.ID, Status.SUCCESS) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", alarm.alarm.off())) + test.wait_for_events() + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_zigbee_smoke_detector.lua b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_zigbee_smoke_detector.lua index 00d0b43c10..b27713fa19 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_zigbee_smoke_detector.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_zigbee_smoke_detector.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-sound-sensor/src/test/test_zigbee_sound_sensor.lua b/drivers/SmartThings/zigbee-sound-sensor/src/test/test_zigbee_sound_sensor.lua index 83717569f0..4ab2f31e92 100644 --- a/drivers/SmartThings/zigbee-sound-sensor/src/test/test_zigbee_sound_sensor.lua +++ b/drivers/SmartThings/zigbee-sound-sensor/src/test/test_zigbee_sound_sensor.lua @@ -267,6 +267,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index e449e1f194..6bb32c5978 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -116,6 +116,11 @@ zigbeeManufacturer: manufacturer: LUMI model: lumi.light.acn004 deviceProfileName: aqara-light + - id: "LUMI/lumi.light.cwacn1" + deviceLabel: Aqara Smart Dimmer Controller T1 (0-10v) + manufacturer: LUMI + model: lumi.light.cwacn1 + deviceProfileName: aqara-light - id: "Aqara/lumi.light.acn014" deviceLabel: Aqara LED Light Bulb T1 (Tunable White) manufacturer: Aqara @@ -500,6 +505,11 @@ zigbeeManufacturer: manufacturer: frient A/S model: SMRZB-342 deviceProfileName: frient-switch-power-energy-voltage + - id: "frient/IOMZB-110" + deviceLabel: frient IO Module + manufacturer: frient A/S + model: IOMZB-110 + deviceProfileName: switch-4inputs-2outputs - id: "AduroSmart Eria/AD-DimmableLight3001" deviceLabel: Eria Light manufacturer: AduroSmart Eria @@ -590,6 +600,11 @@ zigbeeManufacturer: manufacturer: ubisys model: "D1 (5503)" deviceProfileName: switch-level-power + - id: "ubisys/S1 (5501)" + deviceLabel: Switching Actuator S1 + manufacturer: ubisys + model: "S1 (5501)" + deviceProfileName: switch-power - id: "ubisys/S2 (5502)" deviceLabel: ubisys S2 manufacturer: ubisys @@ -600,6 +615,11 @@ zigbeeManufacturer: manufacturer: ubisys model: "1151" deviceProfileName: switch-power + - id: "ubisys/D1-R (5603)" + deviceLabel: Dimmer D1-R + manufacturer: ubisys + model: "D1-R (5603)" + deviceProfileName: switch-level-power - id: "Megaman/AD-DimmableLight3001" deviceLabel: INGENIUM Light manufacturer: Megaman @@ -2359,16 +2379,58 @@ 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 model: VZM31-SN deviceProfileName: inovelli-vzm31-sn + - id: "Inovelli/VZM32-SN" + deviceLabel: "Inovelli mmWave Dimmer Blue Series" + manufacturer: Inovelli + model: VZM32-SN + deviceProfileName: inovelli-vzm32-sn - id: "LAISIAO/BATH" deviceLabel: Laisiao Bathroom Heater manufacturer: LAISIAO model: yuba deviceProfileName: switch-smart-bath-heater-laisiao + # NodOn + - id: "NodOn/SIN-4-1-20" + deviceLabel: Zigbee Multifunction Relay Switch + manufacturer: NodOn + model: SIN-4-1-20 + deviceProfileName: basic-switch + - id: "NodOn/SIN-4-2-20" + deviceLabel: Zigbee ON/OFF Lighting Relay Switch + manufacturer: NodOn + model: SIN-4-2-20 + deviceProfileName: switch-level + - id: "NodOn/SIN-4-1-21" + deviceLabel: Zigbee Multifunction Relay Switch with Metering + manufacturer: NodOn + model: SIN-4-1-21 + deviceProfileName: switch-power-energy + #Yanmi + - id: "JNL/Y-K003-001" + deviceLabel: Yanmi Switch (3 Way) 1 + manufacturer: JNL + model: Y-K003-001 + deviceProfileName: basic-switch + - id: "JNL/Y-K001-001" + deviceLabel: Yanmi Switch (1 Way) + manufacturer: JNL + model: Y-K001-001 + deviceProfileName: basic-switch + - id: "JNL/Y-K002-001" + deviceLabel: Yanmi Switch (2 Way) 1 + manufacturer: JNL + model: Y-K002-001 + deviceProfileName: basic-switch zigbeeGeneric: - id: "genericSwitch" deviceLabel: Zigbee Switch diff --git a/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml b/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml index 3b4462bce2..6c2da08393 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml @@ -12,10 +12,6 @@ components: range: [1, 100] - id: colorTemperature version: 1 - config: - values: - - key: "colorTemperature.value" - range: [2700, 6000] - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/frient-io-output-switch.yml b/drivers/SmartThings/zigbee-switch/profiles/frient-io-output-switch.yml new file mode 100644 index 0000000000..551061d7b5 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/frient-io-output-switch.yml @@ -0,0 +1,25 @@ +name: frient-io-output-switch +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: refresh + version: 1 +preferences: + - title: "Output: On Time" + name: configOnTime + required: true + preferenceType: integer + definition: + minimum: 0 + maximum: 6553 + default: 0 + - title: "Output: Off Wait Time" + name: configOffWaitTime + required: true + preferenceType: integer + definition: + minimum: 0 + maximum: 6553 + default: 0 \ No newline at end of file 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..ff670c0097 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml @@ -0,0 +1,231 @@ +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: 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" + "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." + 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=0" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 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. + (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: "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: "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/profiles/inovelli-vzm32-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml new file mode 100644 index 0000000000..746890a15a --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml @@ -0,0 +1,355 @@ +name: inovelli-vzm32-sn +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + - id: motionSensor + version: 1 + - id: illuminanceMeasurement + version: 1 + config: + values: + - key: "illuminance.value" + range: [0, 5000] + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: refresh + 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 (default)" + "1": "On/Off" + 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: "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: "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 + - name: "parameter101" + title: "101. mmWave Height Minimum (Floor)" + description: "Minimum range of the Z-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: -300 + - name: "parameter102" + title: "102. mmWave Height Maximum (Ceiling)" + description: "Maximum range of the Z-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: 300 + - name: "parameter103" + title: "103. mmWave Width Minimum (Left)" + description: "Minimum range of the X-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: -600 + - name: "parameter104" + title: "104. mmWave Width Maximum (Right)" + description: "Maximum range of the X-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: 600 + - name: "parameter105" + title: "105. mmWave Depth Minimum (Near)" + description: "Minimum range of the Y-Axis in cm" + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 600 + default: 0 + - name: "parameter106" + title: "106. mmWave Depth Maximum (Far)" + description: "Maximum range of the Y-Axis in cm" + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 600 + default: 600 + - name: "parameter110" + title: "110. Light On Presence Behavior" + description: "When presence is detected, choose how to control the light load" + required: true + preferenceType: enumeration + definition: + options: + "0": "Disabled" + "1": "Auto On/Off when occupied (default)" + "2": "Auto Off when vacant" + "3": "Auto On when occupied" + "4": "Auto On/Off when Vacant" + "5": "Auto On when Vacant" + "6": "Auto Off when Occupied" + default: 1 + - name: "parameter111" + title: "111. mmWave Control Commands" + description: "Advanced commands to send to the mmWave Module (Please see documentation)" + required: false + preferenceType: enumeration + definition: + options: + "1": "Set Interference Area" + "3": "Clear Interference Area" + "0": "Factory Reset Module" + default: 3 + - name: "parameter112" + title: "112. mmWave Detection Sensitivity" + description: "Adjust the sensitivity of the mmWave sensor. 0-Low, 1-Medium, 2-High." + required: false + preferenceType: enumeration + definition: + options: + "0": "Low" + "1": "Medium" + "2": "High (default)" + default: 2 + - name: "parameter113" + title: "113. mmWave Detection Delay" + description: "The time from detecting a person to triggering an action. 0-Low (5s), 1-Medium (1s), 2-Fast (0.2s)." + required: false + preferenceType: enumeration + definition: + options: + "0": "5 seconds" + "1": "1 second" + "2": "0.2 seconds (default)" + default: 2 + - name: "parameter114" + title: "114. mmWave Time Out" + description: "Adjust the timeout after presence is no longer detected. After the timeout the load will turn off." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 4294967295 + default: 30 + - name: "parameter34" + title: "34. OTA Image Type" + description: "Which endpoint should the switch advertise for OTA update (Zigbee, mmWave, or both)." + required: true + preferenceType: enumeration + definition: + options: + "0": "Zigbee (default)" + "1": "mmWave" + "2": "Alternating" + default: 0 diff --git a/drivers/SmartThings/zigbee-switch/profiles/switch-4inputs-2outputs.yml b/drivers/SmartThings/zigbee-switch/profiles/switch-4inputs-2outputs.yml new file mode 100644 index 0000000000..c6120b2561 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/switch-4inputs-2outputs.yml @@ -0,0 +1,107 @@ +name: switch-4inputs-2outputs +components: + - id: main + capabilities: + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Switch + - id: input1 + label: "Input 1" + capabilities: + - id: switch + version: 1 + - id: input2 + label: "Input 2" + capabilities: + - id: switch + version: 1 + - id: input3 + label: "Input 3" + capabilities: + - id: switch + version: 1 + - id: input4 + label: "Input 4" + capabilities: + - id: switch + version: 1 +preferences: + # Input 1 + - title: "Input 1: Reverse Polarity" + name: reversePolarity1 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 1: Control Output 1" + name: controlOutput11 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 1: Control Output 2" + name: controlOutput21 + required: true + preferenceType: boolean + definition: + default: false + # Input 2 + - title: "Input 2: Reverse Polarity" + name: reversePolarity2 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 2: Control Output 1" + name: controlOutput12 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 2: Control Output 2" + name: controlOutput22 + required: true + preferenceType: boolean + definition: + default: false + # Input 3 + - title: "Input 3: Reverse Polarity" + name: reversePolarity3 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 3: Control Output 1" + name: controlOutput13 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 3: Control Output 2" + name: controlOutput23 + required: true + preferenceType: boolean + definition: + default: false + # Input 4 + - title: "Input 4: Reverse Polarity" + name: reversePolarity4 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 4: Control Output 1" + name: controlOutput14 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 4: Control Output 2" + name: controlOutput24 + required: true + preferenceType: boolean + definition: + default: false \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/aqara-light/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/aqara-light/can_handle.lua new file mode 100644 index 0000000000..7b20caf5c7 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/aqara-light/can_handle.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.light.acn004" }, + { mfr = "Aqara", model = "lumi.light.acn014" }, + { mfr = "LUMI", model = "lumi.light.cwacn1" } + } + + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("aqara-light") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua index 3009cb6d0c..2192716b21 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" @@ -12,30 +15,27 @@ local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 local MFG_CODE = 0x115F -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.light.acn004" }, - { mfr = "Aqara", model = "lumi.light.acn014" } -} - -local function is_aqara_products(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("aqara-light") - return true, subdriver - end - end - return false -end - local function do_refresh(self, device) device:send(OnOff.attributes.OnOff:read(device)) device:send(Level.attributes.CurrentLevel:read(device)) device:send(ColorControl.attributes.ColorTemperatureMireds:read(device)) end +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + local function device_added(driver, device, event) device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1)) -- private + + local value = { minimum = 2700, maximum = 6000 } + if device:get_model() == "lumi.light.cwacn1" then + value.maximum = 6500 + end + emit_event_if_latest_state_missing(device, "main", capabilities.colorTemperature, capabilities.colorTemperature.colorTemperatureRange.NAME, capabilities.colorTemperature.colorTemperatureRange(value)) end local function do_configure(self, device) @@ -65,7 +65,7 @@ local aqara_light_handler = { [capabilities.switchLevel.commands.setLevel.NAME] = set_level_handler } }, - can_handle = is_aqara_products + can_handle = require("aqara-light.can_handle"), } return aqara_light_handler diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/aqara/can_handle.lua new file mode 100644 index 0000000000..e183f78266 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/aqara/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("aqara") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..63f94e2be3 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/aqara/fingerprints.lua @@ -0,0 +1,22 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "LUMI", model = "lumi.plug.maeu01" }, + { mfr = "LUMI", model = "lumi.plug.macn01" }, + { mfr = "LUMI", model = "lumi.switch.n0agl1" }, + { mfr = "LUMI", model = "lumi.switch.l0agl1" }, + { mfr = "LUMI", model = "lumi.switch.n0acn2" }, + { mfr = "LUMI", model = "lumi.switch.n1acn1" }, + { mfr = "LUMI", model = "lumi.switch.n2acn1" }, + { mfr = "LUMI", model = "lumi.switch.n3acn1" }, + { mfr = "LUMI", model = "lumi.switch.b1laus01" }, + { mfr = "LUMI", model = "lumi.switch.b2laus01" }, + { mfr = "LUMI", model = "lumi.switch.n1aeu1" }, + { mfr = "LUMI", model = "lumi.switch.n2aeu1" }, + { mfr = "LUMI", model = "lumi.switch.l1aeu1" }, + { mfr = "LUMI", model = "lumi.switch.l2aeu1" }, + { mfr = "LUMI", model = "lumi.switch.b1nacn01" }, + { mfr = "LUMI", model = "lumi.switch.b2nacn01" }, + { mfr = "LUMI", model = "lumi.switch.b3n01" } + } diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara/init.lua index 6558f7f282..0c64b594fe 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" @@ -24,26 +27,6 @@ local ELECTRIC_SWITCH_TYPE_ATTRIBUTE_ID = 0x000A local LAST_REPORT_TIME = "LAST_REPORT_TIME" local PRIVATE_MODE = "PRIVATE_MODE" -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.plug.maeu01" }, - { mfr = "LUMI", model = "lumi.plug.macn01" }, - { mfr = "LUMI", model = "lumi.switch.n0agl1" }, - { mfr = "LUMI", model = "lumi.switch.l0agl1" }, - { mfr = "LUMI", model = "lumi.switch.n0acn2" }, - { mfr = "LUMI", model = "lumi.switch.n1acn1" }, - { mfr = "LUMI", model = "lumi.switch.n2acn1" }, - { mfr = "LUMI", model = "lumi.switch.n3acn1" }, - { mfr = "LUMI", model = "lumi.switch.b1laus01" }, - { mfr = "LUMI", model = "lumi.switch.b2laus01" }, - { mfr = "LUMI", model = "lumi.switch.n1aeu1" }, - { mfr = "LUMI", model = "lumi.switch.n2aeu1" }, - { mfr = "LUMI", model = "lumi.switch.l1aeu1" }, - { mfr = "LUMI", model = "lumi.switch.l2aeu1" }, - { mfr = "LUMI", model = "lumi.switch.b1nacn01" }, - { mfr = "LUMI", model = "lumi.switch.b2nacn01" }, - { mfr = "LUMI", model = "lumi.switch.b3n01" } -} - local preference_map = { ["stse.restorePowerState"] = { cluster_id = PRIVATE_CLUSTER_ID, @@ -137,15 +120,6 @@ local preference_map = { }, } -local function is_aqara_products(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("aqara") - return true, subdriver - end - end - return false -end local function private_mode_handler(driver, device, value, zb_rx) device:set_field(PRIVATE_MODE, value.value, { persist = true }) @@ -286,7 +260,7 @@ local aqara_switch_handler = { require("aqara.version"), require("aqara.multi-switch") }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } return aqara_switch_handler diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/can_handle.lua new file mode 100644 index 0000000000..1524296cff --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local FINGERPRINTS = require("aqara.multi-switch.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara.multi-switch") + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/fingerprints.lua new file mode 100644 index 0000000000..71ed1b26b3 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/fingerprints.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "LUMI", model = "lumi.switch.n1acn1", children = 1, child_profile = "" }, + { mfr = "LUMI", model = "lumi.switch.n2acn1", children = 2, child_profile = "aqara-switch-child" }, + { mfr = "LUMI", model = "lumi.switch.n3acn1", children = 3, child_profile = "aqara-switch-child" }, + { mfr = "LUMI", model = "lumi.switch.b1laus01", children = 1, child_profile = "" }, + { mfr = "LUMI", model = "lumi.switch.b2laus01", children = 2, child_profile = "aqara-switch-child" }, + { mfr = "LUMI", model = "lumi.switch.l2aeu1", children = 2, child_profile = "aqara-switch-child" }, + { mfr = "LUMI", model = "lumi.switch.n2aeu1", children = 2, child_profile = "aqara-switch-child" }, + { mfr = "LUMI", model = "lumi.switch.b2nacn01", children = 2, child_profile = "aqara-switch-child" }, + { mfr = "LUMI", model = "lumi.switch.b3n01", children = 3, child_profile = "aqara-switch-child" } +} diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua index 1c1431f518..9280a30d0d 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua @@ -1,33 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_lib = require "st.device" local capabilities = require "st.capabilities" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" local configurations = require "configurations" +local switch_utils = require "switch_utils" local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 local MFG_CODE = 0x115F - -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.switch.n1acn1", children = 1, child_profile = "" }, - { mfr = "LUMI", model = "lumi.switch.n2acn1", children = 2, child_profile = "aqara-switch-child" }, - { mfr = "LUMI", model = "lumi.switch.n3acn1", children = 3, child_profile = "aqara-switch-child" }, - { mfr = "LUMI", model = "lumi.switch.b1laus01", children = 1, child_profile = "" }, - { mfr = "LUMI", model = "lumi.switch.b2laus01", children = 2, child_profile = "aqara-switch-child" }, - { mfr = "LUMI", model = "lumi.switch.l2aeu1", children = 2, child_profile = "aqara-switch-child" }, - { mfr = "LUMI", model = "lumi.switch.n2aeu1", children = 2, child_profile = "aqara-switch-child" }, - { mfr = "LUMI", model = "lumi.switch.b2nacn01", children = 2, child_profile = "aqara-switch-child" }, - { mfr = "LUMI", model = "lumi.switch.b3n01", children = 3, child_profile = "aqara-switch-child" } -} - -local function is_aqara_products(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end +local FINGERPRINTS = require("aqara.multi-switch.fingerprints") local function get_children_amount(device) for _, fingerprint in ipairs(FINGERPRINTS) do @@ -89,7 +73,7 @@ local function device_added(driver, device) end device:emit_event(capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } })) - device:emit_event(capabilities.button.button.pushed({ state_change = false })) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local function device_init(self, device) @@ -105,7 +89,7 @@ local aqara_multi_switch_handler = { init = configurations.power_reconfig_wrapper(device_init), added = device_added }, - can_handle = is_aqara_products + can_handle = require("aqara.multi-switch.can_handle"), } return aqara_multi_switch_handler diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/aqara/sub_drivers.lua new file mode 100644 index 0000000000..d064785cb4 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/aqara/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("aqara.multi-switch"), + lazy_load("aqara.version"), +} diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/version/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/aqara/version/can_handle.lua new file mode 100644 index 0000000000..d56744fefa --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/aqara/version/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function (opts, driver, device) + local PRIVATE_MODE = "PRIVATE_MODE" + local private_mode = device:get_field(PRIVATE_MODE) or 0 + local res = private_mode == 1 + if res then + return res, require("aqara.version") + else + return res + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/version/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara/version/init.lua index 01767da106..bca3e9fb88 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/version/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/version/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local constants = require "st.zigbee.constants" @@ -102,10 +105,7 @@ local aqara_switch_version_handler = { } } }, - can_handle = function (opts, driver, device) - local private_mode = device:get_field(PRIVATE_MODE) or 0 - return private_mode == 1 - end + can_handle = require("aqara.version.can_handle") } return aqara_switch_version_handler diff --git a/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/can_handle.lua new file mode 100644 index 0000000000..49b42ddb35 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/can_handle.lua @@ -0,0 +1,24 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- There are reports of at least one device (SONOFF 01MINIZB) which occasionally +-- reports this value as an Int8, rather than a Boolean, as per the spec +return function(opts, driver, device, zb_rx, ...) + local zcl_clusters = require "st.zigbee.zcl.clusters" + local data_types = require "st.zigbee.data_types" + local can_handle = opts.dispatcher_class == "ZigbeeMessageDispatcher" and + device:get_manufacturer() == "SONOFF" and + zb_rx.body and + zb_rx.body.zcl_body and + zb_rx.body.zcl_body.attr_records and + zb_rx.address_header.cluster.value == zcl_clusters.OnOff.ID and + zb_rx.body.zcl_body.attr_records[1].attr_id.value == zcl_clusters.OnOff.attributes.OnOff.ID and + zb_rx.body.zcl_body.attr_records[1].data_type.value ~= data_types.Boolean.ID + if can_handle then + local subdriver = require("bad_on_off_data_type") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/init.lua b/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/init.lua index ee0c82977e..2a59166bbb 100644 --- a/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/init.lua @@ -1,40 +1,9 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local zcl_clusters = require "st.zigbee.zcl.clusters" -local data_types = require "st.zigbee.data_types" local capabilities = require "st.capabilities" --- There are reports of at least one device (SONOFF 01MINIZB) which occasionally --- reports this value as an Int8, rather than a Boolean, as per the spec -local function incorrect_data_type_detected(opts, driver, device, zb_rx, ...) - local can_handle = opts.dispatcher_class == "ZigbeeMessageDispatcher" and - device:get_manufacturer() == "SONOFF" and - zb_rx.body and - zb_rx.body.zcl_body and - zb_rx.body.zcl_body.attr_records and - zb_rx.address_header.cluster.value == zcl_clusters.OnOff.ID and - zb_rx.body.zcl_body.attr_records[1].attr_id.value == zcl_clusters.OnOff.attributes.OnOff.ID and - zb_rx.body.zcl_body.attr_records[1].data_type.value ~= data_types.Boolean.ID - if can_handle then - local subdriver = require("bad_on_off_data_type") - return true, subdriver - else - return false - end -end - local function on_off_attr_handler(driver, device, value, zb_rx) local attr = capabilities.switch.switch device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, value.value == 0 and attr.off() or attr.on()) @@ -49,7 +18,7 @@ local bad_on_off_data_type = { } } }, - can_handle = incorrect_data_type_detected + can_handle = require("bad_on_off_data_type.can_handle"), } -return bad_on_off_data_type \ No newline at end of file +return bad_on_off_data_type diff --git a/drivers/SmartThings/zigbee-switch/src/configurations.lua b/drivers/SmartThings/zigbee-switch/src/configurations.lua deleted file mode 100644 index f465046e72..0000000000 --- a/drivers/SmartThings/zigbee-switch/src/configurations.lua +++ /dev/null @@ -1,243 +0,0 @@ --- Copyright 2022 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 constants = require "st.zigbee.constants" -local capabilities = require "st.capabilities" -local device_def = require "st.device" - -local ColorControl = clusters.ColorControl -local IASZone = clusters.IASZone -local ElectricalMeasurement = clusters.ElectricalMeasurement -local SimpleMetering = clusters.SimpleMetering -local Alarms = clusters.Alarms -local Status = require "st.zigbee.generated.types.ZclStatus" - -local CONFIGURATION_VERSION_KEY = "_configuration_version" -local CONFIGURATION_ATTEMPTED = "_reconfiguration_attempted" - -local devices = { - IKEA_RGB_BULB = { - FINGERPRINTS = { - { mfr = "IKEA of Sweden", model = "TRADFRI bulb E27 CWS opal 600lm" }, - { mfr = "IKEA of Sweden", model = "TRADFRI bulb E26 CWS opal 600lm" } - }, - CONFIGURATION = { - { - cluster = ColorControl.ID, - attribute = ColorControl.attributes.CurrentX.ID, - minimum_interval = 1, - maximum_interval = 3600, - data_type = ColorControl.attributes.CurrentX.base_type, - reportable_change = 16 - }, - { - cluster = ColorControl.ID, - attribute = ColorControl.attributes.CurrentY.ID, - minimum_interval = 1, - maximum_interval = 3600, - data_type = ColorControl.attributes.CurrentY.base_type, - reportable_change = 16 - } - } - }, - SENGLED_BULB_WITH_MOTION_SENSOR = { - FINGERPRINTS = { - { mfr = "sengled", model = "E13-N11" } - }, - CONFIGURATION = { - { - cluster = IASZone.ID, - attribute = IASZone.attributes.ZoneStatus.ID, - minimum_interval = 30, - maximum_interval = 300, - data_type = IASZone.attributes.ZoneStatus.base_type - } - }, - IAS_ZONE_CONFIG_METHOD = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE - }, - FRIENT_SWITCHES = { - FINGERPRINTS = { - { mfr = "frient A/S", model = "SPLZB-131" }, - { mfr = "frient A/S", model = "SPLZB-132" }, - { mfr = "frient A/S", model = "SPLZB-134" }, - { mfr = "frient A/S", model = "SPLZB-137" }, - { mfr = "frient A/S", model = "SPLZB-141" }, - { mfr = "frient A/S", model = "SPLZB-142" }, - { mfr = "frient A/S", model = "SPLZB-144" }, - { mfr = "frient A/S", model = "SPLZB-147" }, - { mfr = "frient A/S", model = "SMRZB-143" }, - { mfr = "frient A/S", model = "SMRZB-153" }, - { mfr = "frient A/S", model = "SMRZB-332" }, - { mfr = "frient A/S", model = "SMRZB-342" } - }, - CONFIGURATION = { - { - cluster = ElectricalMeasurement.ID, - attribute = ElectricalMeasurement.attributes.RMSVoltage.ID, - minimum_interval = 5, - maximum_interval = 3600, - data_type = ElectricalMeasurement.attributes.RMSVoltage.base_type, - reportable_change = 1 - },{ - cluster = ElectricalMeasurement.ID, - attribute = ElectricalMeasurement.attributes.RMSCurrent.ID, - minimum_interval = 5, - maximum_interval = 3600, - data_type = ElectricalMeasurement.attributes.RMSCurrent.base_type, - reportable_change = 1 - },{ - cluster = ElectricalMeasurement.ID, - attribute = ElectricalMeasurement.attributes.ActivePower.ID, - minimum_interval = 5, - maximum_interval = 3600, - data_type = ElectricalMeasurement.attributes.ActivePower.base_type, - reportable_change = 1 - },{ - cluster = SimpleMetering.ID, - attribute = SimpleMetering.attributes.InstantaneousDemand.ID, - minimum_interval = 5, - maximum_interval = 3600, - data_type = SimpleMetering.attributes.InstantaneousDemand.base_type, - reportable_change = 1 - },{ - cluster = SimpleMetering.ID, - attribute = SimpleMetering.attributes.CurrentSummationDelivered.ID, - minimum_interval = 5, - maximum_interval = 3600, - data_type = SimpleMetering.attributes.CurrentSummationDelivered.base_type, - reportable_change = 1 - },{ - cluster = Alarms.ID, - attribute = Alarms.attributes.AlarmCount.ID, - minimum_interval = 1, - maximum_interval = 3600, - data_type = Alarms.attributes.AlarmCount.base_type, - reportable_change = 1, - }, - } - }, -} - - -local configurations = {} - -local active_power_configuration = { - cluster = clusters.ElectricalMeasurement.ID, - attribute = clusters.ElectricalMeasurement.attributes.ActivePower.ID, - minimum_interval = 5, - maximum_interval = 3600, - data_type = clusters.ElectricalMeasurement.attributes.ActivePower.base_type, - reportable_change = 5 -} - -local instantaneous_demand_configuration = { - cluster = clusters.SimpleMetering.ID, - attribute = clusters.SimpleMetering.attributes.InstantaneousDemand.ID, - minimum_interval = 5, - maximum_interval = 3600, - data_type = clusters.SimpleMetering.attributes.InstantaneousDemand.base_type, - reportable_change = 5 -} - -configurations.check_and_reconfig_devices = function(driver) - for device_id, device in pairs(driver.device_cache) do - local config_version = device:get_field(CONFIGURATION_VERSION_KEY) - if config_version == nil or config_version < driver.current_config_version then - if device:supports_capability(capabilities.powerMeter) then - if device:supports_server_cluster(clusters.ElectricalMeasurement.ID) then - -- Increase minimum reporting interval to 5 seconds - device:send(clusters.ElectricalMeasurement.attributes.ActivePower:configure_reporting(device, 5, 600, 5)) - device:add_configured_attribute(active_power_configuration) - end - if device:supports_server_cluster(clusters.SimpleMetering.ID) then - -- Increase minimum reporting interval to 5 seconds - device:send(clusters.SimpleMetering.attributes.InstantaneousDemand:configure_reporting(device, 5, 600, 5)) - device:add_configured_attribute(instantaneous_demand_configuration) - end - end - device:set_field(CONFIGURATION_ATTEMPTED, true, {persist = true}) - end - end - driver._reconfig_timer = nil -end - -configurations.handle_reporting_config_response = function(driver, device, zb_mess) - local dev = device - local find_child_fn = device:get_field(device_def.FIND_CHILD_KEY) - if find_child_fn ~= nil then - local child = find_child_fn(device, zb_mess.address_header.src_endpoint.value) - if child ~= nil then - dev = child - end - end - if dev:get_field(CONFIGURATION_ATTEMPTED) == true then - if zb_mess.body.zcl_body.global_status ~= nil and zb_mess.body.zcl_body.global_status.value == Status.SUCCESS then - dev:set_field(CONFIGURATION_VERSION_KEY, driver.current_config_version, {persist = true}) - elseif zb_mess.body.zcl_body.config_records ~= nil then - local config_records = zb_mess.body.zcl_body.config_records - for _, record in ipairs(config_records) do - if zb_mess.address_header.cluster.value == clusters.SimpleMetering.ID then - if record.attr_id.value == clusters.SimpleMetering.attributes.InstantaneousDemand.ID - and record.status.value == Status.SUCCESS then - dev:set_field(CONFIGURATION_VERSION_KEY, driver.current_config_version, {persist = true}) - end - elseif zb_mess.address_header.cluster.value == clusters.ElectricalMeasurement.ID then - if record.attr_id.value == clusters.ElectricalMeasurement.attributes.ActivePower.ID - and record.status.value == Status.SUCCESS then - dev:set_field(CONFIGURATION_VERSION_KEY, driver.current_config_version, {persist = true}) - end - end - - end - end - end -end - -configurations.power_reconfig_wrapper = function(orig_function) - local new_init = function(driver, device) - local config_version = device:get_field(CONFIGURATION_VERSION_KEY) - if config_version == nil or config_version < driver.current_config_version then - if driver._reconfig_timer == nil then - driver._reconfig_timer = driver:call_with_delay(5*60, configurations.check_and_reconfig_devices, "reconfig_power_devices") - end - end - orig_function(driver, device) - end - return new_init -end - -configurations.get_device_configuration = function(zigbee_device) - for _, device in pairs(devices) do - for _, fingerprint in pairs(device.FINGERPRINTS) do - if zigbee_device:get_manufacturer() == fingerprint.mfr and zigbee_device:get_model() == fingerprint.model then - return device.CONFIGURATION - end - end - end - return nil -end - -configurations.get_ias_zone_config_method = function(zigbee_device) - for _, device in pairs(devices) do - for _, fingerprint in pairs(device.FINGERPRINTS) do - if zigbee_device:get_manufacturer() == fingerprint.mfr and zigbee_device:get_model() == fingerprint.model then - return device.IAS_ZONE_CONFIG_METHOD - end - end - end - return nil -end - -return configurations diff --git a/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua b/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua new file mode 100644 index 0000000000..d49d10e9c5 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua @@ -0,0 +1,176 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.zigbee.zcl.clusters" +local ColorControl = clusters.ColorControl +local IASZone = clusters.IASZone +local ElectricalMeasurement = clusters.ElectricalMeasurement +local SimpleMetering = clusters.SimpleMetering +local Alarms = clusters.Alarms +local BasicInput = clusters.BasicInput +local OnOff = clusters.OnOff +local constants = require "st.zigbee.constants" +local data_types = require "st.zigbee.data_types" + +local devices = { + IKEA_RGB_BULB = { + FINGERPRINTS = { + { mfr = "IKEA of Sweden", model = "TRADFRI bulb E27 CWS opal 600lm" }, + { mfr = "IKEA of Sweden", model = "TRADFRI bulb E26 CWS opal 600lm" } + }, + CONFIGURATION = { + { + cluster = ColorControl.ID, + attribute = ColorControl.attributes.CurrentX.ID, + minimum_interval = 1, + maximum_interval = 3600, + data_type = ColorControl.attributes.CurrentX.base_type, + reportable_change = 16 + }, + { + cluster = ColorControl.ID, + attribute = ColorControl.attributes.CurrentY.ID, + minimum_interval = 1, + maximum_interval = 3600, + data_type = ColorControl.attributes.CurrentY.base_type, + reportable_change = 16 + } + } + }, + SENGLED_BULB_WITH_MOTION_SENSOR = { + FINGERPRINTS = { + { mfr = "sengled", model = "E13-N11" } + }, + CONFIGURATION = { + { + cluster = IASZone.ID, + attribute = IASZone.attributes.ZoneStatus.ID, + minimum_interval = 30, + maximum_interval = 300, + data_type = IASZone.attributes.ZoneStatus.base_type + } + }, + IAS_ZONE_CONFIG_METHOD = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE + }, + FRIENT_SWITCHES = { + FINGERPRINTS = { + { mfr = "frient A/S", model = "SPLZB-131" }, + { mfr = "frient A/S", model = "SPLZB-132" }, + { mfr = "frient A/S", model = "SPLZB-134" }, + { mfr = "frient A/S", model = "SPLZB-137" }, + { mfr = "frient A/S", model = "SPLZB-141" }, + { mfr = "frient A/S", model = "SPLZB-142" }, + { mfr = "frient A/S", model = "SPLZB-144" }, + { mfr = "frient A/S", model = "SPLZB-147" }, + { mfr = "frient A/S", model = "SMRZB-143" }, + { mfr = "frient A/S", model = "SMRZB-153" }, + { mfr = "frient A/S", model = "SMRZB-332" }, + { mfr = "frient A/S", model = "SMRZB-342" } + }, + CONFIGURATION = { + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSVoltage.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = ElectricalMeasurement.attributes.RMSVoltage.base_type, + reportable_change = 1 + },{ + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSCurrent.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = ElectricalMeasurement.attributes.RMSCurrent.base_type, + reportable_change = 1 + },{ + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.ActivePower.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = ElectricalMeasurement.attributes.ActivePower.base_type, + reportable_change = 1 + },{ + cluster = SimpleMetering.ID, + attribute = SimpleMetering.attributes.InstantaneousDemand.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = SimpleMetering.attributes.InstantaneousDemand.base_type, + reportable_change = 1 + },{ + cluster = SimpleMetering.ID, + attribute = SimpleMetering.attributes.CurrentSummationDelivered.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = SimpleMetering.attributes.CurrentSummationDelivered.base_type, + reportable_change = 1 + },{ + cluster = Alarms.ID, + attribute = Alarms.attributes.AlarmCount.ID, + minimum_interval = 1, + maximum_interval = 3600, + data_type = Alarms.attributes.AlarmCount.base_type, + reportable_change = 1, + }, + } + }, + FRIENT_IO_MODULE = { + FINGERPRINTS = { + { mfr = "frient A/S", model = "IOMZB-110" } + }, + CONFIGURATION = { + { + cluster = OnOff.ID, + attribute = OnOff.attributes.OnTime.ID, + minimum_interval = 10, + maximum_interval = 600, + reportable_change = 1, + data_type = OnOff.attributes.OnOff.base_type, + configurable = true, + monitored = true + }, + { + cluster = OnOff.ID, + attribute = OnOff.attributes.OffWaitTime.ID, + minimum_interval = 10, + maximum_interval = 600, + reportable_change = 1, + data_type = OnOff.attributes.OffWaitTime.base_type, + configurable = true, + monitored = true + }, + { + cluster = BasicInput.ID, + attribute = BasicInput.attributes.PresentValue.ID, + minimum_interval = 10, + maximum_interval = 600, + reportable_change = 0, + data_type = BasicInput.attributes.PresentValue.base_type, + configurable = true, + monitored = true + }, + { + cluster = BasicInput.ID, + attribute = BasicInput.attributes.Polarity.ID, + minimum_interval = 10, + maximum_interval = 600, + reportable_change = 0, + data_type = BasicInput.attributes.Polarity.base_type, + configurable = true, + monitored = true + }, + { + cluster = BasicInput.ID, + attribute = 0x8000, -- IASActivation + minimum_interval = 10, + maximum_interval = 600, + reportable_change = 0, + data_type = data_types.Uint16, + mfg_code = 0x1015, + configurable = true, + monitored = true + } + } + } +} + +return devices \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/configurations/init.lua b/drivers/SmartThings/zigbee-switch/src/configurations/init.lua new file mode 100644 index 0000000000..01f42abf91 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/configurations/init.lua @@ -0,0 +1,124 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local CONFIGURATION_VERSION_KEY = "_configuration_version" +local CONFIGURATION_ATTEMPTED = "_reconfiguration_attempted" + +local configurations = {} + +configurations.check_and_reconfig_devices = function(driver) + local clusters = require "st.zigbee.zcl.clusters" + local capabilities = require "st.capabilities" + local instantaneous_demand_configuration = { + cluster = clusters.SimpleMetering.ID, + attribute = clusters.SimpleMetering.attributes.InstantaneousDemand.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = clusters.SimpleMetering.attributes.InstantaneousDemand.base_type, + reportable_change = 5 + } + local active_power_configuration = { + cluster = clusters.ElectricalMeasurement.ID, + attribute = clusters.ElectricalMeasurement.attributes.ActivePower.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = clusters.ElectricalMeasurement.attributes.ActivePower.base_type, + reportable_change = 5 + } + + for device_id, device in pairs(driver.device_cache) do + local config_version = device:get_field(CONFIGURATION_VERSION_KEY) + if config_version == nil or config_version < driver.current_config_version then + if device:supports_capability(capabilities.powerMeter) then + if device:supports_server_cluster(clusters.ElectricalMeasurement.ID) then + -- Increase minimum reporting interval to 5 seconds + device:send(clusters.ElectricalMeasurement.attributes.ActivePower:configure_reporting(device, 5, 600, 5)) + device:add_configured_attribute(active_power_configuration) + end + if device:supports_server_cluster(clusters.SimpleMetering.ID) then + -- Increase minimum reporting interval to 5 seconds + device:send(clusters.SimpleMetering.attributes.InstantaneousDemand:configure_reporting(device, 5, 600, 5)) + device:add_configured_attribute(instantaneous_demand_configuration) + end + end + device:set_field(CONFIGURATION_ATTEMPTED, true, {persist = true}) + end + end + driver._reconfig_timer = nil +end + +configurations.handle_reporting_config_response = function(driver, device, zb_mess) + local Status = require "st.zigbee.generated.types.ZclStatus" + local clusters = require "st.zigbee.zcl.clusters" + local device_def = require "st.device" + + local dev = device + local find_child_fn = device:get_field(device_def.FIND_CHILD_KEY) + if find_child_fn ~= nil then + local child = find_child_fn(device, zb_mess.address_header.src_endpoint.value) + if child ~= nil then + dev = child + end + end + if dev:get_field(CONFIGURATION_ATTEMPTED) == true then + if zb_mess.body.zcl_body.global_status ~= nil and zb_mess.body.zcl_body.global_status.value == Status.SUCCESS then + dev:set_field(CONFIGURATION_VERSION_KEY, driver.current_config_version, {persist = true}) + elseif zb_mess.body.zcl_body.config_records ~= nil then + local config_records = zb_mess.body.zcl_body.config_records + for _, record in ipairs(config_records) do + if zb_mess.address_header.cluster.value == clusters.SimpleMetering.ID then + if record.attr_id.value == clusters.SimpleMetering.attributes.InstantaneousDemand.ID + and record.status.value == Status.SUCCESS then + dev:set_field(CONFIGURATION_VERSION_KEY, driver.current_config_version, {persist = true}) + end + elseif zb_mess.address_header.cluster.value == clusters.ElectricalMeasurement.ID then + if record.attr_id.value == clusters.ElectricalMeasurement.attributes.ActivePower.ID + and record.status.value == Status.SUCCESS then + dev:set_field(CONFIGURATION_VERSION_KEY, driver.current_config_version, {persist = true}) + end + end + + end + end + end +end + +configurations.power_reconfig_wrapper = function(orig_function) + local new_init = function(driver, device) + local config_version = device:get_field(CONFIGURATION_VERSION_KEY) + if config_version == nil or config_version < driver.current_config_version then + if driver._reconfig_timer == nil then + driver._reconfig_timer = driver:call_with_delay(5*60, configurations.check_and_reconfig_devices, "reconfig_power_devices") + end + end + orig_function(driver, device) + end + return new_init +end + +configurations.get_device_configuration = function(zigbee_device) + local devices = require "configurations.devices" + for _, device in pairs(devices) do + for _, fingerprint in pairs(device.FINGERPRINTS) do + if zigbee_device:get_manufacturer() == fingerprint.mfr and zigbee_device:get_model() == fingerprint.model then + return device.CONFIGURATION + end + end + end + return nil +end + +configurations.get_ias_zone_config_method = function(zigbee_device) + local devices = require "configurations.devices" + for _, device in pairs(devices) do + for _, fingerprint in pairs(device.FINGERPRINTS) do + if zigbee_device:get_manufacturer() == fingerprint.mfr and zigbee_device:get_model() == fingerprint.model then + return device.IAS_ZONE_CONFIG_METHOD + end + end + end + return nil +end + +return configurations diff --git a/drivers/SmartThings/zigbee-switch/src/ezex/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/ezex/can_handle.lua new file mode 100644 index 0000000000..4bb61719ff --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/ezex/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local ZIGBEE_METERING_SWITCH_FINGERPRINTS = { + { model = "E240-KR116Z-HA" } + } + + for _, fingerprint in ipairs(ZIGBEE_METERING_SWITCH_FINGERPRINTS) do + if device:get_model() == fingerprint.model then + local subdriver = require("ezex") + return true, subdriver + end + end + + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/ezex/init.lua b/drivers/SmartThings/zigbee-switch/src/ezex/init.lua index f88c39bb75..6c2d9e45e3 100644 --- a/drivers/SmartThings/zigbee-switch/src/ezex/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/ezex/init.lua @@ -1,35 +1,9 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local zigbee_constants = require "st.zigbee.constants" local configurations = require "configurations" -local ZIGBEE_METERING_SWITCH_FINGERPRINTS = { - { model = "E240-KR116Z-HA" } -} - -local is_zigbee_ezex_switch = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_METERING_SWITCH_FINGERPRINTS) do - if device:get_model() == fingerprint.model then - local subdriver = require("ezex") - return true, subdriver - end - end - - return false -end - local do_init = function(self, device) device:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 1000000, {persist = true}) device:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 1000, {persist = true}) @@ -40,7 +14,7 @@ local ezex_switch_handler = { lifecycle_handlers = { init = configurations.power_reconfig_wrapper(do_init) }, - can_handle = is_zigbee_ezex_switch + can_handle = require("ezex.can_handle"), } return ezex_switch_handler diff --git a/drivers/SmartThings/zigbee-switch/src/frient-IO/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/frient-IO/can_handle.lua new file mode 100644 index 0000000000..4e1d465ee3 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/frient-IO/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Function to determine if the driver can handle this device +return function(opts, driver, device, ...) + if device:get_manufacturer() == "frient A/S" and device:get_model() == "IOMZB-110" then + local subdriver = require("frient-IO") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/frient-IO/init.lua b/drivers/SmartThings/zigbee-switch/src/frient-IO/init.lua new file mode 100644 index 0000000000..293583f442 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/frient-IO/init.lua @@ -0,0 +1,488 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Zigbee Spec Utils +local constants = require "st.zigbee.constants" +local messages = require "st.zigbee.messages" +local zdo_messages = require "st.zigbee.zdo" +local bind_request = require "st.zigbee.zdo.bind_request" +local unbind_request = require "frient-IO.unbind_request" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" +local zcl_global_commands = require "st.zigbee.zcl.global_commands" +local Status = require "st.zigbee.generated.types.ZclStatus" + +local clusters = require "st.zigbee.zcl.clusters" +local BasicInput = clusters.BasicInput +local OnOff = clusters.OnOff +-- Capabilities +local capabilities = require "st.capabilities" +local Switch = capabilities.switch +local CHILD_OUTPUT_PROFILE = "frient-io-output-switch" +local utils = require "st.utils" + +local configurationMap = require "configurations" + +local COMPONENTS = { + INPUT_1 = "input1", + INPUT_2 = "input2", + INPUT_3 = "input3", + INPUT_4 = "input4", + OUTPUT_1 = "output1", + OUTPUT_2 = "output2" +} + +local ZIGBEE_ENDPOINTS = { + INPUT_1 = 0x70, + INPUT_2 = 0x71, + INPUT_3 = 0x72, + INPUT_4 = 0x73, + OUTPUT_1 = 0x74, + OUTPUT_2 = 0x75 +} + +local INPUT_CONFIGS = { + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_1, + reverse_pref = "reversePolarity1", + binds = { + { pref = "controlOutput11", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_1 }, + { pref = "controlOutput21", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_2 } + } + }, + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_2, + reverse_pref = "reversePolarity2", + binds = { + { pref = "controlOutput12", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_1 }, + { pref = "controlOutput22", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_2 } + } + }, + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_3, + reverse_pref = "reversePolarity3", + binds = { + { pref = "controlOutput13", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_1 }, + { pref = "controlOutput23", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_2 } + } + }, + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_4, + reverse_pref = "reversePolarity4", + binds = { + { pref = "controlOutput14", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_1 }, + { pref = "controlOutput24", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_2 } + } + } +} + +local OUTPUT_INFO = { + ["1"] = { endpoint = ZIGBEE_ENDPOINTS.OUTPUT_1, key = "frient-io-output-1", label_suffix = "Output 1" }, + ["2"] = { endpoint = ZIGBEE_ENDPOINTS.OUTPUT_2, key = "frient-io-output-2", label_suffix = "Output 2" } +} + +local OUTPUT_BY_ENDPOINT, OUTPUT_BY_KEY = {}, {} +for suffix, info in pairs(OUTPUT_INFO) do + info.suffix = suffix + OUTPUT_BY_ENDPOINT[info.endpoint] = info + OUTPUT_BY_KEY[info.key] = info +end + +local ZIGBEE_MFG_CODES = { + Develco = 0x1015 +} + +local ZIGBEE_MFG_ATTRIBUTES = { + client = { + OnWithTimeOff_OnTime = { + ID = 0x8000, + data_type = data_types.Uint16 + }, + OnWithTimeOff_OffWaitTime = { + ID = 0x8001, + data_type = data_types.Uint16 + } + }, + server = { IASActivation = { + ID = 0x8000, + data_type = data_types.Uint16 + } } +} + +local function write_client_manufacturer_specific_attribute(device, cluster_id, attr_id, mfg_specific_code, data_type, + payload) + local message = cluster_base.write_manufacturer_specific_attribute(device, cluster_id, attr_id, mfg_specific_code, + data_type, payload) + + message.body.zcl_header.frame_ctrl:set_direction_client() + return message +end + +local function write_basic_input_polarity_attr(device, ep_id, payload) + local value = data_types.validate_or_build_type(payload and 1 or 0, + BasicInput.attributes.Polarity.base_type, + "payload") + device:send(cluster_base.write_attribute(device, data_types.ClusterId(BasicInput.ID), + data_types.AttributeId(BasicInput.attributes.Polarity.ID), + value):to_endpoint(ep_id)) +end + +local function ensure_child_devices(driver, device) + if device.parent_assigned_child_key ~= nil then + return + end + + for _, info in pairs(OUTPUT_INFO) do + local child = device:get_child_by_parent_assigned_key(info.key) + if child == nil then + driver:try_create_device({ + type = "EDGE_CHILD", + parent_device_id = device.id, + parent_assigned_child_key = info.key, + profile = CHILD_OUTPUT_PROFILE, + label = string.format("%s %s", device.label, info.label_suffix), + vendor_provided_label = info.label_suffix + }) + child = device:get_child_by_parent_assigned_key(info.key) + end + if child then + child:set_field("endpoint", info.endpoint, { persist = true }) + end + end +end + +local function sanitize_timing(value) + local int = math.tointeger(value) or 0 + return utils.clamp_value(int, 0, 0xFFFF) +end + +local function get_output_timing(device, suffix) + local info = OUTPUT_INFO[suffix] + if not info then return 0, 0 end + local child = device:get_child_by_parent_assigned_key(info.key) + local on_time = math.floor((sanitize_timing(device.preferences["configOnTime" .. suffix]))*10) + local off_wait = math.floor((sanitize_timing(device.preferences["configOffWaitTime" .. suffix]))*10) + if child then + on_time = math.floor((sanitize_timing(child.preferences.configOnTime)) * 10) + off_wait = math.floor((sanitize_timing(child.preferences.configOffWaitTime)) * 10) + end + return on_time, off_wait +end + +local function handle_output_command(device, suffix, command_name) + local info = OUTPUT_INFO[suffix] + if info == nil then return end + local config_on_time, config_off_wait_time = get_output_timing(device, suffix) + local endpoint = info.endpoint + + if command_name == "on" then + if config_on_time == 0 then + device:send(OnOff.server.commands.On(device):to_endpoint(endpoint)) + else + device:send(OnOff.server.commands.OnWithTimedOff(device, data_types.Uint8(0), + data_types.Uint16(config_on_time), data_types.Uint16(config_off_wait_time)):to_endpoint(endpoint)) + end + else + device:send(OnOff.server.commands.Off(device):to_endpoint(endpoint)) + end +end + +local function emit_switch_event_for_endpoint(device, endpoint, event) + local info = OUTPUT_BY_ENDPOINT[endpoint] + if info ~= nil then + local child = device:get_child_by_parent_assigned_key(info.key) + if child then + child:emit_event(event) + return + end + end + device:emit_event_for_endpoint(endpoint, event) +end + +local function register_native_switch_handler(device, endpoint) + local field_key = string.format("frient_io_native_%02X", endpoint) + local info = OUTPUT_BY_ENDPOINT[endpoint] + if info ~= nil then + local child = device:get_child_by_parent_assigned_key(info.key) + if child and not child:get_field(field_key) then + child:register_native_capability_attr_handler("switch", "switch") + child:set_field(field_key, true) + end + return + end + + if not device:get_field(field_key) then + device:register_native_capability_attr_handler("switch", "switch") + device:set_field(field_key, true) + end +end + +local function on_off_attr_handler(driver, device, value, zb_message) + local endpoint = zb_message.address_header.src_endpoint.value + register_native_switch_handler(device, endpoint) + emit_switch_event_for_endpoint(device, endpoint, value.value and Switch.switch.on() or Switch.switch.off()) +end + +local function build_bind_request(device, src_cluster, src_ep_id, dest_ep_id) + local addr_header = messages.AddressHeader(constants.HUB.ADDR, constants.HUB.ENDPOINT, device:get_short_address(), + device.fingerprinted_endpoint_id, constants.ZDO_PROFILE_ID, bind_request.BindRequest.ID) + + local bind_req = bind_request.BindRequest(device.zigbee_eui, src_ep_id, + src_cluster, + bind_request.ADDRESS_MODE_64_BIT, device.zigbee_eui, dest_ep_id) + local message_body = zdo_messages.ZdoMessageBody({ + zdo_body = bind_req + }) + local bind_cmd = messages.ZigbeeMessageTx({ + address_header = addr_header, + body = message_body + }) + return bind_cmd +end + +local function build_unbind_request(device, src_cluster, src_ep_id, dest_ep_id) + local addr_header = messages.AddressHeader(constants.HUB.ADDR, constants.HUB.ENDPOINT, device:get_short_address(), + device.fingerprinted_endpoint_id, constants.ZDO_PROFILE_ID, unbind_request.UNBIND_REQUEST_CLUSTER_ID) + + local unbind_req = unbind_request.UnbindRequest(device.zigbee_eui, src_ep_id, + src_cluster, + unbind_request.ADDRESS_MODE_64_BIT, device.zigbee_eui, dest_ep_id) + local message_body = zdo_messages.ZdoMessageBody({ + zdo_body = unbind_req + }) + local bind_cmd = messages.ZigbeeMessageTx({ + address_header = addr_header, + body = message_body + }) + return bind_cmd +end + +local function apply_input_preference_changes(device, old_prefs, config) + if old_prefs[config.reverse_pref] ~= device.preferences[config.reverse_pref] then + write_basic_input_polarity_attr(device, config.endpoint, device.preferences[config.reverse_pref]) + end + + for _, bind_cfg in ipairs(config.binds) do + if old_prefs[bind_cfg.pref] ~= device.preferences[bind_cfg.pref] then + device:send(device.preferences[bind_cfg.pref] + and build_bind_request(device, BasicInput.ID, config.endpoint, bind_cfg.endpoint) + or build_unbind_request(device, BasicInput.ID, config.endpoint, bind_cfg.endpoint)) + end + end +end + +local function component_to_endpoint(device, component_id) + if component_id == COMPONENTS.INPUT_1 then + return ZIGBEE_ENDPOINTS.INPUT_1 + elseif component_id == COMPONENTS.INPUT_2 then + return ZIGBEE_ENDPOINTS.INPUT_2 + elseif component_id == COMPONENTS.INPUT_3 then + return ZIGBEE_ENDPOINTS.INPUT_3 + elseif component_id == COMPONENTS.INPUT_4 then + return ZIGBEE_ENDPOINTS.INPUT_4 + elseif component_id == COMPONENTS.OUTPUT_1 then + return ZIGBEE_ENDPOINTS.OUTPUT_1 + elseif component_id == COMPONENTS.OUTPUT_2 then + return ZIGBEE_ENDPOINTS.OUTPUT_2 + else + return device.fingerprinted_endpoint_id + end +end + +local function endpoint_to_component(device, ep) + local ep_id = type(ep) == "table" and ep.value or ep + if ep_id == ZIGBEE_ENDPOINTS.INPUT_1 then + return COMPONENTS.INPUT_1 + elseif ep_id == ZIGBEE_ENDPOINTS.INPUT_2 then + return COMPONENTS.INPUT_2 + elseif ep_id == ZIGBEE_ENDPOINTS.INPUT_3 then + return COMPONENTS.INPUT_3 + elseif ep_id == ZIGBEE_ENDPOINTS.INPUT_4 then + return COMPONENTS.INPUT_4 + elseif ep_id == ZIGBEE_ENDPOINTS.OUTPUT_1 then + return COMPONENTS.OUTPUT_1 + elseif ep_id == ZIGBEE_ENDPOINTS.OUTPUT_2 then + return COMPONENTS.OUTPUT_2 + else + return "main" + end +end + +local function init_handler(self, device) + device:set_component_to_endpoint_fn(component_to_endpoint) + device:set_endpoint_to_component_fn(endpoint_to_component) +end + +local function added_handler(self, device) + ensure_child_devices(self, device) +end + +local function configure_handler(self, device) + local configuration = configurationMap.get_device_configuration(device) + if configuration ~= nil then + for _, attribute in ipairs(configuration) do + if attribute.configurable ~= false then + device:add_configured_attribute(attribute) + end + end + end + device:configure() + if device.parent_assigned_child_key ~= nil then + return + end + + ensure_child_devices(self, device) + + local on1, off1 = get_output_timing(device, "1") + device:send(write_client_manufacturer_specific_attribute(device, BasicInput.ID, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OnTime.ID, ZIGBEE_MFG_CODES.Develco, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OnTime.data_type, + on1):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_1)) + device:send(write_client_manufacturer_specific_attribute(device, BasicInput.ID, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OffWaitTime.ID, ZIGBEE_MFG_CODES.Develco, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OffWaitTime.data_type, + off1):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_1)) + + local on2, off2 = get_output_timing(device, "2") + device:send(write_client_manufacturer_specific_attribute(device, BasicInput.ID, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OnTime.ID, ZIGBEE_MFG_CODES.Develco, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OnTime.data_type, + on2):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_2)) + device:send(write_client_manufacturer_specific_attribute(device, BasicInput.ID, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OffWaitTime.ID, ZIGBEE_MFG_CODES.Develco, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OffWaitTime.data_type, + off2):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_2)) + + -- Input 1 + local default_old_prefs = {} + for _, config in ipairs(INPUT_CONFIGS) do + apply_input_preference_changes(device, default_old_prefs, config) + end +end + +local function info_changed_handler(self, device, event, args) + if device.parent_assigned_child_key ~= nil then + -- This is a child device + local parent = device:get_parent_device() + if not parent then return end + + local info = OUTPUT_BY_KEY[device.parent_assigned_child_key] + if not info then return end + + -- Child devices have simple preference names without suffix + local on_time = math.floor(sanitize_timing(device.preferences.configOnTime) * 10) + local off_wait = math.floor(sanitize_timing(device.preferences.configOffWaitTime) * 10) + + parent:send(write_client_manufacturer_specific_attribute(parent, BasicInput.ID, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OnTime.ID, ZIGBEE_MFG_CODES.Develco, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OnTime.data_type, + on_time):to_endpoint(info.endpoint)) + + parent:send(write_client_manufacturer_specific_attribute(parent, BasicInput.ID, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OffWaitTime.ID, ZIGBEE_MFG_CODES.Develco, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OffWaitTime.data_type, + off_wait):to_endpoint(info.endpoint)) + return + else + local old_prefs = (args.old_st_store and args.old_st_store.preferences) or {} + for _, config in ipairs(INPUT_CONFIGS) do + apply_input_preference_changes(device, old_prefs, config) + end + end +end + +local function present_value_attr_handler(driver, device, value, zb_message) + local ep_id = zb_message.address_header.src_endpoint + register_native_switch_handler(device, ep_id.value) + device:emit_event_for_endpoint(ep_id, value.value and Switch.switch.on() or Switch.switch.off()) +end + +local function on_off_default_response_handler(driver, device, zb_rx) + local status = zb_rx.body.zcl_body.status.value + local endpoint = zb_rx.address_header.src_endpoint.value + + if status == Status.SUCCESS then + local cmd = zb_rx.body.zcl_body.cmd.value + local event = nil + + if cmd == OnOff.server.commands.On.ID then + event = Switch.switch.on() + elseif cmd == OnOff.server.commands.OnWithTimedOff.ID then + device:send(cluster_base.read_attribute(device, data_types.ClusterId(OnOff.ID), + data_types.AttributeId(OnOff.attributes.OnOff.ID)):to_endpoint(endpoint)) + elseif cmd == OnOff.server.commands.Off.ID then + event = Switch.switch.off() + end + + if event ~= nil then + emit_switch_event_for_endpoint(device, endpoint, event) + end + end +end + +local function make_switch_handler(command_name) + return function(driver, device, command) + local parent = device:get_parent_device() + if parent then + local info = OUTPUT_BY_KEY[device.parent_assigned_child_key] + if info then + handle_output_command(parent, info.suffix, command_name) + return + end + end + + local num = command.component and command.component:match("output(%d)") + if num then + handle_output_command(device, num, command_name) + return + end + num = command.component:match("input(%d)") + if num then + local component = device.profile.components[command.component] + local value = device:get_latest_state(command.component, Switch.ID, Switch.switch.NAME) + if value == "on" then + device:emit_component_event(component, + Switch.switch.on({ state_change = true, visibility = { displayed = false } })) + elseif value == "off" then + device:emit_component_event(component, + Switch.switch.off({ state_change = true, visibility = { displayed = false } })) + end + end + end +end + +local frient_bridge_handler = { + NAME = "frient bridge handler", + zigbee_handlers = { + global = { + [OnOff.ID] = { + [zcl_global_commands.DEFAULT_RESPONSE_ID] = on_off_default_response_handler + } + }, + cluster = {}, + attr = { + [BasicInput.ID] = { + [BasicInput.attributes.PresentValue.ID] = present_value_attr_handler + }, + [OnOff.ID] = { + [OnOff.attributes.OnOff.ID] = on_off_attr_handler + } + }, + zdo = {} + }, + capability_handlers = { + [Switch.ID] = { + [Switch.commands.on.NAME] = make_switch_handler("on"), + [Switch.commands.off.NAME] = make_switch_handler("off") + } + }, + lifecycle_handlers = { + added = added_handler, + init = init_handler, + doConfigure = configure_handler, + infoChanged = info_changed_handler + }, + can_handle = require("frient-IO.can_handle"), +} + +return frient_bridge_handler diff --git a/drivers/SmartThings/zigbee-switch/src/frient-IO/unbind_request.lua b/drivers/SmartThings/zigbee-switch/src/frient-IO/unbind_request.lua new file mode 100644 index 0000000000..64c4b05464 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/frient-IO/unbind_request.lua @@ -0,0 +1,84 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +local data_types = require "st.zigbee.data_types" +local utils = require "st.zigbee.utils" + +local unbind_request = {} + +unbind_request.UNBIND_REQUEST_CLUSTER_ID = 0x0022 +unbind_request.ADDRESS_MODE_16_BIT = 0x01 +unbind_request.ADDRESS_MODE_64_BIT = 0x03 + +local UnbindRequest = { + ID = unbind_request.UNBIND_REQUEST_CLUSTER_ID, + NAME = "UnbindRequest", +} +UnbindRequest.__index = UnbindRequest +unbind_request.UnbindRequest = UnbindRequest + +function UnbindRequest.deserialize(buf) + local self = {} + setmetatable(self, UnbindRequest) + + local fields = { + { name = "src_address", type = data_types.IeeeAddress }, + { name = "src_endpoint", type = data_types.Uint8 }, + { name = "cluster_id", type = data_types.ClusterId }, + { name = "dest_addr_mode", type = data_types.Uint8 }, + } + utils.deserialize_field_list(self, fields, buf) + + if self.dest_addr_mode.value == unbind_request.ADDRESS_MODE_16_BIT then + self.dest_address = data_types.Uint16.deserialize(buf) + else + self.dest_address = data_types.IeeeAddress.deserialize(buf) + self.dest_endpoint = data_types.Uint8.deserialize(buf) + end + return self +end + +--- A helper function used by common code to get all the component pieces of this message frame +function UnbindRequest:get_fields() + local out = {} + out[#out + 1] = self.src_address + out[#out + 1] = self.src_endpoint + out[#out + 1] = self.cluster_id + out[#out + 1] = self.dest_addr_mode + out[#out + 1] = self.dest_address + if self.dest_addr_mode.value == unbind_request.ADDRESS_MODE_64_BIT then + out[#out + 1] = self.dest_endpoint + end + return out +end + +UnbindRequest.get_length = utils.length_from_fields +UnbindRequest._serialize = utils.serialize_from_fields +UnbindRequest.pretty_print = utils.print_from_fields +UnbindRequest.__tostring = UnbindRequest.pretty_print +function UnbindRequest.from_values(orig, src_address, src_endpoint, cluster_id, dest_addr_mode, dest_address, + dest_endpoint) + local out = {} + if src_address == nil or src_endpoint == nil or cluster_id == nil or dest_addr_mode == nil or dest_address == nil then + error("Missing necessary values for bind request", 2) + end + + out.src_address = data_types.validate_or_build_type(src_address, data_types.IeeeAddress, "src_address") + out.src_endpoint = data_types.validate_or_build_type(src_endpoint, data_types.Uint8, "src_endpoint") + out.cluster_id = data_types.validate_or_build_type(cluster_id, data_types.ClusterId, "cluster") + out.dest_addr_mode = data_types.validate_or_build_type(dest_addr_mode, data_types.Uint8, "dest_addr_mode") + if (out.dest_addr_mode.value == unbind_request.ADDRESS_MODE_16_BIT) then + out.dest_address = data_types.validate_or_build_type(dest_address, data_types.Uint16, "dest_address") + elseif out.dest_addr_mode.value == unbind_request.ADDRESS_MODE_64_BIT then + out.dest_address = data_types.validate_or_build_type(dest_address, data_types.IeeeAddress, "dest_address") + out.dest_endpoint = data_types.validate_or_build_type(dest_endpoint, data_types.Uint8, "dest_endpoint") + else + error(string.format("Unrecognized destination address mode: %d", out.dest_addr_mode.value), 2) + end + + setmetatable(out, UnbindRequest) + return out +end + +setmetatable(unbind_request.UnbindRequest, { __call = unbind_request.UnbindRequest.from_values }) + +return unbind_request diff --git a/drivers/SmartThings/zigbee-switch/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/frient/can_handle.lua new file mode 100644 index 0000000000..2892763034 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/frient/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Function to determine if the driver can handle this device +return function(opts, driver, device, ...) + local FRIENT_SMART_PLUG_FINGERPRINTS = require("frient.fingerprints") + for _, fingerprint in ipairs(FRIENT_SMART_PLUG_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("frient") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/frient/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/frient/fingerprints.lua new file mode 100644 index 0000000000..e2ac5ceaab --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/frient/fingerprints.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "frient A/S", model = "SPLZB-131" }, + { mfr = "frient A/S", model = "SPLZB-132" }, + { mfr = "frient A/S", model = "SPLZB-134" }, + { mfr = "frient A/S", model = "SPLZB-137" }, + { mfr = "frient A/S", model = "SPLZB-141" }, + { mfr = "frient A/S", model = "SPLZB-142" }, + { mfr = "frient A/S", model = "SPLZB-144" }, + { mfr = "frient A/S", model = "SPLZB-147" }, + { mfr = "frient A/S", model = "SMRZB-143" }, + { mfr = "frient A/S", model = "SMRZB-153" }, + { mfr = "frient A/S", model = "SMRZB-332" }, + { mfr = "frient A/S", model = "SMRZB-342" }, +} diff --git a/drivers/SmartThings/zigbee-switch/src/frient/init.lua b/drivers/SmartThings/zigbee-switch/src/frient/init.lua index 4982fa038a..a615c721f4 100644 --- a/drivers/SmartThings/zigbee-switch/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/frient/init.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -27,20 +16,6 @@ local VOLTAGE_MEASUREMENT_DIVISOR_KEY = "_voltage_measurement_divisor" local CURRENT_MEASUREMENT_MULTIPLIER_KEY = "_current_measurement_multiplier" local CURRENT_MEASUREMENT_DIVISOR_KEY = "_current_measurement_divisor" -local FRIENT_SMART_PLUG_FINGERPRINTS = { - { mfr = "frient A/S", model = "SPLZB-131" }, - { mfr = "frient A/S", model = "SPLZB-132" }, - { mfr = "frient A/S", model = "SPLZB-134" }, - { mfr = "frient A/S", model = "SPLZB-137" }, - { mfr = "frient A/S", model = "SPLZB-141" }, - { mfr = "frient A/S", model = "SPLZB-142" }, - { mfr = "frient A/S", model = "SPLZB-144" }, - { mfr = "frient A/S", model = "SPLZB-147" }, - { mfr = "frient A/S", model = "SMRZB-143" }, - { mfr = "frient A/S", model = "SMRZB-153" }, - { mfr = "frient A/S", model = "SMRZB-332" }, - { mfr = "frient A/S", model = "SMRZB-342" }, -} local POWER_FAILURE_ALARM_CODE = 0x03 @@ -139,7 +114,6 @@ local function do_configure(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end device:configure() @@ -157,17 +131,6 @@ local function do_configure(driver, device) device:refresh() end --- Function to determine if the driver can handle this device -local function can_handle_frient_smart_plug(opts, driver, device, ...) - for _, fingerprint in ipairs(FRIENT_SMART_PLUG_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("frient") - return true, subdriver - end - end - return false -end - -- Main driver definition local frient_smart_plug = { NAME = "frient Smart Plug", @@ -193,7 +156,7 @@ local frient_smart_plug = { doConfigure = do_configure, added = device_added, }, - can_handle = can_handle_frient_smart_plug + can_handle = require("frient.can_handle"), } -return frient_smart_plug \ No newline at end of file +return frient_smart_plug diff --git a/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/can_handle.lua new file mode 100644 index 0000000000..6addbc463c --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/can_handle.lua @@ -0,0 +1,22 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local GE_LINK_BULB_FINGERPRINTS = { + ["GE_Appliances"] = { + ["ZLL Light"] = true, + }, + ["GE"] = { + ["Daylight"] = true, + ["SoftWhite"] = true + } +} + + local can_handle = (GE_LINK_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] + if can_handle then + local subdriver = require("ge-link-bulb") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/init.lua index 3bf3fa8952..b1f5ea8508 100644 --- a/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/init.lua @@ -1,42 +1,11 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local Level = clusters.Level -local GE_LINK_BULB_FINGERPRINTS = { - ["GE_Appliances"] = { - ["ZLL Light"] = true, - }, - ["GE"] = { - ["Daylight"] = true, - ["SoftWhite"] = true - } -} - -local function can_handle_ge_link_bulb(opts, driver, device) - local can_handle = (GE_LINK_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] - if can_handle then - local subdriver = require("ge-link-bulb") - return true, subdriver - else - return false - end -end - local function info_changed(driver, device, event, args) local command local new_dim_onoff_value = tonumber(device.preferences.dimOnOff) @@ -81,7 +50,7 @@ local ge_link_bulb = { [capabilities.switchLevel.commands.setLevel.NAME] = set_level_handler } }, - can_handle = can_handle_ge_link_bulb + can_handle = require("ge-link-bulb.can_handle"), } return ge_link_bulb diff --git a/drivers/SmartThings/zigbee-switch/src/hanssem/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/hanssem/can_handle.lua new file mode 100644 index 0000000000..9f9f1c4ace --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/hanssem/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local FINGERPRINTS = require "hanssem.fingerprints" + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("hanssem") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/hanssem/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/hanssem/fingerprints.lua new file mode 100644 index 0000000000..65c4421053 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/hanssem/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "Winners", model = "LSS1-101", children = 0 }, + { mfr = "Winners", model = "LSS1-102", children = 1 }, + { mfr = "Winners", model = "LSS1-103", children = 2 }, + { mfr = "Winners", model = "LSS1-204", children = 3 }, + { mfr = "Winners", model = "LSS1-205", children = 4 }, + { mfr = "Winners", model = "LSS1-206", children = 5 } +} diff --git a/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua b/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua index bb845984ca..083893448b 100644 --- a/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua @@ -1,40 +1,11 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local stDevice = require "st.device" local configurations = require "configurations" -local FINGERPRINTS = { - { mfr = "Winners", model = "LSS1-101", children = 0 }, - { mfr = "Winners", model = "LSS1-102", children = 1 }, - { mfr = "Winners", model = "LSS1-103", children = 2 }, - { mfr = "Winners", model = "LSS1-204", children = 3 }, - { mfr = "Winners", model = "LSS1-205", children = 4 }, - { mfr = "Winners", model = "LSS1-206", children = 5 } -} - -local function can_handle_hanssem_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("hanssem") - return true, subdriver - end - end - return false -end - local function get_children_amount(device) + local FINGERPRINTS = require "hanssem.fingerprints" for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_model() == fingerprint.model then return fingerprint.children @@ -81,7 +52,7 @@ local HanssemSwitch = { added = device_added, init = configurations.power_reconfig_wrapper(device_init) }, - can_handle = can_handle_hanssem_switch + can_handle = require("hanssem.can_handle"), } -return HanssemSwitch \ No newline at end of file +return HanssemSwitch diff --git a/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/can_handle.lua new file mode 100644 index 0000000000..47992c0559 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(opts, driver, device) + local IKEA_XY_COLOR_BULB_FINGERPRINTS = { + ["IKEA of Sweden"] = { + ["TRADFRI bulb E27 CWS opal 600lm"] = true, + ["TRADFRI bulb E26 CWS opal 600lm"] = true + } + } + local res = (IKEA_XY_COLOR_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] + if res then + return res, require("ikea-xy-color-bulb") + end + return res +end diff --git a/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua new file mode 100644 index 0000000000..a026ac6564 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua @@ -0,0 +1,170 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local switch_defaults = require "st.zigbee.defaults.switch_defaults" +local configurationMap = require "configurations" +local utils = require "st.utils" + +local ColorControl = clusters.ColorControl + +local CURRENT_X = "current_x_value" -- y value from xyY color space +local CURRENT_Y = "current_y_value" -- x value from xyY color space +local Y_TRISTIMULUS_VALUE = "y_tristimulus_value" -- Y tristimulus value which is used to convert color xyY -> RGB -> HSV +local HUESAT_TIMER = "huesat_timer" +local TARGET_HUE = "target_hue" +local TARGET_SAT = "target_sat" + +local device_init = function(self, device) + device:remove_configured_attribute(ColorControl.ID, ColorControl.attributes.CurrentHue.ID) + device:remove_configured_attribute(ColorControl.ID, ColorControl.attributes.CurrentSaturation.ID) + device:remove_monitored_attribute(ColorControl.ID, ColorControl.attributes.CurrentHue.ID) + device:remove_monitored_attribute(ColorControl.ID, ColorControl.attributes.CurrentSaturation.ID) + + local configuration = configurationMap.get_device_configuration(device) + if configuration ~= nil then + for _, attribute in ipairs(configuration) do + device:add_configured_attribute(attribute) + end + end +end + +local function store_xyY_values(device, x, y, Y) + device:set_field(Y_TRISTIMULUS_VALUE, Y) + device:set_field(CURRENT_X, x) + device:set_field(CURRENT_Y, y) +end + +local query_device = function(device) + return function() + device:send(ColorControl.attributes.CurrentX:read(device)) + device:send(ColorControl.attributes.CurrentY:read(device)) + end +end + +local function set_color_handler(driver, device, cmd) + -- Cancel the hue/sat timer if it's running, since setColor includes both hue and saturation + local huesat_timer = device:get_field(HUESAT_TIMER) + if huesat_timer ~= nil then + device.thread:cancel_timer(huesat_timer) + device:set_field(HUESAT_TIMER, nil) + end + + local hue = (cmd.args.color.hue ~= nil and cmd.args.color.hue > 99) and 99 or cmd.args.color.hue + local sat = cmd.args.color.saturation + + local x, y, Y = utils.safe_hsv_to_xy(hue, sat) + store_xyY_values(device, x, y, Y) + switch_defaults.on(driver, device, cmd) + + device:send(ColorControl.commands.MoveToColor(device, x, y, 0x0000)) + + device:set_field(TARGET_HUE, nil) + device:set_field(TARGET_SAT, nil) + device.thread:call_with_delay(2, query_device(device)) +end + +local huesat_timer_callback = function(driver, device, cmd) + return function() + device:set_field(HUESAT_TIMER, nil) + local hue = device:get_field(TARGET_HUE) + local sat = device:get_field(TARGET_SAT) + hue = hue ~= nil and hue or device:get_latest_state("main", capabilities.colorControl.ID, capabilities.colorControl.hue.NAME) + sat = sat ~= nil and sat or device:get_latest_state("main", capabilities.colorControl.ID, capabilities.colorControl.saturation.NAME) + cmd.args = { + color = { + hue = hue, + saturation = sat + } + } + set_color_handler(driver, device, cmd) + end +end + +local function set_hue_sat_helper(driver, device, cmd, hue, sat) + local huesat_timer = device:get_field(HUESAT_TIMER) + if huesat_timer ~= nil then + device.thread:cancel_timer(huesat_timer) + device:set_field(HUESAT_TIMER, nil) + end + if hue ~= nil and sat ~= nil then + cmd.args = { + color = { + hue = hue, + saturation = sat + } + } + set_color_handler(driver, device, cmd) + else + if hue ~= nil then + device:set_field(TARGET_HUE, hue) + elseif sat ~= nil then + device:set_field(TARGET_SAT, sat) + end + device:set_field(HUESAT_TIMER, device.thread:call_with_delay(0.2, huesat_timer_callback(driver, device, cmd))) + end +end + +local function set_hue_handler(driver, device, cmd) + set_hue_sat_helper(driver, device, cmd, cmd.args.hue, device:get_field(TARGET_SAT)) +end + +local function set_saturation_handler(driver, device, cmd) + set_hue_sat_helper(driver, device, cmd, device:get_field(TARGET_HUE), cmd.args.saturation) +end + +local function current_x_attr_handler(driver, device, value, zb_rx) + local Y_tristimulus = device:get_field(Y_TRISTIMULUS_VALUE) + local y = device:get_field(CURRENT_Y) + local x = value.value + + if y then + local hue, saturation = utils.safe_xy_to_hsv(x, y, Y_tristimulus) + + device:emit_event(capabilities.colorControl.hue(hue)) + device:emit_event(capabilities.colorControl.saturation(saturation)) + end + + device:set_field(CURRENT_X, x) +end + +local function current_y_attr_handler(driver, device, value, zb_rx) + local Y_tristimulus = device:get_field(Y_TRISTIMULUS_VALUE) + local x = device:get_field(CURRENT_X) + local y = value.value + + if x then + local hue, saturation = utils.safe_xy_to_hsv(x, y, Y_tristimulus) + + device:emit_event(capabilities.colorControl.hue(hue)) + device:emit_event(capabilities.colorControl.saturation(saturation)) + end + + device:set_field(CURRENT_Y, y) +end + +local ikea_xy_color_bulb = { + NAME = "IKEA XY Color Bulb", + lifecycle_handlers = { + init = configurationMap.power_reconfig_wrapper(device_init) + }, + capability_handlers = { + [capabilities.colorControl.ID] = { + [capabilities.colorControl.commands.setColor.NAME] = set_color_handler, + [capabilities.colorControl.commands.setHue.NAME] = set_hue_handler, + [capabilities.colorControl.commands.setSaturation.NAME] = set_saturation_handler + } + }, + zigbee_handlers = { + attr = { + [ColorControl.ID] = { + [ColorControl.attributes.CurrentX.ID] = current_x_attr_handler, + [ColorControl.attributes.CurrentY.ID] = current_y_attr_handler + } + } + }, + can_handle = require("ikea-xy-color-bulb.can_handle") +} + +return ikea_xy_color_bulb diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index f1e6385f56..aa245f5910 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -1,55 +1,21 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +-- The only reason we need this is because of supported_capabilities on the driver template local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" -local clusters = require "st.zigbee.zcl.clusters" local configurationMap = require "configurations" -local zcl_global_commands = require "st.zigbee.zcl.global_commands" -local SimpleMetering = clusters.SimpleMetering -local ElectricalMeasurement = clusters.ElectricalMeasurement -local preferences = require "preferences" -local device_lib = require "st.device" - -local function lazy_load_if_possible(sub_driver_name) - -- gets the current lua libs api version - local version = require "version" - - -- version 9 will include the lazy loading functions - if version.api >= 9 then - return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) - else - return require(sub_driver_name) - end - -end - -local function info_changed(self, device, event, args) - preferences.update_preferences(self, device, args) -end - -local do_configure = function(self, device) - device:refresh() - device:configure() - - -- Additional one time configuration - if device:supports_capability(capabilities.energyMeter) or device:supports_capability(capabilities.powerMeter) then - -- Divisor and multipler for EnergyMeter - device:send(SimpleMetering.attributes.Divisor:read(device)) - device:send(SimpleMetering.attributes.Multiplier:read(device)) - end +local CONFIGURE_REPORTING_RESPONSE_ID = 0x07 +local SIMPLE_METERING_ID = 0x0702 +local ELECTRICAL_MEASUREMENT_ID = 0x0B04 +local version = require "version" + +local lazy_handler +if version.api >= 15 then + lazy_handler = require "st.utils.lazy_handler" +else + lazy_handler = require end local function component_to_endpoint(device, component_id) @@ -66,10 +32,6 @@ local function endpoint_to_component(device, ep) end end -local function find_child(parent, ep_id) - return parent:get_child_by_parent_assigned_key(string.format("%02X", ep_id)) -end - local device_init = function(driver, device) device:set_component_to_endpoint_fn(component_to_endpoint) device:set_endpoint_to_component_fn(endpoint_to_component) @@ -78,7 +40,6 @@ local device_init = function(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end @@ -86,50 +47,13 @@ local device_init = function(driver, device) if ias_zone_config_method ~= nil then device:set_ias_zone_config_method(ias_zone_config_method) end + local device_lib = require "st.device" if device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then + local find_child = require "lifecycle_handlers.find_child" device:set_find_child(find_child) end end -local function is_mcd_device(device) - local components = device.profile.components - if type(components) == "table" then - local component_count = 0 - for _, component in pairs(components) do - component_count = component_count + 1 - end - return component_count >= 2 - end -end - -local function device_added(driver, device, event) - local main_endpoint = device:get_endpoint(clusters.OnOff.ID) - if is_mcd_device(device) == false and device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then - for _, ep in ipairs(device.zigbee_endpoints) do - if ep.id ~= main_endpoint then - if device:supports_server_cluster(clusters.OnOff.ID, ep.id) then - device:set_find_child(find_child) - if find_child(device, ep.id) == nil then - local name = string.format("%s %d", device.label, ep.id) - local child_profile = "basic-switch" - driver:try_create_device( - { - type = "EDGE_CHILD", - label = name, - profile = child_profile, - parent_device_id = device.id, - parent_assigned_child_key = string.format("%02X", ep.id), - vendor_provided_label = name - } - ) - end - end - end - end - end -end - - local zigbee_switch_driver_template = { supported_capabilities = { capabilities.switch, @@ -138,53 +62,28 @@ local zigbee_switch_driver_template = { capabilities.colorTemperature, capabilities.powerMeter, capabilities.energyMeter, - capabilities.motionSensor - }, - sub_drivers = { - lazy_load_if_possible("hanssem"), - lazy_load_if_possible("aqara"), - lazy_load_if_possible("aqara-light"), - lazy_load_if_possible("ezex"), - lazy_load_if_possible("rexense"), - lazy_load_if_possible("sinope"), - lazy_load_if_possible("sinope-dimmer"), - lazy_load_if_possible("zigbee-dimmer-power-energy"), - lazy_load_if_possible("zigbee-metering-plug-power-consumption-report"), - lazy_load_if_possible("jasco"), - lazy_load_if_possible("multi-switch-no-master"), - lazy_load_if_possible("zigbee-dual-metering-switch"), - lazy_load_if_possible("rgb-bulb"), - lazy_load_if_possible("zigbee-dimming-light"), - lazy_load_if_possible("white-color-temp-bulb"), - lazy_load_if_possible("rgbw-bulb"), - lazy_load_if_possible("zll-dimmer-bulb"), - lazy_load_if_possible("zll-polling"), - lazy_load_if_possible("zigbee-switch-power"), - lazy_load_if_possible("ge-link-bulb"), - lazy_load_if_possible("bad_on_off_data_type"), - lazy_load_if_possible("robb"), - lazy_load_if_possible("wallhero"), - lazy_load_if_possible("inovelli-vzm31-sn"), - lazy_load_if_possible("laisiao"), - lazy_load_if_possible("tuya-multi"), - lazy_load_if_possible("frient") + capabilities.motionSensor, + capabilities.illuminanceMeasurement, + capabilities.relativeHumidityMeasurement, + capabilities.temperatureMeasurement, }, + sub_drivers = require("sub_drivers"), zigbee_handlers = { global = { - [SimpleMetering.ID] = { - [zcl_global_commands.CONFIGURE_REPORTING_RESPONSE_ID] = configurationMap.handle_reporting_config_response + [SIMPLE_METERING_ID] = { + [CONFIGURE_REPORTING_RESPONSE_ID] = configurationMap.handle_reporting_config_response }, - [ElectricalMeasurement.ID] = { - [zcl_global_commands.CONFIGURE_REPORTING_RESPONSE_ID] = configurationMap.handle_reporting_config_response + [ELECTRICAL_MEASUREMENT_ID] = { + [CONFIGURE_REPORTING_RESPONSE_ID] = configurationMap.handle_reporting_config_response } } }, current_config_version = 1, lifecycle_handlers = { init = configurationMap.power_reconfig_wrapper(device_init), - added = device_added, - infoChanged = info_changed, - doConfigure = do_configure + added = lazy_handler("lifecycle_handlers.device_added"), + infoChanged = lazy_handler("lifecycle_handlers.info_changed"), + doConfigure = lazy_handler("lifecycle_handlers.do_configure"), }, health_check = false, } diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua deleted file mode 100644 index d6d094209c..0000000000 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua +++ /dev/null @@ -1,399 +0,0 @@ --- Copyright 2024 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 cluster_base = require "st.zigbee.cluster_base" -local utils = require "st.utils" -local st_device = require "st.device" -local data_types = require "st.zigbee.data_types" -local capabilities = require "st.capabilities" -local device_management = require "st.zigbee.device_management" -local configurations = require "configurations" - -local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" - -local INOVELLI_VZM31_SN_FINGERPRINTS = { - { mfr = "Inovelli", model = "VZM31-SN" } -} - -local PRIVATE_CLUSTER_ID = 0xFC31 -local PRIVATE_CMD_NOTIF_ID = 0x01 -local PRIVATE_CMD_SCENE_ID =0x00 -local MFG_CODE = 0x122F - -local preference_map = { - parameter258 = {parameter_number = 258, size = data_types.Boolean}, - parameter22 = {parameter_number = 22, size = data_types.Uint8}, - parameter52 = {parameter_number = 52, size = data_types.Boolean}, - parameter1 = {parameter_number = 1, size = data_types.Uint8}, - parameter2 = {parameter_number = 2, size = data_types.Uint8}, - parameter3 = {parameter_number = 3, size = data_types.Uint8}, - parameter4 = {parameter_number = 4, size = data_types.Uint8}, - parameter9 = {parameter_number = 9, size = data_types.Uint8}, - parameter10 = {parameter_number = 10, size = data_types.Uint8}, - parameter11 = {parameter_number = 11, size = data_types.Boolean}, - parameter15 = {parameter_number = 15, size = data_types.Uint8}, - parameter17 = {parameter_number = 17, size = data_types.Uint8}, - parameter95 = {parameter_number = 95, size = data_types.Uint8}, - parameter96 = {parameter_number = 96, size = data_types.Uint8}, - parameter97 = {parameter_number = 97, size = data_types.Uint8}, - parameter98 = {parameter_number = 98, size = data_types.Uint8}, -} - -local preferences_to_numeric_value = function(new_value) - local numeric = tonumber(new_value) - if numeric == nil then -- in case the value is Boolean - numeric = new_value and 1 or 0 - end - return numeric -end - -local preferences_calculate_parameter = function(new_value, type, number) - if number == "parameter9" or number == "parameter10" or number == "parameter13" or number == "parameter14" or number == "parameter15" or number == "parameter55" or number == "parameter56" then - if new_value == 101 then - return 255 - else - return utils.round(new_value / 100 * 254) - end - else - return new_value - end -end - -local is_inovelli_vzm31_sn = function(opts, driver, device) - for _, fingerprint in ipairs(INOVELLI_VZM31_SN_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("inovelli-vzm31-sn") - return true, subdriver - end - end - return false -end - -local function to_boolean(value) - if value == 0 or value =="0" then - return false - else - return true - end -end - -local map_key_attribute_to_capability = { - [0x00] = capabilities.button.button.pushed, - [0x01] = capabilities.button.button.held, - [0x02] = capabilities.button.button.down_hold, - [0x03] = capabilities.button.button.pushed_2x, - [0x04] = capabilities.button.button.pushed_3x, - [0x05] = capabilities.button.button.pushed_4x, - [0x06] = capabilities.button.button.pushed_5x, -} - -local function button_to_component(buttonId) - if buttonId > 0 then - return string.format("button%d", buttonId) - end -end - -local function scene_handler(driver, device, zb_rx) - local bytes = zb_rx.body.zcl_body.body_bytes - local button_number = bytes:byte(1) - local capability_attribute = map_key_attribute_to_capability[bytes:byte(2)] - local additional_fields = { - state_change = true - } - - local event - if capability_attribute ~= nil then - event = capability_attribute(additional_fields) - end - - local comp = device.profile.components[button_to_component(button_number)] - if comp ~= nil then - device:emit_component_event(comp, event) - end -end - -local function add_child(driver,parent,profile,child_type) - local child_metadata = { - type = "EDGE_CHILD", - label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)), - profile = profile, - parent_device_id = parent.id, - parent_assigned_child_key = child_type, - vendor_provided_label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)) - } - driver:try_create_device(child_metadata) -end - -local function info_changed(driver, device, event, args) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - local time_diff = 3 - local last_clock_set_time = device:get_field(LATEST_CLOCK_SET_TIMESTAMP) - if last_clock_set_time ~= nil then - time_diff = os.difftime(os.time(), last_clock_set_time) - end - device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time(), {persist = true}) - - if time_diff > 2 then - local preferences = preference_map - if args.old_st_store.preferences["notificationChild"] ~= device.preferences.notificationChild and args.old_st_store.preferences["notificationChild"] == false and device.preferences.notificationChild == true then - if not device:get_child_by_parent_assigned_key('notification') then - add_child(driver,device,'rgbw-bulb-2700K-6500K','notificaiton') - end - end - for id, value in pairs(device.preferences) do - if args.old_st_store.preferences[id] ~= value and preferences and preferences[id] then - local new_parameter_value = preferences_calculate_parameter(preferences_to_numeric_value(device.preferences[id]), preferences[id].size, id) - - if(preferences[id].size == data_types.Boolean) then - device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, to_boolean(new_parameter_value))) - else - device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, new_parameter_value)) - end - end - end - device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) - end - end -end - -local do_configure = function(self, device) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:refresh() - device:configure() - - device:send(device_management.build_bind_request(device, PRIVATE_CLUSTER_ID, self.environment_info.hub_zigbee_eui, 2)) -- Bind device for button presses. - - -- Retrieve Neutral Setting "Parameter 21" - device:send(cluster_base.read_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, 21, MFG_CODE)) - device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) - - -- Additional one time configuration - if device:supports_capability(capabilities.powerMeter) then - -- Divisor and multipler for PowerMeter - device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) - device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) - end - - if device:supports_capability(capabilities.energyMeter) then - -- Divisor and multipler for EnergyMeter - device:send(clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(device)) - device:send(clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(device)) - end - end -end - -local device_init = function(self, device) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time()) - if device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) == nil and device:supports_capability(capabilities.switchLevel)then - device:emit_event(capabilities.switchLevel.level(0)) - end - if device:get_latest_state("main", capabilities.powerMeter.ID, capabilities.powerMeter.power.NAME) == nil and device:supports_capability(capabilities.powerMeter) then - device:emit_event(capabilities.powerMeter.power(0)) - end - if device:get_latest_state("main", capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME) == nil and device:supports_capability(capabilities.energyMeter)then - device:emit_event(capabilities.energyMeter.energy(0)) - end - - for _, component in pairs(device.profile.components) do - if string.find(component.id, "button") ~= nil then - if device:get_latest_state(component.id, capabilities.button.ID, capabilities.button.supportedButtonValues.NAME) == nil then - device:emit_component_event( - component, - capabilities.button.supportedButtonValues( - {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, - { visibility = { displayed = false } } - ) - ) - end - if device:get_latest_state(component.id, capabilities.button.ID, capabilities.button.numberOfButtons.NAME) == nil then - device:emit_component_event( - component, - capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) - ) - end - end - end - device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) - else - device:emit_event(capabilities.colorControl.hue(1)) - device:emit_event(capabilities.colorControl.saturation(1)) - device:emit_event(capabilities.colorTemperature.colorTemperature(6500)) - device:emit_event(capabilities.switchLevel.level(100)) - device:emit_event(capabilities.switch.switch("off")) - 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 - else - return utils.round(value / 100 * 255) - end -end - -local function getNotificationValue(device, value) - local notificationValue = 0 - local level = device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) or 100 - local color = utils.round(device:get_latest_state("main", capabilities.colorControl.ID, capabilities.colorControl.hue.NAME) or 100) - local effect = device:get_parent_device().preferences.notificationType or 1 - notificationValue = notificationValue + (effect*16777216) - notificationValue = notificationValue + (huePercentToValue(value or color)*65536) - notificationValue = notificationValue + (level*256) - notificationValue = notificationValue + (255*1) - return notificationValue -end - -local function on_handler(driver, device, command) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:send(clusters.OnOff.server.commands.On(device)) - else - device:emit_event(capabilities.switch.switch("on")) - local dev = device:get_parent_device() - local send_configuration = function() - dev:send(cluster_base.build_manufacturer_specific_command( - dev, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_NOTIF_ID, - MFG_CODE, - utils.serialize_int(getNotificationValue(device),4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) - end -end - -local function off_handler(driver, device, command) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:send(clusters.OnOff.server.commands.Off(device)) - else - device:emit_event(capabilities.switch.switch("off")) - local dev = device:get_parent_device() - local send_configuration = function() - dev:send(cluster_base.build_manufacturer_specific_command( - dev, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_NOTIF_ID, - MFG_CODE, - utils.serialize_int(0,4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) - end -end - -local function switch_level_handler(driver, device, command) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:send(clusters.Level.server.commands.MoveToLevelWithOnOff(device, math.floor(command.args.level/100.0 * 254), command.args.rate or 0xFFFF)) - else - device:emit_event(capabilities.switchLevel.level(command.args.level)) - device:emit_event(capabilities.switch.switch(command.args.level ~= 0 and "on" or "off")) - local dev = device:get_parent_device() - local send_configuration = function() - dev:send(cluster_base.build_manufacturer_specific_command( - dev, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_NOTIF_ID, - MFG_CODE, - utils.serialize_int(getNotificationValue(device),4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) - end -end - -local function set_color_temperature(driver, device, command) - device:emit_event(capabilities.colorControl.hue(100)) - device:emit_event(capabilities.colorTemperature.colorTemperature(command.args.temperature)) - local dev = device:get_parent_device() - local send_configuration = function() - dev:send(cluster_base.build_manufacturer_specific_command( - dev, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_NOTIF_ID, - MFG_CODE, - utils.serialize_int(getNotificationValue(device, 100),4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) -end - -local function set_color(driver, device, command) - device:emit_event(capabilities.colorControl.hue(command.args.color.hue)) - device:emit_event(capabilities.colorControl.saturation(command.args.color.saturation)) - local dev = device:get_parent_device() - local send_configuration = function() - dev:send(cluster_base.build_manufacturer_specific_command( - dev, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_NOTIF_ID, - MFG_CODE, - utils.serialize_int(getNotificationValue(device),4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) -end - -local inovelli_vzm31_sn = { - NAME = "inovelli vzm31-sn handler", - lifecycle_handlers = { - doConfigure = do_configure, - init = configurations.power_reconfig_wrapper(device_init), - infoChanged = info_changed - }, - 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 - } - }, - cluster = { - [PRIVATE_CLUSTER_ID] = { - [PRIVATE_CMD_SCENE_ID] = scene_handler, - } - } - }, - capability_handlers = { - [capabilities.switch.ID] = { - [capabilities.switch.commands.on.NAME] = on_handler, - [capabilities.switch.commands.off.NAME] = off_handler, - }, - [capabilities.switchLevel.ID] = { - [capabilities.switchLevel.commands.setLevel.NAME] = switch_level_handler - }, - [capabilities.colorControl.ID] = { - [capabilities.colorControl.commands.setColor.NAME] = set_color - }, - [capabilities.colorTemperature.ID] = { - [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature - } - }, - can_handle = is_inovelli_vzm31_sn -} - -return inovelli_vzm31_sn diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua new file mode 100644 index 0000000000..d9b676e307 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(opts, driver, device) + local INOVELLI_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM30-SN" }, + { mfr = "Inovelli", model = "VZM31-SN" }, + { mfr = "Inovelli", model = "VZM32-SN" } + } + for _, fp in ipairs(INOVELLI_FINGERPRINTS) do + if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then + local subdriver = require("inovelli") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua new file mode 100644 index 0000000000..c402731328 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua @@ -0,0 +1,73 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +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 = {} + +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"} +} + +-- 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 +-- - bind PRIVATE cluster for button presses +-- - read metering/electrical measurement divisors/multipliers +function M.base_device_configure(driver, device, private_cluster_id, mfg_code) + device:configure() + -- OTA Image Notify (generic for all devices) + local PAYLOAD_TYPE = 0x00 + local QUERY_JITTER = 100 + local IMAGE_TYPE = 0xFFFF + local NEW_VERSION = 0xFFFFFFFF + device:send(OTAUpgrade.commands.ImageNotify(device, PAYLOAD_TYPE, QUERY_JITTER, mfg_code, IMAGE_TYPE, NEW_VERSION)) + + -- Bind for button presses on manufacturer private cluster + 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 + -- 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( + M.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 new file mode 100644 index 0000000000..a83a343663 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -0,0 +1,411 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" +local st_device = require "st.device" +local data_types = require "st.zigbee.data_types" +local capabilities = require "st.capabilities" +local inovelli_common = require "inovelli.common" + +-- Load VZM32-only dependencies (handlers will check device type) +local OccupancySensing = clusters.OccupancySensing + +local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" + +local PRIVATE_CLUSTER_ID = 0xFC31 +local PRIVATE_CLUSTER_MMWAVE_ID = 0xFC32 +local PRIVATE_CMD_NOTIF_ID = 0x01 +local PRIVATE_CMD_ENERGY_RESET_ID = 0x02 +local PRIVATE_CMD_SCENE_ID = 0x00 +local PRIVATE_CMD_MMWAVE_ID = 0x00 +local MFG_CODE = 0x122F + +-- Base preferences shared by all models +local base_preference_map = { + parameter258 = {parameter_number = 258, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, + parameter52 = {parameter_number = 52, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, + parameter1 = {parameter_number = 1, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + 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}, + 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}, + parameter97 = {parameter_number = 97, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter98 = {parameter_number = 98, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, +} + +-- Model-specific overrides/additions +local model_preference_overrides = { + ["VZM30-SN"] = { + parameter11 = {parameter_number = 11, size = data_types.Boolean, 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}, + parameter103 = {parameter_number = 103, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter104 = {parameter_number = 104, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter105 = {parameter_number = 105, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter106 = {parameter_number = 106, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter110 = {parameter_number = 110, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter111 = {parameter_number = 111, size = data_types.Uint32, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter112 = {parameter_number = 112, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter113 = {parameter_number = 113, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter114 = {parameter_number = 114, size = data_types.Uint32, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter115 = {parameter_number = 115, size = data_types.Uint32, cluster = PRIVATE_CLUSTER_ID}, + } +} + +local function get_preference_map_for_device(device) + -- shallow copy base + local merged = {} + for k, v in pairs(base_preference_map) do merged[k] = v end + -- merge model-specific + local model = device and device:get_model() or nil + local override = model and model_preference_overrides[model] or nil + if override then + for k, v in pairs(override) do merged[k] = v end + end + return merged +end + +local preferences_to_numeric_value = function(new_value) + local numeric = tonumber(new_value) + if numeric == nil then + numeric = new_value and 1 or 0 + end + return numeric +end + +local preferences_calculate_parameter = function(new_value, type, number) + if number == "parameter9" or number == "parameter10" or number == "parameter13" or number == "parameter14" or number == "parameter15" or number == "parameter55" or number == "parameter56" then + if new_value == 101 then + return 255 + else + return utils.round(new_value / 100 * 254) + end + else + return new_value + end +end + +local function to_boolean(value) + if value == 0 or value == "0" then + return false + else + return true + end +end + +local map_key_attribute_to_capability = { + [0x00] = capabilities.button.button.pushed, + [0x01] = capabilities.button.button.held, + [0x02] = capabilities.button.button.down_hold, + [0x03] = capabilities.button.button.pushed_2x, + [0x04] = capabilities.button.button.pushed_3x, + [0x05] = capabilities.button.button.pushed_4x, + [0x06] = capabilities.button.button.pushed_5x, +} + +local function button_to_component(buttonId) + if buttonId > 0 then + return string.format("button%d", buttonId) + end +end + +local function scene_handler(driver, device, zb_rx) + local bytes = zb_rx.body.zcl_body.body_bytes + local button_number = bytes:byte(1) + 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_name = button_to_component(button_number) + local comp = device.profile.components[comp_name] + if comp ~= nil and event ~= nil then + -- 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 + +local function add_child(driver,parent,profile,child_type) + local child_metadata = { + type = "EDGE_CHILD", + label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)), + profile = profile, + parent_device_id = parent.id, + parent_assigned_child_key = child_type, + vendor_provided_label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)) + } + driver:try_create_device(child_metadata) +end + +local function info_changed(driver, device, event, args) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + local time_diff = 3 + local last_clock_set_time = device:get_field(LATEST_CLOCK_SET_TIMESTAMP) + if last_clock_set_time ~= nil then time_diff = os.difftime(os.time(), last_clock_set_time) end + device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time(), {persist = true}) + if time_diff > 2 then + local preferences = get_preference_map_for_device(device) + if args.old_st_store.preferences["notificationChild"] ~= device.preferences.notificationChild and args.old_st_store.preferences["notificationChild"] == false and device.preferences.notificationChild == true then + if not device:get_child_by_parent_assigned_key('notification') then + add_child(driver,device,'rgbw-bulb-2700K-6500K','notification') + end + end + for id, value in pairs(device.preferences) do + if args.old_st_store.preferences[id] ~= value and preferences and preferences[id] then + local new_parameter_value = preferences_calculate_parameter(preferences_to_numeric_value(device.preferences[id]), preferences[id].size, id) + if(preferences[id].size == data_types.Boolean) then + new_parameter_value = to_boolean(new_parameter_value) + end + if id == "parameter111" then + device:send(cluster_base.build_manufacturer_specific_command( + device, + PRIVATE_CLUSTER_MMWAVE_ID, + PRIVATE_CMD_MMWAVE_ID, + MFG_CODE, + utils.serialize_int(new_parameter_value,1,false,false))) + else + device:send(cluster_base.write_manufacturer_specific_attribute(device, preferences[id].cluster, preferences[id].parameter_number, MFG_CODE, preferences[id].size, new_parameter_value)) + end + end + end + end + end +end + +local function device_added(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:refresh() + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + device:emit_event(capabilities.colorTemperature.colorTemperature(6500)) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +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) + else + device:configure() + end +end + +local function huePercentToValue(value) + if value <= 2 then return 0 + elseif value >= 98 then return 255 + else return utils.round(value / 100 * 255) end +end + +local function getNotificationValue(device, value) + local notificationValue = 0 + local level = device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) or 100 + local color = utils.round(device:get_latest_state("main", capabilities.colorControl.ID, capabilities.colorControl.hue.NAME) or 100) + local effect = device:get_parent_device().preferences.notificationType or 1 + notificationValue = notificationValue + (effect*16777216) + notificationValue = notificationValue + (huePercentToValue(value or color)*65536) + notificationValue = notificationValue + (level*256) + notificationValue = notificationValue + (255*1) + return notificationValue +end + +local function on_handler(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(clusters.OnOff.server.commands.On(device)) + else + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + local send_configuration = function() + dev:send(cluster_base.build_manufacturer_specific_command( + dev, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_NOTIF_ID, + MFG_CODE, + utils.serialize_int(getNotificationValue(device),4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) + end + end + + local function off_handler(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(clusters.OnOff.server.commands.Off(device)) + else + device:emit_event(capabilities.switch.switch("off")) + local dev = device:get_parent_device() + local send_configuration = function() + dev:send(cluster_base.build_manufacturer_specific_command( + dev, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_NOTIF_ID, + MFG_CODE, + utils.serialize_int(0,4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) + end + end + +local function switch_level_handler(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(clusters.Level.server.commands.MoveToLevelWithOnOff(device, math.floor(command.args.level/100.0 * 254), command.args.rate or 0xFFFF)) + else + device:emit_event(capabilities.switchLevel.level(command.args.level)) + device:emit_event(capabilities.switch.switch(command.args.level ~= 0 and "on" or "off")) + local dev = device:get_parent_device() + local send_configuration = function() + dev:send(cluster_base.build_manufacturer_specific_command( + dev, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_NOTIF_ID, + MFG_CODE, + utils.serialize_int(getNotificationValue(device),4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) + end + end + +local function set_color_temperature(driver, device, command) + device:emit_event(capabilities.colorControl.hue(100)) + device:emit_event(capabilities.colorTemperature.colorTemperature(command.args.temperature)) + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + local send_configuration = function() + dev:send(cluster_base.build_manufacturer_specific_command( + dev, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_NOTIF_ID, + MFG_CODE, + utils.serialize_int(getNotificationValue(device, 100),4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) + end + + local function set_color(driver, device, command) + device:emit_event(capabilities.colorControl.hue(command.args.color.hue)) + device:emit_event(capabilities.colorControl.saturation(command.args.color.saturation)) + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + local send_configuration = function() + dev:send(cluster_base.build_manufacturer_specific_command( + dev, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_NOTIF_ID, + MFG_CODE, + utils.serialize_int(getNotificationValue(device),4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) + end + +local function occupancy_attr_handler(driver, device, occupancy, zb_rx) + device:emit_event(occupancy.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) +end + +local function handle_resetEnergyMeter(self, device) + device:send(cluster_base.build_manufacturer_specific_command(device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_ENERGY_RESET_ID, MFG_CODE, utils.serialize_int(0,1,false,false))) + device:send(clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(device)) + device:send(clusters.ElectricalMeasurement.attributes.ActivePower:read(device)) +end + +local inovelli = { + NAME = "Inovelli Zigbee Switch", + lifecycle_handlers = { + doConfigure = device_configure, + infoChanged = info_changed, + added = device_added, + }, + zigbee_handlers = { + attr = { + [OccupancySensing.ID] = { + [OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler + }, + }, + cluster = { + [PRIVATE_CLUSTER_ID] = { + [PRIVATE_CMD_SCENE_ID] = scene_handler, + } + } + }, + sub_drivers = require("inovelli.sub_drivers"), + capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = on_handler, + [capabilities.switch.commands.off.NAME] = off_handler, + }, + [capabilities.switchLevel.ID] = { + [capabilities.switchLevel.commands.setLevel.NAME] = switch_level_handler + }, + [capabilities.colorControl.ID] = { + [capabilities.colorControl.commands.setColor.NAME] = set_color + }, + [capabilities.colorTemperature.ID] = { + [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature + }, + [capabilities.energyMeter.ID] = { + [capabilities.energyMeter.commands.resetEnergyMeter.NAME] = handle_resetEnergyMeter, + } + }, + can_handle = require("inovelli.can_handle"), +} + +return inovelli diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua new file mode 100644 index 0000000000..2fc65221be --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("inovelli.vzm30-sn"), + lazy_load("inovelli.vzm32-sn") +} diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/can_handle.lua new file mode 100644 index 0000000000..9d1b285b45 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(opts, driver, device) + local INOVELLI_VZM30_SN_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM30-SN" }, + } + for _, fp in ipairs(INOVELLI_VZM30_SN_FINGERPRINTS) do + if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then + local sub_driver = require("inovelli.vzm30-sn") + return true, sub_driver + end + end + return false +end 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..6ad3ec758d --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua @@ -0,0 +1,53 @@ +-- 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 PRIVATE_CLUSTER_ID = 0xFC31 +local MFG_CODE = 0x122F + +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 Zigbee Switch", + can_handle = require("inovelli.vzm30-sn.can_handle"), + lifecycle_handlers = { + doConfigure = device_configure, + }, +} + +return vzm30_sn \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/can_handle.lua new file mode 100644 index 0000000000..aa0004b193 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(opts, driver, device) + local INOVELLI_VZM32_SN_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM32-SN" }, + } + for _, fp in ipairs(INOVELLI_VZM32_SN_FINGERPRINTS) do + if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then + local sub_driver = require("inovelli.vzm32-sn") + return true, sub_driver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua new file mode 100644 index 0000000000..4e0151aeb6 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua @@ -0,0 +1,68 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local st_device = require "st.device" +local device_management = require "st.zigbee.device_management" +local inovelli_common = require "inovelli.common" + +local OccupancySensing = clusters.OccupancySensing + +local PRIVATE_CLUSTER_ID = 0xFC31 +local MFG_CODE = 0x122F + + +local function configure_illuminance_reporting(device) + local min_lux_change = 15 + local value = math.floor(10000 * math.log(min_lux_change, 10) + 1) + device:send(clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting(device, 10, 600, value)) +end + +local function refresh_handler(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:refresh() + device:send(OccupancySensing.attributes.Occupancy:read(device)) + else + device:refresh() + end +end + +local function device_added(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + refresh_handler(driver, device, {}) + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + device:emit_event(capabilities.colorTemperature.colorTemperature(6500)) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +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) + device:send(device_management.build_bind_request(device, OccupancySensing.ID, driver.environment_info.hub_zigbee_eui)) + configure_illuminance_reporting(device) + else + device:configure() + end +end + +local vzm32_sn = { + NAME = "Inovelli VZM32-SN mmWave Dimmer", + can_handle = require("inovelli.vzm32-sn.can_handle"), + lifecycle_handlers = { + added = device_added, + doConfigure = device_configure, + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh_handler, + } + } +} + +return vzm32_sn \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/jasco/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/jasco/can_handle.lua new file mode 100644 index 0000000000..c97ff700c8 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/jasco/can_handle.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local JASCO_SWTICH_FINGERPRINTS = { + { mfr = "Jasco Products", model = "43095" }, + { mfr = "Jasco Products", model = "43132" }, + { mfr = "Jasco Products", model = "43078" } +} + + for _, fingerprint in ipairs(JASCO_SWTICH_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("jasco") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/jasco/init.lua b/drivers/SmartThings/zigbee-switch/src/jasco/init.lua index 6c90115a22..a21059dfb4 100644 --- a/drivers/SmartThings/zigbee-switch/src/jasco/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/jasco/init.lua @@ -1,38 +1,11 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local SimpleMetering = clusters.SimpleMetering local constants = require "st.zigbee.constants" -local JASCO_SWTICH_FINGERPRINTS = { - { mfr = "Jasco Products", model = "43095" }, - { mfr = "Jasco Products", model = "43132" }, - { mfr = "Jasco Products", model = "43078" } -} - -local is_jasco_switch = function(opts, driver, device) - for _, fingerprint in ipairs(JASCO_SWTICH_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("jasco") - return true, subdriver - end - end - return false -end - local device_added = function(self, device) local customEnergyDivisor = 10000 device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, customEnergyDivisor, {persist = true}) @@ -63,7 +36,7 @@ local jasco_switch = { added = device_added, doConfigure = do_configure, }, - can_handle = is_jasco_switch + can_handle = require("jasco.can_handle"), } return jasco_switch diff --git a/drivers/SmartThings/zigbee-switch/src/laisiao/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/laisiao/can_handle.lua new file mode 100644 index 0000000000..7ed921844b --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/laisiao/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) +local FINGERPRINTS = { + { mfr = "LAISIAO", model = "yuba" }, +} + + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("laisiao") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua b/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua index edb829bf1f..2833843793 100755 --- a/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua @@ -1,34 +1,11 @@ --- Copyright 2024 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" local configurations = require "configurations" -local FINGERPRINTS = { - { mfr = "LAISIAO", model = "yuba" }, -} -local function can_handle_laisiao(opts, driver, device, ...) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("laisiao") - return true, subdriver - end - end - return false -end local function component_to_endpoint(device, component_id) if component_id == "main" then @@ -78,7 +55,7 @@ local laisiao_bath_heater = { [capabilities.switch.commands.on.NAME] = on_handler } }, - can_handle = can_handle_laisiao + can_handle = require("laisiao.can_handle"), } return laisiao_bath_heater diff --git a/drivers/SmartThings/zigbee-switch/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-switch/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua new file mode 100644 index 0000000000..a241ea506c --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua @@ -0,0 +1,50 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local find_child = require "lifecycle_handlers.find_child" + +local function is_mcd_device(device) + local components = device.profile.components + if type(components) == "table" then + local component_count = 0 + for _, component in pairs(components) do + component_count = component_count + 1 + end + return component_count >= 2 + end +end + +return function(driver, device, event) + local clusters = require "st.zigbee.zcl.clusters" + local ZLL_PROFILE_ID = 0xC05E + local device_lib = require "st.device" + local version = require "version" + + local main_endpoint = device:get_endpoint(clusters.OnOff.ID) + if is_mcd_device(device) == false and device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then + for _, ep in ipairs(device.zigbee_endpoints) do + if ep.id ~= main_endpoint then + if device:supports_server_cluster(clusters.OnOff.ID, ep.id) then + device:set_find_child(find_child) + if find_child(device, ep.id) == nil then + local name = string.format("%s %d", device.label, ep.id) + local child_profile = "basic-switch" + driver:try_create_device( + { + type = "EDGE_CHILD", + label = name, + profile = child_profile, + parent_device_id = device.id, + parent_assigned_child_key = string.format("%02X", ep.id), + vendor_provided_label = name + } + ) + end + end + end + end + end + if version.api > 15 and device:get_profile_id() == ZLL_PROFILE_ID then + device:refresh() + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua new file mode 100644 index 0000000000..465969a755 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua @@ -0,0 +1,20 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +return function(self, device) + local ZLL_PROFILE_ID = 0xC05E + local version = require "version" + if version.api < 16 or (version.api > 15 and device:get_profile_id() ~= ZLL_PROFILE_ID) then + device:refresh() + end + device:configure() + + -- Additional one time configuration + if device:supports_capability(capabilities.energyMeter) or device:supports_capability(capabilities.powerMeter) then + local clusters = require "st.zigbee.zcl.clusters" + -- Divisor and multipler for EnergyMeter + device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) + device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/find_child.lua b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/find_child.lua new file mode 100644 index 0000000000..31b1640619 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/find_child.lua @@ -0,0 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(parent, ep_id) + return parent:get_child_by_parent_assigned_key(string.format("%02X", ep_id)) +end diff --git a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/info_changed.lua b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/info_changed.lua new file mode 100644 index 0000000000..bd15064b5e --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/info_changed.lua @@ -0,0 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(self, device, event, args) + local preferences = require "preferences" + preferences.update_preferences(self, device, args) +end diff --git a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/can_handle.lua new file mode 100644 index 0000000000..80cd735e93 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local FINGERPRINTS = require "multi-switch-no-master.fingerprints" + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model and (device:get_manufacturer() == nil or device:get_manufacturer() == fingerprint.mfr) then + local subdriver = require("multi-switch-no-master") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/fingerprints.lua new file mode 100644 index 0000000000..48dd5ac246 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/fingerprints.lua @@ -0,0 +1,47 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "DAWON_DNS", model = "PM-S240-ZB", children = 1 }, + { mfr = "DAWON_DNS", model = "PM-S240R-ZB", children = 1 }, + { mfr = "DAWON_DNS", model = "PM-S250-ZB", children = 1 }, + { mfr = "DAWON_DNS", model = "PM-S340-ZB", children = 2 }, + { mfr = "DAWON_DNS", model = "PM-S340R-ZB", children = 2 }, + { mfr = "DAWON_DNS", model = "PM-S350-ZB", children = 2 }, + { mfr = "DAWON_DNS", model = "ST-S250-ZB", children = 1 }, + { mfr = "DAWON_DNS", model = "ST-S350-ZB", children = 2 }, + { mfr = "ORVIBO", model = "074b3ffba5a045b7afd94c47079dd553", children = 1 }, + { mfr = "ORVIBO", model = "9f76c9f31b4c4a499e3aca0977ac4494", children = 2 }, + { mfr = "REXENSE", model = "HY0002", children = 1 }, + { mfr = "REXENSE", model = "HY0003", children = 2 }, + { mfr = "REX", model = "HY0096", children = 1 }, + { mfr = "REX", model = "HY0097", children = 2 }, + { mfr = "HEIMAN", model = "HS2SW2L-EFR-3.0", children = 1 }, + { mfr = "HEIMAN", model = "HS2SW3L-EFR-3.0", children = 2 }, + { mfr = "HEIMAN", model = "HS6SW2A-W-EF-3.0", children = 1 }, + { mfr = "HEIMAN", model = "HS6SW3A-W-EF-3.0", children = 2 }, + { mfr = "eWeLink", model = "ZB-SW02", children = 1 }, + { mfr = "eWeLink", model = "ZB-SW03", children = 2 }, + { mfr = "eWeLink", model = "ZB-SW04", children = 3 }, + { mfr = "SMARTvill", model = "SLA01", children = 0 }, + { mfr = "SMARTvill", model = "SLA02", children = 1 }, + { mfr = "SMARTvill", model = "SLA03", children = 2 }, + { mfr = "SMARTvill", model = "SLA04", children = 3 }, + { mfr = "SMARTvill", model = "SLA05", children = 4 }, + { mfr = "SMARTvill", model = "SLA06", children = 5 }, + { mfr = "ShinaSystem", model = "SBM300Z2", children = 1 }, + { mfr = "ShinaSystem", model = "SBM300Z3", children = 2 }, + { mfr = "ShinaSystem", model = "SBM300Z4", children = 3 }, + { mfr = "ShinaSystem", model = "SBM300Z5", children = 4 }, + { mfr = "ShinaSystem", model = "SBM300Z6", children = 5 }, + { mfr = "ShinaSystem", model = "SQM300Z2", children = 1 }, + { mfr = "ShinaSystem", model = "SQM300Z3", children = 2 }, + { mfr = "ShinaSystem", model = "SQM300Z4", children = 3 }, + { mfr = "ShinaSystem", model = "SQM300Z6", children = 5 }, + { model = "E220-KR2N0Z0-HA", children = 1 }, + { model = "E220-KR3N0Z0-HA", children = 2 }, + { model = "E220-KR4N0Z0-HA", children = 3 }, + { model = "E220-KR5N0Z0-HA", children = 4 }, + { model = "E220-KR6N0Z0-HA", children = 5 }, + { mfr = "JNL", model = "Y-K003-001", children = 2 } +} diff --git a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua index bec5b1d87d..ba0ed07ae3 100644 --- a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua @@ -1,76 +1,11 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local st_device = require "st.device" local utils = require "st.utils" local configurations = require "configurations" -local MULTI_SWITCH_NO_MASTER_FINGERPRINTS = { - { mfr = "DAWON_DNS", model = "PM-S240-ZB", children = 1 }, - { mfr = "DAWON_DNS", model = "PM-S240R-ZB", children = 1 }, - { mfr = "DAWON_DNS", model = "PM-S250-ZB", children = 1 }, - { mfr = "DAWON_DNS", model = "PM-S340-ZB", children = 2 }, - { mfr = "DAWON_DNS", model = "PM-S340R-ZB", children = 2 }, - { mfr = "DAWON_DNS", model = "PM-S350-ZB", children = 2 }, - { mfr = "DAWON_DNS", model = "ST-S250-ZB", children = 1 }, - { mfr = "DAWON_DNS", model = "ST-S350-ZB", children = 2 }, - { mfr = "ORVIBO", model = "074b3ffba5a045b7afd94c47079dd553", children = 1 }, - { mfr = "ORVIBO", model = "9f76c9f31b4c4a499e3aca0977ac4494", children = 2 }, - { mfr = "REXENSE", model = "HY0002", children = 1 }, - { mfr = "REXENSE", model = "HY0003", children = 2 }, - { mfr = "REX", model = "HY0096", children = 1 }, - { mfr = "REX", model = "HY0097", children = 2 }, - { mfr = "HEIMAN", model = "HS2SW2L-EFR-3.0", children = 1 }, - { mfr = "HEIMAN", model = "HS2SW3L-EFR-3.0", children = 2 }, - { mfr = "HEIMAN", model = "HS6SW2A-W-EF-3.0", children = 1 }, - { mfr = "HEIMAN", model = "HS6SW3A-W-EF-3.0", children = 2 }, - { mfr = "eWeLink", model = "ZB-SW02", children = 1 }, - { mfr = "eWeLink", model = "ZB-SW03", children = 2 }, - { mfr = "eWeLink", model = "ZB-SW04", children = 3 }, - { mfr = "SMARTvill", model = "SLA01", children = 0 }, - { mfr = "SMARTvill", model = "SLA02", children = 1 }, - { mfr = "SMARTvill", model = "SLA03", children = 2 }, - { mfr = "SMARTvill", model = "SLA04", children = 3 }, - { mfr = "SMARTvill", model = "SLA05", children = 4 }, - { mfr = "SMARTvill", model = "SLA06", children = 5 }, - { mfr = "ShinaSystem", model = "SBM300Z2", children = 1 }, - { mfr = "ShinaSystem", model = "SBM300Z3", children = 2 }, - { mfr = "ShinaSystem", model = "SBM300Z4", children = 3 }, - { mfr = "ShinaSystem", model = "SBM300Z5", children = 4 }, - { mfr = "ShinaSystem", model = "SBM300Z6", children = 5 }, - { mfr = "ShinaSystem", model = "SQM300Z2", children = 1 }, - { mfr = "ShinaSystem", model = "SQM300Z3", children = 2 }, - { mfr = "ShinaSystem", model = "SQM300Z4", children = 3 }, - { mfr = "ShinaSystem", model = "SQM300Z6", children = 5 }, - { model = "E220-KR2N0Z0-HA", children = 1 }, - { model = "E220-KR3N0Z0-HA", children = 2 }, - { model = "E220-KR4N0Z0-HA", children = 3 }, - { model = "E220-KR5N0Z0-HA", children = 4 }, - { model = "E220-KR6N0Z0-HA", children = 5 } -} - -local function is_multi_switch_no_master(opts, driver, device) - for _, fingerprint in ipairs(MULTI_SWITCH_NO_MASTER_FINGERPRINTS) do - if device:get_model() == fingerprint.model and (device:get_manufacturer() == nil or device:get_manufacturer() == fingerprint.mfr) then - local subdriver = require("multi-switch-no-master") - return true, subdriver - end - end - return false -end - local function get_children_amount(device) - for _, fingerprint in ipairs(MULTI_SWITCH_NO_MASTER_FINGERPRINTS) do + for _, fingerprint in ipairs(require("multi-switch-no-master.fingerprints")) do if device:get_model() == fingerprint.model then return fingerprint.children end @@ -117,8 +52,7 @@ local multi_switch_no_master = { init = configurations.power_reconfig_wrapper(device_init), added = device_added }, - can_handle = is_multi_switch_no_master + can_handle = require("multi-switch-no-master.can_handle"), } return multi_switch_no_master - diff --git a/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/can_handle.lua new file mode 100644 index 0000000000..bc394d4fa0 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local st_device = require "st.device" + + if device.network_type ~= st_device.NETWORK_TYPE_ZIGBEE and device.network_type ~= st_device.NETWORK_TYPE_CHILD then + return true, require("non_zigbee_devices") + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/init.lua b/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/init.lua new file mode 100644 index 0000000000..63876e658a --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/init.lua @@ -0,0 +1,39 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- This is a patch for the zigbee-switch driver to fix https://smartthings.atlassian.net/browse/CHAD-16558 +-- Several hubs were found that had zigbee switch drivers hosting zwave devices. +-- This patch works around it until hubcore 0.59 is released with +-- https://smartthings.atlassian.net/browse/CHAD-16552 + +local log = require "log" + + +local function device_added(driver, device, event) + log.info(string.format("Non zigbee device added: %s", device)) +end + +local function device_init(driver, device, event) + log.info(string.format("Non zigbee device init: %s", device)) +end + +local function do_configure(driver, device) + log.info(string.format("Non zigbee do configure: %s", device)) +end + +local function info_changed(driver, device, event, args) + log.info(string.format("Non zigbee infoChanged: %s", device)) +end + +local non_zigbee_devices = { + NAME = "non zigbee devices filter", + lifecycle_handlers = { + init = device_init, + added = device_added, + doConfigure = do_configure, + infoChanged = info_changed + }, + can_handle = require("non_zigbee_devices.can_handle"), +} + +return non_zigbee_devices diff --git a/drivers/SmartThings/zigbee-switch/src/preferences.lua b/drivers/SmartThings/zigbee-switch/src/preferences.lua index f0170aaf40..0e9c8f5b87 100644 --- a/drivers/SmartThings/zigbee-switch/src/preferences.lua +++ b/drivers/SmartThings/zigbee-switch/src/preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" @@ -18,7 +7,10 @@ local data_types = require "st.zigbee.data_types" local devices = { AQARA_LIGHT = { - MATCHING_MATRIX = { mfr = "LUMI", model = "lumi.light.acn004" }, + MATCHING_MATRIX = { + { mfr = "LUMI", model = "lumi.light.acn004" }, + { mfr = "LUMI", model = "lumi.light.cwacn1" } + }, PARAMETERS = { ["stse.restorePowerState"] = function(device, value) return cluster_base.write_manufacturer_specific_attribute(device, 0xFCC0, @@ -39,7 +31,7 @@ local devices = { } }, AQARA_LIGHT_BULB = { - MATCHING_MATRIX = { mfr = "Aqara", model = "lumi.light.acn014" }, + MATCHING_MATRIX = {{ mfr = "Aqara", model = "lumi.light.acn014" }}, PARAMETERS = { ["stse.restorePowerState"] = function(device, value) return cluster_base.write_manufacturer_specific_attribute(device, 0xFCC0, @@ -75,12 +67,17 @@ preferences.sync_preferences = function(driver, device) end preferences.get_device_parameters = function(zigbee_device) + local mfr = zigbee_device:get_manufacturer() + local model = zigbee_device:get_model() + for _, device in pairs(devices) do - if zigbee_device:get_manufacturer() == device.MATCHING_MATRIX.mfr and - zigbee_device:get_model() == device.MATCHING_MATRIX.model then - return device.PARAMETERS + for _, fp in ipairs(device.MATCHING_MATRIX) do + if fp.mfr == mfr and fp.model == model then + return device.PARAMETERS + end end end + return nil end diff --git a/drivers/SmartThings/zigbee-switch/src/rexense/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/rexense/can_handle.lua new file mode 100644 index 0000000000..bcdc666417 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/rexense/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) +local ZIGBEE_METERING_PLUG_FINGERPRINTS = { + { mfr = "REXENSE", model = "HY0105" } -- HONYAR Outlet" +} + for _, fingerprint in ipairs(ZIGBEE_METERING_PLUG_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("rexense") + return true, subdriver + end + end + + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/rexense/init.lua b/drivers/SmartThings/zigbee-switch/src/rexense/init.lua index d0d457928e..4b16afe3b0 100644 --- a/drivers/SmartThings/zigbee-switch/src/rexense/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/rexense/init.lua @@ -1,26 +1,11 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local zcl_clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local OnOff = zcl_clusters.OnOff -local ZIGBEE_METERING_PLUG_FINGERPRINTS = { - { mfr = "REXENSE", model = "HY0105" } -- HONYAR Outlet" -} - local function switch_on_handler(driver, device, command) device:send_to_component(command.component, OnOff.server.commands.On(device)) device:send(OnOff.server.commands.On(device):to_endpoint(0x02)) @@ -31,16 +16,6 @@ local function switch_off_handler(driver, device, command) device:send(OnOff.server.commands.Off(device):to_endpoint(0x02)) end -local function is_zigbee_metering_plug(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_METERING_PLUG_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("rexense") - return true, subdriver - end - end - - return false -end local zigbee_metering_plug = { NAME = "zigbee metering plug", @@ -50,7 +25,7 @@ local zigbee_metering_plug = { [capabilities.switch.commands.off.NAME] = switch_off_handler } }, - can_handle = is_zigbee_metering_plug + can_handle = require("rexense.can_handle"), } return zigbee_metering_plug diff --git a/drivers/SmartThings/zigbee-switch/src/rgb-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/rgb-bulb/can_handle.lua new file mode 100644 index 0000000000..e52cf32dcd --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/rgb-bulb/can_handle.lua @@ -0,0 +1,23 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) +local RGB_BULB_FINGERPRINTS = { + ["OSRAM"] = { + ["Gardenspot RGB"] = true, + ["LIGHTIFY Gardenspot RGB"] = true + }, + ["LEDVANCE"] = { + ["Outdoor Accent RGB"] = true + } +} + + + local can_handle = (RGB_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] + if can_handle then + local subdriver = require("rgb-bulb") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/rgb-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/rgb-bulb/init.lua index ce0f9976db..867b069bfe 100644 --- a/drivers/SmartThings/zigbee-switch/src/rgb-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/rgb-bulb/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -19,25 +8,6 @@ local OnOff = clusters.OnOff local Level = clusters.Level local ColorControl = clusters.ColorControl -local RGB_BULB_FINGERPRINTS = { - ["OSRAM"] = { - ["Gardenspot RGB"] = true, - ["LIGHTIFY Gardenspot RGB"] = true - }, - ["LEDVANCE"] = { - ["Outdoor Accent RGB"] = true - } -} - -local function can_handle_rgb_bulb(opts, driver, device) - local can_handle = (RGB_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] - if can_handle then - local subdriver = require("rgb-bulb") - return true, subdriver - else - return false - end -end local function do_refresh(driver, device) local attributes = { @@ -66,7 +36,7 @@ local rgb_bulb = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_rgb_bulb + can_handle = require("rgb-bulb.can_handle"), } return rgb_bulb diff --git a/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/can_handle.lua new file mode 100644 index 0000000000..724bb6266a --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local RGBW_BULB_FINGERPRINTS = require "rgbw-bulb.fingerprints" + local can_handle = (RGBW_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] + if can_handle then + local subdriver = require("rgbw-bulb") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/fingerprints.lua new file mode 100644 index 0000000000..c8eedcfec8 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/fingerprints.lua @@ -0,0 +1,74 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + ["Samsung Electronics"] = { + ["SAMSUNG-ITM-Z-002"] = true + }, + ["Juno"] = { + ["ABL-LIGHT-Z-201"] = true + }, + ["AduroSmart Eria"] = { + ["AD-RGBW3001"] = true + }, + ["Aurora"] = { + ["RGBCXStrip50AU"] = true, + ["RGBGU10Bulb50AU"] = true, + ["RGBBulb51AU"] = true + }, + ["CWD"] = { + ["ZB.A806Ergbw-A001"] = true, + ["ZB.A806Brgbw-A001"] = true, + ["ZB.M350rgbw-A001"] = true + }, + ["innr"] = { + ["RB 285 C"] = true, + ["BY 285 C"] = true, + ["RB 250 C"] = true, + ["RS 230 C"] = true, + ["AE 280 C"] = true + }, + ["MLI"] = { + ["ZBT-ExtendedColor"] = true + }, + ["OSRAM"] = { + ["LIGHTIFY Flex RGBW"] = true, + ["Flex RGBW"] = true, + ["LIGHTIFY A19 RGBW"] = true, + ["LIGHTIFY BR RGBW"] = true, + ["LIGHTIFY RT RGBW"] = true, + ["LIGHTIFY FLEX OUTDOOR RGBW"] = true + }, + ["LEDVANCE"] = { + ["RT HO RGBW"] = true, + ["A19 RGBW"] = true, + ["FLEX Outdoor RGBW"] = true, + ["FLEX RGBW"] = true, + ["BR30 RGBW"] = true, + ["RT RGBW"] = true, + ["Outdoor Pathway RGBW"] = true, + ["Flex RGBW Pro"] = true + }, + ["LEEDARSON LIGHTING"] = { + ["5ZB-A806ST-Q1G"] = true + }, + ["sengled"] = { + ["E11-N1EA"] = true, + ["E12-N1E"] = true, + ["E21-N1EA"] = true, + ["E1G-G8E"] = true, + ["E11-U3E"] = true, + ["E11-U2E"] = true, + ["E1F-N5E"] = true, + ["E23-N13"] = true + }, + ["Neuhaus Lighting Group"] = { + ["ZBT-ExtendedColor"] = true + }, + ["Ajaxonline"] = { + ["AJ-RGBCCT 5 in 1"] = true + }, + ["Ajax online Ltd"] = { + ["AJ_ZB30_GU10"] = true + } +} diff --git a/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/init.lua index fb3a1a32f2..cfa2c81855 100644 --- a/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -20,88 +9,6 @@ local OnOff = clusters.OnOff local Level = clusters.Level local ColorControl = clusters.ColorControl -local RGBW_BULB_FINGERPRINTS = { - ["Samsung Electronics"] = { - ["SAMSUNG-ITM-Z-002"] = true - }, - ["Juno"] = { - ["ABL-LIGHT-Z-201"] = true - }, - ["AduroSmart Eria"] = { - ["AD-RGBW3001"] = true - }, - ["Aurora"] = { - ["RGBCXStrip50AU"] = true, - ["RGBGU10Bulb50AU"] = true, - ["RGBBulb51AU"] = true - }, - ["CWD"] = { - ["ZB.A806Ergbw-A001"] = true, - ["ZB.A806Brgbw-A001"] = true, - ["ZB.M350rgbw-A001"] = true - }, - ["innr"] = { - ["RB 285 C"] = true, - ["BY 285 C"] = true, - ["RB 250 C"] = true, - ["RS 230 C"] = true, - ["AE 280 C"] = true - }, - ["MLI"] = { - ["ZBT-ExtendedColor"] = true - }, - ["OSRAM"] = { - ["LIGHTIFY Flex RGBW"] = true, - ["Flex RGBW"] = true, - ["LIGHTIFY A19 RGBW"] = true, - ["LIGHTIFY BR RGBW"] = true, - ["LIGHTIFY RT RGBW"] = true, - ["LIGHTIFY FLEX OUTDOOR RGBW"] = true - }, - ["LEDVANCE"] = { - ["RT HO RGBW"] = true, - ["A19 RGBW"] = true, - ["FLEX Outdoor RGBW"] = true, - ["FLEX RGBW"] = true, - ["BR30 RGBW"] = true, - ["RT RGBW"] = true, - ["Outdoor Pathway RGBW"] = true, - ["Flex RGBW Pro"] = true - }, - ["LEEDARSON LIGHTING"] = { - ["5ZB-A806ST-Q1G"] = true - }, - ["sengled"] = { - ["E11-N1EA"] = true, - ["E12-N1E"] = true, - ["E21-N1EA"] = true, - ["E1G-G8E"] = true, - ["E11-U3E"] = true, - ["E11-U2E"] = true, - ["E1F-N5E"] = true, - ["E23-N13"] = true - }, - ["Neuhaus Lighting Group"] = { - ["ZBT-ExtendedColor"] = true - }, - ["Ajaxonline"] = { - ["AJ-RGBCCT 5 in 1"] = true - }, - ["Ajax online Ltd"] = { - ["AJ_ZB30_GU10"] = true - } -} - -local function can_handle_rgbw_bulb(opts, driver, device) - local can_handle = (RGBW_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] - if can_handle then - local subdriver = require("rgbw-bulb") - return true, subdriver - else - return false - end -end - local function do_refresh(driver, device) local attributes = { OnOff.attributes.OnOff, @@ -149,7 +56,7 @@ local rgbw_bulb = { doConfigure = do_configure, added = do_added }, - can_handle = can_handle_rgbw_bulb + can_handle = require("rgbw-bulb.can_handle"), } return rgbw_bulb diff --git a/drivers/SmartThings/zigbee-switch/src/robb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/robb/can_handle.lua new file mode 100644 index 0000000000..480bc83ab5 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/robb/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) +local ROBB_DIMMER_FINGERPRINTS = { + { mfr = "ROBB smarrt", model = "ROB_200-011-0" }, + { mfr = "ROBB smarrt", model = "ROB_200-014-0" } +} + for _, fingerprint in ipairs(ROBB_DIMMER_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("robb") + return true, subdriver + end + end + return false +end + diff --git a/drivers/SmartThings/zigbee-switch/src/robb/init.lua b/drivers/SmartThings/zigbee-switch/src/robb/init.lua index 4593b4339f..1a8c2948c3 100644 --- a/drivers/SmartThings/zigbee-switch/src/robb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/robb/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local constants = require "st.zigbee.constants" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -18,27 +7,12 @@ local capabilities = require "st.capabilities" local configurations = require "configurations" local SimpleMetering = zcl_clusters.SimpleMetering -local ROBB_DIMMER_FINGERPRINTS = { - { mfr = "ROBB smarrt", model = "ROB_200-011-0" }, - { mfr = "ROBB smarrt", model = "ROB_200-014-0" } -} - -local function is_robb_dimmer(opts, driver, device) - for _, fingerprint in ipairs(ROBB_DIMMER_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("robb") - return true, subdriver - end - end - return false -end local do_init = function(driver, device) device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, 1000000, {persist = true}) device:set_field(constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10, {persist = true}) end - local function power_meter_handler(driver, device, value, zb_rx) local raw_value = value.value local multiplier = device:get_field(constants.ELECTRICAL_MEASUREMENT_MULTIPLIER_KEY) or 1 @@ -60,7 +34,7 @@ local robb_dimmer_handler = { lifecycle_handlers = { init = configurations.power_reconfig_wrapper(do_init) }, - can_handle = is_robb_dimmer + can_handle = require("robb.can_handle"), } return robb_dimmer_handler diff --git a/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/can_handle.lua new file mode 100644 index 0000000000..3a7233e6b9 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local can_handle = device:get_manufacturer() == "Sinope Technologies" and device:get_model() == "DM2500ZB" + if can_handle then + local subdriver = require("sinope-dimmer") + return true, subdriver + else + return false + end + end diff --git a/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/init.lua b/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/init.lua index 981df6ff5e..d97c873467 100644 --- a/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cluster_base = require "st.zigbee.cluster_base" @@ -104,15 +93,7 @@ local zigbee_sinope_dimmer = { lifecycle_handlers = { infoChanged = info_changed }, - can_handle = function(opts, driver, device, ...) - local can_handle = device:get_manufacturer() == "Sinope Technologies" and device:get_model() == "DM2500ZB" - if can_handle then - local subdriver = require("sinope-dimmer") - return true, subdriver - else - return false - end - end + can_handle = require("sinope-dimmer.can_handle"), } return zigbee_sinope_dimmer diff --git a/drivers/SmartThings/zigbee-switch/src/sinope/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/sinope/can_handle.lua new file mode 100644 index 0000000000..624baee756 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/sinope/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local can_handle = device:get_manufacturer() == "Sinope Technologies" and device:get_model() == "SW2500ZB" + if can_handle then + local subdriver = require("sinope") + return true, subdriver + else + return false + end + end diff --git a/drivers/SmartThings/zigbee-switch/src/sinope/init.lua b/drivers/SmartThings/zigbee-switch/src/sinope/init.lua index d3e34513a5..af0300a377 100644 --- a/drivers/SmartThings/zigbee-switch/src/sinope/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/sinope/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" @@ -41,15 +30,7 @@ local zigbee_sinope_switch = { lifecycle_handlers = { infoChanged = info_changed }, - can_handle = function(opts, driver, device, ...) - local can_handle = device:get_manufacturer() == "Sinope Technologies" and device:get_model() == "SW2500ZB" - if can_handle then - local subdriver = require("sinope") - return true, subdriver - else - return false - end - end + can_handle = require("sinope.can_handle"), } return zigbee_sinope_switch diff --git a/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua new file mode 100644 index 0000000000..c232b40329 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua @@ -0,0 +1,38 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +local version = require "version" + +local lazy_load_if_possible = require "lazy_load_subdriver" + +return { + lazy_load_if_possible("non_zigbee_devices"), + lazy_load_if_possible("hanssem"), + lazy_load_if_possible("aqara"), + lazy_load_if_possible("aqara-light"), + lazy_load_if_possible("ezex"), + lazy_load_if_possible("rexense"), + lazy_load_if_possible("sinope"), + lazy_load_if_possible("sinope-dimmer"), + lazy_load_if_possible("zigbee-dimmer-power-energy"), + lazy_load_if_possible("zigbee-metering-plug-power-consumption-report"), + lazy_load_if_possible("jasco"), + lazy_load_if_possible("multi-switch-no-master"), + lazy_load_if_possible("zigbee-dual-metering-switch"), + lazy_load_if_possible("rgb-bulb"), + lazy_load_if_possible("zigbee-dimming-light"), + lazy_load_if_possible("white-color-temp-bulb"), + lazy_load_if_possible("rgbw-bulb"), + (version.api < 16) and lazy_load_if_possible("zll-dimmer-bulb") or nil, + lazy_load_if_possible("ikea-xy-color-bulb"), + lazy_load_if_possible("zll-polling"), + lazy_load_if_possible("zigbee-switch-power"), + lazy_load_if_possible("ge-link-bulb"), + lazy_load_if_possible("bad_on_off_data_type"), + lazy_load_if_possible("robb"), + lazy_load_if_possible("wallhero"), + lazy_load_if_possible("inovelli"), -- Combined driver for both VZM31-SN and VZM32-SN + lazy_load_if_possible("laisiao"), + lazy_load_if_possible("tuya-multi"), + lazy_load_if_possible("frient"), + lazy_load_if_possible("frient-IO") +} diff --git a/drivers/SmartThings/zigbee-switch/src/switch_utils.lua b/drivers/SmartThings/zigbee-switch/src/switch_utils.lua new file mode 100644 index 0000000000..66ad4715f9 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/switch_utils.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local switch_utils = {} + +switch_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + +return switch_utils diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_all_capability_zigbee_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_all_capability_zigbee_bulb.lua index 0301c0228c..b581c5c61e 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_all_capability_zigbee_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_all_capability_zigbee_bulb.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -202,6 +191,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -236,6 +233,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.hue(0)) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } + } } } ) @@ -253,6 +258,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.saturation(50)) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } + } } } ) @@ -340,7 +353,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 1, 3600, 5) + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -354,7 +367,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 1, 3600, 5) + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -552,6 +565,7 @@ test.register_coroutine_test( test.wait_for_events() test.socket.zigbee:__queue_receive({mock_device.id, ColorControl.attributes.ColorTemperatureMireds:build_test_attr_report(mock_device, 556)}) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800))) + mock_device:expect_native_attr_handler_registration("colorTemperature", "colorTemperature") end ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_led_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_led_bulb.lua index 9c7a7cc0e5..ce4bb13957 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_led_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_led_bulb.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -18,6 +7,7 @@ local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local capabilities = require "st.capabilities" local OnOff = clusters.OnOff local Level = clusters.Level @@ -61,6 +51,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua index 68f54c986f..516a18c326 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -18,6 +7,7 @@ local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local capabilities = require "st.capabilities" local OnOff = clusters.OnOff local Level = clusters.Level @@ -63,6 +53,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end ) @@ -189,4 +180,38 @@ test.register_coroutine_test( end ) +local mock_device_cwacn1 = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("aqara-light.yml"), + preferences = { ["stse.lightFadeInTimeInSec"] = 0, ["stse.lightFadeOutTimeInSec"] = 0 }, + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "LUMI", + model = "lumi.light.cwacn1", + server_clusters = { 0x0006, 0x0008, 0x0300 } + } + } + } +) + +test.register_coroutine_test( + "Handle added lifecycle for lumi.light.cwacn1 model (colorTemperatureRange max = 6500)", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device_cwacn1.id, "added" }) + + test.socket.zigbee:__expect_send({ + mock_device_cwacn1.id, cluster_base.write_manufacturer_specific_attribute(mock_device_cwacn1, PRIVATE_CLUSTER_ID, + PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1) }) + test.socket.capability:__expect_send(mock_device_cwacn1:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6500 }))) + end, + { + test_init = function() + test.mock_device.add_test_device(mock_device_cwacn1) + end + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug.lua index d9edfbe425..0d51bd11c2 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug_t1.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug_t1.lua index c1ba5bef7d..8a544caf13 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug_t1.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug_t1.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module.lua index e0038602fb..f27d84cfc6 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module_no_power.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module_no_power.lua index 6ed61191da..840aadacb9 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module_no_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module_no_power.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_no_power.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_no_power.lua index d605f5480d..207e7bf21b 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_no_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_no_power.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -37,6 +26,23 @@ local PRIVATE_MODE = "PRIVATE_MODE" local mock_device = test.mock_device.build_test_zigbee_device( { + label = "Aqara Smart Wall Switch H1 EU (No Neutral, Double Rocker) 1", + profile = t_utils.get_profile_definition("aqara-switch-no-power.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "LUMI", + model = "lumi.switch.l2aeu1", + server_clusters = { 0x0006 } + } + } + } +) + +local mock_base_device = test.mock_device.build_test_zigbee_device( + { + label = "Aqara Smart Wall Switch H1 EU (No Neutral, Double Rocker) 1", profile = t_utils.get_profile_definition("aqara-switch-no-power.yml"), fingerprinted_endpoint_id = 0x01, zigbee_endpoints = { @@ -62,6 +68,7 @@ zigbee_test_utils.prepare_zigbee_env_info() local function test_init() test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_base_device) test.mock_device.add_test_device(mock_child) end @@ -70,6 +77,7 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Lifecycle - added test", function() + -- The initial switch event should be send during the device's first time onboarding test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({ value = 2 }, @@ -80,13 +88,22 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))) - + -- Avoid sending the initial switch event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({ value = 2 }, + { visibility = { displayed = false } }))) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, + data_types.Uint8, 1) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed" }, + { visibility = { displayed = false } }))) end ) test.register_coroutine_test( "Lifecycle - added test", function() + -- The initial switch event should be send during the device's first time onboarding test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_child.id, "added" }) test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.button.numberOfButtons({ value = 1 }, @@ -94,6 +111,37 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }))) test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))) + -- Avoid sending the initial switch event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_child.id, "added" }) + test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.button.numberOfButtons({ value = 1 }, + { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed" }, + { visibility = { displayed = false } }))) + end +) + +test.register_coroutine_test( + "Lifecycle - added test", + function() + -- The initial switch event should be send during the device's first time onboarding + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_base_device.id, "added" }) + mock_base_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Aqara Smart Wall Switch H1 EU (No Neutral, Double Rocker) 2", + profile = "aqara-switch-child", + parent_device_id = mock_base_device.id, + parent_assigned_child_key = "02" + }) + test.socket.capability:__expect_send(mock_base_device:generate_test_message("main", capabilities.button.numberOfButtons({ value = 2 }, + { visibility = { displayed = false } }))) + test.socket.zigbee:__expect_send({ mock_base_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_base_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, + data_types.Uint8, 1) }) + test.socket.capability:__expect_send(mock_base_device:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed" }, + { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_base_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))) end ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_power.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_power.lua index 8b596191a5..a02ef37aa2 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_power.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_wall_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_wall_switch.lua index b47d0595c8..3711116d99 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_wall_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_wall_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aurora_relay.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aurora_relay.lua index 6dd6ed75b7..57d6576955 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aurora_relay.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aurora_relay.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_bad_data_type.lua b/drivers/SmartThings/zigbee-switch/src/test/test_bad_data_type.lua index 69ee7f043a..b30f49df23 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_bad_data_type.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_bad_data_type.lua @@ -1,17 +1,5 @@ - --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_bad_device_kind.lua b/drivers/SmartThings/zigbee-switch/src/test/test_bad_device_kind.lua new file mode 100644 index 0000000000..bd8ce15b10 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_bad_device_kind.lua @@ -0,0 +1,47 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local dkjson = require 'dkjson' + +-- This test attempts to add a zwave device to this zigbee switch driver +-- Once the monkey-patch is removed with hubcore 59 is released with: +-- https://smartthings.atlassian.net/browse/CHAD-16552 +local mock_device = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("on-off-level.yml"), +}) + +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +-- Just validating that the driver doesn't crash is enough to validate +-- that the work-around is effective in ignoring the incorrect device kind +test.register_coroutine_test("zwave_device_handled", function() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({provisioning_state = "PROVISIONED"}) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", dkjson.encode(mock_device.raw_st_data) }) + test.wait_for_events() + end, + nil +) + +test.register_message_test( + "Capability command for incorrect protocol", + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "main", command = "on", args = { } } } + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_cree_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_cree_bulb.lua index e971201f85..8841c5a53d 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_cree_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_cree_bulb.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_duragreen_color_temp_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_duragreen_color_temp_bulb.lua index 0dee55ebf4..55c491e77d 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_duragreen_color_temp_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_duragreen_color_temp_bulb.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_enbrighten_metering_dimmer.lua b/drivers/SmartThings/zigbee-switch/src/test/test_enbrighten_metering_dimmer.lua index b7b4f67b7a..3b5a5b93b1 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_enbrighten_metering_dimmer.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_enbrighten_metering_dimmer.lua @@ -1,24 +1,15 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" +local ElectricalMeasurementCluster = clusters.ElectricalMeasurement local OnOffCluster = clusters.OnOff local LevelCluster = clusters.Level local SimpleMeteringCluster = clusters.SimpleMetering local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" local mock_device = test.mock_device.build_test_zigbee_device({ profile = t_utils.get_profile_definition("switch-dimmer-power-energy.yml"), @@ -27,12 +18,14 @@ local mock_device = test.mock_device.build_test_zigbee_device({ id = 1, manufacturer = "Jasco Products", model = "43082", - server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0702, 0x0B05 }, + server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0702, 0x0B04 }, client_clusters = { 0x000A, 0x0019 } } } }) +zigbee_test_utils.prepare_zigbee_env_info() + local function test_init() mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) @@ -40,6 +33,104 @@ end test.set_test_init_function(test_init) +test.register_coroutine_test( + "lifecycle configure event should configure device", + function () + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({mock_device.id, "doConfigure"}) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + ElectricalMeasurementCluster.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + OnOffCluster.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + LevelCluster.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + SimpleMeteringCluster.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ActivePower:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ACPowerDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + LevelCluster.attributes.CurrentLevel:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.InstantaneousDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.CurrentSummationDelivered:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.Multiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.Divisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + LevelCluster.attributes.CurrentLevel:configure_reporting(mock_device, 1, 3600, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + test.register_message_test( "Capability command On should be handled", { diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_frient_IO_module.lua b/drivers/SmartThings/zigbee-switch/src/test/test_frient_IO_module.lua new file mode 100644 index 0000000000..c9d5f9b75e --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_frient_IO_module.lua @@ -0,0 +1,683 @@ +-- Copyright 2025 SmartThings +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" + +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" +local messages = require "st.zigbee.messages" +local constants = require "st.zigbee.constants" +local zdo_messages = require "st.zigbee.zdo" +local bind_request = require "st.zigbee.zdo.bind_request" +local unbind_request = require "frient-IO.unbind_request" +local default_response = require "st.zigbee.zcl.global_commands.default_response" +local zcl_messages = require "st.zigbee.zcl" +local Status = require "st.zigbee.generated.types.ZclStatus" +local device_management = require "st.zigbee.device_management" +local configuration_map = require "configurations" +local switch_defaults = require "st.zigbee.defaults.switch_defaults" +local mock_devices_api = require "integration_test.mock_devices_api" + +local BasicInput = clusters.BasicInput +local OnOff = clusters.OnOff +local Switch = capabilities.switch + +local ZIGBEE_ENDPOINTS = { + INPUT_1 = 0x70, + INPUT_2 = 0x71, + INPUT_3 = 0x72, + INPUT_4 = 0x73, + OUTPUT_1 = 0x74, + OUTPUT_2 = 0x75, +} + +local INPUT_CONFIGS = { + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_1, + binds = { + ZIGBEE_ENDPOINTS.OUTPUT_1, + ZIGBEE_ENDPOINTS.OUTPUT_2, + }, + }, + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_2, + binds = { + ZIGBEE_ENDPOINTS.OUTPUT_1, + ZIGBEE_ENDPOINTS.OUTPUT_2, + }, + }, + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_3, + binds = { + ZIGBEE_ENDPOINTS.OUTPUT_1, + ZIGBEE_ENDPOINTS.OUTPUT_2, + }, + }, + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_4, + binds = { + ZIGBEE_ENDPOINTS.OUTPUT_1, + ZIGBEE_ENDPOINTS.OUTPUT_2, + }, + }, +} + +local DEVELCO_MFG_CODE = 0x1015 +local ON_TIME_ATTR = 0x8000 +local OFF_WAIT_ATTR = 0x8001 + +local function sanitize_timing(value) + local v = tonumber(value) or 0 + if v < 0 then + v = 0 + elseif v > 0xFFFF then + v = 0xFFFF + end + return math.tointeger(v) or 0 +end + +local function to_deciseconds(value) + return math.floor(sanitize_timing(value) * 10) +end + +local function build_client_mfg_write(device, endpoint, attr_id, value) + local msg = cluster_base.write_manufacturer_specific_attribute( + device, + BasicInput.ID, + attr_id, + DEVELCO_MFG_CODE, + data_types.Uint16, + value + ) + msg.body.zcl_header.frame_ctrl:set_direction_client() + msg.tx_options = data_types.Uint16(0) + return msg:to_endpoint(endpoint) +end + +local function build_basic_input_polarity_write(device, endpoint, enabled) + local polarity_value = data_types.validate_or_build_type( + enabled and 1 or 0, + BasicInput.attributes.Polarity.base_type, + "payload" + ) + local msg = cluster_base.write_attribute( + device, + data_types.ClusterId(BasicInput.ID), + data_types.AttributeId(BasicInput.attributes.Polarity.ID), + polarity_value + ) + msg.tx_options = data_types.Uint16(0) + return msg:to_endpoint(endpoint) +end + +local function build_bind(device, src_ep, dest_ep) + local addr_header = messages.AddressHeader( + constants.HUB.ADDR, + constants.HUB.ENDPOINT, + device:get_short_address(), + device.fingerprinted_endpoint_id, + constants.ZDO_PROFILE_ID, + bind_request.BindRequest.ID + ) + local bind_body = bind_request.BindRequest( + device.zigbee_eui, + src_ep, + BasicInput.ID, + bind_request.ADDRESS_MODE_64_BIT, + device.zigbee_eui, + dest_ep + ) + local message_body = zdo_messages.ZdoMessageBody({ zdo_body = bind_body }) + local msg = messages.ZigbeeMessageTx({ address_header = addr_header, body = message_body }) + msg.tx_options = data_types.Uint16(0) + return msg +end + +local function build_unbind(device, src_ep, dest_ep) + local addr_header = messages.AddressHeader( + constants.HUB.ADDR, + constants.HUB.ENDPOINT, + device:get_short_address(), + device.fingerprinted_endpoint_id, + constants.ZDO_PROFILE_ID, + unbind_request.UNBIND_REQUEST_CLUSTER_ID + ) + local unbind_body = unbind_request.UnbindRequest( + device.zigbee_eui, + src_ep, + BasicInput.ID, + unbind_request.ADDRESS_MODE_64_BIT, + device.zigbee_eui, + dest_ep + ) + local message_body = zdo_messages.ZdoMessageBody({ zdo_body = unbind_body }) + local msg = messages.ZigbeeMessageTx({ address_header = addr_header, body = message_body }) + msg.tx_options = data_types.Uint16(0) + return msg +end + +local function build_default_response_msg(device, endpoint, command_id) + local addr_header = messages.AddressHeader( + device:get_short_address(), + endpoint, + constants.HUB.ADDR, + constants.HUB.ENDPOINT, + constants.HA_PROFILE_ID, + OnOff.ID + ) + local response_body = default_response.DefaultResponse(command_id, Status.SUCCESS) + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(response_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = response_body + }) + return messages.ZigbeeMessageRx({ address_header = addr_header, body = message_body }) +end + +local function build_output_timing(device, child, suffix) + local on_pref + local off_pref + if child.preferences.configOnTime ~= nil or child.preferences.configOffWaitTime ~= nil then + on_pref = child.preferences.configOnTime or 0 + off_pref = child.preferences.configOffWaitTime or 0 + else + on_pref = device.preferences["configOnTime" .. suffix] or 0 + off_pref = device.preferences["configOffWaitTime" .. suffix] or 0 + end + return to_deciseconds(on_pref), to_deciseconds(off_pref) +end + +local function copy_table(source) + local result = {} + for key, value in pairs(source) do + result[key] = value + end + return result +end + +local parent_preference_state = {} + +local mock_parent_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("switch-4inputs-2outputs.yml"), + fingerprinted_endpoint_id = ZIGBEE_ENDPOINTS.INPUT_1, + label = "frient IO Module", + zigbee_endpoints = { + [ZIGBEE_ENDPOINTS.INPUT_1] = { + id = ZIGBEE_ENDPOINTS.INPUT_1, + manufacturer = "frient A/S", + model = "IOMZB-110", + server_clusters = { BasicInput.ID }, + }, + [ZIGBEE_ENDPOINTS.INPUT_2] = { + id = ZIGBEE_ENDPOINTS.INPUT_2, + manufacturer = "frient A/S", + model = "IOMZB-110", + server_clusters = { BasicInput.ID }, + }, + [ZIGBEE_ENDPOINTS.INPUT_3] = { + id = ZIGBEE_ENDPOINTS.INPUT_3, + manufacturer = "frient A/S", + model = "IOMZB-110", + server_clusters = { BasicInput.ID }, + }, + [ZIGBEE_ENDPOINTS.INPUT_4] = { + id = ZIGBEE_ENDPOINTS.INPUT_4, + manufacturer = "frient A/S", + model = "IOMZB-110", + server_clusters = { BasicInput.ID }, + }, + [ZIGBEE_ENDPOINTS.OUTPUT_1] = { + id = ZIGBEE_ENDPOINTS.OUTPUT_1, + manufacturer = "frient A/S", + model = "IOMZB-110", + server_clusters = { OnOff.ID, BasicInput.ID }, + }, + [ZIGBEE_ENDPOINTS.OUTPUT_2] = { + id = ZIGBEE_ENDPOINTS.OUTPUT_2, + manufacturer = "frient A/S", + model = "IOMZB-110", + server_clusters = { OnOff.ID, BasicInput.ID }, + }, + }, +}) + + function mock_parent_device:get_model() + return "IOMZB-110" + end + + function mock_parent_device:get_manufacturer() + return "frient A/S" + end + + function mock_parent_device:supports_server_cluster(cluster_id, endpoint_id) + local function endpoint_supports(ep) + if not ep or not ep.server_clusters then return false end + for _, server_cluster in ipairs(ep.server_clusters) do + if server_cluster == cluster_id then + return true + end + end + return false + end + + if endpoint_id ~= nil then + return endpoint_supports(self.zigbee_endpoints[endpoint_id]) + end + + for _, endpoint in pairs(self.zigbee_endpoints) do + if endpoint_supports(endpoint) then + return true + end + end + return false + end + +local mock_output_child_1 = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("frient-io-output-switch.yml"), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = "frient-io-output-1", + label = "frient IO Module Output 1", + vendor_provided_label = "Output 1", +}) + +local mock_output_child_2 = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("frient-io-output-switch.yml"), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = "frient-io-output-2", + label = "frient IO Module Output 2", + vendor_provided_label = "Output 2", +}) + +local function reset_preferences() + mock_parent_device.preferences.reversePolarity1 = false + mock_parent_device.preferences.reversePolarity2 = false + mock_parent_device.preferences.reversePolarity3 = false + mock_parent_device.preferences.reversePolarity4 = false + + mock_parent_device.preferences.controlOutput11 = false + mock_parent_device.preferences.controlOutput21 = false + mock_parent_device.preferences.controlOutput12 = false + mock_parent_device.preferences.controlOutput22 = false + mock_parent_device.preferences.controlOutput13 = false + mock_parent_device.preferences.controlOutput23 = false + mock_parent_device.preferences.controlOutput14 = false + mock_parent_device.preferences.controlOutput24 = false + + mock_parent_device.preferences.configOnTime1 = 3 + mock_parent_device.preferences.configOffWaitTime1 = 4 + mock_parent_device.preferences.configOnTime2 = 7 + mock_parent_device.preferences.configOffWaitTime2 = 8 + + mock_output_child_1.preferences.configOnTime = 5 + mock_output_child_1.preferences.configOffWaitTime = 6 + mock_output_child_2.preferences.configOnTime = 0 + mock_output_child_2.preferences.configOffWaitTime = 0 + + parent_preference_state = copy_table(mock_parent_device.preferences) + + local field_keys = { + "frient_io_native_70", + "frient_io_native_71", + "frient_io_native_72", + "frient_io_native_73", + "frient_io_native_74", + "frient_io_native_75", + } + + for _, key in ipairs(field_keys) do + mock_parent_device:set_field(key, nil, { persist = true }) + end + + mock_output_child_1:set_field("frient_io_native_74", nil, { persist = true }) + mock_output_child_2:set_field("frient_io_native_75", nil, { persist = true }) +end + +local function queue_child_info_changed(child, preferences) + local raw = rawget(child, "raw_st_data") + if raw and raw.preferences then + for key, value in pairs(preferences) do + raw.preferences[key] = value + end + end + test.socket.device_lifecycle:__queue_receive(child:generate_info_changed({ preferences = preferences })) +end + +local function queue_parent_info_changed(preferences) + local full_preferences = copy_table(parent_preference_state) + for key, value in pairs(preferences) do + full_preferences[key] = value + end + parent_preference_state = copy_table(full_preferences) + + local raw = rawget(mock_parent_device, "raw_st_data") + if raw and raw.preferences then + for key, value in pairs(full_preferences) do + raw.preferences[key] = value + end + end + + test.socket.device_lifecycle:__queue_receive( + mock_parent_device:generate_info_changed({ preferences = full_preferences }) + ) +end + +local function register_initial_config_expectations() + if test.socket.zigbee and test.socket.zigbee.__set_channel_ordering then + test.socket.zigbee:__set_channel_ordering("relaxed") + end + if test.socket.devices and test.socket.devices.__set_channel_ordering then + test.socket.devices:__set_channel_ordering("relaxed") + end + + local function register_device_configure_expectations() + local configuration = configuration_map.get_device_configuration(mock_parent_device) or {} + local configs_by_cluster = {} + local function add_attribute_config(attribute) + if attribute.configurable ~= false then + configs_by_cluster[attribute.cluster] = configs_by_cluster[attribute.cluster] or {} + table.insert(configs_by_cluster[attribute.cluster], attribute) + end + end + + for _, attribute in ipairs(configuration) do + add_attribute_config(attribute) + end + + local default_configs = switch_defaults.attribute_configurations or {} + for _, attribute in ipairs(default_configs) do + add_attribute_config(attribute) + end + + local cluster_ids = {} + for cluster_id in pairs(configs_by_cluster) do + cluster_ids[#cluster_ids + 1] = cluster_id + end + table.sort(cluster_ids) + + local endpoint_ids = {} + for endpoint_id in pairs(mock_parent_device.zigbee_endpoints) do + endpoint_ids[#endpoint_ids + 1] = endpoint_id + end + table.sort(endpoint_ids) + + for _, cluster_id in ipairs(cluster_ids) do + local attr_configs = configs_by_cluster[cluster_id] + table.sort(attr_configs, function(a, b) + return a.attribute < b.attribute + end) + for _, endpoint_id in ipairs(endpoint_ids) do + local endpoint = mock_parent_device.zigbee_endpoints[endpoint_id] + if endpoint and mock_parent_device:supports_server_cluster(cluster_id, endpoint.id) then + local bind_cmd = device_management.build_bind_request( + mock_parent_device, + cluster_id, + zigbee_test_utils.mock_hub_eui, + endpoint.id + ):to_endpoint(endpoint.id) + bind_cmd.tx_options = data_types.Uint16(0) + test.socket.zigbee:__expect_send({ mock_parent_device.id, bind_cmd }) + for _, attr_config in ipairs(attr_configs) do + local config_cmd = device_management.attr_config(mock_parent_device, attr_config):to_endpoint(endpoint.id) + config_cmd.tx_options = data_types.Uint16(0) + test.socket.zigbee:__expect_send({ mock_parent_device.id, config_cmd }) + end + end + end + end + end + + register_device_configure_expectations() + + local on1, off1 = build_output_timing(mock_parent_device, mock_output_child_1, "1") + local on2, off2 = build_output_timing(mock_parent_device, mock_output_child_2, "2") + + local function enqueue_output_timing_writes() + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_client_mfg_write(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_1, ON_TIME_ATTR, on1) }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_client_mfg_write(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_1, OFF_WAIT_ATTR, off1) }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_client_mfg_write(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_2, ON_TIME_ATTR, on2) }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_client_mfg_write(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_2, OFF_WAIT_ATTR, off2) }) + end + + -- Device init issues one set of manufacturer-specific writes per output during startup + enqueue_output_timing_writes() + + for _, config in ipairs(INPUT_CONFIGS) do + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_basic_input_polarity_write(mock_parent_device, config.endpoint, false) }) + for _, output_ep in ipairs(config.binds) do + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_unbind(mock_parent_device, config.endpoint, output_ep) }) + end + end +end + +local function expect_init_sequence() + mock_devices_api.__expect_update_device( + mock_parent_device.id, + { deviceId = mock_parent_device.id, provisioningState = "PROVISIONED" } + ) + test.socket.device_lifecycle:__queue_receive({ mock_parent_device.id, "doConfigure" }) +end + +local function expect_switch_registration(device) + test.socket.devices:__expect_send({ + "register_native_capability_attr_handler", + { device_uuid = device.id, capability_id = "switch", capability_attr_id = "switch" }, + }) +end + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + reset_preferences() + register_initial_config_expectations() + test.mock_device.add_test_device(mock_parent_device) + test.mock_device.add_test_device(mock_output_child_1) + test.mock_device.add_test_device(mock_output_child_2) + zigbee_test_utils.init_noop_health_check_timer() + --register_initial_config_expectations() +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Init configures outputs and routes attribute reports", + function() + expect_init_sequence() + test.wait_for_events() + + test.socket.capability:__set_channel_ordering("relaxed") + + test.socket.zigbee:__queue_receive({ + mock_parent_device.id, + OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, true):from_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_1), + }) + test.socket.capability:__expect_send(mock_output_child_1:generate_test_message("main", Switch.switch.on())) + expect_switch_registration(mock_output_child_1) + + test.socket.zigbee:__queue_receive({ + mock_parent_device.id, + OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, false):from_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_2), + }) + test.socket.capability:__expect_send(mock_output_child_2:generate_test_message("main", Switch.switch.off())) + expect_switch_registration(mock_output_child_2) + + test.socket.zigbee:__queue_receive({ + mock_parent_device.id, + BasicInput.attributes.PresentValue:build_test_attr_report(mock_parent_device, true):from_endpoint(ZIGBEE_ENDPOINTS.INPUT_3), + }) + test.socket.capability:__expect_send(mock_parent_device:generate_test_message("input3", Switch.switch.on())) + + test.wait_for_events() + + local child1_native = mock_output_child_1:get_field("frient_io_native_74") + assert(child1_native, "expected Output 1 child to register native switch handler") + local child2_native = mock_output_child_2:get_field("frient_io_native_75") + assert(child2_native, "expected Output 2 child to register native switch handler") + local parent_native = mock_parent_device:get_field("frient_io_native_72") + assert(parent_native, "expected parent device to register native switch handler for input 3") + end +) + +test.register_coroutine_test( + "Default responses update state and trigger reads", + function() + expect_init_sequence() + test.wait_for_events() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__set_channel_ordering("relaxed") + + local on_response = build_default_response_msg(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_1, OnOff.server.commands.On.ID) + test.socket.zigbee:__queue_receive({ mock_parent_device.id, on_response }) + test.socket.capability:__expect_send(mock_output_child_1:generate_test_message("main", Switch.switch.on())) + + local timed_response = build_default_response_msg(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_1, OnOff.server.commands.OnWithTimedOff.ID) + test.socket.zigbee:__queue_receive({ mock_parent_device.id, timed_response }) + local read_msg = cluster_base.read_attribute( + mock_parent_device, + data_types.ClusterId(OnOff.ID), + data_types.AttributeId(OnOff.attributes.OnOff.ID) + ) + read_msg.tx_options = data_types.Uint16(0) + read_msg = read_msg:to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_1) + test.socket.zigbee:__expect_send({ mock_parent_device.id, read_msg }) + + local off_response = build_default_response_msg(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_1, OnOff.server.commands.Off.ID) + test.socket.zigbee:__queue_receive({ mock_parent_device.id, off_response }) + test.socket.capability:__expect_send(mock_output_child_1:generate_test_message("main", Switch.switch.off())) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Switch commands drive the correct Zigbee commands", + function() + expect_init_sequence() + test.wait_for_events() + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.capability:__queue_receive({ + mock_output_child_1.id, + { capability = "switch", component = "main", command = "on", args = {} }, + }) + local on1, off1 = build_output_timing(mock_parent_device, mock_output_child_1, "1") + local timed_on = OnOff.server.commands.OnWithTimedOff( + mock_parent_device, + data_types.Uint8(0), + data_types.Uint16(on1), + data_types.Uint16(off1) + ):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_1) + test.socket.zigbee:__expect_send({ mock_parent_device.id, timed_on }) + + test.socket.capability:__queue_receive({ + mock_output_child_1.id, + { capability = "switch", component = "main", command = "off", args = {} }, + }) + local direct_off_output1 = OnOff.server.commands.Off(mock_parent_device):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_1) + test.socket.zigbee:__expect_send({ mock_parent_device.id, direct_off_output1 }) + + test.socket.capability:__queue_receive({ + mock_output_child_2.id, + { capability = "switch", component = "main", command = "on", args = {} }, + }) + local direct_on = OnOff.server.commands.On(mock_parent_device):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_2) + test.socket.zigbee:__expect_send({ mock_parent_device.id, direct_on }) + + test.socket.capability:__queue_receive({ + mock_output_child_2.id, + { capability = "switch", component = "main", command = "off", args = {} }, + }) + local direct_off = OnOff.server.commands.Off(mock_parent_device):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_2) + test.socket.zigbee:__expect_send({ mock_parent_device.id, direct_off }) + + test.socket.capability:__queue_receive({ + mock_parent_device.id, + { capability = "switch", component = "output1", command = "on", args = {} }, + }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, timed_on }) + + test.socket.capability:__queue_receive({ + mock_parent_device.id, + { capability = "switch", component = "output2", command = "off", args = {} }, + }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, direct_off }) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Child preference changes send manufacturer writes", + function() + expect_init_sequence() + test.wait_for_events() + test.socket.zigbee:__set_channel_ordering("relaxed") + + queue_child_info_changed(mock_output_child_1, { configOnTime = 12, configOffWaitTime = 13 }) + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + build_client_mfg_write(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_1, ON_TIME_ATTR, to_deciseconds(12)), + }) + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + build_client_mfg_write(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_1, OFF_WAIT_ATTR, to_deciseconds(13)), + }) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Parent preference changes manage polarity and binds", + function() + expect_init_sequence() + test.wait_for_events() + test.socket.zigbee:__set_channel_ordering("relaxed") + + queue_parent_info_changed({ + reversePolarity1 = true, + controlOutput11 = true, + controlOutput21 = true, + }) + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + build_basic_input_polarity_write(mock_parent_device, ZIGBEE_ENDPOINTS.INPUT_1, true), + }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_bind(mock_parent_device, ZIGBEE_ENDPOINTS.INPUT_1, ZIGBEE_ENDPOINTS.OUTPUT_1) }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_bind(mock_parent_device, ZIGBEE_ENDPOINTS.INPUT_1, ZIGBEE_ENDPOINTS.OUTPUT_2) }) + test.wait_for_events() + + queue_parent_info_changed({ + reversePolarity1 = true, + controlOutput11 = false, + controlOutput21 = true, + }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_unbind(mock_parent_device, ZIGBEE_ENDPOINTS.INPUT_1, ZIGBEE_ENDPOINTS.OUTPUT_1) }) + test.wait_for_events() + + queue_parent_info_changed({ + reversePolarity3 = true, + controlOutput23 = true, + }) + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + build_basic_input_polarity_write(mock_parent_device, ZIGBEE_ENDPOINTS.INPUT_3, true), + }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_bind(mock_parent_device, ZIGBEE_ENDPOINTS.INPUT_3, ZIGBEE_ENDPOINTS.OUTPUT_2) }) + test.wait_for_events() + + queue_parent_info_changed({ + reversePolarity3 = true, + controlOutput23 = false, + }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_unbind(mock_parent_device, ZIGBEE_ENDPOINTS.INPUT_3, ZIGBEE_ENDPOINTS.OUTPUT_2) }) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_frient_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_frient_switch.lua index 8fb1704f38..4290d6ebbe 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_frient_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_frient_switch.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -222,6 +211,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 2.7, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_ge_link_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_ge_link_bulb.lua index cdcd49c616..bd54b1b20f 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_ge_link_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_ge_link_bulb.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" @@ -149,4 +138,38 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Handle infoChanged when dimOnOff changes from 1 to 0 should write transition time 0", + function() + -- First: change dimOnOff from default 0 to 1 (triggers write with dimRate=20) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ + preferences = { dimOnOff = 1 } + })) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.OnOffTransitionTime:write(mock_device, 20) }) + test.wait_for_events() + -- Now: change dimOnOff from 1 to 0 (driver old=1, new=0 -> writes 0) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ + preferences = { dimOnOff = 0 } + })) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.OnOffTransitionTime:write(mock_device, 0) }) + end +) + +test.register_coroutine_test( + "Handle infoChanged when dimRate changes while dimOnOff is 1 should write new dimRate", + function() + -- First: change dimOnOff from default 0 to 1 (triggers write with dimRate=20) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ + preferences = { dimOnOff = 1 } + })) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.OnOffTransitionTime:write(mock_device, 20) }) + test.wait_for_events() + -- Now: change dimRate while dimOnOff stays 1 (driver old={dimOnOff=1,dimRate=20}, new={dimOnOff=1,dimRate=50}) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ + preferences = { dimOnOff = 1, dimRate = 50 } + })) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.OnOffTransitionTime:write(mock_device, 50) }) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_hanssem_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_hanssem_switch.lua index 37de10bdec..96f4581310 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_hanssem_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_hanssem_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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..79e7a66b54 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua @@ -0,0 +1,537 @@ +-- 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 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 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", + { + { + 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..0726a2d575 --- /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( + "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 = {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 + 15, -- 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( + "parameter95 preference should send configuration command", + function() + 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 + 95, -- 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 new file mode 100644 index 0000000000..a3d57415b1 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua @@ -0,0 +1,421 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +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" + +-- Inovelli VZM31-SN device identifiers +local INOVELLI_MANUFACTURER_ID = "Inovelli" +local INOVELLI_VZM31_SN_MODEL = "VZM31-SN" + +-- Device endpoints with supported clusters +local inovelli_vzm31_sn_endpoints = { + [1] = { + id = 1, + manufacturer = INOVELLI_MANUFACTURER_ID, + model = INOVELLI_VZM31_SN_MODEL, + server_clusters = {0x0006, 0x0008, 0x0300} -- OnOff, Level, ColorControl + } +} + +local mock_inovelli_vzm31_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm31-sn.yml"), + zigbee_endpoints = inovelli_vzm31_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_vzm31_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_vzm31_sn.id, "added" }, + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + clusters.Level.attributes.CurrentLevel:read(mock_inovelli_vzm31_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + clusters.OnOff.attributes.OnOff:read(mock_inovelli_vzm31_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_vzm31_sn.id, + { capability = "switch", command = "on", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm31_sn.id, clusters.OnOff.server.commands.On(mock_inovelli_vzm31_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_vzm31_sn.id, + { capability = "switch", command = "off", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm31_sn.id, clusters.OnOff.server.commands.Off(mock_inovelli_vzm31_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_vzm31_sn.id, + { capability = "switchLevel", command = "setLevel", args = { 50 } } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + clusters.Level.server.commands.MoveToLevelWithOnOff(mock_inovelli_vzm31_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 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 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", + 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_coroutine_test( + "Energy meter should emit energy events", + 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 +test.register_message_test( + "Energy meter reset command should send reset commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm31_sn.id, + { capability = "energyMeter", command = "resetEnergyMeter", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + cluster_base.build_manufacturer_specific_command( + mock_inovelli_vzm31_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_vzm31_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(mock_inovelli_vzm31_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:read(mock_inovelli_vzm31_sn) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + + +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) }) + + 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_vzm31_sn_child.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_child.lua new file mode 100644 index 0000000000..c6476cba5e --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_child.lua @@ -0,0 +1,346 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +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_vzm31_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM31-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-vzm31-sn.yml"), + zigbee_endpoints = inovelli_vzm31_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_vzm31_sn_preferences.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_preferences.lua new file mode 100644 index 0000000000..64b0d51ee5 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_preferences.lua @@ -0,0 +1,200 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +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_vzm31_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM31-SN", + server_clusters = {0x0006, 0x0008} -- OnOff, Level + } +} + +local mock_inovelli_vzm31_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm31-sn.yml"), + zigbee_endpoints = inovelli_vzm31_sn_endpoints, + fingerprinted_endpoint_id = 0x01, + label = "Inovelli VZM31-SN" +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm31_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_vzm31_sn:generate_info_changed({preferences = {parameter1 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_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_vzm31_sn:generate_info_changed({preferences = {parameter9 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_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_vzm31_sn:generate_info_changed({preferences = {parameter52 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_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_vzm31_sn:generate_info_changed({preferences = {parameter258 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 258, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter11 preference change (VZM31-only) +test.register_coroutine_test( + "parameter11 preference should send configuration command", + function() + local new_param_value = true + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter11 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 11, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter17 preference change (VZM31-only) +test.register_coroutine_test( + "parameter17 preference should send configuration command", + function() + local new_param_value = 5 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter17 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 17, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test parameter22 preference change (VZM31-only) +test.register_coroutine_test( + "parameter22 preference should send configuration command", + function() + local new_param_value = 2 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter22 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_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_vzm31_sn:expect_device_create({ + type = "EDGE_CHILD", + label = "Inovelli VZM31-SN Notification", + profile = "rgbw-bulb-2700K-6500K", + parent_device_id = mock_inovelli_vzm31_sn.id, + parent_assigned_child_key = "notification" + }) + + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {notificationChild = true}})) + 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 new file mode 100644 index 0000000000..0a288f27d4 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -0,0 +1,527 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +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 + +-- Inovelli VZM32-SN device identifiers +local INOVELLI_MANUFACTURER_ID = "Inovelli" +local INOVELLI_VZM32_SN_MODEL = "VZM32-SN" + +-- Device endpoints with supported clusters +local inovelli_vzm32_sn_endpoints = { + [1] = { + id = 1, + manufacturer = INOVELLI_MANUFACTURER_ID, + model = INOVELLI_VZM32_SN_MODEL, + server_clusters = {0x0006, 0x0008, 0x0300, 0x0406} -- OnOff, Level, ColorControl, OccupancySensing + } +} + +local mock_inovelli_vzm32_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm32-sn.yml"), + zigbee_endpoints = inovelli_vzm32_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_vzm32_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_vzm32_sn.id, "added" }, + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.Level.attributes.CurrentLevel:read(mock_inovelli_vzm32_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.OnOff.attributes.OnOff:read(mock_inovelli_vzm32_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.OccupancySensing.attributes.Occupancy:read(mock_inovelli_vzm32_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_vzm32_sn.id, + { capability = "refresh", command = "refresh", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm32_sn.id, OnOff.attributes.OnOff:read(mock_inovelli_vzm32_sn) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm32_sn.id, Level.attributes.CurrentLevel:read(mock_inovelli_vzm32_sn) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm32_sn.id, clusters.OccupancySensing.attributes.Occupancy:read(mock_inovelli_vzm32_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_vzm32_sn.id, + { capability = "switch", command = "on", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm32_sn.id, clusters.OnOff.server.commands.On(mock_inovelli_vzm32_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_vzm32_sn.id, + { capability = "switch", command = "off", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm32_sn.id, clusters.OnOff.server.commands.Off(mock_inovelli_vzm32_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_vzm32_sn.id, + { capability = "switchLevel", command = "setLevel", args = { 50 } } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.Level.server.commands.MoveToLevelWithOnOff(mock_inovelli_vzm32_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 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 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", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + clusters.IlluminanceMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_inovelli_vzm32_sn, 11271) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance({value = 13})) + } + } +) + +-- Test motion sensor active +test.register_message_test( + "Motion sensor active should emit motion active event", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_attr_report(mock_inovelli_vzm32_sn, 0x01) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.motionSensor.motion.active()) + } + } +) + +-- Test motion sensor inactive +test.register_message_test( + "Motion sensor inactive should emit motion inactive event", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_attr_report(mock_inovelli_vzm32_sn, 0x00) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.motionSensor.motion.inactive()) + } + } +) + +-- 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 + -- 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_coroutine_test( + "Energy meter should emit energy events", + 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 +test.register_message_test( + "Energy meter reset command should send reset commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + { capability = "energyMeter", command = "resetEnergyMeter", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + cluster_base.build_manufacturer_specific_command( + mock_inovelli_vzm32_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_vzm32_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(mock_inovelli_vzm32_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:read(mock_inovelli_vzm32_sn) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + + +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) }) + + -- 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 +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua new file mode 100644 index 0000000000..26346c04a9 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua @@ -0,0 +1,345 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +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_vzm32_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM32-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-vzm32-sn.yml"), + zigbee_endpoints = inovelli_vzm32_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_vzm32_sn_preferences.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua new file mode 100644 index 0000000000..28ae4829a0 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua @@ -0,0 +1,163 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +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_vzm32_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM32-SN", + server_clusters = {0x0006, 0x0008} -- OnOff, Level + } +} + +local mock_inovelli_vzm32_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm32-sn.yml"), + zigbee_endpoints = inovelli_vzm32_sn_endpoints, + fingerprinted_endpoint_id = 0x01, + label = "Inovelli VZM32-SN" +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm32_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_vzm32_sn:generate_info_changed({preferences = {parameter1 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm32_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm32_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_vzm32_sn:generate_info_changed({preferences = {parameter9 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm32_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm32_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_vzm32_sn:generate_info_changed({preferences = {parameter52 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm32_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm32_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_vzm32_sn:generate_info_changed({preferences = {parameter258 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm32_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm32_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 258, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test notificationChild preference change +test.register_coroutine_test( + "notificationChild preference should create child device when enabled", + function() + mock_inovelli_vzm32_sn:expect_device_create({ + type = "EDGE_CHILD", + label = "Inovelli VZM32-SN Notification", + profile = "rgbw-bulb-2700K-6500K", + parent_device_id = mock_inovelli_vzm32_sn.id, + parent_assigned_child_key = "notification" + }) + + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm32_sn:generate_info_changed({preferences = {notificationChild = true}})) + end +) + +-- Test parameter101 preference change +test.register_coroutine_test( + "parameter101 preference should send configuration command", + function() + local new_param_value = 200 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm32_sn:generate_info_changed({preferences = {parameter101 = new_param_value}})) + + local expected_command = cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm32_sn, + 0xFC32, -- PRIVATE_CLUSTER_ID + 101, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Int16, + new_param_value + ) + + print("=== DEBUG: Expected command ===") + print("Command type:", type(expected_command)) + print("Command:", expected_command) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm32_sn.id, + expected_command + }) + end +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua index 6b07420b87..d6dcc08ea4 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua @@ -1,37 +1,31 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" local OnOffCluster = clusters.OnOff local SimpleMeteringCluster = clusters.SimpleMetering +local ElectricalMeasurementCluster = clusters.ElectricalMeasurement local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" local mock_device = test.mock_device.build_test_zigbee_device({ profile = t_utils.get_profile_definition("switch-power-energy.yml"), + fingerprinted_endpoint_id = 0x01, zigbee_endpoints = { [1] = { id = 1, manufacturer = "Jasco Products", model = "43078", - server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0702, 0x0B05 }, + server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0702, 0x0B04 }, client_clusters = { 0x000A, 0x0019 } } } }) +zigbee_test_utils.prepare_zigbee_env_info() + local function test_init() mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) @@ -124,4 +118,30 @@ test.register_message_test( } ) +test.register_coroutine_test( + "doConfigure lifecycle should call refresh and configure on device", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + -- device:refresh() reads + test.socket.zigbee:__expect_send({ mock_device.id, OnOffCluster.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, SimpleMeteringCluster.attributes.InstantaneousDemand:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, SimpleMeteringCluster.attributes.CurrentSummationDelivered:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurementCluster.attributes.ActivePower:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurementCluster.attributes.ACPowerDivisor:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurementCluster.attributes.ACPowerMultiplier:read(mock_device) }) + -- device:configure() bind requests and configure_reporting + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOffCluster.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300) }) + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, SimpleMeteringCluster.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, SimpleMeteringCluster.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) }) + test.socket.zigbee:__expect_send({ mock_device.id, SimpleMeteringCluster.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1) }) + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurementCluster.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurementCluster.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurementCluster.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurementCluster.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_laisiao_bath_heather.lua b/drivers/SmartThings/zigbee-switch/src/test/test_laisiao_bath_heather.lua index ec90d234d8..3b004c8f91 100755 --- a/drivers/SmartThings/zigbee-switch/src/test/test_laisiao_bath_heather.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_laisiao_bath_heather.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local clusters = require "st.zigbee.zcl.clusters" @@ -469,4 +458,21 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "component main Capability on command emits on then off after delay", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "switch", component = "main", command = "on", args = {} } }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.switch.switch.on()) + ) + test.wait_for_events() + test.mock_time.advance_time(1) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.switch.switch.off()) + ) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch.lua index 4b5a10ecfb..1ff11bd754 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_no_master.lua b/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_no_master.lua index 6d1cf40e63..b4ded05175 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_no_master.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_no_master.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -480,8 +469,52 @@ test.register_coroutine_test( 3 ):to_endpoint(3) }) + test.socket.zigbee:__expect_send({ mock_base_device.id, OnOff.attributes.OnOff:read(mock_base_device) }) + test.socket.zigbee:__expect_send({ mock_base_device.id, OnOff.attributes.OnOff:read(mock_base_device):to_endpoint(2) }) + test.socket.zigbee:__expect_send({ mock_base_device.id, OnOff.attributes.OnOff:read(mock_base_device):to_endpoint(3) }) mock_base_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) +local mock_non_mns_device = test.mock_device.build_test_zigbee_device( + { + label = "Generic Switch 1", + profile = profile, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "GenericMfr", + model = "GenericModel-DualSwitch", + server_clusters = { 0x0006 } + }, + [2] = { + id = 2, + manufacturer = "GenericMfr", + model = "GenericModel-DualSwitch", + server_clusters = { 0x0006 } + } + }, + fingerprinted_endpoint_id = 0x01 + } +) + +test.register_coroutine_test( + "device added lifecycle creates child device for secondary OnOff endpoint", + function() + test.socket.device_lifecycle:__queue_receive({ mock_non_mns_device.id, "added" }) + mock_non_mns_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Generic Switch 1 2", + profile = "basic-switch", + parent_device_id = mock_non_mns_device.id, + parent_assigned_child_key = "02" + }) + end, + { + test_init = function() + test.mock_device.add_test_device(mock_non_mns_device) + end + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_power.lua b/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_power.lua index 4605e8bf8b..4e35d5067d 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_power.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -329,6 +318,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_parent_device:generate_test_message("main", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -348,6 +345,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_child_device:generate_test_message("main", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_on_off_zigbee_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_on_off_zigbee_bulb.lua index 51ea4fe9c6..7f0d412016 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_on_off_zigbee_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_on_off_zigbee_bulb.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_osram_iqbr30_light.lua b/drivers/SmartThings/zigbee-switch/src/test/test_osram_iqbr30_light.lua index aa366750d8..082aa0c5c2 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_osram_iqbr30_light.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_osram_iqbr30_light.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_osram_light.lua b/drivers/SmartThings/zigbee-switch/src/test/test_osram_light.lua index 3efe35fc49..8e2e847e2c 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_osram_light.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_osram_light.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_rgb_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_rgb_bulb.lua index 5f7e37f5eb..68af9fdedb 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_rgb_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_rgb_bulb.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_rgbw_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_rgbw_bulb.lua index 61a9fcf1ac..24ec471c9d 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_rgbw_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_rgbw_bulb.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_2-wire_dimmer.lua b/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_2-wire_dimmer.lua index 3e3eb5722a..1c815e2d7c 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_2-wire_dimmer.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_2-wire_dimmer.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -30,7 +19,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( id = 1, manufacturer = "ROBB smarrt", model = "ROB_200-011-0", - server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0702, 0x0B04, 0x0B05 }, + server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0702, 0x0B04 }, client_clusters = { 0x0019 } } } @@ -105,6 +94,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 9.0, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_knob_dimmer.lua b/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_knob_dimmer.lua index 34cbff59fa..3ed587916c 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_knob_dimmer.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_knob_dimmer.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -30,7 +19,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( id = 1, manufacturer = "ROBB smarrt", model = "ROB_200-014-0", - server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0702, 0x0B04, 0x0B05 }, + server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0702, 0x0B04 }, client_clusters = { 0x0019 } } } @@ -105,6 +94,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 9.0, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_sengled_color_temp_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_sengled_color_temp_bulb.lua index c528e22c92..e2ddba42d3 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_sengled_color_temp_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_sengled_color_temp_bulb.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_sengled_dimmer_bulb_with_motion_sensor.lua b/drivers/SmartThings/zigbee-switch/src/test/test_sengled_dimmer_bulb_with_motion_sensor.lua index 6051a9decd..e28c4de0b6 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_sengled_dimmer_bulb_with_motion_sensor.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_sengled_dimmer_bulb_with_motion_sensor.lua @@ -1,27 +1,16 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local t_utils = require "integration_test.utils" +local version = require "version" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local OnOff = clusters.OnOff local Level = clusters.Level local IASZone = clusters.IASZone -local IasEnrollResponseCode = IASZone.types.EnrollResponseCode local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("on-off-level-motion-sensor.yml"), @@ -31,7 +20,8 @@ local mock_device = test.mock_device.build_test_zigbee_device( id = 1, manufacturer = "sengled", model = "E13-N11", - server_clusters = { 0x0006, 0x0008, 0x0500 } + server_clusters = { 0x0006, 0x0008, 0x0500 }, + profile_id = 0xC05E } } } @@ -76,14 +66,6 @@ test.register_coroutine_test( mock_device.id, IASZone.attributes.ZoneStatus:configure_reporting(mock_device, 30, 300, 1) }) - test.socket.zigbee:__expect_send({ - mock_device.id, - IASZone.attributes.IASCIEAddress:write(mock_device, zigbee_test_utils.mock_hub_eui) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - IASZone.server.commands.ZoneEnrollResponse(mock_device, IasEnrollResponseCode.SUCCESS, 0x00) - }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) @@ -116,6 +98,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "on", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "on") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device) }) @@ -134,6 +117,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "off", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "off") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.Off(mock_device) }) @@ -152,6 +136,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57 } } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switchLevel", "setLevel") end test.socket.zigbee:__expect_send({ mock_device.id, Level.server.commands.MoveToLevelWithOnOff(mock_device, 144, 0xFFFF) }) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_sinope_dimmer.lua b/drivers/SmartThings/zigbee-switch/src/test/test_sinope_dimmer.lua index d1aadf29a3..0859138a8c 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_sinope_dimmer.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_sinope_dimmer.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local base64 = require "st.base64" local cluster_base = require "st.zigbee.cluster_base" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_sinope_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_sinope_switch.lua index 35242deb6c..34e5f78640 100755 --- a/drivers/SmartThings/zigbee-switch/src/test/test_sinope_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_sinope_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua b/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua index 4115546f55..04c17b15de 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -35,11 +24,43 @@ local mock_device = test.mock_device.build_test_zigbee_device( } ) + +local mock_aurora_relay_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("switch-power.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "Aurora", + model = "Smart16ARelay51AU", + server_clusters = {0x0006, 0x0B04, 0x0702} + } + } + } +) + +local mock_vimar_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("switch-power.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "Vimar", + model = "Mains_Power_Outlet_v1.0", + server_clusters = {0x0006, 0x0B04, 0x0702} + } + } + } +) + + zigbee_test_utils.prepare_zigbee_env_info() local function test_init() mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_aurora_relay_device) + test.mock_device.add_test_device(mock_vimar_device) end test.set_test_init_function(test_init) @@ -72,7 +93,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send( { mock_device.id, - ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 1, 3600, 5) + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) } ) test.socket.zigbee:__expect_send( @@ -90,7 +111,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send( { mock_device.id, - SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 1, 3600, 5) + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) } ) @@ -159,4 +180,131 @@ test.register_message_test( } ) +test.register_coroutine_test( + "Configure should configure all necessary attributes and refresh device :aurora relay", + function() + test.socket.device_lifecycle:__queue_receive({ mock_aurora_relay_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.zigbee:__expect_send({ + mock_aurora_relay_device.id, + zigbee_test_utils.build_bind_request(mock_aurora_relay_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) + }) + test.socket.zigbee:__expect_send({ + mock_aurora_relay_device.id, + zigbee_test_utils.build_bind_request(mock_aurora_relay_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ + mock_aurora_relay_device.id, + zigbee_test_utils.build_bind_request(mock_aurora_relay_device, zigbee_test_utils.mock_hub_eui, SimpleMetering.ID) + }) + + test.socket.zigbee:__expect_send( + { + mock_aurora_relay_device.id, + OnOff.attributes.OnOff:configure_reporting(mock_aurora_relay_device, 0, 300, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_aurora_relay_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_aurora_relay_device, 5, 3600, 5) + } + ) + test.socket.zigbee:__expect_send( + { + mock_aurora_relay_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_aurora_relay_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_aurora_relay_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_aurora_relay_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_aurora_relay_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_aurora_relay_device, 5, 3600, 5) + } + ) + + test.socket.zigbee:__expect_send({ mock_aurora_relay_device.id, OnOff.attributes.OnOff:read(mock_aurora_relay_device) }) + test.socket.zigbee:__expect_send({ mock_aurora_relay_device.id, ElectricalMeasurement.attributes.ActivePower:read(mock_aurora_relay_device) }) + test.socket.zigbee:__expect_send({ mock_aurora_relay_device.id, ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_aurora_relay_device) }) + test.socket.zigbee:__expect_send({ mock_aurora_relay_device.id, ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_aurora_relay_device) }) + test.socket.zigbee:__expect_send({ mock_aurora_relay_device.id, SimpleMetering.attributes.InstantaneousDemand:read(mock_aurora_relay_device) }) + mock_aurora_relay_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_coroutine_test( + "Configure should configure all necessary attributes and refresh device : vimar", + function() + test.socket.device_lifecycle:__queue_receive({ mock_vimar_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.zigbee:__expect_send({ + mock_vimar_device.id, + zigbee_test_utils.build_bind_request(mock_vimar_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) + }) + test.socket.zigbee:__expect_send({ + mock_vimar_device.id, + zigbee_test_utils.build_bind_request(mock_vimar_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ + mock_vimar_device.id, + zigbee_test_utils.build_bind_request(mock_vimar_device, zigbee_test_utils.mock_hub_eui, SimpleMetering.ID) + }) + test.socket.zigbee:__expect_send( + { + mock_vimar_device.id, + OnOff.attributes.OnOff:configure_reporting(mock_vimar_device, 0, 300, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_vimar_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_vimar_device, 1, 15, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_vimar_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_vimar_device, 5, 3600, 5) + } + ) + test.socket.zigbee:__expect_send( + { + mock_vimar_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_vimar_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_vimar_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_vimar_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_vimar_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_vimar_device, 5, 3600, 5) + } + ) + test.socket.zigbee:__expect_send({ + mock_vimar_device.id, + zigbee_test_utils.build_bind_request(mock_vimar_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ mock_vimar_device.id, OnOff.attributes.OnOff:read(mock_vimar_device) }) + test.socket.zigbee:__expect_send({ mock_vimar_device.id, ElectricalMeasurement.attributes.ActivePower:read(mock_vimar_device) }) + test.socket.zigbee:__expect_send({ mock_vimar_device.id, ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_vimar_device) }) + test.socket.zigbee:__expect_send({ mock_vimar_device.id, ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_vimar_device) }) + test.socket.zigbee:__expect_send({ mock_vimar_device.id, SimpleMetering.attributes.InstantaneousDemand:read(mock_vimar_device) }) + mock_vimar_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi.lua b/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi.lua new file mode 100755 index 0000000000..a00f74bddf --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi.lua @@ -0,0 +1,82 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local BasicCluster = clusters.Basic +local OnOffCluster = clusters.OnOff +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" + +local profile = t_utils.get_profile_definition("basic-switch.yml") + +local mock_device = test.mock_device.build_test_zigbee_device({ + label = "Zigbee Switch", + profile = profile, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "_TZ123fas", + server_clusters = { 0x0006 }, + }, + [2] = { + id = 2, + manufacturer = "_TZ123fas", + server_clusters = { 0x0006 }, + }, + }, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + mock_device:set_field("_configuration_version", 1, {persist = true}) + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "lifecycle configure event should configure device", + function () + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({mock_device.id, "doConfigure"}) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:read(mock_device):to_endpoint(0x02) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(2) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + OnOffCluster.ID, 1):to_endpoint(1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + OnOffCluster.ID, 2):to_endpoint(2) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_attribute_read(mock_device, BasicCluster.ID, {0x0004, 0x0000, 0x0001, 0x0005, 0x0007, 0xfffe}) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi_switch.lua index 41924e891d..1ff11bd754 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi_switch.lua @@ -1,16 +1,5 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_wallhero_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_wallhero_switch.lua index 26e72466c9..1a8d42dba5 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_wallhero_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_wallhero_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local clusters = require "st.zigbee.zcl.clusters" @@ -129,6 +118,23 @@ local mock_seventh_child = test.mock_device.build_test_child_device( } ) +-- Single button device matching WALL HERO fingerprint (used to test button capability events) +local mock_button_device = test.mock_device.build_test_zigbee_device( + { + label = "WALL HERO Switch 1", + profile = scene_switch_profile_def, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "WALL HERO", + model = "ACL-401S1I", + server_clusters = { 0x0003, 0x0004, 0x0005, 0x0006 } + } + }, + fingerprinted_endpoint_id = 0x01 + } +) + zigbee_test_utils.prepare_zigbee_env_info() local function test_init() @@ -140,7 +146,9 @@ local function test_init() test.mock_device.add_test_device(mock_fourth_child) test.mock_device.add_test_device(mock_fifth_child) test.mock_device.add_test_device(mock_sixth_child) - test.mock_device.add_test_device(mock_seventh_child)end + test.mock_device.add_test_device(mock_seventh_child) + test.mock_device.add_test_device(mock_button_device) +end test.set_test_init_function(test_init) @@ -721,4 +729,15 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "device_added lifecycle event should emit button capability events for button device", + function() + test.socket.device_lifecycle:__queue_receive({ mock_button_device.id, "added" }) + test.socket.capability:__expect_send(mock_button_device:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_button_device:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }))) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_white_color_temp_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_white_color_temp_bulb.lua index eea20eb184..f024aa45be 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_white_color_temp_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_white_color_temp_bulb.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_yanmi_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_yanmi_switch.lua new file mode 100755 index 0000000000..61ad14e75d --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_yanmi_switch.lua @@ -0,0 +1,409 @@ +-- 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. + +-- Mock out globals +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" + +local OnOff = clusters.OnOff + +local common_switch_profile_def = t_utils.get_profile_definition("basic-switch.yml") + + +local mock_parent_device = test.mock_device.build_test_zigbee_device( + { + profile = common_switch_profile_def, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "JNL", + model = "Y-K003-001", + server_clusters = { 0003,0004,0005,0006 } + } + }, + fingerprinted_endpoint_id = 0x01 + } +) + +-- Switch 2 (Common Switch) +local mock_first_child = test.mock_device.build_test_child_device( + { + profile = common_switch_profile_def, + device_network_id = string.format("%04X:%02X", mock_parent_device:get_short_address(), 2), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = string.format("%02X", 2) + } +) + +-- Switch 3 (Common Switch) +local mock_second_child = test.mock_device.build_test_child_device( + { + profile = common_switch_profile_def, + device_network_id = string.format("%04X:%02X", mock_parent_device:get_short_address(), 3), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = string.format("%02X", 3) + } +) + + + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_parent_device) + test.mock_device.add_test_device(mock_first_child) + test.mock_device.add_test_device(mock_second_child) +end + +test.set_test_init_function(test_init) + +-- 4 Common Switch Tests +test.register_message_test( + "Reported on off status should be handled by parent device: on", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_parent_device.id, "init" } + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_parent_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, + true) :from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_parent_device:generate_test_message("main", capabilities.switch.switch.on()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + +test.register_message_test( + "Reported on off status should be handled by first child device: on", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_parent_device.id, "init" } + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_first_child.id, OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, + true) :from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_first_child:generate_test_message("main", capabilities.switch.switch.on()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_first_child.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + +test.register_message_test( + "Reported on off status should be handled by Second child device: on", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_parent_device.id, "init" } + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_second_child.id, OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, + true) :from_endpoint(0x03) } + }, + { + channel = "capability", + direction = "send", + message = mock_second_child:generate_test_message("main", capabilities.switch.switch.on()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_second_child.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + + +test.register_message_test( + "reported on off status should be handled by parent device: off", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_parent_device.id, "init" } + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_parent_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, + false) :from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_parent_device:generate_test_message("main", capabilities.switch.switch.off()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + +test.register_message_test( + "Reported on off status should be handled by first child device: off", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_parent_device.id, "init" } + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_first_child.id, OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, + false) :from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_first_child:generate_test_message("main", capabilities.switch.switch.off()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_first_child.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + +test.register_message_test( + "Reported on off status should be handled by Second child device: off", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_parent_device.id, "init" } + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_second_child.id, OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, + false) :from_endpoint(0x03) } + }, + { + channel = "capability", + direction = "send", + message = mock_second_child:generate_test_message("main", capabilities.switch.switch.off()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_second_child.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + + +test.register_message_test( + "Capability on command switch on should be handled : parent device", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_parent_device.id, "init" } + }, + { + channel = "capability", + direction = "receive", + message = { mock_parent_device.id, { capability = "switch", component = "main", command = "on", args = { } } } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_parent_device.id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_parent_device.id, OnOff.server.commands.On(mock_parent_device):to_endpoint(0x01) } + } + } +) + +test.register_message_test( + "Capability on command switch on should be handled : first child device", + { + { + channel = "capability", + direction = "receive", + message = { mock_first_child.id, { capability = "switch", component = "main", command = "on", args = { } } } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_first_child.id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_parent_device.id, OnOff.server.commands.On(mock_parent_device):to_endpoint(0x02) } + } + } +) + +test.register_message_test( + "Capability on command switch on should be handled : second child device", + { + { + channel = "capability", + direction = "receive", + message = { mock_second_child.id, { capability = "switch", component = "main", command = "on", args = { } } } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_second_child.id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_parent_device.id, OnOff.server.commands.On(mock_parent_device):to_endpoint(0x03) } + } + } +) + + + + +test.register_message_test( + "Capability off command switch off should be handled : parent device", + { + { + channel = "capability", + direction = "receive", + message = { mock_parent_device.id, { capability = "switch", component = "main", command = "off", args = { } } } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_parent_device.id, capability_id = "switch", capability_cmd_id = "off" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_parent_device.id, OnOff.server.commands.Off(mock_parent_device):to_endpoint(0x01) } + } + } +) + +test.register_message_test( + "Capability off command switch off should be handled : first child device", + { + { + channel = "capability", + direction = "receive", + message = { mock_first_child.id, { capability = "switch", component = "main", command = "off", args = { } } } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_first_child.id, capability_id = "switch", capability_cmd_id = "off" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_parent_device.id, OnOff.server.commands.Off(mock_parent_device):to_endpoint(0x02) } + } + } +) + +test.register_message_test( + "Capability off command switch off should be handled : second child device", + { + { + channel = "capability", + direction = "receive", + message = { mock_second_child.id, { capability = "switch", component = "main", command = "off", args = { } } } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_second_child.id, capability_id = "switch", capability_cmd_id = "off" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_parent_device.id, OnOff.server.commands.Off(mock_parent_device):to_endpoint(0x03) } + } + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_dimmer_power_energy.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_dimmer_power_energy.lua deleted file mode 100644 index 9b62d57546..0000000000 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_dimmer_power_energy.lua +++ /dev/null @@ -1,190 +0,0 @@ --- Copyright 2022 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 clusters = require "st.zigbee.zcl.clusters" -local OnOffCluster = clusters.OnOff -local LevelCluster = clusters.Level -local SimpleMeteringCluster = clusters.SimpleMetering -local capabilities = require "st.capabilities" -local t_utils = require "integration_test.utils" - -local mock_device = test.mock_device.build_test_zigbee_device({ - profile = t_utils.get_profile_definition("switch-dimmer-power-energy.yml"), - zigbee_endpoints = { - [1] = { - id = 1, - server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0702, 0x0B05 }, - client_clusters = { 0x000A, 0x0019 } - } - } -}) - -local function test_init() - mock_device:set_field("_configuration_version", 1, {persist = true}) - test.mock_device.add_test_device(mock_device) -end - -test.set_test_init_function(test_init) - -test.register_message_test( - "Capability command On should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "switch", component = "main", command = "on", args = { } } } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, OnOffCluster.server.commands.On(mock_device) } - } - } -) - -test.register_message_test( - "Capability command Off should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "switch", component = "main", command = "off", args = { } } } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "off" } - } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, OnOffCluster.server.commands.Off(mock_device) } - } - } -) - -test.register_message_test( - "Capability command setLevel should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57, 0 } } } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switchLevel", capability_cmd_id = "setLevel" } - } - }, - { - channel = "zigbee", - direction = "send", - message = { - mock_device.id, - LevelCluster.server.commands.MoveToLevelWithOnOff(mock_device, math.floor(57 * 254 / 100)) - } - } - } -) - -test.register_message_test( - "Handle Switch Level", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_device.id, - LevelCluster.attributes.CurrentLevel:build_test_attr_report(mock_device, math.floor(57 / 100 * 254)) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.switchLevel.level(57)) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } - } - }, - } -) - -test.register_message_test( - "Handle Power meter, Sensor value is in kW, capability attribute value is in W", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, SimpleMeteringCluster.attributes.Divisor:build_test_attr_report(mock_device, 0x0A) } - }, - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, SimpleMeteringCluster.attributes.InstantaneousDemand:build_test_attr_report(mock_device, 0x14D) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 33300.0, unit = "W" })) - } - } -) - -test.register_message_test( - "Handle Energy meter", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, SimpleMeteringCluster.attributes.Multiplier:build_test_attr_report(mock_device, 1) } - }, - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, SimpleMeteringCluster.attributes.Divisor:build_test_attr_report(mock_device, 0x2710) } - }, - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, SimpleMeteringCluster.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 0x15B3) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 0.5555, unit = "kWh" })) - } - } -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_ezex_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_ezex_switch.lua index 28460999f2..8995523952 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_ezex_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_ezex_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -118,6 +107,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 9.766, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_power_consumption_report.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_power_consumption_report.lua index 5128aa1cb4..d701bd89aa 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_power_consumption_report.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_power_consumption_report.lua @@ -1,22 +1,13 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local SimpleMetering = clusters.SimpleMetering +local ElectricalMeasurement = clusters.ElectricalMeasurement +local OnOff = clusters.OnOff local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" @@ -28,7 +19,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( id = 1, manufacturer = "DAWON_DNS", model = "PM-B430-ZB", - server_clusters = {} + server_clusters = { 0x0000, 0x0002, 0x0003, 0x0006, 0x0702, 0x0B04 } } } } @@ -92,4 +83,69 @@ test.register_message_test( } ) +test.register_coroutine_test( + "Configure should configure all necessary attributes and refresh device", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, SimpleMetering.ID) + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 300, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1) + } + ) + + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurement.attributes.ActivePower:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, SimpleMetering.attributes.InstantaneousDemand:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_rexense.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_rexense.lua index a3be084eb2..e1519a4d1a 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_rexense.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_rexense.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zll_color_temp_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zll_color_temp_bulb.lua index 6d7986700e..1a13044f48 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zll_color_temp_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zll_color_temp_bulb.lua @@ -1,20 +1,10 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" local t_utils = require "integration_test.utils" +local version = require "version" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local OnOff = clusters.OnOff @@ -83,6 +73,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "on", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "on") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device)}) test.wait_for_events() test.mock_time.advance_time(2) @@ -98,6 +89,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "off", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "off") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.Off(mock_device)}) test.wait_for_events() test.mock_time.advance_time(2) @@ -113,6 +105,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = {50} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switchLevel", "setLevel") end test.socket.zigbee:__expect_send({ mock_device.id, Level.commands.MoveToLevelWithOnOff(mock_device, math.floor(50 / 100.0 * 254), 0xFFFF)}) test.wait_for_events() test.mock_time.advance_time(2) @@ -128,6 +121,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {200} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("colorTemperature", "setColorTemperature") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device)}) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.commands.MoveToColorTemperature(mock_device, 5000, 0x0000)}) test.wait_for_events() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer.lua index eb590c3874..71305fba20 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer_bulb.lua index 922a0ce364..0ac17313d3 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer_bulb.lua @@ -1,20 +1,10 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" local t_utils = require "integration_test.utils" +local version = require "version" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local OnOff = clusters.OnOff @@ -121,6 +111,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "on", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "on") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device) }) @@ -138,6 +129,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "off", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "off") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.Off(mock_device) }) @@ -155,6 +147,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57 } } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switchLevel", "setLevel") end test.socket.zigbee:__expect_send({ mock_device.id, Level.server.commands.MoveToLevelWithOnOff(mock_device, 144, 0xFFFF) }) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgb_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgb_bulb.lua index b58acb6dc2..a8384d1b13 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgb_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgb_bulb.lua @@ -1,21 +1,11 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local t_utils = require "integration_test.utils" +local version = require "version" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local OnOff = clusters.OnOff @@ -149,6 +139,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "on", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "on") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device) }) @@ -168,6 +159,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "off", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "off") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.Off(mock_device) }) @@ -187,6 +179,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57 } } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switchLevel", "setLevel") end test.socket.zigbee:__expect_send({ mock_device.id, Level.server.commands.MoveToLevelWithOnOff(mock_device, 144, 0xFFFF) }) @@ -212,6 +205,7 @@ for _, data in ipairs(test_data) do if data.saturation ~= nil then test.socket.zigbee:__queue_receive({mock_device.id, ColorControl.attributes.CurrentSaturation:build_test_attr_report(mock_device, math.ceil(data.saturation / 100 * 0xFE))}) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorControl.saturation(data.saturation))) + mock_device:expect_native_attr_handler_registration("colorControl", "saturation") end test.timer.__create_and_queue_test_time_advance_timer(0.2, "oneshot") @@ -256,6 +250,7 @@ for _, data in ipairs(test_data) do if data.hue ~= nil then test.socket.zigbee:__queue_receive({mock_device.id, ColorControl.attributes.CurrentHue:build_test_attr_report(mock_device, math.ceil(data.hue / 100 * 0xFE))}) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorControl.hue(data.hue))) + mock_device:expect_native_attr_handler_registration("colorControl", "hue") end test.timer.__create_and_queue_test_time_advance_timer(0.2, "oneshot") diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgbw_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgbw_bulb.lua index 51edc21777..cf53f7e359 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgbw_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgbw_bulb.lua @@ -1,20 +1,10 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" local t_utils = require "integration_test.utils" +local version = require "version" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local OnOff = clusters.OnOff @@ -140,6 +130,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "on", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "on") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device) }) @@ -160,6 +151,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "off", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "off") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.Off(mock_device) }) @@ -180,6 +172,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57 } } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switchLevel", "setLevel") end test.socket.zigbee:__expect_send({ mock_device.id, Level.server.commands.MoveToLevelWithOnOff(mock_device, 144, 0xFFFF) }) @@ -200,6 +193,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {200} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("colorTemperature", "setColorTemperature") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device)}) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.commands.MoveToColorTemperature(mock_device, 5000, 0x0000)}) diff --git a/drivers/SmartThings/zigbee-switch/src/tuya-multi/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/tuya-multi/can_handle.lua new file mode 100644 index 0000000000..c4878c95c2 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/tuya-multi/can_handle.lua @@ -0,0 +1,21 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_multi_endpoint(device) + local main_endpoint = device:get_endpoint(0x0006) + for _, ep in ipairs(device.zigbee_endpoints) do + if ep.id ~= main_endpoint then + return true + end + end + return false +end + +return function(opts, driver, device) + local TUYA_MFR_HEADER = "_TZ" + if string.sub(device:get_manufacturer(),1,3) == TUYA_MFR_HEADER and is_multi_endpoint(device) then -- if it is a tuya device, then send the magic packet + local subdriver = require("tuya-multi") + return true, subdriver + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/tuya-multi/init.lua b/drivers/SmartThings/zigbee-switch/src/tuya-multi/init.lua index c4857e9085..51c91ad768 100644 --- a/drivers/SmartThings/zigbee-switch/src/tuya-multi/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/tuya-multi/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local data_types = require "st.zigbee.data_types" @@ -6,26 +9,6 @@ local messages = require "st.zigbee.messages" local zb_const = require "st.zigbee.constants" local read_attribute = require "st.zigbee.zcl.global_commands.read_attribute" -local TUYA_MFR_HEADER = "_TZ" - -local function is_multi_endpoint(device) - local main_endpoint = device:get_endpoint(clusters.OnOff.ID) - for _, ep in ipairs(device.zigbee_endpoints) do - if ep.id ~= main_endpoint then - return true - end - end - return false -end - -local function is_tuya_products(opts, driver, device) - if string.sub(device:get_manufacturer(),1,3) == TUYA_MFR_HEADER and is_multi_endpoint(device) then -- if it is a tuya device, then send the magic packet - local subdriver = require("tuya-multi") - return true, subdriver - end - return false -end - local function read_attribute_function(device, cluster_id, attr_id) local read_body = read_attribute.ReadAttribute( attr_id ) local zclh = zcl_messages.ZclHeader({ @@ -65,7 +48,7 @@ local tuya_switch_handler = { supported_capabilities = { capabilities.switch }, - can_handle = is_tuya_products + can_handle = require("tuya-multi.can_handle"), } -return tuya_switch_handler \ No newline at end of file +return tuya_switch_handler diff --git a/drivers/SmartThings/zigbee-switch/src/wallhero/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/wallhero/can_handle.lua new file mode 100644 index 0000000000..69bcf73fce --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/wallhero/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local FINGERPRINTS = require "wallhero.fingerprints" + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("wallhero") + return true, subdriver + end + end + return false +end + diff --git a/drivers/SmartThings/zigbee-switch/src/wallhero/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/wallhero/fingerprints.lua new file mode 100644 index 0000000000..9d58e6457a --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/wallhero/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "WALL HERO", model = "ACL-401S4I", switches = 4, buttons = 0 }, + { mfr = "WALL HERO", model = "ACL-401S8I", switches = 4, buttons = 4 }, + { mfr = "WALL HERO", model = "ACL-401S3I", switches = 3, buttons = 0 }, + { mfr = "WALL HERO", model = "ACL-401S2I", switches = 2, buttons = 0 }, + { mfr = "WALL HERO", model = "ACL-401S1I", switches = 1, buttons = 0 }, + { mfr = "WALL HERO", model = "ACL-401ON", switches = 1, buttons = 0 } +} diff --git a/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua b/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua index 79f3110e41..1e0e7eb26e 100644 --- a/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local log = require "log" @@ -25,26 +14,8 @@ local PRIVATE_CLUSTER_ID = 0x0006 local PRIVATE_ATTRIBUTE_ID = 0x6000 local MFG_CODE = 0x1235 -local FINGERPRINTS = { - { mfr = "WALL HERO", model = "ACL-401S4I", switches = 4, buttons = 0 }, - { mfr = "WALL HERO", model = "ACL-401S8I", switches = 4, buttons = 4 }, - { mfr = "WALL HERO", model = "ACL-401S3I", switches = 3, buttons = 0 }, - { mfr = "WALL HERO", model = "ACL-401S2I", switches = 2, buttons = 0 }, - { mfr = "WALL HERO", model = "ACL-401S1I", switches = 1, buttons = 0 }, - { mfr = "WALL HERO", model = "ACL-401ON", switches = 1, buttons = 0 } -} - -local function can_handle_wallhero_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("wallhero") - return true, subdriver - end - end - return false -end - local function get_children_info(device) + local FINGERPRINTS = require "wallhero.fingerprints" for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_model() == fingerprint.model then return fingerprint.switches, fingerprint.buttons @@ -141,7 +112,7 @@ local wallheroswitch = { } } }, - can_handle = can_handle_wallhero_switch + can_handle = require("wallhero.can_handle"), } return wallheroswitch diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/can_handle.lua new file mode 100644 index 0000000000..d45b7b745f --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local WHITE_COLOR_TEMP_BULB_FINGERPRINTS = require "white-color-temp-bulb.fingerprints" + local can_handle = (WHITE_COLOR_TEMP_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] + if can_handle then + local subdriver = require("white-color-temp-bulb") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/can_handle.lua new file mode 100644 index 0000000000..b3b8bd591b --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/can_handle.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local DURAGREEN_BULB_FINGERPRINTS = { + ["DURAGREEN"] = { + ["DG-CW-02"] = true, + ["DG-CW-01"] = true, + ["DG-CCT-01"] = true + }, + } + local res = (DURAGREEN_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] or false + if res then + return res, require("white-color-temp-bulb.duragreen") + else + return res + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/init.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/init.lua index 7df79be32e..b6f96842c3 100644 --- a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/init.lua @@ -1,34 +1,11 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local Level = clusters.Level -local DURAGREEN_BULB_FINGERPRINTS = { - ["DURAGREEN"] = { - ["DG-CW-02"] = true, - ["DG-CW-01"] = true, - ["DG-CCT-01"] = true - }, -} - -local function can_handle_duragreen_bulb(opts, driver, device) - return (DURAGREEN_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] or false -end - local function handle_set_level(driver, device, cmd) local level = math.floor(cmd.args.level/100.0 * 254) local transtition_time = cmd.args.rate or 0xFFFF @@ -46,7 +23,7 @@ local duragreen_color_temp_bulb = { [capabilities.switchLevel.commands.setLevel.NAME] = handle_set_level } }, - can_handle = can_handle_duragreen_bulb + can_handle = require("white-color-temp-bulb.duragreen.can_handle") } return duragreen_color_temp_bulb diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/fingerprints.lua new file mode 100644 index 0000000000..02bb28e88c --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/fingerprints.lua @@ -0,0 +1,99 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + ["DURAGREEN"] = { + ["DG-CW-02"] = true, + ["DG-CW-01"] = true, + ["DG-CCT-01"] = true + }, + ["Samsung Electronics"] = { + ["ABL-LIGHT-Z-001"] = true, + ["SAMSUNG-ITM-Z-001"] = true + }, + ["Juno"] = { + ["ABL-LIGHT-Z-001"] = true + }, + ["AduroSmart Eria"] = { + ["AD-ColorTemperature3001"] = true + }, + ["Aurora"] = { + ["TWBulb51AU"] = true, + ["TWMPROZXBulb50AU"] = true, + ["TWStrip50AU"] = true, + ["TWGU10Bulb50AU"] = true, + ["TWCLBulb50AU"] = true + }, + ["CWD"] = { + ["ZB.A806Ecct-A001"] = true, + ["ZB.A806Bcct-A001"] = true, + ["ZB.M350cct-A001"] = true + }, + ["ETI"] = { + ["Zigbee CCT Downlight"] = true + }, + ["The Home Depot"] = { + ["Ecosmart-ZBT-BR30-CCT-Bulb"] = true, + ["Ecosmart-ZBT-A19-CCT-Bulb"] = true + }, + ["IKEA of Sweden"] = { + ["GUNNARP panel round"] = true, + ["LEPTITER Recessed spot light"] = true, + ["TRADFRI bulb E12 WS opal 600lm"] = true, + ["TRADFRI bulb E14 WS 470lm"] = true, + ["TRADFRI bulb E14 WS opal 600lm"] = true, + ["TRADFRI bulb E26 WS clear 806lm"] = true, + ["TRADFRI bulb E27 WS clear 806lm"] = true, + ["TRADFRI bulb E26 WS opal 1000lm"] = true, + ["TRADFRI bulb E27 WS opal 1000lm"] = true + }, + ["Megaman"] = { + ["Z3-ColorTemperature"] = true + }, + ["innr"] = { + ["RB 248 T"] = true, + ["RB 278 T"] = true, + ["RS 228 T"] = true + }, + ["OSRAM"] = { + ["LIGHTIFY BR Tunable White"] = true, + ["LIGHTIFY RT Tunable White"] = true, + ["Classic A60 TW"] = true, + ["LIGHTIFY A19 Tunable White"] = true, + ["Classic B40 TW - LIGHTIFY"] = true, + ["LIGHTIFY Conv Under Cabinet TW"] = true, + ["ColorstripRGBW"] = true, + ["LIGHTIFY Edge-lit Flushmount TW"] = true, + ["LIGHTIFY Surface TW"] = true, + ["LIGHTIFY Under Cabinet TW"] = true, + ["LIGHTIFY Edge-lit flushmount"] = true + }, + ["LEDVANCE"] = { + ["A19 TW 10 year"] = true, + ["MR16 TW"] = true, + ["BR30 TW"] = true, + ["RT TW"] = true + }, + ["Smarthome"] = { + ["S111-202A"] = true + }, + ["lk"] = { + ["ZBT-CCTLight-GLS0108"] = true + }, + ["MLI"] = { + ["ZBT-ColorTemperature"] = true + }, + ["sengled"] = { + ["Z01-A19NAE26"] = true, + ["Z01-A191AE26W"] = true, + ["Z01-A60EAB22"] = true, + ["Z01-A60EAE27"] = true + }, + ["Third Reality, Inc"] = { + ["3RSL011Z"] = true, + ["3RSL012Z"] = true + }, + ["Ajax Online"] = { + ["CCT"] = true + } +} diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/init.lua index 70efb29afa..25bc13b65f 100644 --- a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -18,113 +7,6 @@ local colorTemperature_defaults = require "st.zigbee.defaults.colorTemperature_d local ColorControl = clusters.ColorControl -local WHITE_COLOR_TEMP_BULB_FINGERPRINTS = { - ["DURAGREEN"] = { - ["DG-CW-02"] = true, - ["DG-CW-01"] = true, - ["DG-CCT-01"] = true - }, - ["Samsung Electronics"] = { - ["ABL-LIGHT-Z-001"] = true, - ["SAMSUNG-ITM-Z-001"] = true - }, - ["Juno"] = { - ["ABL-LIGHT-Z-001"] = true - }, - ["AduroSmart Eria"] = { - ["AD-ColorTemperature3001"] = true - }, - ["Aurora"] = { - ["TWBulb51AU"] = true, - ["TWMPROZXBulb50AU"] = true, - ["TWStrip50AU"] = true, - ["TWGU10Bulb50AU"] = true, - ["TWCLBulb50AU"] = true - }, - ["CWD"] = { - ["ZB.A806Ecct-A001"] = true, - ["ZB.A806Bcct-A001"] = true, - ["ZB.M350cct-A001"] = true - }, - ["ETI"] = { - ["Zigbee CCT Downlight"] = true - }, - ["The Home Depot"] = { - ["Ecosmart-ZBT-BR30-CCT-Bulb"] = true, - ["Ecosmart-ZBT-A19-CCT-Bulb"] = true - }, - ["IKEA of Sweden"] = { - ["GUNNARP panel round"] = true, - ["LEPTITER Recessed spot light"] = true, - ["TRADFRI bulb E12 WS opal 600lm"] = true, - ["TRADFRI bulb E14 WS 470lm"] = true, - ["TRADFRI bulb E14 WS opal 600lm"] = true, - ["TRADFRI bulb E26 WS clear 806lm"] = true, - ["TRADFRI bulb E27 WS clear 806lm"] = true, - ["TRADFRI bulb E26 WS opal 1000lm"] = true, - ["TRADFRI bulb E27 WS opal 1000lm"] = true - }, - ["Megaman"] = { - ["Z3-ColorTemperature"] = true - }, - ["innr"] = { - ["RB 248 T"] = true, - ["RB 278 T"] = true, - ["RS 228 T"] = true - }, - ["OSRAM"] = { - ["LIGHTIFY BR Tunable White"] = true, - ["LIGHTIFY RT Tunable White"] = true, - ["Classic A60 TW"] = true, - ["LIGHTIFY A19 Tunable White"] = true, - ["Classic B40 TW - LIGHTIFY"] = true, - ["LIGHTIFY Conv Under Cabinet TW"] = true, - ["ColorstripRGBW"] = true, - ["LIGHTIFY Edge-lit Flushmount TW"] = true, - ["LIGHTIFY Surface TW"] = true, - ["LIGHTIFY Under Cabinet TW"] = true, - ["LIGHTIFY Edge-lit flushmount"] = true - }, - ["LEDVANCE"] = { - ["A19 TW 10 year"] = true, - ["MR16 TW"] = true, - ["BR30 TW"] = true, - ["RT TW"] = true - }, - ["Smarthome"] = { - ["S111-202A"] = true - }, - ["lk"] = { - ["ZBT-CCTLight-GLS0108"] = true - }, - ["MLI"] = { - ["ZBT-ColorTemperature"] = true - }, - ["sengled"] = { - ["Z01-A19NAE26"] = true, - ["Z01-A191AE26W"] = true, - ["Z01-A60EAB22"] = true, - ["Z01-A60EAE27"] = true - }, - ["Third Reality, Inc"] = { - ["3RSL011Z"] = true, - ["3RSL012Z"] = true - }, - ["Ajax Online"] = { - ["CCT"] = true - } -} - -local function can_handle_white_color_temp_bulb(opts, driver, device) - local can_handle = (WHITE_COLOR_TEMP_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] - if can_handle then - local subdriver = require("white-color-temp-bulb") - return true, subdriver - else - return false - end -end - local function set_color_temperature_handler(driver, device, cmd) colorTemperature_defaults.set_color_temperature(driver, device, cmd) @@ -140,10 +22,8 @@ local white_color_temp_bulb = { [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature_handler } }, - sub_drivers = { - require("white-color-temp-bulb.duragreen"), - }, - can_handle = can_handle_white_color_temp_bulb + sub_drivers = require("white-color-temp-bulb.sub_drivers"), + can_handle = require("white-color-temp-bulb.can_handle"), } return white_color_temp_bulb diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/sub_drivers.lua new file mode 100644 index 0000000000..9beaf743bc --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/sub_drivers.lua @@ -0,0 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load = require "lazy_load_subdriver" +return { + lazy_load("white-color-temp-bulb.duragreen"), +} diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/can_handle.lua new file mode 100644 index 0000000000..7b13c4b630 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_DIMMER_POWER_ENERGY_FINGERPRINTS = { + { mfr = "Jasco Products", model = "43082" } +} + +return function(opts, driver, device) + for _, fingerprint in ipairs(ZIGBEE_DIMMER_POWER_ENERGY_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("zigbee-dimmer-power-energy") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/enbrighten-metering-dimmer/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/enbrighten-metering-dimmer/init.lua deleted file mode 100644 index d4fcdb3990..0000000000 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/enbrighten-metering-dimmer/init.lua +++ /dev/null @@ -1,67 +0,0 @@ --- Copyright 2022 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 capabilities = require "st.capabilities" -local constants = require "st.zigbee.constants" -local SimpleMetering = clusters.SimpleMetering -local configurations = require "configurations" - -local ENBRIGHTEN_METERING_DIMMER_FINGERPRINTS = { - { mfr = "Jasco Products", model = "43082" } -} - -local is_enbrighten_metering_dimmer = function(opts, driver, device) - for _, fingerprint in ipairs(ENBRIGHTEN_METERING_DIMMER_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - -local device_init = function(self, device) - local customEnergyDivisor = 10000 - device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, customEnergyDivisor, {persist = true}) -end - -local do_configure = function(self, device) - device:refresh() - device:configure() -end - -local instantaneous_demand_handler = function(driver, device, value, zb_rx) - local raw_value = value.value - local divisor = 10 - raw_value = raw_value / divisor - device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.powerMeter.power({value = raw_value, unit = "W" })) -end - -local enbrighten_metering_dimmer = { - NAME = "enbrighten metering dimmer", - zigbee_handlers = { - attr = { - [SimpleMetering.ID] = { - [SimpleMetering.attributes.InstantaneousDemand.ID] = instantaneous_demand_handler - } - } - }, - lifecycle_handlers = { - init = configurations.power_reconfig_wrapper(device_init), - doConfigure = do_configure - }, - can_handle = is_enbrighten_metering_dimmer -} - -return enbrighten_metering_dimmer diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua index f799a165aa..dc37bf1181 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua @@ -1,34 +1,15 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local SimpleMetering = clusters.SimpleMetering -local ElectricalMeasurement = clusters.ElectricalMeasurement +local constants = require "st.zigbee.constants" +local configurations = require "configurations" -local ZIGBEE_DIMMER_POWER_ENERGY_FINGERPRINTS = { - { mfr = "Jasco Products", model = "43082" } -} - -local is_zigbee_dimmer_power_energy = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_DIMMER_POWER_ENERGY_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("zigbee-dimmer-power-energy") - return true, subdriver - end - end - return false +local device_init = function(self, device) + local customEnergyDivisor = 10000 + device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, customEnergyDivisor, {persist = true}) end local do_configure = function(self, device) @@ -41,24 +22,29 @@ local do_configure = function(self, device) device:send(SimpleMetering.attributes.Divisor:read(device)) device:send(SimpleMetering.attributes.Multiplier:read(device)) end +end - if device:supports_capability(capabilities.energyMeter) then - -- Divisor and multipler for EnergyMeter - device:send(ElectricalMeasurement.attributes.ACPowerDivisor:read(device)) - device:send(ElectricalMeasurement.attributes.ACPowerMultiplier:read(device)) - end +local instantaneous_demand_handler = function(driver, device, value, zb_rx) + local raw_value = value.value + local divisor = 10 + raw_value = raw_value / divisor + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.powerMeter.power({value = raw_value, unit = "W" })) end local zigbee_dimmer_power_energy_handler = { NAME = "zigbee dimmer power energy handler", + zigbee_handlers = { + attr = { + [SimpleMetering.ID] = { + [SimpleMetering.attributes.InstantaneousDemand.ID] = instantaneous_demand_handler + } + } + }, lifecycle_handlers = { + init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure, }, - sub_drivers = { - require("zigbee-dimmer-power-energy/enbrighten-metering-dimmer") - }, - can_handle = is_zigbee_dimmer_power_energy - + can_handle = require("zigbee-dimmer-power-energy.can_handle"), } return zigbee_dimmer_power_energy_handler diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/can_handle.lua new file mode 100644 index 0000000000..cbbc8dfeab --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local DIMMING_LIGHT_FINGERPRINTS = require "zigbee-dimming-light.fingerprints" + for _, fingerprint in ipairs(DIMMING_LIGHT_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("zigbee-dimming-light") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/fingerprints.lua new file mode 100644 index 0000000000..820bd633b6 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/fingerprints.lua @@ -0,0 +1,36 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + {mfr = "Vimar", model = "DimmerSwitch_v1.0"}, -- Vimar Smart Dimmer Switch + {mfr = "OSRAM", model = "LIGHTIFY A19 ON/OFF/DIM"}, -- SYLVANIA Smart A19 Soft White + {mfr = "OSRAM", model = "LIGHTIFY A19 ON/OFF/DIM 10 Year"}, -- SYLVANIA Smart 10-Year A19 + {mfr = "OSRAM SYLVANIA", model = "iQBR30"}, -- SYLVANIA Ultra iQ + {mfr = "OSRAM", model = "LIGHTIFY PAR38 ON/OFF/DIM"}, -- SYLVANIA Smart PAR38 Soft White + {mfr = "OSRAM", model = "LIGHTIFY BR ON/OFF/DIM"}, -- SYLVANIA Smart BR30 Soft White + {mfr = "sengled", model = "E11-G13"}, -- Sengled Element Classic + {mfr = "sengled", model = "E11-G14"}, -- Sengled Element Classic + {mfr = "sengled", model = "E11-G23"}, -- Sengled Element Classic + {mfr = "sengled", model = "E11-G33"}, -- Sengled Element Classic + {mfr = "sengled", model = "E12-N13"}, -- Sengled Element Classic + {mfr = "sengled", model = "E12-N14"}, -- Sengled Element Classic + {mfr = "sengled", model = "E12-N15"}, -- Sengled Element Classic + {mfr = "sengled", model = "E11-N13"}, -- Sengled Element Classic + {mfr = "sengled", model = "E11-N14"}, -- Sengled Element Classic + {mfr = "sengled", model = "E1A-AC2"}, -- Sengled DownLight + {mfr = "sengled", model = "E11-N13A"}, -- Sengled Extra Bright Soft White + {mfr = "sengled", model = "E11-N14A"}, -- Sengled Extra Bright Daylight + {mfr = "sengled", model = "E21-N13A"}, -- Sengled Soft White + {mfr = "sengled", model = "E21-N14A"}, -- Sengled Daylight + {mfr = "sengled", model = "E11-U21U31"}, -- Sengled Element Touch + {mfr = "sengled", model = "E13-A21"}, -- Sengled LED Flood Light + {mfr = "sengled", model = "E11-N1G"}, -- Sengled Smart LED Vintage Edison Bulb + {mfr = "sengled", model = "E23-N11"}, -- Sengled Element Classic par38 + {mfr = "Leviton", model = "DL6HD"}, -- Leviton Dimmer Switch + {mfr = "Leviton", model = "DL3HL"}, -- Leviton Lumina RF Plug-In Dimmer + {mfr = "Leviton", model = "DL1KD"}, -- Leviton Lumina RF Dimmer Switch + {mfr = "Leviton", model = "ZSD07"}, -- Leviton Lumina RF 0-10V Dimming Wall Switch + {mfr = "MRVL", model = "MZ100"}, + {mfr = "CREE", model = "Connected A-19 60W Equivalent"}, + {mfr = "Insta GmbH", model = "NEXENTRO Dimming Actuator"} +} diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua index 153c8325e8..dc8629c57b 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua @@ -1,58 +1,14 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local configurations = require "configurations" +local switch_utils = require "switch_utils" local OnOff = clusters.OnOff local Level = clusters.Level -local DIMMING_LIGHT_FINGERPRINTS = { - {mfr = "Vimar", model = "DimmerSwitch_v1.0"}, -- Vimar Smart Dimmer Switch - {mfr = "OSRAM", model = "LIGHTIFY A19 ON/OFF/DIM"}, -- SYLVANIA Smart A19 Soft White - {mfr = "OSRAM", model = "LIGHTIFY A19 ON/OFF/DIM 10 Year"}, -- SYLVANIA Smart 10-Year A19 - {mfr = "OSRAM SYLVANIA", model = "iQBR30"}, -- SYLVANIA Ultra iQ - {mfr = "OSRAM", model = "LIGHTIFY PAR38 ON/OFF/DIM"}, -- SYLVANIA Smart PAR38 Soft White - {mfr = "OSRAM", model = "LIGHTIFY BR ON/OFF/DIM"}, -- SYLVANIA Smart BR30 Soft White - {mfr = "sengled", model = "E11-G13"}, -- Sengled Element Classic - {mfr = "sengled", model = "E11-G14"}, -- Sengled Element Classic - {mfr = "sengled", model = "E11-G23"}, -- Sengled Element Classic - {mfr = "sengled", model = "E11-G33"}, -- Sengled Element Classic - {mfr = "sengled", model = "E12-N13"}, -- Sengled Element Classic - {mfr = "sengled", model = "E12-N14"}, -- Sengled Element Classic - {mfr = "sengled", model = "E12-N15"}, -- Sengled Element Classic - {mfr = "sengled", model = "E11-N13"}, -- Sengled Element Classic - {mfr = "sengled", model = "E11-N14"}, -- Sengled Element Classic - {mfr = "sengled", model = "E1A-AC2"}, -- Sengled DownLight - {mfr = "sengled", model = "E11-N13A"}, -- Sengled Extra Bright Soft White - {mfr = "sengled", model = "E11-N14A"}, -- Sengled Extra Bright Daylight - {mfr = "sengled", model = "E21-N13A"}, -- Sengled Soft White - {mfr = "sengled", model = "E21-N14A"}, -- Sengled Daylight - {mfr = "sengled", model = "E11-U21U31"}, -- Sengled Element Touch - {mfr = "sengled", model = "E13-A21"}, -- Sengled LED Flood Light - {mfr = "sengled", model = "E11-N1G"}, -- Sengled Smart LED Vintage Edison Bulb - {mfr = "sengled", model = "E23-N11"}, -- Sengled Element Classic par38 - {mfr = "Leviton", model = "DL6HD"}, -- Leviton Dimmer Switch - {mfr = "Leviton", model = "DL3HL"}, -- Leviton Lumina RF Plug-In Dimmer - {mfr = "Leviton", model = "DL1KD"}, -- Leviton Lumina RF Dimmer Switch - {mfr = "Leviton", model = "ZSD07"}, -- Leviton Lumina RF 0-10V Dimming Wall Switch - {mfr = "MRVL", model = "MZ100"}, - {mfr = "CREE", model = "Connected A-19 60W Equivalent"}, - {mfr = "Insta GmbH", model = "NEXENTRO Dimming Actuator"} -} - local DIMMING_LIGHT_CONFIGURATION = { { cluster = OnOff.ID, @@ -74,38 +30,30 @@ local DIMMING_LIGHT_CONFIGURATION = { } } -local function can_handle_zigbee_dimming_light(opts, driver, device) - for _, fingerprint in ipairs(DIMMING_LIGHT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("zigbee-dimming-light") - return true, subdriver - end - end - return false +local function do_configure(driver, device) + device:refresh() + device:configure() end local function device_init(driver, device) for _,attribute in ipairs(DIMMING_LIGHT_CONFIGURATION) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end local function device_added(driver, device) - device:emit_event(capabilities.switchLevel.level(100)) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.switchLevel, capabilities.switchLevel.level.NAME, capabilities.switchLevel.level(100)) end local zigbee_dimming_light = { NAME = "Zigbee Dimming Light", lifecycle_handlers = { init = configurations.power_reconfig_wrapper(device_init), - added = device_added - }, - sub_drivers = { - require("zigbee-dimming-light/osram-iqbr30"), - require("zigbee-dimming-light/zll-dimmer") + added = device_added, + doConfigure = do_configure }, - can_handle = can_handle_zigbee_dimming_light + sub_drivers = require("zigbee-dimming-light.sub_drivers"), + can_handle = require("zigbee-dimming-light.can_handle"), } return zigbee_dimming_light diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/can_handle.lua new file mode 100644 index 0000000000..8463ce2a77 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/can_handle.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local res = device:get_manufacturer() == "OSRAM SYLVANIA" and device:get_model() == "iQBR30" + if res then + return res, require("zigbee-dimming-light.osram-iqbr30") + end + return res +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/init.lua index c6d31b7e10..74d9acb903 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -20,10 +9,6 @@ local Level = clusters.Level local SwitchLevel = capabilities.switchLevel -local function can_handle_osram_iqbr30(opts, driver, device, ...) - return device:get_manufacturer() == "OSRAM SYLVANIA" and device:get_model() == "iQBR30" -end - local function set_switch_level_handler(driver, device, cmd) local level = math.floor(cmd.args.level / 100.0 * 254) @@ -40,7 +25,7 @@ local osram_iqbr30 = { [SwitchLevel.commands.setLevel.NAME] = set_switch_level_handler } }, - can_handle = can_handle_osram_iqbr30 + can_handle = require("zigbee-dimming-light.osram-iqbr30.can_handle"), } return osram_iqbr30 diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/sub_drivers.lua new file mode 100644 index 0000000000..fa188a4e7e --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("zigbee-dimming-light.osram-iqbr30"), + lazy_load("zigbee-dimming-light.zll-dimmer") +} diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/can_handle.lua new file mode 100644 index 0000000000..d75b86b353 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local ZLL_DIMMER_FINGERPRINTS = require("zigbee-dimming-light.zll-dimmer.fingerprints") + for _, fingerprint in ipairs(ZLL_DIMMER_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-dimming-light.zll-dimmer") + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/fingerprints.lua new file mode 100644 index 0000000000..1a5176bcd5 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + {mfr = "Leviton", model = "DL6HD"}, -- Leviton Dimmer Switch + {mfr = "Leviton", model = "DL3HL"}, -- Leviton Lumina RF Plug-In Dimmer + {mfr = "Leviton", model = "DL1KD"}, -- Leviton Lumina RF Dimmer Switch + {mfr = "Leviton", model = "ZSD07"}, -- Leviton Lumina RF 0-10V Dimming Wall Switch + {mfr = "MRVL", model = "MZ100"}, + {mfr = "CREE", model = "Connected A-19 60W Equivalent"} +} diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/init.lua index 72b35fd580..039b049fc7 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -19,24 +8,6 @@ local Level = clusters.Level local SwitchLevel = capabilities.switchLevel -local ZLL_DIMMER_FINGERPRINTS = { - {mfr = "Leviton", model = "DL6HD"}, -- Leviton Dimmer Switch - {mfr = "Leviton", model = "DL3HL"}, -- Leviton Lumina RF Plug-In Dimmer - {mfr = "Leviton", model = "DL1KD"}, -- Leviton Lumina RF Dimmer Switch - {mfr = "Leviton", model = "ZSD07"}, -- Leviton Lumina RF 0-10V Dimming Wall Switch - {mfr = "MRVL", model = "MZ100"}, - {mfr = "CREE", model = "Connected A-19 60W Equivalent"} -} - -local function can_handle_zll_dimmer(opts, driver, device) - for _, fingerprint in ipairs(ZLL_DIMMER_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - local function set_switch_level_handler(driver, device, cmd) local level = math.floor(cmd.args.level / 100.0 * 254) @@ -51,7 +22,7 @@ local zll_dimmer = { [SwitchLevel.commands.setLevel.NAME] = set_switch_level_handler } }, - can_handle = can_handle_zll_dimmer + can_handle = require("zigbee-dimming-light.zll-dimmer.can_handle") } return zll_dimmer diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/can_handle.lua new file mode 100644 index 0000000000..fbc679fb18 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_DUAL_METERING_SWITCH_FINGERPRINT = { + {mfr = "Aurora", model = "DoubleSocket50AU"} +} + +return function(opts, driver, device, ...) + for _, fingerprint in ipairs(ZIGBEE_DUAL_METERING_SWITCH_FINGERPRINT) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("zigbee-dual-metering-switch") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua index 68f7d63675..b1d10f94a6 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local st_device = require "st.device" local clusters = require "st.zigbee.zcl.clusters" @@ -21,20 +10,6 @@ local configurations = require "configurations" local CHILD_ENDPOINT = 2 -local ZIGBEE_DUAL_METERING_SWITCH_FINGERPRINT = { - {mfr = "Aurora", model = "DoubleSocket50AU"} -} - -local function can_handle_zigbee_dual_metering_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(ZIGBEE_DUAL_METERING_SWITCH_FINGERPRINT) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("zigbee-dual-metering-switch") - return true, subdriver - end - end - return false -end - local function do_refresh(self, device) device:send(OnOff.attributes.OnOff:read(device)) device:send(ElectricalMeasurement.attributes.ActivePower:read(device)) @@ -80,7 +55,7 @@ local zigbee_dual_metering_switch = { init = configurations.power_reconfig_wrapper(device_init), added = device_added }, - can_handle = can_handle_zigbee_dual_metering_switch + can_handle = require("zigbee-dual-metering-switch.can_handle"), } return zigbee_dual_metering_switch diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/can_handle.lua new file mode 100644 index 0000000000..e6e3165aa3 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local can_handle = device:get_manufacturer() == "DAWON_DNS" + if can_handle then + local subdriver = require("zigbee-metering-plug-power-consumption-report") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua index 044bf509df..53a7e1eb99 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -35,6 +24,7 @@ end local do_configure = function(self, device) device:configure() + device:refresh() end local device_init = function(self, device) @@ -54,15 +44,7 @@ local zigbee_metering_plug_power_conumption_report = { init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - local can_handle = device:get_manufacturer() == "DAWON_DNS" - if can_handle then - local subdriver = require("zigbee-metering-plug-power-consumption-report") - return true, subdriver - else - return false - end - end + can_handle = require("zigbee-metering-plug-power-consumption-report.can_handle"), } return zigbee_metering_plug_power_conumption_report diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/can_handle.lua new file mode 100644 index 0000000000..18cff448c1 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(opts, driver, device) + local AURORA_RELAY_FINGERPRINTS = { + { mfr = "Aurora", model = "Smart16ARelay51AU" }, + { mfr = "Develco Products A/S", model = "Smart16ARelay51AU" }, + { mfr = "SALUS", model = "SX885ZB" } + } + for _, fingerprint in ipairs(AURORA_RELAY_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-switch-power.aurora-relay") + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/init.lua index 0f8eac96bc..266ff9b4e7 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/init.lua @@ -1,34 +1,8 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local constants = require "st.zigbee.constants" -local AURORA_RELAY_FINGERPRINTS = { - { mfr = "Aurora", model = "Smart16ARelay51AU" }, - { mfr = "Develco Products A/S", model = "Smart16ARelay51AU" }, - { mfr = "SALUS", model = "SX885ZB" } -} - -local function can_handle_aurora_relay(opts, driver, device) - for _, fingerprint in ipairs(AURORA_RELAY_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - local function do_configure(driver, device) device:configure() @@ -43,7 +17,7 @@ local aurora_relay = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_aurora_relay + can_handle = require("zigbee-switch-power.aurora-relay.can_handle"), } return aurora_relay diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/can_handle.lua new file mode 100644 index 0000000000..ed4db3dc52 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local SWITCH_POWER_FINGERPRINTS = require "zigbee-switch-power.fingerprints" + for _, fingerprint in ipairs(SWITCH_POWER_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("zigbee-switch-power") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/fingerprints.lua new file mode 100644 index 0000000000..d277d36967 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/fingerprints.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "Vimar", model = "Mains_Power_Outlet_v1.0" }, + { model = "PAN18-v1.0.7" }, + { model = "E210-KR210Z1-HA" }, + { mfr = "Aurora", model = "Smart16ARelay51AU" }, + { mfr = "Develco Products A/S", model = "Smart16ARelay51AU" }, + { mfr = "Jasco Products", model = "45853" }, + { mfr = "Jasco Products", model = "45856" }, + { mfr = "MEGAMAN", model = "SH-PSUKC44B-E" }, + { mfr = "ClimaxTechnology", model = "PSM_00.00.00.35TC" }, + { mfr = "SALUS", model = "SX885ZB" }, + { mfr = "AduroSmart Eria", model = "AD-SmartPlug3001" }, + { mfr = "AduroSmart Eria", model = "BPU3" }, + { mfr = "AduroSmart Eria", model = "BDP3001" } +} diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/init.lua index d9ae8f4750..0ea2224a6f 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -19,32 +8,6 @@ local constants = require "st.zigbee.constants" local SimpleMetering = clusters.SimpleMetering local ElectricalMeasurement = clusters.ElectricalMeasurement -local SWITCH_POWER_FINGERPRINTS = { - { mfr = "Vimar", model = "Mains_Power_Outlet_v1.0" }, - { model = "PAN18-v1.0.7" }, - { model = "E210-KR210Z1-HA" }, - { mfr = "Aurora", model = "Smart16ARelay51AU" }, - { mfr = "Develco Products A/S", model = "Smart16ARelay51AU" }, - { mfr = "Jasco Products", model = "45853" }, - { mfr = "Jasco Products", model = "45856" }, - { mfr = "MEGAMAN", model = "SH-PSUKC44B-E" }, - { mfr = "ClimaxTechnology", model = "PSM_00.00.00.35TC" }, - { mfr = "SALUS", model = "SX885ZB" }, - { mfr = "AduroSmart Eria", model = "AD-SmartPlug3001" }, - { mfr = "AduroSmart Eria", model = "BPU3" }, - { mfr = "AduroSmart Eria", model = "BDP3001" } -} - -local function can_handle_zigbee_switch_power(opts, driver, device) - for _, fingerprint in ipairs(SWITCH_POWER_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("zigbee-switch-power") - return true, subdriver - end - end - return false -end - local function active_power_meter_handler(driver, device, value, zb_rx) local raw_value = value.value local divisor = device:get_field(constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY) or 10 @@ -75,11 +38,8 @@ local zigbee_switch_power = { } } }, - sub_drivers = { - require("zigbee-switch-power/aurora-relay"), - require("zigbee-switch-power/vimar") - }, - can_handle = can_handle_zigbee_switch_power + sub_drivers = require("zigbee-switch-power.sub_drivers"), + can_handle = require("zigbee-switch-power.can_handle"), } return zigbee_switch_power diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/sub_drivers.lua new file mode 100644 index 0000000000..340e1f27c6 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("zigbee-switch-power.aurora-relay"), + lazy_load("zigbee-switch-power.vimar") +} diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/can_handle.lua new file mode 100644 index 0000000000..8143fb6a8e --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(opts, driver, device) + local VIMAR_FINGERPRINTS = { + { mfr = "Vimar", model = "Mains_Power_Outlet_v1.0" } + } + for _, fingerprint in ipairs(VIMAR_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-switch-power.vimar") + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/init.lua index ed65ce785b..82eb0674d4 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local constants = require "st.zigbee.constants" local device_management = require "st.zigbee.device_management" @@ -18,19 +7,6 @@ local zcl_clusters = require "st.zigbee.zcl.clusters" local ElectricalMeasurement = zcl_clusters.ElectricalMeasurement -local VIMAR_FINGERPRINTS = { - { mfr = "Vimar", model = "Mains_Power_Outlet_v1.0" } -} - -local function can_handle_vimar_switch_power(opts, driver, device) - for _, fingerprint in ipairs(VIMAR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - local function do_configure(driver, device) device:configure() device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, 1, {persist = true}) @@ -45,7 +21,7 @@ local vimar_switch_power = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_vimar_switch_power + can_handle = require("zigbee-switch-power.vimar.can_handle"), } return vimar_switch_power diff --git a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/can_handle.lua new file mode 100644 index 0000000000..f2de6f2d81 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device) + local ZLL_DIMMER_BULB_FINGERPRINTS = require "zll-dimmer-bulb.fingerprints" + local can_handle = (ZLL_DIMMER_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] + if can_handle then + local subdriver = require("zll-dimmer-bulb") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/fingerprints.lua new file mode 100644 index 0000000000..ab13464d62 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/fingerprints.lua @@ -0,0 +1,117 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + ["AduroSmart Eria"] = { + ["ZLL-DimmableLight"] = true, + ["ZLL-ExtendedColor"] = true, + ["ZLL-ColorTemperature"] = true + }, + ["IKEA of Sweden"] = { + ["TRADFRI bulb E26 opal 1000lm"] = true, + ["TRADFRI bulb E12 W op/ch 400lm"] = true, + ["TRADFRI bulb E17 W op/ch 400lm"] = true, + ["TRADFRI bulb GU10 W 400lm"] = true, + ["TRADFRI bulb E27 W opal 1000lm"] = true, + ["TRADFRI bulb E26 W opal 1000lm"] = true, + ["TRADFRI bulb E14 W op/ch 400lm"] = true, + ["TRADFRI transformer 10W"] = true, + ["TRADFRI Driver 10W"] = true, + ["TRADFRI transformer 30W"] = true, + ["TRADFRI Driver 30W"] = true, + ["TRADFRI bulb E26 WS clear 950lm"] = true, + ["TRADFRI bulb GU10 WS 400lm"] = true, + ["TRADFRI bulb E12 WS opal 400lm"] = true, + ["TRADFRI bulb E26 WS opal 980lm"] = true, + ["TRADFRI bulb E27 WS clear 950lm"] = true, + ["TRADFRI bulb E14 WS opal 400lm"] = true, + ["TRADFRI bulb E27 WS opal 980lm"] = true, + ["FLOALT panel WS 30x30"] = true, + ["FLOALT panel WS 30x90"] = true, + ["FLOALT panel WS 60x60"] = true, + ["SURTE door WS 38x64"] = true, + ["JORMLIEN door WS 40x80"] = true, + ["TRADFRI bulb E27 CWS opal 600lm"] = true, + ["TRADFRI bulb E26 CWS opal 600lm"] = true + }, + ["Eaton"] = { + ["Halo_RL5601"] = true + }, + ["Megaman"] = { + ["ZLL-DimmableLight"] = true, + ["ZLL-ExtendedColor"] = true + }, + ["MEGAMAN"] = { + ["BSZTM002"] = true, + ["BSZTM003"] = true + }, + ["innr"] = { + ["RS 125"] = true, + ["RB 165"] = true, + ["RB 175 W"] = true, + ["RB 145"] = true, + ["RS 128 T"] = true, + ["RB 178 T"] = true, + ["RB 148 T"] = true, + ["RB 185 C"] = true, + ["FL 130 C"] = true, + ["OFL 120 C"] = true, + ["OFL 140 C"] = true, + ["OSL 130 C"] = true + }, + ["Leviton"] = { + ["DG3HL"] = true, + ["DG6HD"] = true + }, + ["OSRAM"] = { + ["Classic A60 W clear"] = true, + ["Classic A60 W clear - LIGHTIFY"] = true, + ["CLA60 OFD OSRAM"] = true, + ["Classic A60 RGBW"] = true, + ["PAR 16 50 RGBW - LIGHTIFY"] = true, + ["CLA60 RGBW OSRAM"] = true, + ["Flex RGBW"] = true, + ["Gardenpole RGBW-Lightify"] = true, + ["LIGHTIFY Outdoor Flex RGBW"] = true, + ["LIGHTIFY Indoor Flex RGBW"] = true, + ["Classic B40 TW - LIGHTIFY"] = true, + ["CLA60 TW OSRAM"] = true + }, + ["Philips"] = { + ["LWB006"] = true, + ["LWB007"] = true, + ["LWB010"] = true, + ["LWB014"] = true, + ["LCT001"] = true, + ["LCT002"] = true, + ["LCT003"] = true, + ["LCT007"] = true, + ["LCT010"] = true, + ["LCT011"] = true, + ["LCT012"] = true, + ["LCT014"] = true, + ["LCT015"] = true, + ["LCT016"] = true, + ["LST001"] = true, + ["LST002"] = true, + ["LTW001"] = true, + ["LTW004"] = true, + ["LTW010"] = true, + ["LTW011"] = true, + ["LTW012"] = true, + ["LTW013"] = true, + ["LTW014"] = true, + ["LTW015"] = true + }, + ["sengled"] = { + ["E14-U43"] = true, + ["E13-N11"] = true + }, + ["GLEDOPTO"] = { + ["GL-C-008"] = true, + ["GL-B-001Z"] = true + }, + ["Ubec"] = { + ["BBB65L-HY"] = true + } +} diff --git a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/ikea-xy-color-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/ikea-xy-color-bulb/init.lua deleted file mode 100644 index cf18d2ff82..0000000000 --- a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/ikea-xy-color-bulb/init.lua +++ /dev/null @@ -1,193 +0,0 @@ --- Copyright 2022 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 capabilities = require "st.capabilities" -local clusters = require "st.zigbee.zcl.clusters" -local switch_defaults = require "st.zigbee.defaults.switch_defaults" -local configurationMap = require "configurations" -local utils = require "st.utils" - -local ColorControl = clusters.ColorControl - -local CURRENT_X = "current_x_value" -- y value from xyY color space -local CURRENT_Y = "current_y_value" -- x value from xyY color space -local Y_TRISTIMULUS_VALUE = "y_tristimulus_value" -- Y tristimulus value which is used to convert color xyY -> RGB -> HSV -local HUESAT_TIMER = "huesat_timer" -local TARGET_HUE = "target_hue" -local TARGET_SAT = "target_sat" - -local IKEA_XY_COLOR_BULB_FINGERPRINTS = { - ["IKEA of Sweden"] = { - ["TRADFRI bulb E27 CWS opal 600lm"] = true, - ["TRADFRI bulb E26 CWS opal 600lm"] = true - } -} - -local function can_handle_ikea_xy_color_bulb(opts, driver, device) - return (IKEA_XY_COLOR_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] or false -end - -local device_init = function(self, device) - device:remove_configured_attribute(ColorControl.ID, ColorControl.attributes.CurrentHue.ID) - device:remove_configured_attribute(ColorControl.ID, ColorControl.attributes.CurrentSaturation.ID) - device:remove_monitored_attribute(ColorControl.ID, ColorControl.attributes.CurrentHue.ID) - device:remove_monitored_attribute(ColorControl.ID, ColorControl.attributes.CurrentSaturation.ID) - - local configuration = configurationMap.get_device_configuration(device) - if configuration ~= nil then - for _, attribute in ipairs(configuration) do - device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) - end - end -end - -local function store_xyY_values(device, x, y, Y) - device:set_field(Y_TRISTIMULUS_VALUE, Y) - device:set_field(CURRENT_X, x) - device:set_field(CURRENT_Y, y) -end - -local query_device = function(device) - return function() - device:send(ColorControl.attributes.CurrentX:read(device)) - device:send(ColorControl.attributes.CurrentY:read(device)) - end -end - -local function set_color_handler(driver, device, cmd) - -- Cancel the hue/sat timer if it's running, since setColor includes both hue and saturation - local huesat_timer = device:get_field(HUESAT_TIMER) - if huesat_timer ~= nil then - device.thread:cancel_timer(huesat_timer) - device:set_field(HUESAT_TIMER, nil) - end - - local hue = (cmd.args.color.hue ~= nil and cmd.args.color.hue > 99) and 99 or cmd.args.color.hue - local sat = cmd.args.color.saturation - - local x, y, Y = utils.safe_hsv_to_xy(hue, sat) - store_xyY_values(device, x, y, Y) - switch_defaults.on(driver, device, cmd) - - device:send(ColorControl.commands.MoveToColor(device, x, y, 0x0000)) - - device:set_field(TARGET_HUE, nil) - device:set_field(TARGET_SAT, nil) - device.thread:call_with_delay(2, query_device(device)) -end - -local huesat_timer_callback = function(driver, device, cmd) - return function() - device:set_field(HUESAT_TIMER, nil) - local hue = device:get_field(TARGET_HUE) - local sat = device:get_field(TARGET_SAT) - hue = hue ~= nil and hue or device:get_latest_state("main", capabilities.colorControl.ID, capabilities.colorControl.hue.NAME) - sat = sat ~= nil and sat or device:get_latest_state("main", capabilities.colorControl.ID, capabilities.colorControl.saturation.NAME) - cmd.args = { - color = { - hue = hue, - saturation = sat - } - } - set_color_handler(driver, device, cmd) - end -end - -local function set_hue_sat_helper(driver, device, cmd, hue, sat) - local huesat_timer = device:get_field(HUESAT_TIMER) - if huesat_timer ~= nil then - device.thread:cancel_timer(huesat_timer) - device:set_field(HUESAT_TIMER, nil) - end - if hue ~= nil and sat ~= nil then - cmd.args = { - color = { - hue = hue, - saturation = sat - } - } - set_color_handler(driver, device, cmd) - else - if hue ~= nil then - device:set_field(TARGET_HUE, hue) - elseif sat ~= nil then - device:set_field(TARGET_SAT, sat) - end - device:set_field(HUESAT_TIMER, device.thread:call_with_delay(0.2, huesat_timer_callback(driver, device, cmd))) - end -end - -local function set_hue_handler(driver, device, cmd) - set_hue_sat_helper(driver, device, cmd, cmd.args.hue, device:get_field(TARGET_SAT)) -end - -local function set_saturation_handler(driver, device, cmd) - set_hue_sat_helper(driver, device, cmd, device:get_field(TARGET_HUE), cmd.args.saturation) -end - -local function current_x_attr_handler(driver, device, value, zb_rx) - local Y_tristimulus = device:get_field(Y_TRISTIMULUS_VALUE) - local y = device:get_field(CURRENT_Y) - local x = value.value - - if y then - local hue, saturation = utils.safe_xy_to_hsv(x, y, Y_tristimulus) - - device:emit_event(capabilities.colorControl.hue(hue)) - device:emit_event(capabilities.colorControl.saturation(saturation)) - end - - device:set_field(CURRENT_X, x) -end - -local function current_y_attr_handler(driver, device, value, zb_rx) - local Y_tristimulus = device:get_field(Y_TRISTIMULUS_VALUE) - local x = device:get_field(CURRENT_X) - local y = value.value - - if x then - local hue, saturation = utils.safe_xy_to_hsv(x, y, Y_tristimulus) - - device:emit_event(capabilities.colorControl.hue(hue)) - device:emit_event(capabilities.colorControl.saturation(saturation)) - end - - device:set_field(CURRENT_Y, y) -end - -local ikea_xy_color_bulb = { - NAME = "IKEA XY Color Bulb", - lifecycle_handlers = { - init = configurationMap.power_reconfig_wrapper(device_init) - }, - capability_handlers = { - [capabilities.colorControl.ID] = { - [capabilities.colorControl.commands.setColor.NAME] = set_color_handler, - [capabilities.colorControl.commands.setHue.NAME] = set_hue_handler, - [capabilities.colorControl.commands.setSaturation.NAME] = set_saturation_handler - } - }, - zigbee_handlers = { - attr = { - [ColorControl.ID] = { - [ColorControl.attributes.CurrentX.ID] = current_x_attr_handler, - [ColorControl.attributes.CurrentY.ID] = current_y_attr_handler - } - } - }, - can_handle = can_handle_ikea_xy_color_bulb -} - -return ikea_xy_color_bulb diff --git a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua index 6c74f9b89f..d6becf1467 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -19,131 +8,6 @@ local colorTemperature_defaults = require "st.zigbee.defaults.colorTemperature_d local OnOff = clusters.OnOff local Level = clusters.Level -local ZLL_DIMMER_BULB_FINGERPRINTS = { - ["AduroSmart Eria"] = { - ["ZLL-DimmableLight"] = true, - ["ZLL-ExtendedColor"] = true, - ["ZLL-ColorTemperature"] = true - }, - ["IKEA of Sweden"] = { - ["TRADFRI bulb E26 opal 1000lm"] = true, - ["TRADFRI bulb E12 W op/ch 400lm"] = true, - ["TRADFRI bulb E17 W op/ch 400lm"] = true, - ["TRADFRI bulb GU10 W 400lm"] = true, - ["TRADFRI bulb E27 W opal 1000lm"] = true, - ["TRADFRI bulb E26 W opal 1000lm"] = true, - ["TRADFRI bulb E14 W op/ch 400lm"] = true, - ["TRADFRI transformer 10W"] = true, - ["TRADFRI Driver 10W"] = true, - ["TRADFRI transformer 30W"] = true, - ["TRADFRI Driver 30W"] = true, - ["TRADFRI bulb E26 WS clear 950lm"] = true, - ["TRADFRI bulb GU10 WS 400lm"] = true, - ["TRADFRI bulb E12 WS opal 400lm"] = true, - ["TRADFRI bulb E26 WS opal 980lm"] = true, - ["TRADFRI bulb E27 WS clear 950lm"] = true, - ["TRADFRI bulb E14 WS opal 400lm"] = true, - ["TRADFRI bulb E27 WS opal 980lm"] = true, - ["FLOALT panel WS 30x30"] = true, - ["FLOALT panel WS 30x90"] = true, - ["FLOALT panel WS 60x60"] = true, - ["SURTE door WS 38x64"] = true, - ["JORMLIEN door WS 40x80"] = true, - ["TRADFRI bulb E27 CWS opal 600lm"] = true, - ["TRADFRI bulb E26 CWS opal 600lm"] = true - }, - ["Eaton"] = { - ["Halo_RL5601"] = true - }, - ["Megaman"] = { - ["ZLL-DimmableLight"] = true, - ["ZLL-ExtendedColor"] = true - }, - ["MEGAMAN"] = { - ["BSZTM002"] = true, - ["BSZTM003"] = true - }, - ["innr"] = { - ["RS 125"] = true, - ["RB 165"] = true, - ["RB 175 W"] = true, - ["RB 145"] = true, - ["RS 128 T"] = true, - ["RB 178 T"] = true, - ["RB 148 T"] = true, - ["RB 185 C"] = true, - ["FL 130 C"] = true, - ["OFL 120 C"] = true, - ["OFL 140 C"] = true, - ["OSL 130 C"] = true - }, - ["Leviton"] = { - ["DG3HL"] = true, - ["DG6HD"] = true - }, - ["OSRAM"] = { - ["Classic A60 W clear"] = true, - ["Classic A60 W clear - LIGHTIFY"] = true, - ["CLA60 OFD OSRAM"] = true, - ["Classic A60 RGBW"] = true, - ["PAR 16 50 RGBW - LIGHTIFY"] = true, - ["CLA60 RGBW OSRAM"] = true, - ["Flex RGBW"] = true, - ["Gardenpole RGBW-Lightify"] = true, - ["LIGHTIFY Outdoor Flex RGBW"] = true, - ["LIGHTIFY Indoor Flex RGBW"] = true, - ["Classic B40 TW - LIGHTIFY"] = true, - ["CLA60 TW OSRAM"] = true - }, - ["Philips"] = { - ["LWB006"] = true, - ["LWB007"] = true, - ["LWB010"] = true, - ["LWB014"] = true, - ["LCT001"] = true, - ["LCT002"] = true, - ["LCT003"] = true, - ["LCT007"] = true, - ["LCT010"] = true, - ["LCT011"] = true, - ["LCT012"] = true, - ["LCT014"] = true, - ["LCT015"] = true, - ["LCT016"] = true, - ["LST001"] = true, - ["LST002"] = true, - ["LTW001"] = true, - ["LTW004"] = true, - ["LTW010"] = true, - ["LTW011"] = true, - ["LTW012"] = true, - ["LTW013"] = true, - ["LTW014"] = true, - ["LTW015"] = true - }, - ["sengled"] = { - ["E14-U43"] = true, - ["E13-N11"] = true - }, - ["GLEDOPTO"] = { - ["GL-C-008"] = true, - ["GL-B-001Z"] = true - }, - ["Ubec"] = { - ["BBB65L-HY"] = true - } -} - -local function can_handle_zll_dimmer_bulb(opts, driver, device) - local can_handle = (ZLL_DIMMER_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] - if can_handle then - local subdriver = require("zll-dimmer-bulb") - return true, subdriver - else - return false - end -end - local function do_configure(driver, device) device:configure() end @@ -204,10 +68,7 @@ local zll_dimmer_bulb = { [capabilities.colorTemperature.commands.setColorTemperature.NAME] = handle_set_color_temperature } }, - sub_drivers = { - require("zll-dimmer-bulb/ikea-xy-color-bulb") - }, - can_handle = can_handle_zll_dimmer_bulb + can_handle = require("zll-dimmer-bulb.can_handle"), } return zll_dimmer_bulb diff --git a/drivers/SmartThings/zigbee-switch/src/zll-polling/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zll-polling/can_handle.lua new file mode 100644 index 0000000000..c39ee387e3 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zll-polling/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, zb_rx, ...) + local constants = require "st.zigbee.constants" + + local endpoint = device.zigbee_endpoints[device.fingerprinted_endpoint_id] or device.zigbee_endpoints[tostring(device.fingerprinted_endpoint_id)] + if (endpoint ~= nil and endpoint.profile_id == constants.ZLL_PROFILE_ID) then + local subdriver = require("zll-polling") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua b/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua index 59424dd784..3478b39bd5 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua @@ -1,31 +1,9 @@ --- 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local device_lib = require "st.device" -local constants = require "st.zigbee.constants" local clusters = require "st.zigbee.zcl.clusters" -local function zll_profile(opts, driver, device, zb_rx, ...) - local endpoint = device.zigbee_endpoints[device.fingerprinted_endpoint_id] or device.zigbee_endpoints[tostring(device.fingerprinted_endpoint_id)] - if (endpoint.profile_id == constants.ZLL_PROFILE_ID) then - local subdriver = require("zll-polling") - return true, subdriver - else - return false - end -end - local function set_up_zll_polling(driver, device) local INFREQUENT_POLL_COUNTER = "_infrequent_poll_counter" local function poll() @@ -57,7 +35,7 @@ local ZLL_polling = { lifecycle_handlers = { init = set_up_zll_polling }, - can_handle = zll_profile + can_handle = require("zll-polling.can_handle"), } -return ZLL_polling \ No newline at end of file +return ZLL_polling diff --git a/drivers/SmartThings/zigbee-thermostat/fingerprints.yml b/drivers/SmartThings/zigbee-thermostat/fingerprints.yml index 1dc993ea26..d88b3ff89a 100644 --- a/drivers/SmartThings/zigbee-thermostat/fingerprints.yml +++ b/drivers/SmartThings/zigbee-thermostat/fingerprints.yml @@ -124,6 +124,11 @@ zigbeeManufacturer: manufacturer: Resideo Korea model: DT300ST-M000 deviceProfileName: thermostat-resideo-dt300st-m000 + - id: "Resideo Korea/MC200ST" + deviceLabel: Valve Controller + manufacturer: Resideo Korea + model: MC200ST + deviceProfileName: thermostat-resideo-dt300st-m000 - id: "LUMI/lumi.airrtc.agl001" deviceLabel: Aqara Smart Radiator Thermostat E1 manufacturer: LUMI diff --git a/drivers/SmartThings/zigbee-thermostat/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/aqara/can_handle.lua new file mode 100644 index 0000000000..e4453597ed --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_aqara_products(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-thermostat/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..30f1243e76 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/aqara/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.airrtc.agl001" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-thermostat/src/aqara/init.lua b/drivers/SmartThings/zigbee-thermostat/src/aqara/init.lua index 9523614bcc..b17b3d9e1c 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/aqara/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.zigbee.data_types" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" @@ -34,9 +24,6 @@ local PRIVATE_ANTIFREEZE_MODE_TEMPERATURE_SETTING_ID = 0x0279 local PRIVATE_VALVE_RESULT_CALIBRATION_ID = 0x027B local PRIVATE_BATTERY_ENERGY_ID = 0x040A -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.airrtc.agl001" } -} local preference_map = { ["stse.notificationOfValveTest"] = { @@ -82,14 +69,6 @@ local function device_info_changed(driver, device, event, args) end end -local function is_aqara_products(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function supported_thermostat_modes_handler(driver, device, value) device:emit_event(capabilities.thermostatMode.supportedThermostatModes({ @@ -110,17 +89,23 @@ local function device_init(driver, device) do_refresh(driver, device) end +local function emit_component_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component.id, capability.ID, attribute_name) == nil then + device:emit_component_event(component, value) + end +end + local function device_added(driver, device) supported_thermostat_modes_handler(driver, device, nil) device:emit_event(capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 21.0, unit = "C"})) device:emit_event(capabilities.temperatureMeasurement.temperature({value = 27.0, unit = "C"})) device:emit_event(capabilities.thermostatMode.thermostatMode.manual()) - device:emit_event(capabilities.valve.valve.open()) - device:emit_component_event(device.profile.components.ChildLock, capabilities.lock.lock.unlocked()) device:emit_event(capabilities.hardwareFault.hardwareFault.clear()) device:emit_event(valveCalibration.calibrationState.calibrationPending()) device:emit_event(invisibleCapabilities.invisibleCapabilities({""})) device:emit_event(capabilities.battery.battery(100)) + emit_component_event_if_latest_state_missing(device, device.profile.components.main, capabilities.valve, capabilities.valve.valve.NAME, capabilities.valve.valve.open()) + emit_component_event_if_latest_state_missing(device, device.profile.components.ChildLock, capabilities.lock, capabilities.lock.lock.NAME, capabilities.lock.lock.unlocked()) end local function thermostat_alarm_status_handler(driver, device, value, zb_rx) @@ -271,7 +256,7 @@ local aqara_radiator_thermostat_e1_handler = { [capabilities.refresh.commands.refresh.NAME] = do_refresh, } }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } -return aqara_radiator_thermostat_e1_handler \ No newline at end of file +return aqara_radiator_thermostat_e1_handler diff --git a/drivers/SmartThings/zigbee-thermostat/src/danfoss/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/danfoss/can_handle.lua new file mode 100644 index 0000000000..2f9ba97b72 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/danfoss/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_danfoss_thermostat = function(opts, driver, device) + local FINGERPRINTS = require("danfoss.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("danfoss") + end + end + return false +end + +return is_danfoss_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/danfoss/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/danfoss/fingerprints.lua new file mode 100644 index 0000000000..26386b21a9 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/danfoss/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local DANFOSS_THERMOSTAT_FINGERPRINTS = { + { mfr = "Danfoss", model = "eTRV0100" } +} + +return DANFOSS_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-thermostat/src/danfoss/init.lua b/drivers/SmartThings/zigbee-thermostat/src/danfoss/init.lua index c5b181955f..f5b0d5434c 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/danfoss/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/danfoss/init.lua @@ -1,19 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local PowerConfiguration = clusters.PowerConfiguration -local DANFOSS_THERMOSTAT_FINGERPRINTS = { - { mfr = "Danfoss", model = "eTRV0100" } -} -local is_danfoss_thermostat = function(opts, driver, device) - for _, fingerprint in ipairs(DANFOSS_THERMOSTAT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local danfoss_thermostat = { NAME = "Danfoss Thermostat Handler", @@ -27,7 +19,7 @@ local danfoss_thermostat = { lifecycle_handlers = { init = battery_defaults.build_linear_voltage_init(2.4, 3.2) }, - can_handle = is_danfoss_thermostat + can_handle = require("danfoss.can_handle"), } -return danfoss_thermostat \ No newline at end of file +return danfoss_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/fidure/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/fidure/can_handle.lua new file mode 100644 index 0000000000..24ee0ea0e2 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/fidure/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function fidure_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Fidure" and device:get_model() == "A1732R3" then + return true, require("fidure") + end + return false +end + +return fidure_can_handle diff --git a/drivers/SmartThings/zigbee-thermostat/src/fidure/init.lua b/drivers/SmartThings/zigbee-thermostat/src/fidure/init.lua index 8b46c65e73..5d87089aa1 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/fidure/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/fidure/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" @@ -38,9 +28,7 @@ local fidure_thermostat = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Fidure" and device:get_model() == "A1732R3" - end + can_handle = require("fidure.can_handle"), } return fidure_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/init.lua b/drivers/SmartThings/zigbee-thermostat/src/init.lua index 0a5e82350e..b1766e7892 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Zigbee Driver utilities local ZigbeeDriver = require "st.zigbee" @@ -362,20 +352,7 @@ local zigbee_thermostat_driver = { doConfigure = do_configure, added = device_added }, - sub_drivers = { - require("zenwithin"), - require("fidure"), - require("sinope"), - require("stelpro-ki-zigbee-thermostat"), - require("stelpro"), - require("lux-konoz"), - require("leviton"), - require("danfoss"), - require("popp"), - require("vimar"), - require("resideo_korea"), - require("aqara") - }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-thermostat/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-thermostat/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-thermostat/src/leviton/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/leviton/can_handle.lua new file mode 100644 index 0000000000..054aaa821b --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/leviton/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function leviton_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "HAI" and device:get_model() == "65A01-1" then + return true, require("leviton") + end + return false +end + +return leviton_can_handle diff --git a/drivers/SmartThings/zigbee-thermostat/src/leviton/init.lua b/drivers/SmartThings/zigbee-thermostat/src/leviton/init.lua index 9b1150ae25..4b35916229 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/leviton/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/leviton/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local Thermostat = clusters.Thermostat @@ -128,9 +118,7 @@ local leviton_thermostat = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "HAI" and device:get_model() == "65A01-1" - end + can_handle = require("leviton.can_handle"), } return leviton_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/can_handle.lua new file mode 100644 index 0000000000..b7f7e5220f --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_lux_konoz = function(opts, driver, device) + local FINGERPRINTS = require("lux-konoz.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("lux-konoz") + end + end + return false +end + +return is_lux_konoz diff --git a/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/fingerprints.lua new file mode 100644 index 0000000000..01bb0bac8c --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local LUX_KONOZ_THERMOSTAT_FINGERPRINTS = { + { mfr = "LUX", model = "KONOZ" } +} + +return LUX_KONOZ_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/init.lua b/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/init.lua index 815639985c..0609128ac2 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local Thermostat = clusters.Thermostat @@ -19,18 +9,7 @@ local capabilities = require "st.capabilities" local ThermostatMode = capabilities.thermostatMode -local LUX_KONOZ_THERMOSTAT_FINGERPRINTS = { - { mfr = "LUX", model = "KONOZ" } -} -local is_lux_konoz = function(opts, driver, device) - for _, fingerprint in ipairs(LUX_KONOZ_THERMOSTAT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end -- LUX KONOz reports extra ["auto", "emergency heat"] which, actually, aren't supported local supported_thermostat_modes_handler = function(driver, device, supported_modes) @@ -46,7 +25,7 @@ local lux_konoz = { } } }, - can_handle = is_lux_konoz + can_handle = require("lux-konoz.can_handle"), } return lux_konoz diff --git a/drivers/SmartThings/zigbee-thermostat/src/popp/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/popp/can_handle.lua new file mode 100644 index 0000000000..907350cd1c --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/popp/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_popp_thermostat = function(opts, driver, device) + local FINGERPRINTS = require("popp.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("popp") + end + end + return false +end + +return is_popp_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/popp/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/popp/fingerprints.lua new file mode 100644 index 0000000000..16a5cc0942 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/popp/fingerprints.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local POPP_THERMOSTAT_FINGERPRINTS = { + { + mfr = "D5X84YU", + model = "eT093WRO" + }, + { + mfr = "D5X84YU", + model = "eT093WRG" + } +} + +return POPP_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-thermostat/src/popp/init.lua b/drivers/SmartThings/zigbee-thermostat/src/popp/init.lua index bcd5a41dad..f842fe8c2b 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/popp/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/popp/init.lua @@ -1,17 +1,6 @@ --- Copyright 2023 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. --- Zigbee driver utilities +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local data_types = require "st.zigbee.data_types" @@ -34,14 +23,6 @@ local ThermostatMode = capabilities.thermostatMode local TemperatureAlarm = capabilities.temperatureAlarm local Switch = capabilities.switch -local POPP_THERMOSTAT_FINGERPRINTS = { { - mfr = "D5X84YU", - model = "eT093WRO" -}, { - mfr = "D5X84YU", - model = "eT093WRG" -} } - local STORED_HEAT_MODE = "stored_heat_mode" local MFG_CODE = 0x1246 @@ -114,15 +95,6 @@ local PREFERENCE_TABLES = { local SUPPORTED_MODES = { ThermostatMode.thermostatMode.heat.NAME, ThermostatMode.thermostatMode.eco.NAME } -local is_popp_thermostat = function(opts, driver, device) - for _, fingerprint in ipairs(POPP_THERMOSTAT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - -- Helpers -- has member check function @@ -325,7 +297,6 @@ local function device_init(driver, device) -- Add the manufacturer-specific attributes to generate their configure reporting and bind requests for _, config in pairs(cluster_configurations) do device:add_configured_attribute(config) - device:add_monitored_attribute(config) end -- initial set of heating mode local stored_heat_mode = device:get_field(STORED_HEAT_MODE) or 'eco' @@ -426,7 +397,7 @@ local popp_thermostat = { doConfigure = do_configure, infoChanged = info_changed }, - can_handle = is_popp_thermostat + can_handle = require("popp.can_handle"), } return popp_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/resideo_korea/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/resideo_korea/can_handle.lua new file mode 100644 index 0000000000..94eee72e87 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/resideo_korea/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function resideo_korea_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Resideo Korea" and device:get_model() == "DT300ST-M000" then + return true, require("resideo_korea") + end + return false +end + +return resideo_korea_can_handle diff --git a/drivers/SmartThings/zigbee-thermostat/src/resideo_korea/init.lua b/drivers/SmartThings/zigbee-thermostat/src/resideo_korea/init.lua index a1e68ef02a..14221dd223 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/resideo_korea/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/resideo_korea/init.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_lib = require "st.device" local device_management = require "st.zigbee.device_management" @@ -178,9 +168,7 @@ local resideo_thermostat = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Resideo Korea" and device:get_model() == "DT300ST-M000" - end + can_handle = require("resideo_korea.can_handle"), } return resideo_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/sinope/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/sinope/can_handle.lua new file mode 100644 index 0000000000..fdc43dab3b --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/sinope/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_sinope_thermostat = function(opts, driver, device) + local SINOPE_TECHNOLOGIES_MFR_STRING = "Sinope Technologies" + if device:get_manufacturer() == SINOPE_TECHNOLOGIES_MFR_STRING then + return true, require("sinope") + else + return false + end +end + +return is_sinope_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/sinope/init.lua b/drivers/SmartThings/zigbee-thermostat/src/sinope/init.lua index bef41f2b9c..7926b1e0c7 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/sinope/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/sinope/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local clusters = require "st.zigbee.zcl.clusters" @@ -26,8 +16,6 @@ local ThermostatOperatingState = capabilities.thermostatOperatingState local ThermostatHeatingSetpoint = capabilities.thermostatHeatingSetpoint local TemperatureMeasurement = capabilities.temperatureMeasurement -local SINOPE_TECHNOLOGIES_MFR_STRING = "Sinope Technologies" - local SINOPE_CUSTOM_CLUSTER = 0xFF01 local MFR_TIME_FORMAT_ATTRIBUTE = 0x0114 local MFR_AIR_FLOOR_MODE_ATTRIBUTE = 0x0105 @@ -91,13 +79,6 @@ local PREFERENCE_TABLES = { } } -local is_sinope_thermostat = function(opts, driver, device) - if device:get_manufacturer() == SINOPE_TECHNOLOGIES_MFR_STRING then - return true - else - return false - end -end local do_refresh = function(self, device) local attributes = { @@ -176,7 +157,7 @@ local sinope_thermostat = { doConfigure = do_configure, infoChanged = info_changed }, - can_handle = is_sinope_thermostat + can_handle = require("sinope.can_handle"), } return sinope_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/can_handle.lua new file mode 100644 index 0000000000..3f0bebb126 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_stelpro_ki_zigbee_thermostat = function(opts, driver, device) + local FINGERPRINTS = require("stelpro-ki-zigbee-thermostat.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("stelpro-ki-zigbee-thermostat") + end + end + return false +end + +return is_stelpro_ki_zigbee_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/fingerprints.lua new file mode 100644 index 0000000000..2cf1a884d7 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local STELPRO_KI_ZIGBEE_THERMOSTAT_FINGERPRINTS = { + { mfr = "Stelpro", model = "STZB402+" }, + { mfr = "Stelpro", model = "ST218" }, +} + +return STELPRO_KI_ZIGBEE_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/init.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/init.lua index bcc09b271c..7dd1d5f0d2 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" @@ -30,10 +20,6 @@ local ThermostatHeatingSetpoint = capabilities.thermostatHeatingSetpoint local TemperatureMeasurement = capabilities.temperatureMeasurement local TemperatureAlarm = capabilities.temperatureAlarm -local STELPRO_KI_ZIGBEE_THERMOSTAT_FINGERPRINTS = { - { mfr = "Stelpro", model = "STZB402+" }, - { mfr = "Stelpro", model = "ST218" }, -} -- The Groovy DTH stored the raw Celsius values because it was responsible for converting -- to Farenheit if the user's location necessitated. Right now the driver only operates @@ -60,14 +46,6 @@ local THERMOSTAT_MODE_MAP = { [ThermostatSystemMode.EMERGENCY_HEATING] = ThermostatMode.thermostatMode.eco } -local is_stelpro_ki_zigbee_thermostat = function(opts, driver, device) - for _, fingerprint in ipairs(STELPRO_KI_ZIGBEE_THERMOSTAT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function has_member(haystack, needle) for _, value in ipairs(haystack) do @@ -342,7 +320,7 @@ local stelpro_ki_zigbee_thermostat = { added = device_added, doConfigure = do_configure }, - can_handle = is_stelpro_ki_zigbee_thermostat + can_handle = require("stelpro-ki-zigbee-thermostat.can_handle"), } return stelpro_ki_zigbee_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/can_handle.lua new file mode 100644 index 0000000000..117d8ca51d --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_stelpro_thermostat = function(opts, driver, device) + local FINGERPRINTS = require("stelpro.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("stelpro") + end + end + return false +end + +return is_stelpro_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/fingerprints.lua new file mode 100644 index 0000000000..9271bba198 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local STELPRO_THERMOSTAT_FINGERPRINTS = { + { mfr = "Stelpro", model = "MaestroStat" }, + { mfr = "Stelpro", model = "SORB" }, + { mfr = "Stelpro", model = "SonomaStyle" } +} + +return STELPRO_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/init.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/init.lua index 063f423517..d186c46b67 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/stelpro/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -25,20 +15,7 @@ local RX_HEAT_VALUE = 0x7fff local FREEZE_ALRAM_TEMPERATURE = 0 local HEAT_ALRAM_TEMPERATURE = 50 -local STELPRO_THERMOSTAT_FINGERPRINTS = { - { mfr = "Stelpro", model = "MaestroStat" }, - { mfr = "Stelpro", model = "SORB" }, - { mfr = "Stelpro", model = "SonomaStyle" } -} -local is_stelpro_thermostat = function(opts, driver, device) - for _, fingerprint in ipairs(STELPRO_THERMOSTAT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function get_temperature(temperature) return temperature / 100 @@ -128,8 +105,8 @@ local stelpro_thermostat = { added = device_added, infoChanged = info_changed }, - sub_drivers = { require("stelpro.stelpro_sorb"), require("stelpro.stelpro_maestrostat") }, - can_handle = is_stelpro_thermostat + sub_drivers = require("stelpro.sub_drivers"), + can_handle = require("stelpro.can_handle"), } return stelpro_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat.lua deleted file mode 100644 index c4d81b944a..0000000000 --- a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat.lua +++ /dev/null @@ -1,80 +0,0 @@ --- Copyright 2022 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 capabilities = require "st.capabilities" -local clusters = require "st.zigbee.zcl.clusters" -local device_management = require "st.zigbee.device_management" - -local RelativeHumidity = clusters.RelativeHumidity -local Thermostat = clusters.Thermostat -local ThermostatUserInterfaceConfiguration = clusters.ThermostatUserInterfaceConfiguration - -local STELPRO_THERMOSTAT_FINGERPRINTS = { - { mfr = "Stelpro", model = "MaestroStat" }, -} - -local is_stelpro_thermostat = function(opts, driver, device) - for _, fingerprint in ipairs(STELPRO_THERMOSTAT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - -local do_refresh = function(self, device) - local attributes = { - Thermostat.attributes.LocalTemperature, - Thermostat.attributes.PIHeatingDemand, - Thermostat.attributes.OccupiedHeatingSetpoint, - ThermostatUserInterfaceConfiguration.attributes.TemperatureDisplayMode, - ThermostatUserInterfaceConfiguration.attributes.KeypadLockout, - RelativeHumidity.attributes.MeasuredValue - } - for _, attribute in pairs(attributes) do - device:send(attribute:read(device)) - end -end - -local device_added = function(self, device) - device:emit_event(capabilities.temperatureAlarm.temperatureAlarm.cleared()) - do_refresh(self, device) -end - -local function do_configure(self, device) - device:send(device_management.build_bind_request(device, Thermostat.ID, self.environment_info.hub_zigbee_eui)) - device:send(Thermostat.attributes.LocalTemperature:configure_reporting(device, 10, 60, 50)) - device:send(Thermostat.attributes.OccupiedHeatingSetpoint:configure_reporting(device, 1, 600, 50)) - device:send(Thermostat.attributes.PIHeatingDemand:configure_reporting(device, 1, 3600, 1)) - - device:send(ThermostatUserInterfaceConfiguration.attributes.TemperatureDisplayMode:configure_reporting(device, 1, 0, 1)) - device:send(ThermostatUserInterfaceConfiguration.attributes.KeypadLockout:configure_reporting(device, 1, 0, 1)) - device:send(RelativeHumidity.attributes.MeasuredValue:configure_reporting(device, 10, 300, 1)) -end - -local stelpro_maestro_othermostat = { - NAME = "Stelpro Maestro Thermostat Handler", - capability_handlers = { - [capabilities.refresh.ID] = { - [capabilities.refresh.commands.refresh.NAME] = do_refresh, - } - }, - lifecycle_handlers = { - added = device_added, - doConfigure = do_configure - }, - can_handle = is_stelpro_thermostat -} - -return stelpro_maestro_othermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/can_handle.lua new file mode 100644 index 0000000000..f9410f80ff --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_stelpro_thermostat = function(opts, driver, device) + local FINGERPRINTS = require "stelpro.stelpro_maestrostat.fingerprints" + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("stelpro.stelpro_maestrostat") + end + end + return false +end + +return is_stelpro_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/fingerprints.lua new file mode 100644 index 0000000000..600fe09180 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/fingerprints.lua @@ -0,0 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "Stelpro", model = "MaestroStat" }, +} diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/init.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/init.lua new file mode 100644 index 0000000000..fecb34666a --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/init.lua @@ -0,0 +1,56 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local device_management = require "st.zigbee.device_management" + +local RelativeHumidity = clusters.RelativeHumidity +local Thermostat = clusters.Thermostat +local ThermostatUserInterfaceConfiguration = clusters.ThermostatUserInterfaceConfiguration + +local do_refresh = function(self, device) + local attributes = { + Thermostat.attributes.LocalTemperature, + Thermostat.attributes.PIHeatingDemand, + Thermostat.attributes.OccupiedHeatingSetpoint, + ThermostatUserInterfaceConfiguration.attributes.TemperatureDisplayMode, + ThermostatUserInterfaceConfiguration.attributes.KeypadLockout, + RelativeHumidity.attributes.MeasuredValue + } + for _, attribute in pairs(attributes) do + device:send(attribute:read(device)) + end +end + +local device_added = function(self, device) + device:emit_event(capabilities.temperatureAlarm.temperatureAlarm.cleared()) + do_refresh(self, device) +end + +local function do_configure(self, device) + device:send(device_management.build_bind_request(device, Thermostat.ID, self.environment_info.hub_zigbee_eui)) + device:send(Thermostat.attributes.LocalTemperature:configure_reporting(device, 10, 60, 50)) + device:send(Thermostat.attributes.OccupiedHeatingSetpoint:configure_reporting(device, 1, 600, 50)) + device:send(Thermostat.attributes.PIHeatingDemand:configure_reporting(device, 1, 3600, 1)) + + device:send(ThermostatUserInterfaceConfiguration.attributes.TemperatureDisplayMode:configure_reporting(device, 1, 0, 1)) + device:send(ThermostatUserInterfaceConfiguration.attributes.KeypadLockout:configure_reporting(device, 1, 0, 1)) + device:send(RelativeHumidity.attributes.MeasuredValue:configure_reporting(device, 10, 300, 1)) +end + +local stelpro_maestro_othermostat = { + NAME = "Stelpro Maestro Thermostat Handler", + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh, + } + }, + lifecycle_handlers = { + added = device_added, + doConfigure = do_configure + }, + can_handle = require("stelpro.stelpro_maestrostat.can_handle") +} + +return stelpro_maestro_othermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb.lua deleted file mode 100644 index 504d4351e0..0000000000 --- a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb.lua +++ /dev/null @@ -1,81 +0,0 @@ --- Copyright 2022 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 capabilities = require "st.capabilities" -local clusters = require "st.zigbee.zcl.clusters" -local device_management = require "st.zigbee.device_management" - -local RelativeHumidity = clusters.RelativeHumidity -local Thermostat = clusters.Thermostat -local ThermostatUserInterfaceConfiguration = clusters.ThermostatUserInterfaceConfiguration - -local STELPRO_THERMOSTAT_FINGERPRINTS = { - { mfr = "Stelpro", model = "SORB" }, - { mfr = "Stelpro", model = "SonomaStyle" } -} - -local is_stelpro_sorb_thermostat = function(opts, driver, device) - for _, fingerprint in ipairs(STELPRO_THERMOSTAT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - -local do_refresh = function(self, device) - local attributes = { - Thermostat.attributes.LocalTemperature, - Thermostat.attributes.PIHeatingDemand, - Thermostat.attributes.OccupiedHeatingSetpoint, - ThermostatUserInterfaceConfiguration.attributes.TemperatureDisplayMode, - ThermostatUserInterfaceConfiguration.attributes.KeypadLockout, - RelativeHumidity.attributes.MeasuredValue - } - for _, attribute in pairs(attributes) do - device:send(attribute:read(device)) - end -end - -local device_added = function(self, device) - device:emit_event(capabilities.temperatureAlarm.temperatureAlarm.cleared()) - do_refresh(self, device) -end - -local function do_configure(self, device) - device:send(device_management.build_bind_request(device, Thermostat.ID, self.environment_info.hub_zigbee_eui)) - device:send(Thermostat.attributes.LocalTemperature:configure_reporting(device, 10, 60, 50)) - device:send(Thermostat.attributes.OccupiedHeatingSetpoint:configure_reporting(device, 1, 600, 50)) - device:send(Thermostat.attributes.PIHeatingDemand:configure_reporting(device, 1, 3600, 1)) - - device:send(ThermostatUserInterfaceConfiguration.attributes.TemperatureDisplayMode:configure_reporting(device, 1, 0, 1)) - device:send(ThermostatUserInterfaceConfiguration.attributes.KeypadLockout:configure_reporting(device, 1, 0, 1)) - device:send(RelativeHumidity.attributes.MeasuredValue:configure_reporting(device, 10, 300, 1)) -end - -local stelpro_sorb_thermostat = { - NAME = "Stelpro SORB SonomaStyle Thermostat Handler", - capability_handlers = { - [capabilities.refresh.ID] = { - [capabilities.refresh.commands.refresh.NAME] = do_refresh, - } - }, - lifecycle_handlers = { - added = device_added, - doConfigure = do_configure - }, - can_handle = is_stelpro_sorb_thermostat -} - -return stelpro_sorb_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/can_handle.lua new file mode 100644 index 0000000000..d9e99178e5 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_stelpro_sorb_thermostat = function(opts, driver, device) + local FINGERPRINTS = require("stelpro.stelpro_sorb.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("stelpro.stelpro_sorb") + end + end + return false +end + +return is_stelpro_sorb_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/fingerprints.lua new file mode 100644 index 0000000000..eb8731f6dc --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/fingerprints.lua @@ -0,0 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "Stelpro", model = "SORB" }, + { mfr = "Stelpro", model = "SonomaStyle" } +} diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/init.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/init.lua new file mode 100644 index 0000000000..39c370960f --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/init.lua @@ -0,0 +1,56 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local device_management = require "st.zigbee.device_management" + +local RelativeHumidity = clusters.RelativeHumidity +local Thermostat = clusters.Thermostat +local ThermostatUserInterfaceConfiguration = clusters.ThermostatUserInterfaceConfiguration + +local do_refresh = function(self, device) + local attributes = { + Thermostat.attributes.LocalTemperature, + Thermostat.attributes.PIHeatingDemand, + Thermostat.attributes.OccupiedHeatingSetpoint, + ThermostatUserInterfaceConfiguration.attributes.TemperatureDisplayMode, + ThermostatUserInterfaceConfiguration.attributes.KeypadLockout, + RelativeHumidity.attributes.MeasuredValue + } + for _, attribute in pairs(attributes) do + device:send(attribute:read(device)) + end +end + +local device_added = function(self, device) + device:emit_event(capabilities.temperatureAlarm.temperatureAlarm.cleared()) + do_refresh(self, device) +end + +local function do_configure(self, device) + device:send(device_management.build_bind_request(device, Thermostat.ID, self.environment_info.hub_zigbee_eui)) + device:send(Thermostat.attributes.LocalTemperature:configure_reporting(device, 10, 60, 50)) + device:send(Thermostat.attributes.OccupiedHeatingSetpoint:configure_reporting(device, 1, 600, 50)) + device:send(Thermostat.attributes.PIHeatingDemand:configure_reporting(device, 1, 3600, 1)) + + device:send(ThermostatUserInterfaceConfiguration.attributes.TemperatureDisplayMode:configure_reporting(device, 1, 0, 1)) + device:send(ThermostatUserInterfaceConfiguration.attributes.KeypadLockout:configure_reporting(device, 1, 0, 1)) + device:send(RelativeHumidity.attributes.MeasuredValue:configure_reporting(device, 10, 300, 1)) +end + +local stelpro_sorb_thermostat = { + NAME = "Stelpro SORB SonomaStyle Thermostat Handler", + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh, + } + }, + lifecycle_handlers = { + added = device_added, + doConfigure = do_configure + }, + can_handle = require("stelpro.stelpro_sorb.can_handle") +} + +return stelpro_sorb_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/sub_drivers.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/sub_drivers.lua new file mode 100644 index 0000000000..aa48c7eda6 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" + +return { + lazy_load_if_possible("stelpro.stelpro_sorb"), + lazy_load_if_possible("stelpro.stelpro_maestrostat") +} diff --git a/drivers/SmartThings/zigbee-thermostat/src/sub_drivers.lua b/drivers/SmartThings/zigbee-thermostat/src/sub_drivers.lua new file mode 100644 index 0000000000..7f62589c24 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/sub_drivers.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("zenwithin"), + lazy_load_if_possible("fidure"), + lazy_load_if_possible("sinope"), + lazy_load_if_possible("stelpro-ki-zigbee-thermostat"), + lazy_load_if_possible("stelpro"), + lazy_load_if_possible("lux-konoz"), + lazy_load_if_possible("leviton"), + lazy_load_if_possible("danfoss"), + lazy_load_if_possible("popp"), + lazy_load_if_possible("vimar"), + lazy_load_if_possible("resideo_korea"), + lazy_load_if_possible("aqara"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua index 355c48b240..e026edb5f8 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local zigbee_test_utils = require "integration_test.zigbee_test_utils" local cluster_base = require "st.zigbee.cluster_base" local clusters = require "st.zigbee.zcl.clusters" @@ -73,46 +62,8 @@ end test.set_test_init_function(test_init) --- test.register_coroutine_test( --- "Handle added lifecycle", --- function() --- test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", --- capabilities.thermostatMode.supportedThermostatModes({ --- capabilities.thermostatMode.thermostatMode.manual.NAME, --- capabilities.thermostatMode.thermostatMode.antifreezing.NAME --- }, { visibility = { displayed = false } })) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 21.0, unit = "C"})) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({value = 27.0, unit = "C"})) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.manual()) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", capabilities.valve.valve.open()) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("ChildLock", capabilities.lock.lock.unlocked()) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", capabilities.hardwareFault.hardwareFault.clear()) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", valveCalibration.calibrationState.calibrationPending()) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", invisibleCapabilities.invisibleCapabilities({""})) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", capabilities.battery.battery(100)) --- ) --- end --- ) + + test.register_coroutine_test( @@ -151,6 +102,16 @@ test.register_coroutine_test( }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.hardwareFault.hardwareFault.detected())) + + local attr_report_data_1 = { + { PRIVATE_THERMOSTAT_ALARM_INFORMATION_ID, data_types.Uint32.ID, 0x00000000 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_1, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.hardwareFault.hardwareFault.clear())) end ) @@ -166,6 +127,26 @@ test.register_coroutine_test( }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", valveCalibration.calibrationState.calibrationSuccess())) + + local attr_report_data_1 = { + { PRIVATE_VALVE_RESULT_CALIBRATION_ID, data_types.Uint8.ID, 0x00 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_1, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + valveCalibration.calibrationState.calibrationPending())) + + local attr_report_data_2 = { + { PRIVATE_VALVE_RESULT_CALIBRATION_ID, data_types.Uint8.ID, 0x02 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_2, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + valveCalibration.calibrationState.calibrationFailure())) end ) @@ -183,6 +164,18 @@ test.register_coroutine_test( capabilities.thermostatMode.thermostatMode.manual())) test.socket.capability:__expect_send(mock_device:generate_test_message("main", invisibleCapabilities.invisibleCapabilities({""}))) + + local attr_report_data_1 = { + { PRIVATE_THERMOSTAT_OPERATING_MODE_ATTRIBUTE_ID, data_types.Uint8.ID, 0x02 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_1, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatMode.thermostatMode.antifreezing())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + invisibleCapabilities.invisibleCapabilities({"thermostatHeatingSetpoint"}))) end ) @@ -200,6 +193,54 @@ test.register_coroutine_test( capabilities.valve.valve.closed())) test.socket.capability:__expect_send(mock_device:generate_test_message("main", invisibleCapabilities.invisibleCapabilities({"thermostatHeatingSetpoint","stse.valveCalibration","thermostatMode","lock"}))) + + local attr_report_data_1 = { + { PRIVATE_THERMOSTAT_OPERATING_MODE_ATTRIBUTE_ID, data_types.Uint8.ID, 0x00 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_1, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatMode.thermostatMode.manual())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + invisibleCapabilities.invisibleCapabilities({""}))) + + local attr_report_data_2 = { + { PRIVATE_VALVE_SWITCH_ATTRIBUTE_ID, data_types.Uint8.ID, 0x01 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_2, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.valve.valve.open())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + invisibleCapabilities.invisibleCapabilities({""}))) + + local attr_report_data_3 = { + { PRIVATE_THERMOSTAT_OPERATING_MODE_ATTRIBUTE_ID, data_types.Uint8.ID, 0x02 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_3, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatMode.thermostatMode.antifreezing())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + invisibleCapabilities.invisibleCapabilities({"thermostatHeatingSetpoint"}))) + + local attr_report_data_4 = { + { PRIVATE_VALVE_SWITCH_ATTRIBUTE_ID, data_types.Uint8.ID, 0x01 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_4, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.valve.valve.open())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + invisibleCapabilities.invisibleCapabilities({"thermostatHeatingSetpoint"}))) end ) @@ -215,6 +256,16 @@ test.register_coroutine_test( }) test.socket.capability:__expect_send(mock_device:generate_test_message("ChildLock", capabilities.lock.lock.unlocked())) + + local attr_report_data_1 = { + { PRIVATE_CHILD_LOCK_ID, data_types.Uint8.ID, 0x01 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_1, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("ChildLock", + capabilities.lock.lock.locked())) end ) @@ -312,4 +363,76 @@ test.register_coroutine_test( end ) --]] +test.register_coroutine_test( + "Handle added lifecycle", + function() + -- The initial valve and lock event should be send during the device's first time onboarding + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.thermostatMode.supportedThermostatModes({ + capabilities.thermostatMode.thermostatMode.manual.NAME, + capabilities.thermostatMode.thermostatMode.antifreezing.NAME + }, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 21.0, unit = "C"})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({value = 27.0, unit = "C"})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.manual()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.hardwareFault.hardwareFault.clear()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", valveCalibration.calibrationState.calibrationPending()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", invisibleCapabilities.invisibleCapabilities({""})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.battery.battery(100)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.valve.valve.open()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("ChildLock", capabilities.lock.lock.unlocked()) + ) + -- Avoid sending the initial open and lock event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.thermostatMode.supportedThermostatModes({ + capabilities.thermostatMode.thermostatMode.manual.NAME, + capabilities.thermostatMode.thermostatMode.antifreezing.NAME + }, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 21.0, unit = "C"})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({value = 27.0, unit = "C"})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.manual()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.hardwareFault.hardwareFault.clear()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", valveCalibration.calibrationState.calibrationPending()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", invisibleCapabilities.invisibleCapabilities({""})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.battery.battery(100)) + ) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_centralite_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_centralite_thermostat.lua index ce735209c4..206333ffa7 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_centralite_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_centralite_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_danfoss_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_danfoss_thermostat.lua index 51782ec8e5..98727e563e 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_danfoss_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_danfoss_thermostat.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -104,4 +107,4 @@ test.register_message_test( } ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_fidure_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_fidure_thermostat.lua index 6ceb8fdc52..1f7a03f049 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_fidure_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_fidure_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_leviton_rc.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_leviton_rc.lua index 6bc2cfda5c..6e6cdb0305 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_leviton_rc.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_leviton_rc.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" @@ -192,4 +181,36 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Handle added lifecycle", + function() + -- The initial valve and lock event should be send during the device's first time onboarding + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.thermostatMode.supportedThermostatModes({ + capabilities.thermostatMode.thermostatMode.auto.NAME, + capabilities.thermostatMode.thermostatMode.cool.NAME, + capabilities.thermostatMode.thermostatMode.heat.NAME, + capabilities.thermostatMode.thermostatMode.emergency_heat.NAME + }, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.thermostatFanMode.supportedThermostatFanModes({ + capabilities.thermostatFanMode.thermostatFanMode.auto.NAME, + capabilities.thermostatFanMode.thermostatFanMode.on.NAME, + capabilities.thermostatFanMode.thermostatFanMode.circulate.NAME + }, { visibility = { displayed = false } })) + ) + test.socket.zigbee:__expect_send( { mock_device.id, FanControl.attributes.FanMode:read(mock_device):to_endpoint(ENDPOINT) }) + test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.SystemMode:read(mock_device):to_endpoint(ENDPOINT) }) + test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.ControlSequenceOfOperation:read(mock_device):to_endpoint(ENDPOINT) }) + test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.OccupiedCoolingSetpoint:read(mock_device):to_endpoint(ENDPOINT) }) + test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device):to_endpoint(ENDPOINT) }) + test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.LocalTemperature:read(mock_device):to_endpoint(ENDPOINT) }) + end +) + + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_popp_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_popp_thermostat.lua index e3d30b04c7..ecff7883da 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_popp_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_popp_thermostat.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + require "integration_test" -- Mock out globals local test = require "integration_test" @@ -361,5 +364,95 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Setting the thermostat mode should generate the appropriate messages", + function () + test.socket.capability:__queue_receive({ mock_device.id, { component = "main", capability = capabilities.thermostatMode.ID, command = "setThermostatMode", args = {"eco"} } }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.build_manufacturer_specific_command(mock_device, Thermostat.ID, THERMOSTAT_SETPOINT_CMD_ID, MFG_CODE, string.char(0x00, (math.floor(21.0 * 100) & 0xFF), (math.floor(21.0 * 100) >> 8))) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.eco())) + end +) + +test.register_coroutine_test( + "init and doConfigure lifecycles should be handled properly", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.eco())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.on())) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ThermostatUIConfig.attributes.KeypadLockout:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Thermostat.attributes.LocalTemperature:read(mock_device) + } + ) + test.socket.zigbee:__expect_send({mock_device.id, cluster_base.read_manufacturer_specific_attribute( + mock_device, + Thermostat.ID, + ETRV_WINDOW_OPEN_DETECTION_ATTR_ID, + MFG_CODE + )}) + test.socket.zigbee:__expect_send({mock_device.id, cluster_base.read_manufacturer_specific_attribute( + mock_device, + Thermostat.ID, + EXTERNAL_WINDOW_OPEN_DETECTION, + MFG_CODE + )}) + end +) + +test.register_coroutine_test( + "Device reported Thermostat WINDOW_OPEN_DETECTION_ATTR_ID attribute", + function() + local attr_report_data = { + { ETRV_WINDOW_OPEN_DETECTION_ATTR_ID, data_types.Uint8.ID, 1 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Thermostat.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared())) + end +) + +test.register_coroutine_test( + "Device reported Thermostat WINDOW_OPEN_DETECTION_ATTR_ID attribute", + function() + local attr_report_data = { + { EXTERNAL_WINDOW_OPEN_DETECTION, data_types.Uint8.ID, 1 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Thermostat.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) + end +) test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_resideo_dt300st_m000.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_resideo_dt300st_m000.lua index 3eeaa383f7..4c7f4bdcaf 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_resideo_dt300st_m000.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_resideo_dt300st_m000.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" @@ -713,5 +702,19 @@ test.register_coroutine_test("Setting the thermostat mode to heat should generat Thermostat.attributes.SystemMode.HEAT)}) end) +test.register_coroutine_test("ThermostatRunningState reporting shoulb create the appropriate events", function() + test.socket.zigbee:__queue_receive({mock_device.id, + Thermostat.attributes.ThermostatRunningState:build_test_attr_report(mock_device, 0x0001)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatOperatingState.thermostatOperatingState({value="heating"}))) + test.socket.zigbee:__queue_receive({mock_device.id, + Thermostat.attributes.ThermostatRunningState:build_test_attr_report(mock_device, 0x0002)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatOperatingState.thermostatOperatingState({value="cooling"}))) + test.socket.zigbee:__queue_receive({mock_device.id, + Thermostat.attributes.ThermostatRunningState:build_test_attr_report(mock_device, 0x0004)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatOperatingState.thermostatOperatingState({value="fan only"}))) +end) test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1300_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1300_thermostat.lua index 2411d7638c..6805e6c604 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1300_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1300_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1400_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1400_thermostat.lua index 88810a3c80..5ca78c3675 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1400_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1400_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_thermostat.lua index 9a5ba8aed5..74cc7a2115 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" @@ -185,4 +174,31 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Refresh should send read requests for all necessary attributes", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.LocalTemperature:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.PIHeatingDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.SystemMode:read(mock_device) + }) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_ki_zigbee_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_ki_zigbee_thermostat.lua index dd5e425549..4ff17803fa 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_ki_zigbee_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_ki_zigbee_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -481,4 +470,98 @@ test.register_message_test( } ) +test.register_coroutine_test("Setting the heating setpoint should generate the appropriate messages", function() + test.socket.capability:__queue_receive({mock_device.id, { + component = "main", + capability = capabilities.thermostatHeatingSetpoint.ID, + command = "setHeatingSetpoint", + args = {21} + }}) + test.socket.zigbee:__expect_send({mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 2100)}) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.PIHeatingDemand:read(mock_device) + }) +end) + +test.register_coroutine_test( + "Setting thermostat mode to eco should generate correct zigbee messages", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "thermostatMode", component = "main", command = "setThermostatMode", args = {"eco"}} + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.SystemMode:write(mock_device, ThermostatSystemMode.HEAT) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, Thermostat.ID, MFR_SETPOINT_MODE_ATTTRIBUTE, MFG_CODE, data_types.Enum8, 0x05) + }) + test.wait_for_events() + + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.SystemMode:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Thermostat.ID, MFR_SETPOINT_MODE_ATTTRIBUTE, MFG_CODE) + }) + end +) +test.register_coroutine_test( + "LocalTemperature handler should request PIHeatingDemand when setpoint > temperature", + function() + local RAW_SETPOINT_FIELD = "raw_setpoint" + mock_device:set_field(RAW_SETPOINT_FIELD, 3000, { persist = true }) + + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.LocalTemperature:build_test_attr_report(mock_device, 2000) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared()) + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.PIHeatingDemand:read(mock_device) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 20.0, unit = "C" })) + ) + end +) + +test.register_coroutine_test( + "Setting an unsupported thermostat mode should re-emit the current mode", + function() + -- Establish a known current mode state + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.SystemMode:build_test_attr_report(mock_device, ThermostatSystemMode.OFF) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", ThermostatMode.thermostatMode.off()) + ) + test.wait_for_events() + + -- "cool" is not in SUPPORTED_MODES for stelpro-ki; the driver re-emits the current mode + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "thermostatMode", component = "main", command = "setThermostatMode", args = { "cool" } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", ThermostatMode.thermostatMode.off()) + ) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_thermostat.lua index 312a8a5890..3380856f1c 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" @@ -474,4 +463,39 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Handle added lifecycle", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared()) + ) + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.LocalTemperature:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.PIHeatingDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ThermostatUserInterfaceConfiguration.attributes.TemperatureDisplayMode:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ThermostatUserInterfaceConfiguration.attributes.KeypadLockout:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + RelativeHumidity.attributes.MeasuredValue:read(mock_device) + }) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_vimar_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_vimar_thermostat.lua index d41d4f23aa..7b2a0cc8f6 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_vimar_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_vimar_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua index bd16d17a62..0886a7cd7f 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" @@ -461,4 +450,44 @@ test.register_message_test( } ) +test.register_coroutine_test( + "Setting cooling setpoint while in heat mode should re-emit the current cooling setpoint", + function() + -- Put device in heat mode + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.SystemMode:build_test_attr_report(mock_device, 0x04) -- HEAT + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.heat()) + ) + test.wait_for_events() + + -- Set a known cooling setpoint state + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.OccupiedCoolingSetpoint:build_test_attr_report(mock_device, 2500) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpoint({ value = 25.0, unit = "C" })) + ) + test.wait_for_events() + + -- Try to set cooling setpoint while in heat mode; driver defers and re-emits current + test.timer.__create_and_queue_test_time_advance_timer(10, "oneshot") + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "thermostatCoolingSetpoint", component = "main", command = "setCoolingSetpoint", args = { 27 } } + }) + test.wait_for_events() + + test.mock_time.advance_time(10) + -- After the delay, update_device_setpoint re-emits the unchanged cooling setpoint (25.0 C) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpoint({ value = 25.0, unit = "C" })) + ) + test.wait_for_events() + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat.lua index bdb2b681cb..380eacc1ce 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -120,7 +109,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) - } + }, } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/vimar/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/vimar/can_handle.lua new file mode 100644 index 0000000000..3aa0478f46 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/vimar/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local vimar_thermostat_can_handle = function(opts, driver, device) + local VIMAR_THERMOSTAT_FINGERPRINT = { + mfr = "Vimar", + model = "WheelThermostat_v1.0" + } + + if device:get_manufacturer() == VIMAR_THERMOSTAT_FINGERPRINT.mfr and + device:get_model() == VIMAR_THERMOSTAT_FINGERPRINT.model then + return true, require("vimar") + end + return false +end + +return vimar_thermostat_can_handle diff --git a/drivers/SmartThings/zigbee-thermostat/src/vimar/init.lua b/drivers/SmartThings/zigbee-thermostat/src/vimar/init.lua index b4070cf569..7ad274449f 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/vimar/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/vimar/init.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local clusters = require "st.zigbee.zcl.clusters" @@ -38,11 +28,6 @@ local VIMAR_THERMOSTAT_MODE_MAP = { [ThermostatSystemMode.HEAT] = ThermostatMode.thermostatMode.heat, } -local VIMAR_THERMOSTAT_FINGERPRINT = { - mfr = "Vimar", - model = "WheelThermostat_v1.0" -} - -- NOTE: This is a global variable to use in order to save the current thermostat profile local VIMAR_CURRENT_PROFILE = "_vimarThermostatCurrentProfile" @@ -50,10 +35,6 @@ local VIMAR_THERMOSTAT_HEATING_PROFILE = "thermostat-fanless-heating-no-fw" local VIMAR_THERMOSTAT_COOLING_PROFILE = "thermostat-fanless-cooling-no-fw" -local vimar_thermostat_can_handle = function(opts, driver, device) - return device:get_manufacturer() == VIMAR_THERMOSTAT_FINGERPRINT.mfr and - device:get_model() == VIMAR_THERMOSTAT_FINGERPRINT.model -end local vimar_thermostat_supported_modes_handler = function(driver, device, supported_modes) device:emit_event( @@ -182,7 +163,7 @@ local vimar_thermostat_subdriver = { } }, doConfigure = vimar_thermostat_do_configure, - can_handle = vimar_thermostat_can_handle + can_handle = require("vimar.can_handle"), } return vimar_thermostat_subdriver diff --git a/drivers/SmartThings/zigbee-thermostat/src/zenwithin/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/zenwithin/can_handle.lua new file mode 100644 index 0000000000..818764c88a --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/zenwithin/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function zenwithin_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Zen Within" and device:get_model() == "Zen-01" then + return true, require("zenwithin") + end + return false +end + +return zenwithin_can_handle diff --git a/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua b/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua index e4b11de8be..1b71db2609 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local utils = require "st.utils" @@ -213,9 +203,7 @@ local zenwithin_thermostat = { infoChanged = info_changed, init = battery_defaults.build_linear_voltage_init(BAT_MIN, BAT_MAX) }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Zen Within" and device:get_model() == "Zen-01" - end + can_handle = require("zenwithin.can_handle"), } return zenwithin_thermostat diff --git a/drivers/SmartThings/zigbee-valve/src/ezex/can_handle.lua b/drivers/SmartThings/zigbee-valve/src/ezex/can_handle.lua new file mode 100644 index 0000000000..f16f04858c --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/ezex/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function ezex_can_handle(opts, driver, device, ...) + local clusters = require "st.zigbee.zcl.clusters" + if device:get_model() == "E253-KR0B0ZX-HA" and not device:supports_server_cluster(clusters.PowerConfiguration.ID) then + return true, require("ezex") + end + return false +end + +return ezex_can_handle diff --git a/drivers/SmartThings/zigbee-valve/src/ezex/init.lua b/drivers/SmartThings/zigbee-valve/src/ezex/init.lua index 53eddb5a1c..c32fe60bac 100644 --- a/drivers/SmartThings/zigbee-valve/src/ezex/init.lua +++ b/drivers/SmartThings/zigbee-valve/src/ezex/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -56,7 +46,6 @@ end local function device_init(driver, device) for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end @@ -72,9 +61,7 @@ local ezex_valve = { lifecycle_handlers = { init = device_init }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "E253-KR0B0ZX-HA" and not device:supports_server_cluster(clusters.PowerConfiguration.ID) - end + can_handle = require("ezex.can_handle"), } return ezex_valve diff --git a/drivers/SmartThings/zigbee-valve/src/init.lua b/drivers/SmartThings/zigbee-valve/src/init.lua index 6d355de8ec..1840b55be0 100644 --- a/drivers/SmartThings/zigbee-valve/src/init.lua +++ b/drivers/SmartThings/zigbee-valve/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" @@ -51,10 +41,7 @@ local zigbee_valve_driver_template = { lifecycle_handlers = { added = device_added }, - sub_drivers = { - require("sinope"), - require("ezex") - }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-valve/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-valve/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-valve/src/sinope/can_handle.lua b/drivers/SmartThings/zigbee-valve/src/sinope/can_handle.lua new file mode 100644 index 0000000000..f533d120e0 --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/sinope/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function sinope_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Sinope Technologies" then + return true, require("sinope") + end + return false +end + +return sinope_can_handle diff --git a/drivers/SmartThings/zigbee-valve/src/sinope/init.lua b/drivers/SmartThings/zigbee-valve/src/sinope/init.lua index 6a0075cd33..ab3786511c 100644 --- a/drivers/SmartThings/zigbee-valve/src/sinope/init.lua +++ b/drivers/SmartThings/zigbee-valve/src/sinope/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" @@ -59,9 +49,7 @@ local sinope_valve = { lifecycle_handlers = { init = device_init }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Sinope Technologies" - end + can_handle = require("sinope.can_handle"), } return sinope_valve diff --git a/drivers/SmartThings/zigbee-valve/src/sub_drivers.lua b/drivers/SmartThings/zigbee-valve/src/sub_drivers.lua new file mode 100644 index 0000000000..258579c7fb --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("sinope"), + lazy_load_if_possible("ezex"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua b/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua index eb96363d99..083d522bb3 100644 --- a/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua +++ b/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -92,6 +82,22 @@ test.register_message_test( } ) +test.register_message_test( + "Battery percentage report - not low should return 50%", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0000) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(50)) + } + } +) + test.register_message_test( "PowerSource(unknown) reporting should be handled", { diff --git a/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua b/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua index 344c6b0814..d30c60ddc6 100644 --- a/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua +++ b/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -141,4 +131,12 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Battery voltage above max should clamp to 100 percent", + function() + test.socket.zigbee:__queue_receive({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 65) }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(100)) ) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-valve/src/test/test_zigbee_valve.lua b/drivers/SmartThings/zigbee-valve/src/test/test_zigbee_valve.lua index baa4252bd2..5b4756f207 100644 --- a/drivers/SmartThings/zigbee-valve/src/test/test_zigbee_valve.lua +++ b/drivers/SmartThings/zigbee-valve/src/test/test_zigbee_valve.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/can_handle.lua new file mode 100644 index 0000000000..e4453597ed --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_aqara_products(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..f0ba630b6f --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.flood.agl02" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/init.lua index 7ad385a13a..595a10f64e 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" @@ -10,9 +13,6 @@ local MFG_CODE = 0x115F local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.flood.agl02" } -} local CONFIGURATIONS = { { @@ -25,14 +25,6 @@ local CONFIGURATIONS = { } } -local function is_aqara_products(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function device_added(driver, device) device:emit_event(capabilities.waterSensor.water.dry()) @@ -47,7 +39,6 @@ local function device_init(driver, device) for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end @@ -57,7 +48,7 @@ local aqara_contact_handler = { init = device_init, added = device_added }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } return aqara_contact_handler diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/configurations.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/configurations.lua index a5086d979e..92b7ef226b 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/configurations.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/configurations.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local PowerConfiguration = clusters.PowerConfiguration diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/can_handle.lua new file mode 100644 index 0000000000..e7c5dd9c23 --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function frient_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "frient A/S" and device:get_model() == "FLSZB-110" then + return true, require("frient") + end + return false +end + +return frient_can_handle diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/init.lua index b85b58eb7b..4357eb91b2 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local TemperatureMeasurement = clusters.TemperatureMeasurement @@ -26,7 +16,6 @@ local function device_init(driver, device) battery_defaults.build_linear_voltage_init(config.minV, config.maxV)(driver, device) elseif (config.cluster) then device:add_configured_attribute(config) - device:add_monitored_attribute(config) end end end @@ -44,9 +33,7 @@ local frient_water_leak_sensor = { init = device_init, doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "frient A/S" and device:get_model() == "FLSZB-110" - end + can_handle = require("frient.can_handle"), } return frient_water_leak_sensor diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua index 994d9d40e4..4ded4e195c 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -59,7 +49,6 @@ local function device_init(driver, device) battery_defaults.enable_battery_voltage_table(device, config.battery_voltage_table) elseif (config.cluster) then device:add_configured_attribute(config) - device:add_monitored_attribute(config) end end end @@ -90,15 +79,7 @@ local zigbee_water_driver_template = { added = added_handler }, ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, - sub_drivers = { - require("aqara"), - require("zigbee-water-freeze"), - require("leaksmart"), - require("frient"), - require("thirdreality"), - require("sengled"), - require("sinope") - }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/leaksmart/can_handle.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/leaksmart/can_handle.lua new file mode 100644 index 0000000000..67c6f425ca --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/leaksmart/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function leaksmart_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "WAXMAN" and device:get_model() == "leakSMART Water Sensor V2" then + return true, require("leaksmart") + end + return false +end + +return leaksmart_can_handle diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/leaksmart/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/leaksmart/init.lua index 010379df0f..a8ab5212b5 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/leaksmart/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/leaksmart/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -58,9 +48,7 @@ local leaksmart_water_sensor = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "WAXMAN" and device:get_model() == "leakSMART Water Sensor V2" - end + can_handle = require("leaksmart.can_handle"), } return leaksmart_water_sensor diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/can_handle.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/can_handle.lua new file mode 100644 index 0000000000..68d774fb8f --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_sengled_products = function(opts, driver, device, ...) + local FINGERPRINTS = require("sengled.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("sengled") + end + end + return false +end + +return is_sengled_products diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/fingerprints.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/fingerprints.lua new file mode 100644 index 0000000000..2c3f527da6 --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "sengled", model = "E1L-G7K" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/init.lua index f34cd8181f..0010073033 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/init.lua @@ -1,12 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local IASZone = clusters.IASZone local PowerConfiguration = clusters.PowerConfiguration -local FINGERPRINTS = { - { mfr = "sengled", model = "E1L-G7K" } -} local CONFIGURATIONS = { { @@ -27,21 +27,12 @@ local CONFIGURATIONS = { } } -local is_sengled_products = function(opts, driver, device, ...) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function device_init(driver, device) battery_defaults.build_linear_voltage_init(2.6, 3.0)(driver, device) for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end @@ -50,7 +41,7 @@ local sengled_water_leak_sensor_handler = { lifecycle_handlers = { init = device_init }, - can_handle = is_sengled_products + can_handle = require("sengled.can_handle"), } return sengled_water_leak_sensor_handler diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/sinope/can_handle.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/sinope/can_handle.lua new file mode 100644 index 0000000000..757e3ca0db --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/sinope/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_sinope_water_sensor = function(opts, driver, device) + local SINOPE_TECHNOLOGIES_MFR_STRING = "Sinope Technologies" + if device:get_manufacturer() == SINOPE_TECHNOLOGIES_MFR_STRING then + return true, require("sinope") + else + return false + end +end + +return is_sinope_water_sensor diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/sinope/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/sinope/init.lua index 65cc802697..7bf6f8be82 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/sinope/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/sinope/init.lua @@ -1,23 +1,11 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" local IASZone = zcl_clusters.IASZone -local SINOPE_TECHNOLOGIES_MFR_STRING = "Sinope Technologies" - local generate_event_from_zone_status = function(driver, device, zone_status, zb_rx) local event if zone_status:is_alarm1_set() then @@ -39,13 +27,6 @@ local ias_zone_status_change_handler = function(driver, device, zb_rx) generate_event_from_zone_status(driver, device, zb_rx.body.zcl_body.zone_status, zb_rx) end -local is_sinope_water_sensor = function(opts, driver, device) - if device:get_manufacturer() == SINOPE_TECHNOLOGIES_MFR_STRING then - return true - else - return false - end -end local sinope_water_sensor = { NAME = "Sinope Water Leak Sensor", @@ -61,7 +42,7 @@ local sinope_water_sensor = { } } }, - can_handle = is_sinope_water_sensor + can_handle = require("sinope.can_handle"), } -return sinope_water_sensor \ No newline at end of file +return sinope_water_sensor diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/sub_drivers.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/sub_drivers.lua new file mode 100644 index 0000000000..79d572579e --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/sub_drivers.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("aqara"), + lazy_load_if_possible("zigbee-water-freeze"), + lazy_load_if_possible("leaksmart"), + lazy_load_if_possible("frient"), + lazy_load_if_possible("thirdreality"), + lazy_load_if_possible("sengled"), + lazy_load_if_possible("sinope"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_aqara_water_leak_sensor.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_aqara_water_leak_sensor.lua index 495a8455b3..ea80228581 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_aqara_water_leak_sensor.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_aqara_water_leak_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_centralite_water_leak_sensor.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_centralite_water_leak_sensor.lua index fd6403d1cd..b42f3484ae 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_centralite_water_leak_sensor.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_centralite_water_leak_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" @@ -247,6 +237,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_frient_water_leak_sensor.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_frient_water_leak_sensor.lua index 1d3480ef99..56c264a505 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_frient_water_leak_sensor.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_frient_water_leak_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" @@ -253,6 +243,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_leaksmart_water.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_leaksmart_water.lua index 7b20d7c3bb..082c9fb97c 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_leaksmart_water.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_leaksmart_water.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -89,27 +79,15 @@ test.register_coroutine_test( end ) --- test.register_coroutine_test( --- "Health check should check all relevant attributes", --- function() --- test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) --- test.socket.capability:__expect_send( --- { --- mock_device.id, --- { --- capability_id = "waterSensor", component_id = "main", --- attribute_id = "water", state={value="dry"} --- } --- } --- ) --- end, --- { --- test_init = function() --- test.mock_device.add_test_device(mock_device) --- test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") --- end --- } --- ) +test.register_coroutine_test( + "Added lifecycle should emit water dry event", + function() + test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.waterSensor.water.dry()) + ) + end +) test.register_coroutine_test( "Configure should configure all necessary attributes", diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_samjin_water_leak_sensor.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_samjin_water_leak_sensor.lua index 154413af45..3eb7cb201a 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_samjin_water_leak_sensor.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_samjin_water_leak_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" @@ -215,6 +205,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sengled_water_leak_sensor.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sengled_water_leak_sensor.lua index d4ad7bccff..348ab81f0e 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sengled_water_leak_sensor.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sengled_water_leak_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sinope_zigbee_water.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sinope_zigbee_water.lua index 7b85749b29..19e1c7053e 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sinope_zigbee_water.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sinope_zigbee_water.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -135,6 +125,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_smartthings_water_leak_sensor.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_smartthings_water_leak_sensor.lua index 8896cbba36..2b9ee2d3c1 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_smartthings_water_leak_sensor.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_smartthings_water_leak_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" @@ -213,6 +203,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_thirdreality_water_leak_sensor.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_thirdreality_water_leak_sensor.lua index d32f94e255..63be06f0a8 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_thirdreality_water_leak_sensor.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_thirdreality_water_leak_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" @@ -46,6 +36,14 @@ end test.set_test_init_function(test_init) +test.register_coroutine_test( + "Added lifecycle should read ApplicationVersion", + function() + test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) + test.socket.zigbee:__expect_send({mock_device.id, Basic.attributes.ApplicationVersion:read(mock_device)}) + end +) + test.register_coroutine_test( "Refresh necessary attributes", function() diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water.lua index 8ad7bef789..3221e4fc11 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -141,6 +131,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water_freeze.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water_freeze.lua index 3186af8245..9188bbc996 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water_freeze.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water_freeze.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/can_handle.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/can_handle.lua new file mode 100644 index 0000000000..044050b14f --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_third_reality_water_leak_sensor(opts, driver, device) + local FINGERPRINTS = require("thirdreality.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("thirdreality") + end + end + return false +end + +return can_handle_third_reality_water_leak_sensor diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/fingerprints.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/fingerprints.lua new file mode 100644 index 0000000000..f56a027f4f --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local THIRD_REALITY_WATER_LEAK_SENSOR_FINGERPRINTS = { + { mfr = "Third Reality, Inc", model = "3RWS18BZ"}, + { mfr = "THIRDREALITY", model = "3RWS18BZ"} +} + +return THIRD_REALITY_WATER_LEAK_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/init.lua index 99f14294e9..8a3819ac44 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" local utils = require "st.utils" @@ -20,19 +10,7 @@ local PowerConfiguration = zcl_clusters.PowerConfiguration local APPLICATION_VERSION = "application_version" -local THIRD_REALITY_WATER_LEAK_SENSOR_FINGERPRINTS = { - { mfr = "Third Reality, Inc", model = "3RWS18BZ"}, - { mfr = "THIRDREALITY", model = "3RWS18BZ"} -} -local function can_handle_third_reality_water_leak_sensor(opts, driver, device) - for _, fingerprint in ipairs(THIRD_REALITY_WATER_LEAK_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function device_added(driver, device) device:set_field(APPLICATION_VERSION, 0) @@ -73,7 +51,7 @@ local third_reality_water_leak_sensor = { lifecycle_handlers = { added = device_added }, - can_handle = can_handle_third_reality_water_leak_sensor + can_handle = require("thirdreality.can_handle"), } -return third_reality_water_leak_sensor \ No newline at end of file +return third_reality_water_leak_sensor diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/zigbee-water-freeze/can_handle.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/zigbee-water-freeze/can_handle.lua new file mode 100644 index 0000000000..5850a8b11c --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/zigbee-water-freeze/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function zigbee_water_freeze_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Ecolink" and device:get_model() == "FLZB1-ECO" then + return true, require("zigbee-water-freeze") + end + return false +end + +return zigbee_water_freeze_can_handle diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/zigbee-water-freeze/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/zigbee-water-freeze/init.lua index 68bd2ba9e0..82aca1b710 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/zigbee-water-freeze/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/zigbee-water-freeze/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -68,9 +58,7 @@ local zigbee_water_freeze = { init = battery_defaults.build_linear_voltage_init(2.2, 3.0), doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Ecolink" and device:get_model() == "FLZB1-ECO" - end + can_handle = require("zigbee-water-freeze.can_handle"), } return zigbee_water_freeze diff --git a/drivers/SmartThings/zigbee-watering-kit/src/init.lua b/drivers/SmartThings/zigbee-watering-kit/src/init.lua index f39c04beaa..7dd35e6f09 100644 --- a/drivers/SmartThings/zigbee-watering-kit/src/init.lua +++ b/drivers/SmartThings/zigbee-watering-kit/src/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" @@ -10,9 +13,7 @@ local zigbee_water_driver_template = { capabilities.fanSpeed, capabilities.mode }, - sub_drivers = { - require("thirdreality") - }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-watering-kit/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-watering-kit/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-watering-kit/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-watering-kit/src/sub_drivers.lua b/drivers/SmartThings/zigbee-watering-kit/src/sub_drivers.lua new file mode 100644 index 0000000000..e33b31c978 --- /dev/null +++ b/drivers/SmartThings/zigbee-watering-kit/src/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("thirdreality"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-watering-kit/src/test/test_thirdreality_watering_kit.lua b/drivers/SmartThings/zigbee-watering-kit/src/test/test_thirdreality_watering_kit.lua index 35ab4d915c..1ad780782c 100644 --- a/drivers/SmartThings/zigbee-watering-kit/src/test/test_thirdreality_watering_kit.lua +++ b/drivers/SmartThings/zigbee-watering-kit/src/test/test_thirdreality_watering_kit.lua @@ -218,4 +218,50 @@ test.register_coroutine_test( end ) +test.register_message_test( + "ZoneStatusChangeNotification should be handled: detected", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0001, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.hardwareFault.hardwareFault.detected()) + } + } +) + +test.register_message_test( + "ZoneStatusChangeNotification should be handled: clear", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0000, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.hardwareFault.hardwareFault.clear()) + } + } +) + +test.register_coroutine_test( + "fanspeed reported should be clamped to 0 when value >= 1000", + function() + local attr_report_data = { + { WATERING_TIME_ATTR, data_types.Uint16.ID, 1000 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, THIRDREALITY_WATERING_CLUSTER, attr_report_data, 0x1407) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.fanSpeed.fanSpeed(0))) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-watering-kit/src/thirdreality/can_handle.lua b/drivers/SmartThings/zigbee-watering-kit/src/thirdreality/can_handle.lua new file mode 100644 index 0000000000..12afaa7d98 --- /dev/null +++ b/drivers/SmartThings/zigbee-watering-kit/src/thirdreality/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function thirdreality_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Third Reality, Inc" and device:get_model() == "3RWK0148Z" then + return true, require("thirdreality") + end + return false +end + +return thirdreality_can_handle diff --git a/drivers/SmartThings/zigbee-watering-kit/src/thirdreality/init.lua b/drivers/SmartThings/zigbee-watering-kit/src/thirdreality/init.lua index 5f73f3f9ca..5e36e15624 100644 --- a/drivers/SmartThings/zigbee-watering-kit/src/thirdreality/init.lua +++ b/drivers/SmartThings/zigbee-watering-kit/src/thirdreality/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local IASZone = clusters.IASZone @@ -97,9 +100,7 @@ local thirdreality_device_handler = { lifecycle_handlers = { added = device_added }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Third Reality, Inc" and device:get_model() == "3RWK0148Z" - end + can_handle = require("thirdreality.can_handle"), } return thirdreality_device_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml index 723c0d1064..e15dd6f3a2 100644 --- a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml +++ b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml @@ -99,7 +99,7 @@ zigbeeManufacturer: model: Gear deviceProfileName: window-treatment-battery - id: "_TZE204_fzbskaga/TS0601" - deviceLabel: Hanssem Window Treatment + deviceLabel: Window Treatment manufacturer: _TZE204_fzbskaga model: TS0601 deviceProfileName: window-treatment-hanssem @@ -123,6 +123,21 @@ zigbeeManufacturer: manufacturer: Shade Revolution model: Indoor Shade Motors deviceProfileName: window-treatment-powerSource + - id: "VIVIDSTORM/VWSDSTUST120H" + deviceLabel: VIVIDSTORM Smart Screen VWSDSTUST120H + manufacturer: VIVIDSTORM + model: VWSDSTUST120H + deviceProfileName: projector-screen-VWSDSTUST120H + - id: "NodOn/SIN-4-RS-20" + deviceLabel: Zigbee Roller Shutter Relay Switch + manufacturer: NodOn + model: SIN-4-RS-20 + deviceProfileName: window-treatment-profile + - id: "HOPOsmart/A2230011" + deviceLabel: HOPOsmart Window Opener A2230011 + manufacturer: HOPOsmart + model: A2230011 + deviceProfileName: window-shade-only zigbeeGeneric: - id: "genericShade" diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/projector-screen-VWSDSTUST120H.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/projector-screen-VWSDSTUST120H.yml new file mode 100755 index 0000000000..85519e9f31 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/projector-screen-VWSDSTUST120H.yml @@ -0,0 +1,23 @@ +name: projector-screen-VWSDSTUST120H +components: + - label: " " + id: main + capabilities: + - id: windowShade + version: 1 + - id: mode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Projector + - label: " " + id: hardwareFault + capabilities: + - id: hardwareFault + version: 1 +metadata: + mnmn: SolutionsEngineering + vid: SmartThings-smartthings-VIVIDSTORM_Projector_Screen diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-shade-only.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-shade-only.yml new file mode 100755 index 0000000000..7adffe309e --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-shade-only.yml @@ -0,0 +1,12 @@ +name: window-shade-only +components: +- id: main + capabilities: + - id: windowShade + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: WindowOpener diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-battery.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-battery.yml index 275697cb27..be0ae73d76 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-battery.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-battery.yml @@ -16,6 +16,3 @@ components: version: 1 categories: - name: Blind -preferences: - - preferenceId: presetPosition - explicit: true diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-powerSource.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-powerSource.yml index 7c5255527f..7323ee0218 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-powerSource.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-powerSource.yml @@ -17,7 +17,4 @@ components: - id: firmwareUpdate version: 1 categories: - - name: Blind -preferences: - - preferenceId: presetPosition - explicit: true \ No newline at end of file + - name: Blind \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile-no-firmware-update.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile-no-firmware-update.yml index ded16d963b..8e641e6b42 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile-no-firmware-update.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile-no-firmware-update.yml @@ -11,7 +11,4 @@ components: - id: refresh version: 1 categories: - - name: Blind -preferences: - - preferenceId: presetPosition - explicit: true + - name: Blind \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile.yml index 933dd85321..b10a5ea101 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile.yml @@ -14,7 +14,3 @@ components: version: 1 categories: - name: Blind -preferences: - - preferenceId: presetPosition - explicit: true - diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-reverse.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-reverse.yml index 91b21811f9..0eb666a5d2 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-reverse.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-reverse.yml @@ -18,5 +18,3 @@ components: preferences: - preferenceId: reverse explicit: true - - preferenceId: presetPosition - explicit: true diff --git a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/can_handle.lua new file mode 100644 index 0000000000..85db6c0447 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + local FINGERPRINTS = require("HOPOsmart.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("HOPOsmart") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/custom_clusters.lua b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/custom_clusters.lua new file mode 100755 index 0000000000..b0394aa3fe --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/custom_clusters.lua @@ -0,0 +1,20 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local data_types = require "st.zigbee.data_types" + +local custom_clusters = { + motor = { + id = 0xFCC8, + mfg_specific_code = 0x1235, + attributes = { + state_value = { + id = 0x0000, + value_type = data_types.Uint8, + } + } + } +} + +return custom_clusters diff --git a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/fingerprints.lua new file mode 100644 index 0000000000..abf5e54ae5 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "HOPOsmart", model = "A2230011" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/init.lua new file mode 100755 index 0000000000..3267eefc33 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/init.lua @@ -0,0 +1,68 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local capabilities = require "st.capabilities" +local custom_clusters = require "HOPOsmart/custom_clusters" +local cluster_base = require "st.zigbee.cluster_base" + + + + +local function send_read_attr_request(device, cluster, attr) + device:send( + cluster_base.read_manufacturer_specific_attribute( + device, + cluster.id, + attr.id, + cluster.mfg_specific_code + ) + ) +end + +local function state_value_attr_handler(driver, device, value, zb_rx) + if value.value == 0 then + device:emit_event(capabilities.windowShade.windowShade.open()) + elseif value.value == 1 then + device:emit_event(capabilities.windowShade.windowShade.opening()) + elseif value.value == 2 then + device:emit_event(capabilities.windowShade.windowShade.closed()) + elseif value.value == 3 then + device:emit_event(capabilities.windowShade.windowShade.closing()) + elseif value.value == 4 then + device:emit_event(capabilities.windowShade.windowShade.partially_open()) + end +end + +local function do_refresh(driver, device) + send_read_attr_request(device, custom_clusters.motor, custom_clusters.motor.attributes.state_value) +end + +local function added_handler(self, device) + do_refresh(self, device) +end + +local HOPOsmart_handler = { + NAME = "HOPOsmart Device Handler", + supported_capabilities = { + capabilities.refresh + }, + lifecycle_handlers = { + added = added_handler + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + } + }, + zigbee_handlers = { + attr = { + [custom_clusters.motor.id] = { + [custom_clusters.motor.attributes.state_value.id] = state_value_attr_handler + } + } + }, + can_handle = require("HOPOsmart.can_handle"), +} + +return HOPOsmart_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/can_handle.lua new file mode 100644 index 0000000000..d6907690c3 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + local FINGERPRINTS = require("VIVIDSTORM.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("VIVIDSTORM") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua new file mode 100755 index 0000000000..8efbc0e654 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua @@ -0,0 +1,24 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local data_types = require "st.zigbee.data_types" + +local custom_clusters = { + motor = { + id = 0xFCC9, + mfg_specific_code = 0x1235, + attributes = { + mode_value = { + id = 0x0000, + value_type = data_types.Uint8, + }, + hardwareFault = { + id = 0x0001, + value_type = data_types.Uint8, + } + } + } +} + +return custom_clusters diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/fingerprints.lua new file mode 100644 index 0000000000..ff1bbcb00d --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "VIVIDSTORM", model = "VWSDSTUST120H" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua new file mode 100755 index 0000000000..106115edee --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua @@ -0,0 +1,141 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local zcl_clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local custom_clusters = require "VIVIDSTORM/custom_clusters" +local cluster_base = require "st.zigbee.cluster_base" +local WindowCovering = zcl_clusters.WindowCovering + +local MOST_RECENT_SETLEVEL = "windowShade_recent_setlevel" +local TIMER = "liftPercentage_timer" + + + + +local function send_read_attr_request(device, cluster, attr) + device:send( + cluster_base.read_manufacturer_specific_attribute( + device, + cluster.id, + attr.id, + cluster.mfg_specific_code + ) + ) +end + +local mode_str = { "Delete upper limit","Set the upper limit","Delete lower limit","Set the lower limit" } + +local function mode_attr_handler(driver, device, value, zb_rx) + if value.value <= 3 then + local value = mode_str[value.value+1] + if value ~= nil then + device:emit_component_event(device.profile.components.main,capabilities.mode.mode(value)) + end + end +end + + +local function liftPercentage_attr_handler(driver, device, value, zb_rx) + local windowShade = capabilities.windowShade.windowShade + local components = device.profile.components.main + local most_recent_setlevel = device:get_field(MOST_RECENT_SETLEVEL) + if value.value and most_recent_setlevel and value.value ~= most_recent_setlevel then + if value.value > most_recent_setlevel then + device:emit_component_event(components,windowShade.opening()) + elseif value.value < most_recent_setlevel then + device:emit_component_event(components,windowShade.closing()) + end + end + device:set_field(MOST_RECENT_SETLEVEL, value.value) + + local timer = device:get_field(TIMER) + if timer ~= nil then driver:cancel_timer(timer) end + timer = device.thread:call_with_delay(5, function(d) + if most_recent_setlevel == 0 then + device:emit_component_event(components,windowShade.closed()) + elseif most_recent_setlevel == 100 then + device:emit_component_event(components,windowShade.open()) + else + device:emit_component_event(components,windowShade.partially_open()) + end + end + ) + device:set_field(TIMER, timer) +end + +local function hardwareFault_attr_handler(driver, device, value, zb_rx) + if value.value == 1 then + device:emit_component_event(device.profile.components.hardwareFault,capabilities.hardwareFault.hardwareFault.detected()) + elseif value.value == 0 then + device:emit_component_event(device.profile.components.hardwareFault,capabilities.hardwareFault.hardwareFault.clear()) + end +end + +local function capabilities_mode_handler(driver, device, command) + local value = 0 + if command.args.mode == "Delete upper limit" then + value = 0 + elseif command.args.mode == "Set the upper limit" then + value = 1 + elseif command.args.mode == "Delete lower limit" then + value = 2 + elseif command.args.mode == "Set the lower limit" then + value = 3 + end + + device:send( + cluster_base.write_manufacturer_specific_attribute( + device, + custom_clusters.motor.id, + custom_clusters.motor.attributes.mode_value.id, + custom_clusters.motor.mfg_specific_code, + custom_clusters.motor.attributes.mode_value.value_type, + value + ) + ) +end + +local function do_refresh(driver, device) + device:send(WindowCovering.attributes.CurrentPositionLiftPercentage:read(device):to_endpoint(0x01)) + send_read_attr_request(device, custom_clusters.motor, custom_clusters.motor.attributes.mode_value) + send_read_attr_request(device, custom_clusters.motor, custom_clusters.motor.attributes.hardwareFault) +end + +local function added_handler(self, device) + device:emit_component_event(device.profile.components.hardwareFault,capabilities.hardwareFault.hardwareFault.clear()) + do_refresh(self, device) +end + +local screen_handler = { + NAME = "VWSDSTUST120H Device Handler", + supported_capabilities = { + capabilities.refresh + }, + lifecycle_handlers = { + added = added_handler + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + }, + [capabilities.mode.ID] = { + [capabilities.mode.commands.setMode.NAME] = capabilities_mode_handler + }, + }, + zigbee_handlers = { + attr = { + [WindowCovering.ID] = { + [WindowCovering.attributes.CurrentPositionLiftPercentage.ID] = liftPercentage_attr_handler + }, + [custom_clusters.motor.id] = { + [custom_clusters.motor.attributes.mode_value.id] = mode_attr_handler, + [custom_clusters.motor.attributes.hardwareFault.id] = hardwareFault_attr_handler + } + } + }, + can_handle = require("VIVIDSTORM.can_handle"), +} + +return screen_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/can_handle.lua new file mode 100644 index 0000000000..e4453597ed --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_aqara_products(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/can_handle.lua new file mode 100644 index 0000000000..7eb40a7c10 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function curtain_driver_e1_can_handle(opts, driver, device, ...) + if device:get_model() == "lumi.curtain.agl001" then + return true, require("aqara.curtain-driver-e1") + end + return false +end + +return curtain_driver_e1_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua index 3af5364bbe..03e651b679 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua @@ -1,21 +1,12 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" local aqara_utils = require "aqara/aqara_utils" +local window_treatment_utils = require "window_treatment_utils" local Groups = clusters.Groups local Basic = clusters.Basic @@ -28,10 +19,10 @@ local PRIVATE_CURTAIN_LOCKING_SETTING_ATTRIBUTE_ID = 0x0427 local PRIVATE_CURTAIN_LOCKING_STATUS_ATTRIBUTE_ID = 0x0428 local initializedStateWithGuide = capabilities["stse.initializedStateWithGuide"] -local reverseCurtainDirection = capabilities["stse.reverseCurtainDirection"] +local reverseCurtainDirection = "stse.reverseCurtainDirection" local hookLockState = capabilities["stse.hookLockState"] local chargingState = capabilities["stse.chargingState"] -local softTouch = capabilities["stse.softTouch"] +local softTouch = "stse.softTouch" local hookUnlockCommandName = "hookUnlock" local hookLockCommandName = "hookLock" @@ -41,8 +32,8 @@ local SHADE_STATE_STOP = 2 local function device_added(driver, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) - device:emit_event(capabilities.windowShadeLevel.shadeLevel(0)) - device:emit_event(capabilities.windowShade.windowShade.closed()) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) device:emit_event(initializedStateWithGuide.initializedStateWithGuide.notInitialized()) device:emit_event(hookLockState.hookLockState.unlocked()) device:emit_event(chargingState.chargingState.stopped()) @@ -76,18 +67,17 @@ local CONFIGURATIONS = { local function device_init(driver, device) for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end local function device_info_changed(driver, device, event, args) if device.preferences ~= nil then - local reverseCurtainDirectionPrefValue = device.preferences[reverseCurtainDirection.ID] - local softTouchPrefValue = device.preferences[softTouch.ID] + local reverseCurtainDirectionPrefValue = device.preferences[reverseCurtainDirection] + local softTouchPrefValue = device.preferences[softTouch] -- reverse direction if reverseCurtainDirectionPrefValue ~= nil and - reverseCurtainDirectionPrefValue ~= args.old_st_store.preferences[reverseCurtainDirection.ID] then + reverseCurtainDirectionPrefValue ~= args.old_st_store.preferences[reverseCurtainDirection] then local raw_value = reverseCurtainDirectionPrefValue and 0x01 or 0x00 device:send(aqara_utils.custom_write_attribute(device, WindowCovering.ID, WindowCovering.attributes.Mode.ID, data_types.Bitmap8, raw_value, nil)) @@ -95,7 +85,7 @@ local function device_info_changed(driver, device, event, args) -- soft touch if softTouchPrefValue ~= nil and - softTouchPrefValue ~= args.old_st_store.preferences[softTouch.ID] then + softTouchPrefValue ~= args.old_st_store.preferences[softTouch] then device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, PRIVATE_CURTAIN_MANUAL_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Boolean, (not softTouchPrefValue))) end @@ -224,9 +214,7 @@ local aqara_curtain_driver_e1_handler = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "lumi.curtain.agl001" - end + can_handle = require("aqara.curtain-driver-e1.can_handle"), } return aqara_curtain_driver_e1_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..8ad05530a5 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.curtain" }, + { mfr = "LUMI", model = "lumi.curtain.v1" }, + { mfr = "LUMI", model = "lumi.curtain.aq2" }, + { mfr = "LUMI", model = "lumi.curtain.agl001" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua index 929d5b713d..1b66236038 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua @@ -1,8 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" local aqara_utils = require "aqara/aqara_utils" +local window_treatment_utils = require "window_treatment_utils" local Basic = clusters.Basic local WindowCovering = clusters.WindowCovering @@ -10,8 +15,8 @@ local AnalogOutput = clusters.AnalogOutput local Groups = clusters.Groups local deviceInitialization = capabilities["stse.deviceInitialization"] -local reverseCurtainDirection = capabilities["stse.reverseCurtainDirection"] -local softTouch = capabilities["stse.softTouch"] +local reverseCurtainDirection = "stse.reverseCurtainDirection" +local softTouch = "stse.softTouch" local setInitializedStateCommandName = "setInitializedState" local INIT_STATE = "initState" @@ -26,21 +31,7 @@ local PREF_SOFT_TOUCH_ON = "\x00\x08\x00\x00\x00\x00\x00" local APPLICATION_VERSION = "application_version" -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.curtain" }, - { mfr = "LUMI", model = "lumi.curtain.v1" }, - { mfr = "LUMI", model = "lumi.curtain.aq2" }, - { mfr = "LUMI", model = "lumi.curtain.agl001" } -} -local function is_aqara_products(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function window_shade_level_cmd(driver, device, command) aqara_utils.shade_level_cmd(driver, device, command) @@ -132,12 +123,12 @@ end local function device_info_changed(driver, device, event, args) if device.preferences ~= nil then - local reverseCurtainDirectionPrefValue = device.preferences[reverseCurtainDirection.ID] - local softTouchPrefValue = device.preferences[softTouch.ID] + local reverseCurtainDirectionPrefValue = device.preferences[reverseCurtainDirection] + local softTouchPrefValue = device.preferences[softTouch] -- reverse direction if reverseCurtainDirectionPrefValue ~= nil and - reverseCurtainDirectionPrefValue ~= args.old_st_store.preferences[reverseCurtainDirection.ID] then + reverseCurtainDirectionPrefValue ~= args.old_st_store.preferences[reverseCurtainDirection] then local raw_value = reverseCurtainDirectionPrefValue and aqara_utils.PREF_REVERSE_ON or aqara_utils.PREF_REVERSE_OFF device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.CharString, raw_value)) @@ -151,7 +142,7 @@ local function device_info_changed(driver, device, event, args) -- soft touch if softTouchPrefValue ~= nil and - softTouchPrefValue ~= args.old_st_store.preferences[softTouch.ID] then + softTouchPrefValue ~= args.old_st_store.preferences[softTouch] then local raw_value = softTouchPrefValue and PREF_SOFT_TOUCH_ON or PREF_SOFT_TOUCH_OFF device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.CharString, raw_value)) @@ -169,8 +160,8 @@ end local function device_added(driver, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) device:emit_event(deviceInitialization.supportedInitializedState({ "notInitialized", "initializing", "initialized" }, {visibility = {displayed = false}})) - device:emit_event(capabilities.windowShadeLevel.shadeLevel(0)) - device:emit_event(capabilities.windowShade.windowShade.closed()) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) device:emit_event(deviceInitialization.initializedState.notInitialized()) device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, @@ -216,12 +207,8 @@ local aqara_window_treatment_handler = { } } }, - sub_drivers = { - require("aqara.roller-shade"), - require("aqara.curtain-driver-e1"), - require("aqara.version") - }, - can_handle = is_aqara_products + sub_drivers = require("aqara.sub_drivers"), + can_handle = require("aqara.can_handle"), } return aqara_window_treatment_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/can_handle.lua new file mode 100644 index 0000000000..2e91fa090d --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function roller_shade_can_handle(opts, driver, device, ...) + if device:get_model() == "lumi.curtain.aq2" then + return true, require("aqara.roller-shade") + end + return false +end + +return roller_shade_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua index 6239fb52f0..909b65dd3e 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua @@ -1,140 +1,142 @@ -local capabilities = require "st.capabilities" -local clusters = require "st.zigbee.zcl.clusters" -local cluster_base = require "st.zigbee.cluster_base" -local FrameCtrl = require "st.zigbee.zcl.frame_ctrl" -local data_types = require "st.zigbee.data_types" -local aqara_utils = require "aqara/aqara_utils" - -local Basic = clusters.Basic -local WindowCovering = clusters.WindowCovering - -local initializedStateWithGuide = capabilities["stse.initializedStateWithGuide"] -local reverseRollerShadeDir = capabilities["stse.reverseRollerShadeDir"] -local shadeRotateState = capabilities["stse.shadeRotateState"] -local setRotateStateCommandName = "setRotateState" - -local MULTISTATE_CLUSTER_ID = 0x0013 -local MULTISTATE_ATTRIBUTE_ID = 0x0055 -local ROTATE_UP_VALUE = 0x0004 -local ROTATE_DOWN_VALUE = 0x0005 - - -local function window_shade_level_cmd(driver, device, command) - -- Cannot be controlled if not initialized - local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, - initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 - if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then - aqara_utils.shade_level_cmd(driver, device, command) - end -end - -local function window_shade_open_cmd(driver, device, command) - -- Cannot be controlled if not initialized - local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, - initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 - if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then - device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 100)) - end -end - -local function window_shade_close_cmd(driver, device, command) - -- Cannot be controlled if not initialized - local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, - initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 - if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then - device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 0)) - end -end - -local function set_rotate_command_handler(driver, device, command) - device:emit_event(shadeRotateState.rotateState.idle({state_change = true, visibility = { displayed = false }})) -- update UI - - -- Cannot be controlled if not initialized - local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, - initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 - if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then - local state = command.args.state - if state == "rotateUp" then - local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID, - MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_UP_VALUE) - message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) - device:send(message) - elseif state == "rotateDown" then - local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID, - MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_DOWN_VALUE) - message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) - device:send(message) - end - end -end - -local function shade_state_report_handler(driver, device, value, zb_rx) - aqara_utils.emit_shade_event_by_state(device, value) -end - -local function pref_report_handler(driver, device, value, zb_rx) - -- initializedState - local initialized = string.byte(value.value, 3) & 0xFF - device:emit_event(initialized == 1 and initializedStateWithGuide.initializedStateWithGuide.initialized() or - initializedStateWithGuide.initializedStateWithGuide.notInitialized()) -end - -local function device_info_changed(driver, device, event, args) - if device.preferences ~= nil then - local reverseRollerShadeDirPrefValue = device.preferences[reverseRollerShadeDir.ID] - if reverseRollerShadeDirPrefValue ~= nil and - reverseRollerShadeDirPrefValue ~= args.old_st_store.preferences[reverseRollerShadeDir.ID] then - local raw_value = reverseRollerShadeDirPrefValue and aqara_utils.PREF_REVERSE_ON or aqara_utils.PREF_REVERSE_OFF - device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, - aqara_utils.MFG_CODE, data_types.CharString, raw_value)) - end - end -end - -local function device_added(driver, device) - device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) - device:emit_event(capabilities.windowShadeLevel.shadeLevel(0)) - device:emit_event(capabilities.windowShade.windowShade.closed()) - device:emit_event(initializedStateWithGuide.initializedStateWithGuide.notInitialized()) - device:emit_event(shadeRotateState.rotateState.idle({ visibility = { displayed = false }})) - - device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, - aqara_utils.PRIVATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint8, 1)) - - -- Initial default settings - device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, - aqara_utils.MFG_CODE, data_types.CharString, aqara_utils.PREF_REVERSE_OFF)) -end - -local aqara_roller_shade_handler = { - NAME = "Aqara Roller Shade Handler", - lifecycle_handlers = { - added = device_added, - infoChanged = device_info_changed - }, - capability_handlers = { - [capabilities.windowShadeLevel.ID] = { - [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = window_shade_level_cmd - }, - [capabilities.windowShade.ID] = { - [capabilities.windowShade.commands.open.NAME] = window_shade_open_cmd, - [capabilities.windowShade.commands.close.NAME] = window_shade_close_cmd, - }, - [shadeRotateState.ID] = { - [setRotateStateCommandName] = set_rotate_command_handler - } - }, - zigbee_handlers = { - attr = { - [Basic.ID] = { - [aqara_utils.SHADE_STATE_ATTRIBUTE_ID] = shade_state_report_handler, - [aqara_utils.PREF_ATTRIBUTE_ID] = pref_report_handler - } - } - }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "lumi.curtain.aq2" - end -} - -return aqara_roller_shade_handler +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local FrameCtrl = require "st.zigbee.zcl.frame_ctrl" +local data_types = require "st.zigbee.data_types" +local aqara_utils = require "aqara/aqara_utils" +local window_treatment_utils = require "window_treatment_utils" + +local Basic = clusters.Basic +local WindowCovering = clusters.WindowCovering + +local initializedStateWithGuide = capabilities["stse.initializedStateWithGuide"] +local reverseRollerShadeDir = capabilities["stse.reverseRollerShadeDir"] +local shadeRotateState = capabilities["stse.shadeRotateState"] +local setRotateStateCommandName = "setRotateState" + +local MULTISTATE_CLUSTER_ID = 0x0013 +local MULTISTATE_ATTRIBUTE_ID = 0x0055 +local ROTATE_UP_VALUE = 0x0004 +local ROTATE_DOWN_VALUE = 0x0005 + + +local function window_shade_level_cmd(driver, device, command) + -- Cannot be controlled if not initialized + local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, + initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 + if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then + aqara_utils.shade_level_cmd(driver, device, command) + end +end + +local function window_shade_open_cmd(driver, device, command) + -- Cannot be controlled if not initialized + local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, + initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 + if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then + device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 100)) + end +end + +local function window_shade_close_cmd(driver, device, command) + -- Cannot be controlled if not initialized + local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, + initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 + if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then + device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 0)) + end +end + +local function set_rotate_command_handler(driver, device, command) + device:emit_event(shadeRotateState.rotateState.idle({state_change = true, visibility = { displayed = false }})) -- update UI + + -- Cannot be controlled if not initialized + local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, + initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 + if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then + local state = command.args.state + if state == "rotateUp" then + local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID, + MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_UP_VALUE) + message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) + device:send(message) + elseif state == "rotateDown" then + local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID, + MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_DOWN_VALUE) + message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) + device:send(message) + end + end +end + +local function shade_state_report_handler(driver, device, value, zb_rx) + aqara_utils.emit_shade_event_by_state(device, value) +end + +local function pref_report_handler(driver, device, value, zb_rx) + -- initializedState + local initialized = string.byte(value.value, 3) & 0xFF + device:emit_event(initialized == 1 and initializedStateWithGuide.initializedStateWithGuide.initialized() or + initializedStateWithGuide.initializedStateWithGuide.notInitialized()) +end + +local function device_info_changed(driver, device, event, args) + if device.preferences ~= nil then + local reverseRollerShadeDirPrefValue = device.preferences[reverseRollerShadeDir.ID] + if reverseRollerShadeDirPrefValue ~= nil and + reverseRollerShadeDirPrefValue ~= args.old_st_store.preferences[reverseRollerShadeDir.ID] then + local raw_value = reverseRollerShadeDirPrefValue and aqara_utils.PREF_REVERSE_ON or aqara_utils.PREF_REVERSE_OFF + device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, + aqara_utils.MFG_CODE, data_types.CharString, raw_value)) + end + end +end + +local function device_added(driver, device) + device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) + device:emit_event(initializedStateWithGuide.initializedStateWithGuide.notInitialized()) + device:emit_event(shadeRotateState.rotateState.idle({ visibility = { displayed = false }})) + + device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, + aqara_utils.PRIVATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint8, 1)) + + -- Initial default settings + device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, + aqara_utils.MFG_CODE, data_types.CharString, aqara_utils.PREF_REVERSE_OFF)) +end + +local aqara_roller_shade_handler = { + NAME = "Aqara Roller Shade Handler", + lifecycle_handlers = { + added = device_added, + infoChanged = device_info_changed + }, + capability_handlers = { + [capabilities.windowShadeLevel.ID] = { + [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = window_shade_level_cmd + }, + [capabilities.windowShade.ID] = { + [capabilities.windowShade.commands.open.NAME] = window_shade_open_cmd, + [capabilities.windowShade.commands.close.NAME] = window_shade_close_cmd, + }, + [shadeRotateState.ID] = { + [setRotateStateCommandName] = set_rotate_command_handler + } + }, + zigbee_handlers = { + attr = { + [Basic.ID] = { + [aqara_utils.SHADE_STATE_ATTRIBUTE_ID] = shade_state_report_handler, + [aqara_utils.PREF_ATTRIBUTE_ID] = pref_report_handler + } + } + }, + can_handle = require("aqara.roller-shade.can_handle"), +} + +return aqara_roller_shade_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/sub_drivers.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/sub_drivers.lua new file mode 100644 index 0000000000..297f29d970 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/sub_drivers.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("aqara.roller-shade"), + lazy_load_if_possible("aqara.curtain-driver-e1"), + lazy_load_if_possible("aqara.version"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/can_handle.lua new file mode 100644 index 0000000000..5d9f7f0135 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function version_can_handle(opts, driver, device) + local APPLICATION_VERSION = "application_version" + local softwareVersion = device:get_field(APPLICATION_VERSION) + if softwareVersion and softwareVersion ~= 34 then + return true, require("aqara.version") + end + return false +end + +return version_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/init.lua index 10182ee928..ae1887d79a 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/init.lua @@ -1,9 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local WindowCovering = clusters.WindowCovering -local APPLICATION_VERSION = "application_version" - local function shade_level_report_legacy_handler(driver, device, value, zb_rx) -- not implemented end @@ -17,10 +18,7 @@ local aqara_window_treatment_version_handler = { } } }, - can_handle = function(opts, driver, device) - local softwareVersion = device:get_field(APPLICATION_VERSION) - return softwareVersion and softwareVersion ~= 34 - end + can_handle = require("aqara.version.can_handle"), } return aqara_window_treatment_version_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/can_handle.lua new file mode 100644 index 0000000000..828eb170fa --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_axis_gear_version = function(opts, driver, device) + local SOFTWARE_VERSION = "software_version" + local MIN_WINDOW_COVERING_VERSION = 1093 + local version = device:get_field(SOFTWARE_VERSION) or 0 + + if version >= MIN_WINDOW_COVERING_VERSION then + return true, require("axis.axis_version") + end + return false +end + +return is_axis_gear_version diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua index 1bc09cbb6b..cbb2930bc3 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua @@ -1,19 +1,9 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" +local window_shade_utils = require "window_shade_utils" local zcl_clusters = require "st.zigbee.zcl.clusters" local utils = require "st.utils" @@ -22,18 +12,8 @@ local Level = zcl_clusters.Level local PowerConfiguration = zcl_clusters.PowerConfiguration local WindowCovering = zcl_clusters.WindowCovering -local SOFTWARE_VERSION = "software_version" -local MIN_WINDOW_COVERING_VERSION = 1093 local DEFAULT_LEVEL = 0 -local is_axis_gear_version = function(opts, driver, device) - local version = device:get_field(SOFTWARE_VERSION) or 0 - - if version >= MIN_WINDOW_COVERING_VERSION then - return true - end - return false -end -- Commands local function window_shade_set_level(device, command, level) @@ -52,7 +32,7 @@ local function window_shade_level_cmd_handler(driver, device, command) end local function window_shade_preset_cmd(driver, device, command) - local level = device.preferences and device.preferences.presetPosition or window_preset_defaults.PRESET_LEVEL + local level = window_shade_utils.get_preset_level(device, command.component) window_shade_set_level(device, command, level) end @@ -141,7 +121,7 @@ local axis_handler_version = { } } }, - can_handle = is_axis_gear_version, + can_handle = require("axis.axis_version.can_handle"), } return axis_handler_version diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/can_handle.lua new file mode 100644 index 0000000000..049e47acb5 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + if device:get_manufacturer() == "AXIS" then + return true, require("axis") + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua index acd67d2d32..ec7c96b975 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua @@ -1,20 +1,11 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" local device_management = require "st.zigbee.device_management" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" +local window_shade_utils = require "window_shade_utils" local zcl_clusters = require "st.zigbee.zcl.clusters" local utils = require "st.utils" @@ -26,12 +17,6 @@ local WindowCovering = zcl_clusters.WindowCovering local SOFTWARE_VERSION = "software_version" local DEFAULT_LEVEL = 0 -local is_zigbee_window_shade = function(opts, driver, device) - if device:get_manufacturer() == "AXIS" then - return true - end - return false -end -- Commands local function window_shade_set_level(device, command, level) @@ -50,7 +35,7 @@ local function window_shade_level_cmd_handler(driver, device, command) end local function window_shade_preset_cmd(driver, device, command) - local level = device.preferences and device.preferences.presetPosition or window_preset_defaults.PRESET_LEVEL + local level = window_shade_utils.get_preset_level(device, command.component) window_shade_set_level(device, command, level) end @@ -110,6 +95,7 @@ local do_configure = function(self, device) device:send(WindowCovering.attributes.CurrentPositionLiftPercentage:configure_reporting(device, 1, 3600, 1)) device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 1, 3600, 1)) + device:refresh() end local device_added = function(self, device) @@ -150,8 +136,8 @@ local axis_handler = { added = device_added, doConfigure = do_configure, }, - sub_drivers = { require("axis.axis_version") }, - can_handle = is_zigbee_window_shade, + sub_drivers = require("axis.sub_drivers"), + can_handle = require("axis.can_handle"), } return axis_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/sub_drivers.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/sub_drivers.lua new file mode 100644 index 0000000000..e3ea740478 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require("lazy_load_subdriver") + +return { + lazy_load_if_possible("axis.axis_version") +} diff --git a/drivers/SmartThings/zigbee-window-treatment/src/feibit/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/feibit/can_handle.lua new file mode 100644 index 0000000000..32457dde8a --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/feibit/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + local FINGERPRINTS = require("feibit.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("feibit") + end + end + + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/feibit/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/feibit/fingerprints.lua new file mode 100644 index 0000000000..95781ff992 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/feibit/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "Feibit Co.Ltd", model = "FTB56-ZT218AK1.6" }, + { mfr = "Feibit Co.Ltd", model = "FTB56-ZT218AK1.8" }, +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua index 0c1d20be11..1e97fbc4ce 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua @@ -1,38 +1,15 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" +local window_shade_utils = require "window_shade_utils" local window_shade_defaults = require "st.zigbee.defaults.windowShade_defaults" local device_management = require "st.zigbee.device_management" local Level = zcl_clusters.Level -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "Feibit Co.Ltd", model = "FTB56-ZT218AK1.6" }, - { mfr = "Feibit Co.Ltd", model = "FTB56-ZT218AK1.8" }, -} - -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function set_shade_level(device, value, component) local level = math.floor(value / 100.0 * 254) @@ -50,7 +27,7 @@ local function level_attr_handler(driver, device, value, zb_rx) end local function window_shade_preset_cmd(driver, device, command) - local level = device.preferences.presetPosition or device:get_field(window_preset_defaults.PRESET_LEVEL_KEY) or window_preset_defaults.PRESET_LEVEL + local level = window_shade_utils.get_preset_level(device, command.component) set_shade_level(device, level, command.component) end @@ -61,6 +38,7 @@ end local do_configure = function(self, device) device:send(device_management.build_bind_request(device, Level.ID, self.environment_info.hub_zigbee_eui)) device:send(Level.attributes.CurrentLevel:configure_reporting(device, 1, 3600, 1)) + device:refresh() end local feibit_handler = { @@ -86,7 +64,7 @@ local feibit_handler = { lifecycle_handlers = { doConfigure = do_configure, }, - can_handle = is_zigbee_window_shade, + can_handle = require("feibit.can_handle"), } return feibit_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/hanssem/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/hanssem/can_handle.lua new file mode 100644 index 0000000000..65f1a6dc75 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/hanssem/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function hanssem_can_handle(opts, driver, device, ...) + if device:get_model() == "TS0601" then + return true, require("hanssem") + end + return false +end + +return hanssem_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua index bc974706b6..41b4eb1cac 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2021-2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- -- Based on https://github.com/iquix/ST-Edge-Driver/blob/master/tuya-window-shade/src/init.lua -- Copyright 2021-2022 Jaewon Park (iquix) @@ -16,7 +19,7 @@ local Messages = require "st.zigbee.messages" local data_types = require "st.zigbee.data_types" local ZigbeeConstants = require "st.zigbee.constants" local generic_body = require "st.zigbee.generic_body" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" +local window_shade_utils = require "window_shade_utils" local TUYA_CLUSTER = 0xEF00 local DP_TYPE_VALUE = "\x02" @@ -148,7 +151,7 @@ local function SetShadeLevelHandler(driver, device, capability_command) end local function PresetPositionHandler(driver, device, capability_command) - local level = device.preferences.presetPosition or device:get_field(window_preset_defaults.PRESET_LEVEL_KEY) or window_preset_defaults.PRESET_LEVEL + local level = window_shade_utils.get_preset_level(device, capability_command.component) SetShadeLevelHandler(driver, device, {args = { shadeLevel = level }}) end @@ -241,9 +244,7 @@ local hanssem_window_treatment = { added = device_added, infoChanged = device_info_changed }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "TS0601" - end + can_handle = require("hanssem.can_handle"), } -return hanssem_window_treatment \ No newline at end of file +return hanssem_window_treatment diff --git a/drivers/SmartThings/zigbee-window-treatment/src/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/init.lua index fc3535c043..16783a8726 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/init.lua @@ -1,20 +1,28 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" +local window_shade_utils = require "window_shade_utils" + +local function init_handler(self, device) + if device:supports_capability_by_id(capabilities.windowShadePreset.ID) and + device:get_latest_state("main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME) == nil then + + -- These should only ever be nil once (and at the same time) for already-installed devices + -- It can be relocated to `added` after migration is complete + device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, { visibility = { displayed = false }})) + + local preset_position = device:get_field(window_shade_utils.PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + window_shade_utils.PRESET_LEVEL + + device:emit_event(capabilities.windowShadePreset.position(preset_position, { visibility = {displayed = false}})) + device:set_field(window_shade_utils.PRESET_LEVEL_KEY, preset_position, {persist = true}) + end +end local function added_handler(self, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, { visibility = { displayed = false }})) @@ -28,20 +36,17 @@ local zigbee_window_treatment_driver_template = { capabilities.powerSource, capabilities.battery }, - sub_drivers = { - require("vimar"), - require("aqara"), - require("feibit"), - require("somfy"), - require("invert-lift-percentage"), - require("rooms-beautiful"), - require("axis"), - require("yoolax"), - require("hanssem"), - require("screen-innovations")}, + capability_handlers = { + [capabilities.windowShadePreset.ID] = { + [capabilities.windowShadePreset.commands.setPresetPosition.NAME] = window_shade_utils.set_preset_position_cmd, + [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_utils.window_shade_preset_cmd, + } + }, lifecycle_handlers = { + init = init_handler, added = added_handler }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/can_handle.lua new file mode 100644 index 0000000000..69cfd4393a --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function invert_lift_percentage_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "IKEA of Sweden" or + device:get_manufacturer() == "Smartwings" or + device:get_manufacturer() == "Insta GmbH" + then + return true, require("invert-lift-percentage") + end + return false +end + +return invert_lift_percentage_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua index 62c3afc7eb..b586459b9a 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua @@ -1,19 +1,10 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" +local window_shade_utils = require "window_shade_utils" local WindowCovering = zcl_clusters.WindowCovering @@ -75,9 +66,8 @@ local function window_shade_level_cmd(driver, device, command) end local function window_shade_preset_cmd(driver, device, command) - if device.preferences ~= nil and device.preferences.presetPosition ~= nil then - set_shade_level(device, device.preferences.presetPosition, command) - end + local level = window_shade_utils.get_preset_level(device, command.component) + set_shade_level(device, level, command) end local ikea_window_treatment = { @@ -97,11 +87,7 @@ local ikea_window_treatment = { [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_preset_cmd } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "IKEA of Sweden" or - device:get_manufacturer() == "Smartwings" or - device:get_manufacturer() == "Insta GmbH" - end + can_handle = require("invert-lift-percentage.can_handle"), } return ikea_window_treatment diff --git a/drivers/SmartThings/zigbee-window-treatment/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-window-treatment/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/can_handle.lua new file mode 100644 index 0000000000..6bc25d2f91 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + local FINGERPRINTS = require("rooms-beautiful.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("rooms-beautiful") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/fingerprints.lua new file mode 100644 index 0000000000..71ece32b8e --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "Rooms Beautiful", model = "C001" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/init.lua index fc4883aa7f..bb868a8716 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -21,22 +11,11 @@ local PowerConfiguration = zcl_clusters.PowerConfiguration local OnOff = zcl_clusters.OnOff local WindowCovering = zcl_clusters.WindowCovering -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "Rooms Beautiful", model = "C001" } -} local INVERT_CLUSTER = 0xFC00 local INVERT_CLUSTER_ATTRIBUTE = 0x0000 local PREV_TIME = "shadeLevelCmdTime" -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function invert_preference_handler(device) local window_level = device:get_latest_state("main", capabilities.windowShadeLevel.ID, capabilities.windowShadeLevel.shadeLevel.NAME) or 0 @@ -129,7 +108,7 @@ local rooms_beautiful_handler = { init = battery_defaults.build_linear_voltage_init(2.5, 3.0), infoChanged = info_changed }, - can_handle = is_zigbee_window_shade, + can_handle = require("rooms-beautiful.can_handle"), } return rooms_beautiful_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/can_handle.lua new file mode 100644 index 0000000000..df291c2612 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function screen_innovations_can_handle(opts, driver, device, ...) + if device:get_model() == "WM25/L-Z" then + return true, require("screen-innovations") + end + return false +end + +return screen_innovations_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua index 11d4ff17f9..49397a5369 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua @@ -1,21 +1,11 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- require st provided libraries local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" +local window_shade_utils = require "window_shade_utils" local device_management = require "st.zigbee.device_management" local utils = require "st.utils" @@ -52,9 +42,9 @@ end -- this is window_shade_preset_cmd local function window_shade_preset_cmd(driver, device, command) - local go_to_level = device.preferences.presetPosition or device:get_field(window_preset_defaults.PRESET_LEVEL_KEY) or window_preset_defaults.PRESET_LEVEL + local level = window_shade_utils.get_preset_level(device, command.component) -- send levels without inverting as: 0% closed (i.e., open) to 100% closed - device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, go_to_level)) + device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, level)) end -- this is device_added @@ -173,9 +163,7 @@ local screeninnovations_roller_shade_handler = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "WM25/L-Z" - end + can_handle = require("screen-innovations.can_handle"), } -- return the handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/somfy/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/somfy/can_handle.lua new file mode 100644 index 0000000000..27a6c83ca3 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/somfy/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + local FINGERPRINTS = require("somfy.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("somfy") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/somfy/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/somfy/fingerprints.lua new file mode 100644 index 0000000000..ce6094564c --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/somfy/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "SOMFY", model = "Glydea Ultra Curtain" }, + { mfr = "SOMFY", model = "Sonesse 30 WF Roller" }, + { mfr = "SOMFY", model = "Sonesse 40 Roller" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua index c90a0c833a..da416ba9ea 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua @@ -1,42 +1,19 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local utils = require "st.utils" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" +local window_shade_utils = require "window_shade_utils" local zcl_clusters = require "st.zigbee.zcl.clusters" local WindowCovering = zcl_clusters.WindowCovering local GLYDEA_MOVE_THRESHOLD = 3 -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "SOMFY", model = "Glydea Ultra Curtain" }, - { mfr = "SOMFY", model = "Sonesse 30 WF Roller" }, - { mfr = "SOMFY", model = "Sonesse 40 Roller" } -} local MOVE_LESS_THAN_THRESHOLD = "_sameLevelEvent" local FINAL_STATE_POLL_TIMER = "_finalStatePollTimer" -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function overwrite_existing_timer_if_needed(device, new_timer) local old_timer = device:get_field(FINAL_STATE_POLL_TIMER) @@ -109,7 +86,7 @@ local function window_shade_level_cmd(driver, device, command) end local function window_shade_preset_cmd(driver, device, command) - local level = device.preferences.presetPosition or device:get_field(window_preset_defaults.PRESET_LEVEL_KEY) or window_preset_defaults.PRESET_LEVEL + local level = window_shade_utils.get_preset_level(device, command.component) command.args.shadeLevel = level window_shade_level_cmd(driver, device, command) end @@ -132,7 +109,7 @@ local somfy_handler = { } } }, - can_handle = is_zigbee_window_shade, + can_handle = require("somfy.can_handle"), } return somfy_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua b/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua new file mode 100644 index 0000000000..959c8d8c22 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("vimar"), + lazy_load_if_possible("aqara"), + lazy_load_if_possible("feibit"), + lazy_load_if_possible("somfy"), + lazy_load_if_possible("invert-lift-percentage"), + lazy_load_if_possible("rooms-beautiful"), + lazy_load_if_possible("axis"), + lazy_load_if_possible("yoolax"), + lazy_load_if_possible("hanssem"), + lazy_load_if_possible("screen-innovations"), + lazy_load_if_possible("VIVIDSTORM"), + lazy_load_if_possible("HOPOsmart"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_ikea.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_ikea.lua index 4fee02c00f..8c87907886 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_ikea.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_ikea.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -37,7 +27,14 @@ local mock_device = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() - test.mock_device.add_test_device(mock_device)end + test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) +end test.set_test_init_function(test_init) @@ -60,6 +57,25 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "State transition to unknown", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 255) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.unknown()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100)) + ) + end +) + test.register_coroutine_test( "State transition from opening to partially open", function() @@ -162,7 +178,13 @@ test.register_coroutine_test( test.register_coroutine_test( "windowShadePreset capability should be handled", function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 30}})) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {30}} + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.windowShadePreset.position(30))) test.wait_for_events() test.socket.capability:__queue_receive( { @@ -177,4 +199,130 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "SetShadeLevel command handler", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 50 }} + } + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 50) + }) + end +) + +test.register_coroutine_test( + "Cancel existing set-status timer when a new partial level report arrives", + function() + -- First attr: level 90 sets T1 + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 10) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 90 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + -- Second attr arrives before T1 fires: should cancel T1 and create T2 + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 15) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 85 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) + ) + -- T2 fires; T1 was cancelled so only partially_open from T2 + test.mock_time.advance_time(1) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Timer callback emits closed when shade reaches level 0", + function() + -- First attr starts partial movement and arms a 1-second status timer + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 10) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 90 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + -- Second attr reports fully closed (level=0); goes through elseif branch, T1 still pending + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 100) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(0)) + ) + -- T1 fires; get_latest_state returns 0 so the callback emits closed() + test.mock_time.advance_time(1) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Timer callback emits open when shade reaches level 100", + function() + -- First attr starts partial movement and arms a 1-second status timer + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 10) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 90 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + -- Second attr reports fully open (level=100); goes through elseif branch, T1 still pending + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 0) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.open()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100)) + ) + -- T1 fires; get_latest_state returns 100 so the callback emits open() + test.mock_time.advance_time(1) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.open()) + ) + test.wait_for_events() + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua index 106730e7fd..725fda3f62 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -67,7 +57,14 @@ end zigbee_test_utils.prepare_zigbee_env_info() local function test_init() - test.mock_device.add_test_device(mock_device)end + test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) +end test.set_test_init_function(test_init) @@ -136,9 +133,10 @@ test.register_coroutine_test( { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) + -- newly added devices will ignore the preference test.socket.zigbee:__expect_send({ mock_device.id, - WindowCovering.server.commands.GoToLiftPercentage(mock_device, 70) + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 50) }) end ) @@ -324,4 +322,109 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Default response emits opening when current level is higher than target", + function() + -- Establish a partially-closed shade state (zigbee value 90 → shadeLevel 10) + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 90) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(10)) + ) + test.wait_for_events() + -- Send open command: MOST_RECENT_SETLEVEL = 0 (level = 100 - 100 = 0) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "windowShade", component = "main", command = "open", args = {} } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 0) + }) + test.wait_for_events() + -- Default response: current_level_zigbee=90, most_recent=0 → 90 > 0 → opening() + test.socket.zigbee:__queue_receive({ + mock_device.id, + build_default_response_msg(WindowCovering.ID, WindowCovering.server.commands.GoToLiftPercentage.ID, Status.SUCCESS) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Attr handler emits partially_open when report matches most-recent set level", + function() + -- Send presetPosition; preset level = 50 so MOST_RECENT_SETLEVEL = 50 (100-50=50) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 50) + }) + test.wait_for_events() + -- Attr report value=50 matches MOST_RECENT_SETLEVEL; shade stops at partial level + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 50) + }) + -- current_level was nil → partially_open from the nil-check branch + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + -- most_recent matches and value is partial → partially_open again (from the match branch) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50)) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Spontaneous level report towards open emits opening event", + function() + -- First attr establishes a partial shade level (value=10 → shadeLevel=90) + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 10) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(90)) + ) + -- Second attr moves toward open (value=5 < current zigbee 10 → opening) + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 5) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(95)) + ) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_only_HOPOsmart.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_only_HOPOsmart.lua new file mode 100755 index 0000000000..4c3028fd46 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_only_HOPOsmart.lua @@ -0,0 +1,188 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- Mock out globals +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" + +local PRIVATE_CLUSTER_ID = 0xFCC8 +local MFG_CODE = 0x1235 + +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("window-shade-only.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "HOPOsmart", + model = "A2230011", + server_clusters = {0x0000, 0xFCC8} + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) + zigbee_test_utils.init_noop_health_check_timer() +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "lifecycle - added test", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + local read_0x0000_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0000, MFG_CODE) + test.socket.zigbee:__expect_send({mock_device.id, read_0x0000_messge}) + end +) + +test.register_message_test( + "Handle Window shade open command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShade", component = "main", command = "open", args = {} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, clusters.WindowCovering.server.commands.UpOrOpen(mock_device) } + } + } +) + +test.register_message_test( + "Handle Window shade close command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShade", component = "main", command = "close", args = {} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.DownOrClose(mock_device) + } + } + } +) + +test.register_message_test( + "Handle Window shade pause command", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "windowShade", component = "main", command = "pause", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.Stop(mock_device) + } + } + } +) + +test.register_coroutine_test( + "Device reported 0 and driver emit windowShade.open", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 0 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.windowShade.windowShade.open())) + end +) + +test.register_coroutine_test( + "Device reported 1 and driver emit windowShade.opening", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 1 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.windowShade.windowShade.opening())) + end +) + +test.register_coroutine_test( + "Device reported 2 and driver emit windowShade.closed", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 2 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.windowShade.windowShade.closed())) + end +) + +test.register_coroutine_test( + "Device reported 3 and driver emit windowShade.closeing", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 3 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.windowShade.windowShade.closing())) + end +) + +test.register_coroutine_test( + "Device reported 4 and driver emit windowShade.partially_open", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 4 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.windowShade.windowShade.partially_open())) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua index b177eaacec..16d7ebf366 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -25,7 +15,14 @@ local mock_device = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() - test.mock_device.add_test_device(mock_device)end + test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) +end test.set_test_init_function(test_init) @@ -219,7 +216,42 @@ test.register_message_test( mock_device.id, clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 50) } - } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShadePreset", component = "main", + command = "setPresetPosition", args = {20} + } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShadePreset.position(20)) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShadePreset", component = "main", + command = "presetPosition", args = {} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 20) + } + }, } ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua new file mode 100755 index 0000000000..69da00efb8 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua @@ -0,0 +1,317 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- Mock out globals +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" + +local PRIVATE_CLUSTER_ID = 0xFCC9 +local MFG_CODE = 0x1235 + +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("projector-screen-VWSDSTUST120H.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "VIVIDSTORM", + model = "VWSDSTUST120H", + server_clusters = {0x0000, 0x0102, 0xFCC9} + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) + zigbee_test_utils.init_noop_health_check_timer() +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "capability - refresh", + function() + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } }) + + local read_0x0000_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0000, MFG_CODE) + local read_0x0001_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0001, MFG_CODE) + test.socket.zigbee:__expect_send({mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) + }) + test.socket.zigbee:__expect_send({mock_device.id, read_0x0000_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_0x0001_messge}) + end +) + +test.register_coroutine_test( + "lifecycle - added test", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send(mock_device:generate_test_message("hardwareFault", capabilities.hardwareFault.hardwareFault.clear())) + + local read_0x0000_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0000, MFG_CODE) + local read_0x0001_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0001, MFG_CODE) + test.socket.zigbee:__expect_send({mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) + }) + test.socket.zigbee:__expect_send({mock_device.id, read_0x0000_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_0x0001_messge}) + end +) + +test.register_message_test( + "Handle Window shade open command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShade", component = "main", command = "open", args = {} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, clusters.WindowCovering.server.commands.UpOrOpen(mock_device) } + } + } +) + +test.register_message_test( + "Handle Window shade close command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShade", component = "main", command = "close", args = {} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.DownOrClose(mock_device) + } + } + } +) + +test.register_message_test( + "Handle Window shade pause command", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "windowShade", component = "main", command = "pause", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.Stop(mock_device) + } + } + } +) + +test.register_coroutine_test( + "Handle Setlimit Delete upper limit", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "main", command ="setMode" , args = {"Delete upper limit"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + 0x0000, MFG_CODE, data_types.Uint8, 0) + }) + end +) + +test.register_coroutine_test( + "Handle Setlimit Set the upper limit", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "main", command ="setMode" , args = {"Set the upper limit"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + 0x0000, MFG_CODE, data_types.Uint8, 1) + }) + end +) + +test.register_coroutine_test( + "Handle Setlimit Delete lower limit", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "main", command ="setMode" , args = {"Delete lower limit"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + 0x0000, MFG_CODE, data_types.Uint8, 2) + }) + end +) + +test.register_coroutine_test( + "Handle Setlimit Set the lower limit", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "main", command ="setMode" , args = {"Set the lower limit"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + 0x0000, MFG_CODE, data_types.Uint8, 3) + }) + end +) + +test.register_coroutine_test( + "Device reported mode 0 and driver emit Delete upper limit", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 0 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.mode.mode("Delete upper limit"))) + end +) + +test.register_coroutine_test( + "Device reported mode 1 and driver emit Set the upper limit", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 1 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.mode.mode("Set the upper limit"))) + end +) + +test.register_coroutine_test( + "Device reported mode 2 and driver emit Delete lower limit", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 2 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.mode.mode("Delete lower limit"))) + end +) + +test.register_coroutine_test( + "Device reported mode 3 and driver emit Set the lower limit", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 3 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.mode.mode("Set the lower limit"))) + end +) + +test.register_coroutine_test( + "Device reported hardwareFault 0 and driver emit capabilities.hardwareFault.hardwareFault.clear()", + function() + local attr_report_data = { + { 0x0001, data_types.Uint8.ID, 0 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("hardwareFault", + capabilities.hardwareFault.hardwareFault.clear())) + end +) + +test.register_coroutine_test( + "Device reported hardwareFault 1 and driver emit capabilities.hardwareFault.hardwareFault.detected()", + function() + local attr_report_data = { + { 0x0001, data_types.Uint8.ID, 1 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("hardwareFault", + capabilities.hardwareFault.hardwareFault.detected())) + end +) + +test.register_coroutine_test( + "WindowCovering CurrentPositionLiftPercentage report 5 emit closing", + function() + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 5) + } + ) + test.mock_time.advance_time(5) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "WindowCovering CurrentPositionLiftPercentage report 0 emit closed", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 0) + } + ) + test.mock_time.advance_time(5) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara.lua index 37c6b501ed..050d0b34f0 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local base64 = require "st.base64" local capabilities = require "st.capabilities" @@ -39,6 +29,8 @@ local PREF_REVERSE_ON = "\x00\x02\x00\x01\x00\x00\x00" local PREF_SOFT_TOUCH_OFF = "\x00\x08\x00\x00\x00\x01\x00" local PREF_SOFT_TOUCH_ON = "\x00\x08\x00\x00\x00\x00\x00" +local SHADE_STATE_ATTRIBUTE_ID = 0x0404 + local APPLICATION_VERSION = "application_version" local mock_device = test.mock_device.build_test_zigbee_device( @@ -81,6 +73,7 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Handle added lifecycle", function() + -- The initial window shade event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -117,6 +110,37 @@ test.register_coroutine_test( cluster_base.write_manufacturer_specific_attribute(mock_device, Basic.ID, PREF_ATTRIBUTE_ID, MFG_CODE, data_types.CharString, PREF_SOFT_TOUCH_ON) }) + -- Avoid sending the initial window shade event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "stse.deviceInitialization", component_id = "main", + attribute_id = "supportedInitializedState", + state = { value = { "notInitialized", "initializing", "initialized" } }, + visibility = { displayed = false } + } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", deviceInitialization.initializedState.notInitialized()) + ) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE + , + data_types.Uint8, + 1) }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, Basic.ID, PREF_ATTRIBUTE_ID, MFG_CODE, + data_types.CharString, + PREF_REVERSE_OFF) }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, Basic.ID, PREF_ATTRIBUTE_ID, MFG_CODE, + data_types.CharString, + PREF_SOFT_TOUCH_ON) }) end ) @@ -351,8 +375,6 @@ test.register_coroutine_test( end ) --- mock_version_device - test.register_coroutine_test( "Window shade state closed with application version handler", function() @@ -434,4 +456,147 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Set Level handler", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 50 }} + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50))) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 50) + }) + end +) + +test.register_coroutine_test( + "shade state attribute handler - initial state open", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + mock_device:set_field("initState", "open") + local attr_report_data = { + { SHADE_STATE_ATTRIBUTE_ID, data_types.Uint8.ID, 0 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + AnalogOutput.attributes.PresentValue:read(mock_device) + } + ) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 0) + }) + end +) + +test.register_coroutine_test( + "shade state attribute handler - initial state close", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + + mock_device:set_field("initState", "close") + local attr_report_data = { + { SHADE_STATE_ATTRIBUTE_ID, data_types.Uint8.ID, 0 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + AnalogOutput.attributes.PresentValue:read(mock_device) + } + ) + test.mock_time.advance_time(2) + + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 100) + }) + end +) + +test.register_coroutine_test( + "shade state attribute handler - initial state reverse", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + + mock_device:set_field("initState", "reverse") + local attr_report_data = { + { SHADE_STATE_ATTRIBUTE_ID, data_types.Uint8.ID, 0 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + AnalogOutput.attributes.PresentValue:read(mock_device) + } + ) + test.mock_time.advance_time(2) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Basic.ID, PREF_ATTRIBUTE_ID, + MFG_CODE) + }) + end +) + +test.register_coroutine_test( + "shade state attribute handler - open", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + + local attr_report_data = { + { SHADE_STATE_ATTRIBUTE_ID, data_types.Uint8.ID, 1 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + end +) + +test.register_coroutine_test( + "shade state attribute handler - close", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + + local attr_report_data = { + { SHADE_STATE_ATTRIBUTE_ID, data_types.Uint8.ID, 2 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) + ) + end +) + + + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_curtain_driver_e1.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_curtain_driver_e1.lua index f9a9785429..ea389680f2 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_curtain_driver_e1.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_curtain_driver_e1.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zigbee_test_utils = require "integration_test.zigbee_test_utils" local cluster_base = require "st.zigbee.cluster_base" local clusters = require "st.zigbee.zcl.clusters" @@ -77,6 +67,7 @@ end test.register_coroutine_test( "Handle added lifecycle", function() + -- The initial window shade event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -100,6 +91,24 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(100)) ) + -- Avoid sending the initial window shade event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", initializedStateWithGuide.initializedStateWithGuide.notInitialized()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", hookLockState.hookLockState.unlocked()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", chargingState.chargingState.stopped()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.battery.battery(100)) + ) end ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_rotate.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_rotate.lua index fd8153ab96..bd9d5684e6 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_rotate.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_rotate.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local base64 = require "st.base64" local capabilities = require "st.capabilities" @@ -21,6 +11,7 @@ local SinglePrecisionFloat = require "st.zigbee.data_types".SinglePrecisionFloat local t_utils = require "integration_test.utils" local test = require "integration_test" local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local FrameCtrl = require "st.zigbee.zcl.frame_ctrl" local initializedStateWithGuide = capabilities["stse.initializedStateWithGuide"] local shadeRotateState = capabilities["stse.shadeRotateState"] @@ -39,10 +30,16 @@ local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 local MFG_CODE = 0x115F local PREF_ATTRIBUTE_ID = 0x0401 +local SHADE_STATE_ATTRIBUTE_ID = 0x0404 local PREF_REVERSE_OFF = "\x00\x02\x00\x00\x00\x00\x00" local PREF_REVERSE_ON = "\x00\x02\x00\x01\x00\x00\x00" +local MULTISTATE_CLUSTER_ID = 0x0013 +local MULTISTATE_ATTRIBUTE_ID = 0x0055 +local ROTATE_UP_VALUE = 0x0004 +local ROTATE_DOWN_VALUE = 0x0005 + local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("window-treatment-aqara-roller-shade-rotate.yml"), @@ -67,6 +64,7 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Handle added lifecycle", function() + -- The initial window shade event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -85,6 +83,28 @@ test.register_coroutine_test( mock_device:generate_test_message("main", shadeRotateState.rotateState.idle({visibility = { displayed = false }})) ) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE + , + data_types.Uint8, + 1) }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, Basic.ID, PREF_ATTRIBUTE_ID, MFG_CODE, + data_types.CharString, + PREF_REVERSE_OFF) }) + -- Avoid sending the initial window shade event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", initializedStateWithGuide.initializedStateWithGuide.notInitialized()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", shadeRotateState.rotateState.idle({visibility = { displayed = false }})) + ) + test.socket.zigbee:__expect_send({ mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE , @@ -189,24 +209,52 @@ test.register_coroutine_test( test.register_coroutine_test( "Window shade open cmd handler", function() + local attr_report_data = { + { PREF_ATTRIBUTE_ID, data_types.CharString.ID, "\x00\x00\x01\x00\x00\x00\x00" } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + initializedStateWithGuide.initializedStateWithGuide.initialized())) + test.wait_for_events() test.socket.capability:__queue_receive( { mock_device.id, { capability = "windowShade", component = "main", command = "open", args = {} } } ) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 100) + }) end ) test.register_coroutine_test( "Window shade close cmd handler", function() + local attr_report_data = { + { PREF_ATTRIBUTE_ID, data_types.CharString.ID, "\x00\x00\x01\x00\x00\x00\x00" } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + initializedStateWithGuide.initializedStateWithGuide.initialized())) + test.wait_for_events() test.socket.capability:__queue_receive( { mock_device.id, { capability = "windowShade", component = "main", command = "close", args = {} } } ) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 0) + }) end ) @@ -272,6 +320,8 @@ test.register_coroutine_test( } updates.preferences["stse.reverseRollerShadeDir"] = true test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + test.mock_time.advance_time(1) + test.socket.zigbee:__expect_send( { mock_device.id, @@ -313,4 +363,105 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "SetShadeLevel command handler", + function() + local attr_report_data = { + { PREF_ATTRIBUTE_ID, data_types.CharString.ID, "\x00\x00\x01\x00\x00\x00\x00" } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + initializedStateWithGuide.initializedStateWithGuide.initialized())) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 50 }} + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50)) + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 50) + }) + end +) + + +test.register_coroutine_test( + "PREF_ATTRIBUTE_ID attribute handler - notInitialized", + function() + local attr_report_data = { + { PREF_ATTRIBUTE_ID, data_types.CharString.ID, "\x00\x00\x00\x00\x00\x00\x00" } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + initializedStateWithGuide.initializedStateWithGuide.notInitialized())) + end +) + +test.register_coroutine_test( + "SHADE_STATE_ATTRIBUTE_ID attribute handler", + function() + local attr_report_data = { + { SHADE_STATE_ATTRIBUTE_ID, data_types.Uint8.ID, 0 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + AnalogOutput.attributes.PresentValue:read(mock_device) + }) + end +) + +test.register_coroutine_test( + "Handle sensitivity adjustment capability", + function() + local attr_report_data = { + { PREF_ATTRIBUTE_ID, data_types.CharString.ID, "\x00\x00\x01\x00\x00\x00\x00" } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + initializedStateWithGuide.initializedStateWithGuide.initialized())) + test.wait_for_events() + + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "stse.shadeRotateState", component = "main", command = "setRotateState", args = {"rotateUp"} }}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + shadeRotateState.rotateState.idle({state_change = true, visibility = { displayed = false }}) )) + + local message = cluster_base.write_manufacturer_specific_attribute(mock_device, MULTISTATE_CLUSTER_ID, + MULTISTATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint16, ROTATE_UP_VALUE) + message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) + + test.socket.zigbee:__expect_send({ mock_device.id, message }) + + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "stse.shadeRotateState", component = "main", command = "setRotateState", args = {"rotateDown"} }}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + shadeRotateState.rotateState.idle({state_change = true, visibility = { displayed = false }}) )) + + local message = cluster_base.write_manufacturer_specific_attribute(mock_device, MULTISTATE_CLUSTER_ID, + MULTISTATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint16, ROTATE_DOWN_VALUE) + message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) + + test.socket.zigbee:__expect_send({ mock_device.id, message }) + end +) + + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_axis.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_axis.lua index 3148f48965..e8faf2a33d 100755 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_axis.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_axis.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local base64 = require "st.base64" @@ -43,7 +33,14 @@ local mock_device = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() - test.mock_device.add_test_device(mock_device)end + test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) +end test.set_test_init_function(test_init) @@ -458,8 +455,9 @@ test.register_coroutine_test( } ) test.socket.capability:__set_channel_ordering("relaxed") + -- freshly joined devices will ignore the preference value test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(30)) + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50)) ) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) @@ -467,7 +465,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send( { mock_device.id, - WindowCovering.server.commands.GoToLiftPercentage(mock_device, 100 - 30) + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 100 - 50) } ) end @@ -629,6 +627,8 @@ test.register_coroutine_test( zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) }) end ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua index 747895e515..7cfb443256 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -20,6 +10,7 @@ local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local Level = clusters.Level +local WindowCovering = clusters.WindowCovering local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("window-treatment-profile.yml"), @@ -37,7 +28,14 @@ local mock_device = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() - test.mock_device.add_test_device(mock_device)end + test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) +end test.set_test_init_function(test_init) @@ -221,9 +219,10 @@ test.register_coroutine_test( { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) + -- newly joined devices will ignore the preference test.socket.zigbee:__expect_send({ mock_device.id, - Level.server.commands.MoveToLevelWithOnOff(mock_device,math.floor(30/100 * 254)) + Level.server.commands.MoveToLevelWithOnOff(mock_device,math.floor(50/100 * 254)) }) end ) @@ -257,9 +256,10 @@ test.register_coroutine_test( { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) + -- newly joined devices will ignore the preference test.socket.zigbee:__expect_send({ mock_device.id, - Level.server.commands.MoveToLevelWithOnOff(mock_device,math.floor(100/100 * 254)) + Level.server.commands.MoveToLevelWithOnOff(mock_device,math.floor(50/100 * 254)) }) end ) @@ -267,7 +267,16 @@ test.register_coroutine_test( test.register_coroutine_test( "windowShadePreset capability should be handled with preset value = 1 ", function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 1}})) + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 10}})) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {1} } + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.position(1)) + ) test.wait_for_events() test.socket.capability:__queue_receive( { @@ -285,7 +294,16 @@ test.register_coroutine_test( test.register_coroutine_test( "windowShadePreset capability should be handled with a positive preset value of < 1", function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 0}})) + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 1}})) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {0} } + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.position(0)) + ) test.wait_for_events() test.socket.capability:__queue_receive( { @@ -369,6 +387,7 @@ test.register_coroutine_test( zigbee_test_utils.mock_hub_eui, Level.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua index a0e0fbf844..db50f28d32 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zigbee_test_utils = require "integration_test.zigbee_test_utils" @@ -43,7 +33,14 @@ local mock_device = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() - test.mock_device.add_test_device(mock_device)end + test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) +end test.set_test_init_function(test_init) @@ -81,6 +78,7 @@ end test.register_coroutine_test( "Device Added ", function() + SeqNum = 0 test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send( @@ -99,6 +97,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Open handler", function() + SeqNum = 0 test.socket.zigbee:__queue_receive({ mock_device.id, build_rx_message(mock_device,"\x03\x02\x00\x04\x00\x00\x00\x00") @@ -140,6 +139,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Close handler", function() + SeqNum = 0 test.socket.zigbee:__queue_receive({ mock_device.id, build_rx_message(mock_device,"\x03\x02\x00\x04\x00\x00\x00\x64") @@ -182,6 +182,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Pause handler", function() + SeqNum = 0 test.socket.zigbee:__queue_receive({ mock_device.id, build_rx_message(mock_device,"\x03\x02\x00\x04\x00\x00\x00\x64") @@ -236,6 +237,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Set Level handler", function() + SeqNum = 0 test.socket.zigbee:__queue_receive({ mock_device.id, build_rx_message(mock_device,"\x03\x02\x00\x04\x00\x00\x00\x64") @@ -278,7 +280,14 @@ test.register_coroutine_test( test.register_coroutine_test( "Preset position handler", function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 30}})) + SeqNum = 0 + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {30}} + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.windowShadePreset.position(30))) test.wait_for_events() test.socket.zigbee:__queue_receive({ @@ -323,6 +332,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Information changed : Reverse", function() + SeqNum = 0 test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") test.socket.zigbee:__queue_receive({ mock_device.id, @@ -351,4 +361,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_rooms.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_rooms.lua index b3bc0b6c29..303397191d 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_rooms.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_rooms.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local base64 = require "st.base64" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_screen_innovations.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_screen_innovations.lua index 717731124e..c0a004ff6e 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_screen_innovations.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_screen_innovations.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -45,7 +35,14 @@ local mock_device = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() - test.mock_device.add_test_device(mock_device)end + test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) +end test.set_test_init_function(test_init) @@ -422,4 +419,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_somfy.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_somfy.lua index 2e7c140f17..fa764a5db0 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_somfy.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_somfy.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -38,7 +28,14 @@ local mock_device = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() - test.mock_device.add_test_device(mock_device)end + test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) +end test.set_test_init_function(test_init) @@ -356,7 +353,13 @@ test.register_message_test( test.register_coroutine_test( "windowShadePreset capability should be handled with preset value of 1", function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 1}})) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {1}} + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.windowShadePreset.position(1))) test.wait_for_events() test.socket.capability:__queue_receive( { @@ -374,7 +377,13 @@ test.register_coroutine_test( test.register_coroutine_test( "windowShadePreset capability should be handled with preset value of 100", function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 100}})) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {100}} + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.windowShadePreset.position(100))) test.wait_for_events() test.socket.capability:__queue_receive( { @@ -408,27 +417,15 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "windowShadePreset capability should be handled with preset value of > 100", + "windowShadePreset capability should be handled with preset value of < 1 (but positive)", function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 101}})) - test.wait_for_events() test.socket.capability:__queue_receive( { mock_device.id, - { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {0}} } ) - test.socket.zigbee:__expect_send({ - mock_device.id, - WindowCovering.server.commands.GoToLiftPercentage(mock_device, 0) - }) - end -) - -test.register_coroutine_test( - "windowShadePreset capability should be handled with preset value of < 1 (but positive)", - function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 0}})) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.windowShadePreset.position(0))) test.wait_for_events() test.socket.capability:__queue_receive( { @@ -498,4 +495,118 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "PhysicalClosedLimitLift attribute handler", + function() + --test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.PhysicalClosedLimitLift:build_test_attr_report(mock_device, 10) + } + ) + test.socket.capability:__expect_send( + { + mock_device.id, + { + capability_id = "windowShadeLevel", component_id = "main", + attribute_id = "shadeLevel", state = { value = 0 } + } + } + ) + end +) + +test.register_message_test( + "Attribute handler reports closed when level is 0", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 100) + } + }, + { + channel = "capability", + direction = "send", + message = { + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 0 } } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + } + } +) + +test.register_message_test( + "Attribute handler reports open when level is 100", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 0) + } + }, + { + channel = "capability", + direction = "send", + message = { + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 100 } } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShade.windowShade.open()) + } + } +) + +test.register_coroutine_test( + "Cancel existing poll timer when a new partial level report arrives", + function() + -- First attr: level 90 creates T1 via overwrite_existing_timer_if_needed + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 10) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 90 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + -- Second attr before T1 fires: overwrite_existing_timer_if_needed cancels T1 and stores T2 + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 15) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 85 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) + ) + -- T2 fires; T1 was cancelled so only one partially_open + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua new file mode 100755 index 0000000000..37841524c4 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua @@ -0,0 +1,436 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- Mock out globals +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("window-treatment-profile-no-firmware-update.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "Vimar", + model = "Window_Cov_Module_v1.0", + server_clusters = {0x000, 0x0003, 0x0004, 0x0005, 0x0102} + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "State transnsition from opening to partially open", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 99) + } + ) + test.socket.capability:__expect_send( + { + mock_device.id, + { + capability_id = "windowShadeLevel", component_id = "main", + attribute_id = "shadeLevel", state = { value = 1 } + } + } + ) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "State transnsition from opening to closing", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 90) + } + ) + test.socket.capability:__expect_send( + { + mock_device.id, + { + capability_id = "windowShadeLevel", component_id = "main", + attribute_id = "shadeLevel", state = { value = 10 } + } + } + ) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 95) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "windowShadeLevel", component_id = "main", + attribute_id = "shadeLevel", state = { value = 5 } + } + }) + test.mock_time.advance_time(3) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end +) + +test.register_message_test( + "Handle Window shade open command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShade", component = "main", command = "open", args = {} + } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100)) + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 0) + } + }, + } +) + +test.register_message_test( + "Handle Window shade close command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShade", component = "main", command = "close", args = {} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 100) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(0)) + }, + } +) + +test.register_message_test( + "Handle Window shade pause command", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "windowShade", component = "main", command = "pause", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.Stop(mock_device) + } + } + } +) + +test.register_message_test( + "Handle Window Shade level command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShadeLevel", component = "main", + command = "setShadeLevel", args = { 33 } + } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(33)) + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 67) + } + }, + } +) + +test.register_message_test( + "Handle Window Shade Preset command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShadePreset", component = "main", + command = "presetPosition", args = {} + } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50)) + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 50) + } + } + } +) + +test.register_coroutine_test( + "Refresh necessary attributes", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" },{ visibility = { displayed = false }})) + ) + test.wait_for_events() + + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ + mock_device.id, + { + capability = "refresh", component = "main", command = "refresh", args = {} + } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) + }) + end +) + +test.register_coroutine_test( + "Configure should configure all necessary attributes", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added"}) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" },{ visibility = { displayed = false }})) + ) + test.wait_for_events() + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:configure_reporting(mock_device, + 0, + 600, + 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + clusters.WindowCovering.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) + }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "Attribute handler reports closed when shade reaches level 0", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 100) + } + }, + { + channel = "capability", + direction = "send", + message = { + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 0 } } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + } + } +) + +test.register_message_test( + "Attribute handler reports open when shade reaches level 100", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 0) + } + }, + { + channel = "capability", + direction = "send", + message = { + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 100 } } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShade.windowShade.open()) + } + } +) + +test.register_coroutine_test( + "SetLevel command emits closing when requested level is below current level", + function() + -- Attr report sets current shade level to 90 (inverted value=10) + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 10) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 90 } } + }) + test.wait_for_events() + -- Now both vimar flags are false; requesting level 30 (< 90) triggers closing branch + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 30 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(30)) + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 70) + }) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "SetLevel command is ignored (early return) when shades are already moving", + function() + -- Open command: current=0 < 100 → opening, sets VIMAR_SHADES_OPENING=true + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "windowShade", component = "main", command = "open", args = {} } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100)) + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 0) + }) + test.wait_for_events() + -- While opening, a different setShadeLevel is requested: ignored with current level re-emitted + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 50 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100)) + ) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/vimar/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/vimar/can_handle.lua new file mode 100644 index 0000000000..1d72817eae --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/vimar/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + local FINGERPRINTS = require("vimar.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("vimar") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/vimar/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/vimar/fingerprints.lua new file mode 100644 index 0000000000..ea7f4cd3bf --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/vimar/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "Vimar", model = "Window_Cov_v1.0" }, + { mfr = "Vimar", model = "Window_Cov_Module_v1.0" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua index 6d2d6bb11d..dd5ea15aed 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua @@ -1,20 +1,10 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local utils = require "st.utils" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" +local window_shade_utils = require "window_shade_utils" local zcl_clusters = require "st.zigbee.zcl.clusters" local WindowCovering = zcl_clusters.WindowCovering local windowShade = capabilities.windowShade.windowShade @@ -27,20 +17,8 @@ local windowShade = capabilities.windowShade.windowShade local VIMAR_SHADES_OPENING = "_vimarShadesOpening" local VIMAR_SHADES_CLOSING = "_vimarShadesClosing" -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "Vimar", model = "Window_Cov_v1.0" }, - { mfr = "Vimar", model = "Window_Cov_Module_v1.0" } -} -- UTILS to check manufacturer details -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end -- ATTRIBUTE HANDLER FOR CurrentPositionLiftPercentage local function current_position_attr_handler(driver, device, value, zb_rx) @@ -124,7 +102,7 @@ end -- COMMAND HANDLER for PresetPosition local function window_shade_preset_handler(driver, device, command) - local level = device.preferences.presetPosition or device:get_field(window_preset_defaults.PRESET_LEVEL_KEY) or window_preset_defaults.PRESET_LEVEL + local level = window_shade_utils.get_preset_level(device, command.component) command.args.shadeLevel = level window_shade_set_level_handler(driver, device, command) end @@ -134,6 +112,20 @@ local device_init = function(self, device) -- Reset Status device:set_field(VIMAR_SHADES_CLOSING, false) device:set_field(VIMAR_SHADES_OPENING, false) + + -- for windowshadepreset update migration + if device:supports_capability_by_id(capabilities.windowShadePreset.ID) and + device:get_latest_state("main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME) == nil then + + -- These should only ever be nil once (and at the same time) for already-installed devices + -- It can be removed after migration is complete + device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, { visibility = { displayed = false }})) + + local preset_position = window_shade_utils.get_preset_level(device, "main") + + device:emit_event(capabilities.windowShadePreset.position(preset_position, { visibility = {displayed = false}})) + device:set_field(window_shade_utils.PRESET_LEVEL_KEY, preset_position, {persist = true}) + end end -- DRIVER HANDLER CONFIGURATION @@ -162,7 +154,7 @@ local vimar_handler = { lifecycle_handlers = { init = device_init }, - can_handle = is_zigbee_window_shade, + can_handle = require("vimar.can_handle"), } return vimar_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua b/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua new file mode 100644 index 0000000000..f3e09c20a6 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua @@ -0,0 +1,34 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local zcl_clusters = require "st.zigbee.zcl.clusters" + +local utils = {} + +utils.PRESET_LEVEL = 50 +utils.PRESET_LEVEL_KEY = "_presetLevel" + +utils.get_preset_level = function(device, component) + local level = device:get_latest_state(component, "windowShadePreset", "position") or + device:get_field(utils.PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + utils.PRESET_LEVEL + + return level +end + +utils.window_shade_preset_cmd = function(driver, device, command) + local level = device:get_latest_state(command.component, "windowShadePreset", "position") or + device:get_field(utils.PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + utils.PRESET_LEVEL + device:send_to_component(command.component, zcl_clusters.WindowCovering.server.commands.GoToLiftPercentage(device, level)) +end + +utils.set_preset_position_cmd = function(driver, device, command) + device:emit_component_event({id = command.component}, capabilities.windowShadePreset.position(command.args.position)) + device:set_field(utils.PRESET_LEVEL_KEY, command.args.position, {persist = true}) +end + +return utils diff --git a/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua b/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua new file mode 100644 index 0000000000..5b2ec304e6 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local window_treatment_utils = {} + +window_treatment_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + +return window_treatment_utils diff --git a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/can_handle.lua new file mode 100644 index 0000000000..006fa3e1bb --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_yoolax_window_shade(opts, driver, device) + local FINGERPRINTS = require("yoolax.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("yoolax") + end + end + return false +end + +return is_yoolax_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/fingerprints.lua new file mode 100644 index 0000000000..30e0dd4c62 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local YOOLAX_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "Yookee", model = "D10110" }, -- Yookee Window Treatment + { mfr = "yooksmart", model = "D10110" } -- yooksmart Window Treatment +} + +return YOOLAX_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua index d98924f54a..5a593cdf2c 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua @@ -1,41 +1,20 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" local zcl_global_commands = require "st.zigbee.zcl.global_commands" local Status = require "st.zigbee.generated.types.ZclStatus" local WindowCovering = zcl_clusters.WindowCovering +local window_shade_utils = require "window_shade_utils" local device_management = require "st.zigbee.device_management" local LEVEL_UPDATE_TIMEOUT = "__level_update_timeout" local MOST_RECENT_SETLEVEL = "__most_recent_setlevel" -local YOOLAX_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "Yookee", model = "D10110" }, -- Yookee Window Treatment - { mfr = "yooksmart", model = "D10110" } -- yooksmart Window Treatment -} -local function is_yoolax_window_shade(opts, driver, device) - for _, fingerprint in ipairs(YOOLAX_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function default_response_handler(driver, device, zb_message) local is_success = zb_message.body.zcl_body.status.value @@ -79,7 +58,8 @@ local function window_shade_level_cmd(driver, device, command) end local function window_shade_preset_cmd(driver, device, command) - set_shade_level(driver, device, device.preferences.presetPosition, command) + local level = window_shade_utils.get_preset_level(device, command.component) + set_shade_level(driver, device, level, command) end local function set_window_shade_level(level) @@ -158,7 +138,7 @@ local yoolax_window_shade = { } }, }, - can_handle = is_yoolax_window_shade + can_handle = require("yoolax.can_handle"), } return yoolax_window_shade diff --git a/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/can_handle.lua b/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/can_handle.lua new file mode 100644 index 0000000000..6ff48d2fe2 --- /dev/null +++ b/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeon_multiwhite_bulb(opts, driver, device, ...) + local FINGERPRINTS = require("aeon-multiwhite-bulb.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("aeon-multiwhite-bulb") + end + end + return false +end + +return can_handle_aeon_multiwhite_bulb diff --git a/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/fingerprints.lua b/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/fingerprints.lua new file mode 100644 index 0000000000..6ec474efa9 --- /dev/null +++ b/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local AEON_MULTIWHITE_BULB_FINGERPRINTS = { + {mfr = 0x0371, prod = 0x0103, model = 0x0001}, -- Aeon LED Bulb 6 Multi-White US + {mfr = 0x0371, prod = 0x0003, model = 0x0001}, -- Aeon LED Bulb 6 Multi-White EU + {mfr = 0x0300, prod = 0x0003, model = 0x0004} -- ilumin Tunable White +} + +return AEON_MULTIWHITE_BULB_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/init.lua b/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/init.lua index 9907fefbb5..4a9cc277ca 100644 --- a/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/init.lua +++ b/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.utils @@ -26,25 +16,12 @@ local SwitchColor = (require "st.zwave.CommandClass.SwitchColor")({ version = 3 --- @type st.zwave.CommandClass.SwitchMultilevel local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version = 4 }) -local AEON_MULTIWHITE_BULB_FINGERPRINTS = { - {mfr = 0x0371, prod = 0x0103, model = 0x0001}, -- Aeon LED Bulb 6 Multi-White US - {mfr = 0x0371, prod = 0x0003, model = 0x0001}, -- Aeon LED Bulb 6 Multi-White EU - {mfr = 0x0300, prod = 0x0003, model = 0x0004} -- ilumin Tunable White -} local WARM_WHITE_CONFIG = 0x51 local COLD_WHITE_CONFIG = 0x52 local SWITCH_COLOR_QUERY_DELAY = 2 local DEFAULT_COLOR_TEMPERATURE = 2700 -local function can_handle_aeon_multiwhite_bulb(opts, driver, device, ...) - for _, fingerprint in ipairs(AEON_MULTIWHITE_BULB_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function onoff_level_report_handler(self, device, cmd) local value = cmd.args.target_value and cmd.args.target_value or cmd.args.value @@ -126,7 +103,7 @@ local aeon_multiwhite_bulb = { [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature } }, - can_handle = can_handle_aeon_multiwhite_bulb, + can_handle = require("aeon-multiwhite-bulb.can_handle"), lifecycle_handlers = { added = device_added } diff --git a/drivers/SmartThings/zwave-bulb/src/aeotec-led-bulb-6/can_handle.lua b/drivers/SmartThings/zwave-bulb/src/aeotec-led-bulb-6/can_handle.lua new file mode 100644 index 0000000000..ebeafc2c5c --- /dev/null +++ b/drivers/SmartThings/zwave-bulb/src/aeotec-led-bulb-6/can_handle.lua @@ -0,0 +1,23 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +--- Determine whether the passed device is an Aeotec LED Bulb 6. +--- +--- @param driver Driver driver instance +--- @param device Device device isntance +--- @return boolean true if the device is an Aeotec LED Bulb 6, else false +local function is_aeotec_led_bulb_6(opts, driver, device, ...) + local AEOTEC_MFR_ID = 0x0371 + local AEOTEC_LED_BULB_6_PRODUCT_TYPE_US = 0x0103 + local AEOTEC_LED_BULB_6_PRODUCT_TYPE_EU = 0x0003 + local AEOTEC_LED_BULB_6_PRODUCT_ID = 0x0002 + if device:id_match( + AEOTEC_MFR_ID, + { AEOTEC_LED_BULB_6_PRODUCT_TYPE_US, AEOTEC_LED_BULB_6_PRODUCT_TYPE_EU }, + AEOTEC_LED_BULB_6_PRODUCT_ID) then + return true, require("aeotec-led-bulb-6") + end + return false +end + +return is_aeotec_led_bulb_6 diff --git a/drivers/SmartThings/zwave-bulb/src/aeotec-led-bulb-6/init.lua b/drivers/SmartThings/zwave-bulb/src/aeotec-led-bulb-6/init.lua index 67755878db..e4886c5897 100644 --- a/drivers/SmartThings/zwave-bulb/src/aeotec-led-bulb-6/init.lua +++ b/drivers/SmartThings/zwave-bulb/src/aeotec-led-bulb-6/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.utils @@ -26,10 +16,6 @@ local SwitchColor = (require "st.zwave.CommandClass.SwitchColor")({ version=3 }) --- @type st.zwave.CommandClass.SwitchMultilevel local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version=4 }) -local AEOTEC_MFR_ID = 0x0371 -local AEOTEC_LED_BULB_6_PRODUCT_TYPE_US = 0x0103 -local AEOTEC_LED_BULB_6_PRODUCT_TYPE_EU = 0x0003 -local AEOTEC_LED_BULB_6_PRODUCT_ID = 0x0002 local WARM_WHITE_CONFIG = 0x51 local COLD_WHITE_CONFIG = 0x52 @@ -102,18 +88,6 @@ function capability_handlers.refresh(driver, device) device:send(Configuration:Get({ parameter_number=COLD_WHITE_CONFIG })) end ---- Determine whether the passed device is an Aeotec LED Bulb 6. ---- ---- @param driver Driver driver instance ---- @param device Device device isntance ---- @return boolean true if the device is an Aeotec LED Bulb 6, else false -local function is_aeotec_led_bulb_6(opts, driver, device, ...) - return device:id_match( - AEOTEC_MFR_ID, - { AEOTEC_LED_BULB_6_PRODUCT_TYPE_US, AEOTEC_LED_BULB_6_PRODUCT_TYPE_EU }, - AEOTEC_LED_BULB_6_PRODUCT_ID) -end - local aeotec_led_bulb_6 = { NAME = "Aeotec LED Bulb 6", zwave_handlers = { @@ -129,7 +103,7 @@ local aeotec_led_bulb_6 = { [capabilities.refresh.commands.refresh.NAME] = capability_handlers.refresh } }, - can_handle = is_aeotec_led_bulb_6, + can_handle = require("aeotec-led-bulb-6.can_handle"), } return aeotec_led_bulb_6 diff --git a/drivers/SmartThings/zwave-bulb/src/fibaro-rgbw-controller/can_handle.lua b/drivers/SmartThings/zwave-bulb/src/fibaro-rgbw-controller/can_handle.lua new file mode 100644 index 0000000000..aeef24b0ea --- /dev/null +++ b/drivers/SmartThings/zwave-bulb/src/fibaro-rgbw-controller/can_handle.lua @@ -0,0 +1,20 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_fibaro_rgbw_controller(opts, driver, device, ...) + local FIBARO_MFR_ID = 0x010F + local FIBARO_RGBW_CONTROLLER_PROD_TYPE = 0x0900 + local FIBARO_RGBW_CONTROLLER_PROD_ID_US = 0x2000 + local FIBARO_RGBW_CONTROLLER_PROD_ID_EU = 0x1000 + + if device:id_match( + FIBARO_MFR_ID, + FIBARO_RGBW_CONTROLLER_PROD_TYPE, + {FIBARO_RGBW_CONTROLLER_PROD_ID_US, FIBARO_RGBW_CONTROLLER_PROD_ID_EU} + ) then + return true, require("fibaro-rgbw-controller") + end + return false +end + +return is_fibaro_rgbw_controller diff --git a/drivers/SmartThings/zwave-bulb/src/fibaro-rgbw-controller/init.lua b/drivers/SmartThings/zwave-bulb/src/fibaro-rgbw-controller/init.lua index 80dc8d4dc7..b83be97c23 100644 --- a/drivers/SmartThings/zwave-bulb/src/fibaro-rgbw-controller/init.lua +++ b/drivers/SmartThings/zwave-bulb/src/fibaro-rgbw-controller/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.Association @@ -33,19 +23,6 @@ local CAP_CACHE_KEY = "st.capabilities." .. capabilities.colorControl.ID local LAST_COLOR_SWITCH_CMD_FIELD = "lastColorSwitchCmd" local FAKE_RGB_ENDPOINT = 10 -local FIBARO_MFR_ID = 0x010F -local FIBARO_RGBW_CONTROLLER_PROD_TYPE = 0x0900 -local FIBARO_RGBW_CONTROLLER_PROD_ID_US = 0x2000 -local FIBARO_RGBW_CONTROLLER_PROD_ID_EU = 0x1000 - -local function is_fibaro_rgbw_controller(opts, driver, device, ...) - return device:id_match( - FIBARO_MFR_ID, - FIBARO_RGBW_CONTROLLER_PROD_TYPE, - {FIBARO_RGBW_CONTROLLER_PROD_ID_US, FIBARO_RGBW_CONTROLLER_PROD_ID_EU} - ) -end - -- This handler is copied from defaults with scraped of sets for both WHITE channels local function set_color(driver, device, command) local r, g, b = utils.hsl_to_rgb(command.args.color.hue, command.args.color.saturation, command.args.color.lightness) @@ -201,7 +178,7 @@ local fibaro_rgbw_controller = { added = device_added, init = device_init }, - can_handle = is_fibaro_rgbw_controller, + can_handle = require("fibaro-rgbw-controller.can_handle"), } return fibaro_rgbw_controller diff --git a/drivers/SmartThings/zwave-bulb/src/init.lua b/drivers/SmartThings/zwave-bulb/src/init.lua index 62079594fa..58e2f32a7e 100644 --- a/drivers/SmartThings/zwave-bulb/src/init.lua +++ b/drivers/SmartThings/zwave-bulb/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.Driver @@ -30,11 +20,7 @@ local driver_template = { capabilities.colorTemperature, capabilities.powerMeter }, - sub_drivers = { - require("aeotec-led-bulb-6"), - require("aeon-multiwhite-bulb"), - require("fibaro-rgbw-controller") - }, + sub_drivers = require("sub_drivers"), } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities, {native_capability_cmds_enabled = true}) diff --git a/drivers/SmartThings/zwave-bulb/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-bulb/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-bulb/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-bulb/src/sub_drivers.lua b/drivers/SmartThings/zwave-bulb/src/sub_drivers.lua new file mode 100644 index 0000000000..842e2056bd --- /dev/null +++ b/drivers/SmartThings/zwave-bulb/src/sub_drivers.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("aeotec-led-bulb-6"), + lazy_load_if_possible("aeon-multiwhite-bulb"), + lazy_load_if_possible("fibaro-rgbw-controller"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-bulb/src/test/test_aeon_multiwhite_bulb.lua b/drivers/SmartThings/zwave-bulb/src/test/test_aeon_multiwhite_bulb.lua index d6cdc044e0..7ae91bc853 100644 --- a/drivers/SmartThings/zwave-bulb/src/test/test_aeon_multiwhite_bulb.lua +++ b/drivers/SmartThings/zwave-bulb/src/test/test_aeon_multiwhite_bulb.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-bulb/src/test/test_aeotec_led_bulb_6.lua b/drivers/SmartThings/zwave-bulb/src/test/test_aeotec_led_bulb_6.lua index 0a1fa7e955..8757c4e11a 100644 --- a/drivers/SmartThings/zwave-bulb/src/test/test_aeotec_led_bulb_6.lua +++ b/drivers/SmartThings/zwave-bulb/src/test/test_aeotec_led_bulb_6.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-bulb/src/test/test_fibaro_rgbw_controller.lua b/drivers/SmartThings/zwave-bulb/src/test/test_fibaro_rgbw_controller.lua index 0fd3e98995..1610440aea 100644 --- a/drivers/SmartThings/zwave-bulb/src/test/test_fibaro_rgbw_controller.lua +++ b/drivers/SmartThings/zwave-bulb/src/test/test_fibaro_rgbw_controller.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-bulb/src/test/test_zwave_bulb.lua b/drivers/SmartThings/zwave-bulb/src/test/test_zwave_bulb.lua index cf704bec2d..9d5e199927 100644 --- a/drivers/SmartThings/zwave-bulb/src/test/test_zwave_bulb.lua +++ b/drivers/SmartThings/zwave-bulb/src/test/test_zwave_bulb.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-button/src/test/test_zwave_multi_button.lua b/drivers/SmartThings/zwave-button/src/test/test_zwave_multi_button.lua index 27c9b851f9..3ddf585681 100644 --- a/drivers/SmartThings/zwave-button/src/test/test_zwave_multi_button.lua +++ b/drivers/SmartThings/zwave-button/src/test/test_zwave_multi_button.lua @@ -864,4 +864,30 @@ test.register_coroutine_test( end ) +test.register_message_test( + "Central scene notification with scene_number beyond profile buttons falls back to main component", + { + { + channel = "zwave", + direction = "receive", + message = { + mock_aeotec_wallmote_quad.id, + zw_test_utils.zwave_test_build_receive_command( + CentralScene:Notification({ key_attributes = CentralScene.key_attributes.KEY_PRESSED_1_TIME, scene_number = 5 }) + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_aeotec_wallmote_quad:generate_test_message("main", capabilities.button.button.pushed({ state_change = true })) + }, + { + channel = "capability", + direction = "send", + message = mock_aeotec_wallmote_quad:generate_test_message("main", capabilities.button.button.pushed({ state_change = true })) + } + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/can_handle.lua b/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/can_handle.lua new file mode 100644 index 0000000000..a0bcfde376 --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeon_meter(opts, driver, device, ...) + local FINGERPRINTS = require("aeon-meter.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("aeon-meter") + end + end + return false +end + +return can_handle_aeon_meter diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/fingerprints.lua b/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/fingerprints.lua new file mode 100644 index 0000000000..5970f40d6d --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local AEON_FINGERPRINTS = { + {mfr = 0x0086, prod = 0x0002, model = 0x0009}, -- DSB09xxx-ZWUS + {mfr = 0x0086, prod = 0x0002, model = 0x0001}, -- DSB28-ZWEU +} + +return AEON_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/init.lua b/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/init.lua index 9f5ec4bee4..334bbb71cd 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/init.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/init.lua @@ -1,34 +1,9 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=1 }) -local AEON_FINGERPRINTS = { - {mfr = 0x0086, prod = 0x0002, model = 0x0009}, -- DSB09xxx-ZWUS - {mfr = 0x0086, prod = 0x0002, model = 0x0001}, -- DSB28-ZWEU -} - -local function can_handle_aeon_meter(opts, driver, device, ...) - for _, fingerprint in ipairs(AEON_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end - local do_configure = function (self, device) device:send(Configuration:Set({parameter_number = 101, size = 4, configuration_value = 4})) -- combined power in watts... device:send(Configuration:Set({parameter_number = 111, size = 4, configuration_value = 300})) -- ...every 5 min @@ -42,7 +17,7 @@ local aeon_meter = { doConfigure = do_configure }, NAME = "aeon meter", - can_handle = can_handle_aeon_meter + can_handle = require("aeon-meter.can_handle"), } return aeon_meter diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/can_handle.lua b/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/can_handle.lua new file mode 100644 index 0000000000..356dbd55df --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeotec_gen5_meter(opts, driver, device, ...) + local FINGERPRINTS = require("aeotec-gen5-meter.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("aeotec-gen5-meter") + end + end + return false +end + +return can_handle_aeotec_gen5_meter diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/fingerprints.lua b/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/fingerprints.lua new file mode 100644 index 0000000000..3863c27b51 --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local AEOTEC_GEN5_FINGERPRINTS = { + {mfr = 0x0086, prod = 0x0102, model = 0x005F}, -- Aeotec Home Energy Meter (Gen5) US + {mfr = 0x0086, prod = 0x0002, model = 0x005F}, -- Aeotec Home Energy Meter (Gen5) EU +} + +return AEOTEC_GEN5_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/init.lua b/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/init.lua index 870e104891..1633427372 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/init.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/init.lua @@ -1,34 +1,9 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=1 }) -local AEOTEC_GEN5_FINGERPRINTS = { - {mfr = 0x0086, prod = 0x0102, model = 0x005F}, -- Aeotec Home Energy Meter (Gen5) US - {mfr = 0x0086, prod = 0x0002, model = 0x005F}, -- Aeotec Home Energy Meter (Gen5) EU -} - -local function can_handle_aeotec_gen5_meter(opts, driver, device, ...) - for _, fingerprint in ipairs(AEOTEC_GEN5_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end - local do_configure = function (self, device) device:send(Configuration:Set({parameter_number = 101, size = 4, configuration_value = 3})) -- report total power in Watts and total energy in kWh... device:send(Configuration:Set({parameter_number = 102, size = 4, configuration_value = 0})) -- disable group 2... @@ -43,7 +18,7 @@ local aeotec_gen5_meter = { doConfigure = do_configure }, NAME = "aeotec gen5 meter", - can_handle = can_handle_aeotec_gen5_meter + can_handle = require("aeotec-gen5-meter.can_handle"), } return aeotec_gen5_meter diff --git a/drivers/SmartThings/zwave-electric-meter/src/init.lua b/drivers/SmartThings/zwave-electric-meter/src/init.lua index 2ef9f20281..66205758bd 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/init.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.defaults @@ -31,11 +21,7 @@ local driver_template = { lifecycle_handlers = { added = device_added }, - sub_drivers = { - require("qubino-meter"), - require("aeotec-gen5-meter"), - require("aeon-meter") - } + sub_drivers = require("sub_drivers"), } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-electric-meter/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-electric-meter/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/can_handle.lua b/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/can_handle.lua new file mode 100644 index 0000000000..dd9ae17949 --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_qubino_meter(opts, driver, device, ...) + local FINGERPRINTS = require("qubino-meter.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("qubino-meter") + end + end + return false +end + +return can_handle_qubino_meter diff --git a/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/fingerprints.lua b/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/fingerprints.lua new file mode 100644 index 0000000000..5a5fd44be5 --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local QUBINO_FINGERPRINTS = { + {mfr = 0x0159, prod = 0x0007, model = 0x0054}, -- Qubino 3 Phase Meter + {mfr = 0x0159, prod = 0x0007, model = 0x0052} -- Qubino Smart Meter +} + +return QUBINO_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/init.lua b/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/init.lua index a8e2b23805..4c2a40a56e 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/init.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.Configuration @@ -20,22 +10,10 @@ local Meter = (require "st.zwave.CommandClass.Meter")({ version=3 }) --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" -local QUBINO_FINGERPRINTS = { - {mfr = 0x0159, prod = 0x0007, model = 0x0054}, -- Qubino 3 Phase Meter - {mfr = 0x0159, prod = 0x0007, model = 0x0052} -- Qubino Smart Meter -} local POWER_UNIT_WATT = "W" local ENERGY_UNIT_KWH = "kWh" -local function can_handle_qubino_meter(opts, driver, device, ...) - for _, fingerprint in ipairs(QUBINO_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function meter_report_handler(self, device, cmd) @@ -101,7 +79,7 @@ local qubino_meter = { init = device_init }, NAME = "qubino meter", - can_handle = can_handle_qubino_meter + can_handle = require("qubino-meter.can_handle"), } return qubino_meter diff --git a/drivers/SmartThings/zwave-electric-meter/src/sub_drivers.lua b/drivers/SmartThings/zwave-electric-meter/src/sub_drivers.lua new file mode 100644 index 0000000000..60d4a7380b --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/sub_drivers.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("qubino-meter"), + lazy_load_if_possible("aeotec-gen5-meter"), + lazy_load_if_possible("aeon-meter"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-electric-meter/src/test/test_aeon_meter.lua b/drivers/SmartThings/zwave-electric-meter/src/test/test_aeon_meter.lua index d82e1549fa..cb92a861fe 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/test/test_aeon_meter.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/test/test_aeon_meter.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_gen5_meter.lua b/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_gen5_meter.lua index 09b897ba9d..29255434d1 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_gen5_meter.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_gen5_meter.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_3_phase_meter.lua b/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_3_phase_meter.lua index dad4b1030e..55f1b9e436 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_3_phase_meter.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_3_phase_meter.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_smart_meter.lua b/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_smart_meter.lua index 11a1317a74..47391c078f 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_smart_meter.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_smart_meter.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-electric-meter/src/test/test_zwave_electric_meter.lua b/drivers/SmartThings/zwave-electric-meter/src/test/test_zwave_electric_meter.lua index cf640c3655..01c3227147 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/test/test_zwave_electric_meter.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/test/test_zwave_electric_meter.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-lock/fingerprints.yml b/drivers/SmartThings/zwave-lock/fingerprints.yml index 29d07e956c..a472db1613 100644 --- a/drivers/SmartThings/zwave-lock/fingerprints.yml +++ b/drivers/SmartThings/zwave-lock/fingerprints.yml @@ -375,6 +375,13 @@ zwaveManufacturer: productType: 0x0005 productId: 0x0001 deviceProfileName: lock-battery + # ULTRALOQ + - id: "1106/4/2" + deviceLabel: ULTRALOQ Bolt Z-Wave Smart Deadbolt + manufacturerId: 0x0452 + productId: 0x0002 + productType: 0x0004 + deviceProfileName: base-lock zwaveGeneric: - id: "GenericZwaveLock" deviceLabel: Door Lock diff --git a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/can_handle.lua new file mode 100644 index 0000000000..8a9b8cc6cc --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, cmd, ...) + local cc = require "st.zwave.CommandClass" + local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) + local version = require "version" + if version.api == 6 and + cmd.cmd_class == cc.WAKE_UP and + cmd.cmd_id == WakeUp.NOTIFICATION then + return true, require("apiv6_bugfix") + end + return false +end + +return can_handle diff --git a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua index 0204b7b2d5..94dc5975ab 100644 --- a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua +++ b/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua @@ -1,14 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - return version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION -end - local function wakeup_notification(driver, device, cmd) device:refresh() end @@ -20,7 +15,7 @@ local apiv6_bugfix = { } }, NAME = "apiv6_bugfix", - can_handle = can_handle + can_handle = require("apiv6_bugfix.can_handle"), } return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index b83b196256..925452c431 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -182,13 +171,7 @@ local driver_template = { [Time.GET] = time_get_handler -- used by DanaLock } }, - sub_drivers = { - require("zwave-alarm-v1-lock"), - require("schlage-lock"), - require("samsung-lock"), - require("keywe-lock"), - require("apiv6_bugfix"), - } + sub_drivers = require("sub_drivers"), } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua new file mode 100644 index 0000000000..d8bcd5756e --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_keywe_lock(opts, self, device, cmd, ...) + local KEYWE_MFR = 0x037B + if device.zwave_manufacturer_id == KEYWE_MFR then + return true, require("keywe-lock") + end + return false +end + +return can_handle_keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua b/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua index d39aa45d1c..a51af26e00 100644 --- a/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -23,13 +13,8 @@ local LockDefaults = require "st.zwave.defaults.lock" local LockCodesDefaults = require "st.zwave.defaults.lockCodes" local TamperDefaults = require "st.zwave.defaults.tamperAlert" -local KEYWE_MFR = 0x037B local TAMPER_CLEAR_DELAY = 10 -local function can_handle_keywe_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == KEYWE_MFR -end - local function clear_tamper_if_needed(device) local current_tamper_state = device:get_latest_state("main", capabilities.tamperAlert.ID, capabilities.tamperAlert.tamper.NAME) if current_tamper_state == "detected" then @@ -80,7 +65,7 @@ local keywe_lock = { doConfigure = do_configure }, NAME = "Keywe Lock", - can_handle = can_handle_keywe_lock, + can_handle = require("keywe-lock.can_handle"), } return keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua new file mode 100644 index 0000000000..e9222cb8fb --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_samsung_lock(opts, self, device, cmd, ...) + local SAMSUNG_MFR = 0x022E + if device.zwave_manufacturer_id == SAMSUNG_MFR then + return true, require("samsung-lock") + end + return false +end + +return can_handle_samsung_lock diff --git a/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua b/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua index 813c6217b4..b2f4f60975 100644 --- a/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -28,11 +18,6 @@ local get_lock_codes = LockCodesDefaults.get_lock_codes local clear_code_state = LockCodesDefaults.clear_code_state local code_deleted = LockCodesDefaults.code_deleted -local SAMSUNG_MFR = 0x022E - -local function can_handle_samsung_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == SAMSUNG_MFR -end local function get_ongoing_code_set(device) local code_id @@ -105,7 +90,7 @@ local samsung_lock = { doConfigure = do_configure }, NAME = "Samsung Lock", - can_handle = can_handle_samsung_lock, + can_handle = require("samsung-lock.can_handle"), } return samsung_lock diff --git a/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua new file mode 100644 index 0000000000..e9f3cfb84c --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_schlage_lock(opts, self, device, cmd, ...) + local SCHLAGE_MFR = 0x003B + if device.zwave_manufacturer_id == SCHLAGE_MFR then + return true, require("schlage-lock") + end + return false +end + +return can_handle_schlage_lock diff --git a/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua b/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua index 67e649d869..6b22049beb 100644 --- a/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -27,15 +17,10 @@ local Association = (require "st.zwave.CommandClass.Association")({version=1}) local LockCodesDefaults = require "st.zwave.defaults.lockCodes" -local SCHLAGE_MFR = 0x003B local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} local DEFAULT_COMMANDS_DELAY = 4.2 -- seconds -local function can_handle_schlage_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == SCHLAGE_MFR -end - local function set_code_length(self, device, cmd) local length = cmd.args.length if length >= 4 and length <= 8 then @@ -187,7 +172,7 @@ local schlage_lock = { doConfigure = do_configure, }, NAME = "Schlage Lock", - can_handle = can_handle_schlage_lock, + can_handle = require("schlage-lock.can_handle"), } return schlage_lock diff --git a/drivers/SmartThings/zwave-lock/src/sub_drivers.lua b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua new file mode 100644 index 0000000000..46700ce154 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("zwave-alarm-v1-lock"), + lazy_load_if_possible("schlage-lock"), + lazy_load_if_possible("samsung-lock"), + lazy_load_if_possible("keywe-lock"), + lazy_load_if_possible("apiv6_bugfix"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua index d8e8ecc1bc..0266391d85 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua index 9aac02c6b2..7667842e61 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua index 7707b8a850..9dc1e38bcf 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua index b1a5964502..189184f19e 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua index 52144295b3..b105707c7d 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua index e4a9b50758..a6b0b5a2a3 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua new file mode 100644 index 0000000000..7bb54f23f2 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_v1_alarm(opts, driver, device, cmd, ...) + if opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 then + return true, require("zwave-alarm-v1-lock") + end + return false +end + +return can_handle_v1_alarm diff --git a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua index 44d978999b..d7c862f22a 100644 --- a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -35,9 +25,6 @@ local METHOD = { --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is smoke co alarm -local function can_handle_v1_alarm(opts, driver, device, cmd, ...) - return opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 -end --- Default handler for alarm command class reports, these were largely OEM-defined --- @@ -159,7 +146,7 @@ local zwave_lock = { } }, NAME = "Z-Wave lock alarm V1", - can_handle = can_handle_v1_alarm, + can_handle = require("zwave-alarm-v1-lock.can_handle"), } return zwave_lock diff --git a/drivers/SmartThings/zwave-sensor/fingerprints.yml b/drivers/SmartThings/zwave-sensor/fingerprints.yml index 0d04f7aff9..0810b20a38 100644 --- a/drivers/SmartThings/zwave-sensor/fingerprints.yml +++ b/drivers/SmartThings/zwave-sensor/fingerprints.yml @@ -446,11 +446,23 @@ zwaveManufacturer: productId: 0x000B deviceProfileName: contact-battery-tamperalert - id: 027A/7000/E001 - deviceLabel: Zooz Open/Closed Sensor + deviceLabel: Open Close XS Sensor manufacturerId: 0x027A productType: 0x7000 productId: 0xE001 - deviceProfileName: contact-battery-tamperalert + deviceProfileName: base-contact + - id: 027A/0201/0006 + deviceLabel: Q Sensor + manufacturerId: 0x027A + productType: 0x0201 + productId: 0x0006 + deviceProfileName: multi-functional-motion + - id: 027A/7000/E004 + deviceLabel: Temperature Humidity XS Sensor + manufacturerId: 0x027A + productType: 0x7000 + productId: 0xE004 + deviceProfileName: humidity-temperature-battery - id: "aeotec/multisensor/7" deviceLabel: Aeotec Multipurpose Sensor manufacturerId: 0x0371 @@ -502,12 +514,12 @@ zwaveManufacturer: productType: 0x4C47 productId: 0x4C44 deviceProfileName: base-water - - id: 027A/7000/E002 - deviceLabel: Zooz Water Leak Sensor + - id: "Zooz/ZSE42" + deviceLabel: Zooz ZSE42 XS Water Leak Sensor manufacturerId: 0x027A productType: 0x7000 productId: 0xE002 - deviceProfileName: base-water + deviceProfileName: zooz-zse42-water - id: 010F/0800 deviceLabel: Fibaro Motion Sensor manufacturerId: 0x010F @@ -542,6 +554,12 @@ zwaveManufacturer: productType: 0x0000 productId: 0x0001 deviceProfileName: base-water + - id: shelly/wave/motion + deviceLabel: Shelly Wave Motion + manufacturerId: 0x0460 + productType: 0x0100 + productId: 0x0082 + deviceProfileName: shelly-wave-motion zwaveGeneric: - id: "GenericSensorAlarm" deviceLabel: Z-Wave Sensor diff --git a/drivers/SmartThings/zwave-sensor/profiles/shelly-wave-motion.yml b/drivers/SmartThings/zwave-sensor/profiles/shelly-wave-motion.yml new file mode 100644 index 0000000000..5bbeab8ee5 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/profiles/shelly-wave-motion.yml @@ -0,0 +1,59 @@ +name: shelly-wave-motion +components: +- id: main + capabilities: + - id: motionSensor + version: 1 + - id: illuminanceMeasurement + version: 1 + config: + values: + - key: "illuminance.value" + range: [0, 10000] + - id: battery + version: 1 + - id: refresh + version: 1 + categories: + - name: ContactSensor +preferences: + - name: "ledOpnClsChangeStat" + title: "P157: open/close change status" + description: "This parameter enables open/close status change by LED indicator." + required: false + preferenceType: enumeration + definition: + options: + 0 : "LED ind.disabled" + 1 : "LED ind.enabled" + default: 0 + - name: "sensitivity" + title: "P158: sensitivity" + description: "Sensitivity" + + required: false + preferenceType: enumeration + definition: + options: + 0 : "low sensitivity" + 1 : "moderate sensitivity" + 2 : "high sensitivity" + default: 0 + - name: "blindTime" + title: "P159: Motion Blind time" + description: "Blind time in seconds after last detected motion" + required: false + preferenceType: integer + definition: + minimum: 2 + maximum : 8 + default: 5 + - name: "motionNotdetRepT" + title: "P160:Motion not detect.rep.time" + description: "Time after last detected motion for device to send motion not detected" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum : 32767 + default: 30 \ No newline at end of file diff --git a/drivers/SmartThings/zwave-sensor/profiles/zooz-zse42-water.yml b/drivers/SmartThings/zwave-sensor/profiles/zooz-zse42-water.yml new file mode 100644 index 0000000000..1a497e7438 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/profiles/zooz-zse42-water.yml @@ -0,0 +1,52 @@ +name: zooz-zse42-water +components: + - id: main + capabilities: + - id: waterSensor + version: 1 + - id: battery + version: 1 + - id: refresh + version: 1 + - id: firmwareUpdate + version: 1 + categories: + - name: LeakSensor +preferences: + #param 1 + - name: "ledIndicator" + title: "LED Indicator" + description: "When enabled the LED indicator will blink continuously when water is detected." + required: false + preferenceType: enumeration + definition: + options: + 1: "Enabled *" + 0: "Disabled" + default: 1 + #param 2 + - name: "leakAlertClearDelay" + title: "Leak Alert Clear Delay" + description: "Default = 0; How long the sensor will wait before sending a 'dry' report to your hub after water is no longer detected." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 3600 + default: 0 + #param 4 + - name: "lowBatteryAlert" + title: "Low Battery Alert" + description: "Battery level threshold for low battery reports." + required: false + preferenceType: enumeration + definition: + options: + 10: "10%" + 15: "15%" + 20: "20% *" + 25: "25%" + 30: "30%" + 40: "40%" + 50: "50%" + default: 20 diff --git a/drivers/SmartThings/zwave-sensor/src/configurations.lua b/drivers/SmartThings/zwave-sensor/src/configurations.lua index 89c89428fa..2883e70384 100644 --- a/drivers/SmartThings/zwave-sensor/src/configurations.lua +++ b/drivers/SmartThings/zwave-sensor/src/configurations.lua @@ -12,6 +12,7 @@ -- See the License for the specific language governing permissions and -- limitations under the License. +local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) --- @type st.zwave.CommandClass.Association @@ -345,6 +346,20 @@ local devices = { ASSOCIATION = { {grouping_identifier = 1} } + }, + ZOOZ_ZSE42_WATER_LEAK = { + MATCHING_MATRIX = { + mfrs = 0x027A, + product_types = 0x7000, + product_ids = 0xE002 + }, + BATTERY = { + quantity = 1, + type = "CR2450" + }, + WAKE_UP = { + { seconds = 21600 } --6 hours + } } } local configurations = {} @@ -376,6 +391,11 @@ configurations.initial_configuration = function(driver, device) device:send(WakeUp:IntervalSet({seconds = value.seconds, node_id = _node_id})) end end + local battery = configurations.get_device_battery(device) + if battery ~= nil then + device:emit_event(capabilities.battery.quantity({ value = battery.quantity or 1 })) + device:emit_event(capabilities.battery.type({ value = battery.type or "Unspecified" })) + end end configurations.get_device_configuration = function(zw_device) @@ -426,4 +446,16 @@ configurations.get_device_wake_up = function(zw_device) return nil end +configurations.get_device_battery = function(zw_device) + for _, device in pairs(devices) do + if zw_device:id_match( + device.MATCHING_MATRIX.mfrs, + device.MATCHING_MATRIX.product_types, + device.MATCHING_MATRIX.product_ids) then + return device.BATTERY + end + end + return nil +end + return configurations diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua index 63f4d8d8a0..698fffcceb 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua @@ -66,13 +66,18 @@ local function do_configure(driver, device) device:send(Association:Remove({grouping_identifier = 1, node_ids = driver.environment_info.hub_zwave_id})) end +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + local function device_added(driver, device) do_refresh(driver, device) - device:emit_event(capabilities.tamperAlert.tamper.clear()) - device:emit_event(capabilities.contactSensor.contact.open()) + emit_event_if_latest_state_missing(device, "main", capabilities.contactSensor, capabilities.contactSensor.contact.NAME, capabilities.contactSensor.contact.open()) + emit_event_if_latest_state_missing(device, "main", capabilities.tamperAlert, capabilities.tamperAlert.tamper.NAME, capabilities.tamperAlert.tamper.clear()) end - local fibaro_door_window_sensor_1 = { NAME = "fibaro door window sensor 1", lifecycle_handlers = { diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua index 6290f15a41..16c5ec2017 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua @@ -33,10 +33,16 @@ local function can_handle_fibaro_door_window_sensor_2(opts, driver, device, cmd, return false end +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + local function device_added(self, device) - device:emit_event(capabilities.tamperAlert.tamper.clear()) - device:emit_event(capabilities.contactSensor.contact.open()) - device:emit_event(capabilities.temperatureAlarm.temperatureAlarm.cleared()) + emit_event_if_latest_state_missing(device, "main", capabilities.tamperAlert, capabilities.tamperAlert.tamper.NAME, capabilities.tamperAlert.tamper.clear()) + emit_event_if_latest_state_missing(device, "main", capabilities.contactSensor, capabilities.contactSensor.contact.NAME, capabilities.contactSensor.contact.open()) + emit_event_if_latest_state_missing(device, "main", capabilities.temperatureAlarm, capabilities.temperatureAlarm.temperatureAlarm.NAME, capabilities.temperatureAlarm.temperatureAlarm.cleared()) end local function alarm_report_handler(self, device, cmd) diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua index a407322212..144be985ae 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua @@ -22,7 +22,6 @@ local SensorAlarm = (require "st.zwave.CommandClass.SensorAlarm")({ version = 1 --- @type st.zwave.CommandClass.SensorBinary local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 2 }) --- @type st.zwave.CommandClass.SensorMultilevel -local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ version = 5 }) local preferences = require "preferences" local configurations = require "configurations" @@ -73,14 +72,6 @@ local function sensor_binary_report_handler(self, device, cmd) device:emit_event(event) end -local function sensor_multilevel_report_handler(self, device, cmd) - if (cmd.args.sensor_type == SensorMultilevel.sensor_type.TEMPERATURE) then - local scale = 'C' - if (cmd.args.scale == SensorMultilevel.scale.temperature.FAHRENHEIT) then scale = 'F' end - device:emit_event(capabilities.temperatureMeasurement.temperature({value = cmd.args.sensor_value, unit = scale})) - end -end - local function do_configure(driver, device) configurations.initial_configuration(driver, device) -- The flood sensor can be hardwired, so update any preferences @@ -101,9 +92,6 @@ local fibaro_flood_sensor = { [cc.SENSOR_BINARY] = { [SensorBinary.REPORT] = sensor_binary_report_handler }, - [cc.SENSOR_MULTILEVEL] = { - [SensorMultilevel.REPORT] = sensor_multilevel_report_handler - } }, lifecycle_handlers = { doConfigure = do_configure diff --git a/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua b/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua new file mode 100644 index 0000000000..058a7f955c --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua @@ -0,0 +1,91 @@ +-- 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 capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.Version +local Version = (require "st.zwave.CommandClass.Version")({ version = 1 }) +--- @type st.zwave.CommandClass.WakeUp +local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) + +--This sub_driver will populate the currentVersion (firmware) when the firmwareUpdate capability is enabled +local FINGERPRINTS = { + { manufacturerId = 0x027A, productType = 0x7000, productId = 0xE002 } -- Zooz ZSE42 Water Sensor +} + +local function can_handle_fw(opts, driver, device, ...) + if device:supports_capability_by_id(capabilities.firmwareUpdate.ID) then + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subDriver = require("firmware-version") + return true, subDriver + end + end + end + return false +end + +--Runs upstream handlers (ex zwave_handlers) +local function call_parent_handler(handlers, self, device, event, args) + for _, func in ipairs(handlers or {}) do + func(self, device, event, args) + end +end + +--Request version if not populated yet +local function send_version_get(driver, device) + if device:get_latest_state("main", capabilities.firmwareUpdate.ID, capabilities.firmwareUpdate.currentVersion.NAME) == nil then + device:send(Version:Get({})) + end +end + +local function version_report(driver, device, cmd) + local major = cmd.args.application_version + local minor = cmd.args.application_sub_version + local fmtFirmwareVersion = string.format("%d.%02d", major, minor) + device:emit_event(capabilities.firmwareUpdate.currentVersion({ value = fmtFirmwareVersion })) +end + +local function wakeup_notification(driver, device, cmd) + send_version_get(driver, device) + --Call parent WakeUp functions + call_parent_handler(driver.zwave_handlers[cc.WAKE_UP][WakeUp.NOTIFICATION], driver, device, cmd) +end + +local function added_handler(driver, device) + --Call main function + driver.lifecycle_handlers.added(driver, device) + --Extras for this sub_driver + send_version_get(driver, device) +end + +local firmware_version = { + NAME = "firmware_version", + can_handle = can_handle_fw, + + lifecycle_handlers = { + added = added_handler, + }, + zwave_handlers = { + [cc.VERSION] = { + [Version.REPORT] = version_report + }, + [cc.WAKE_UP] = { + [WakeUp.NOTIFICATION] = wakeup_notification + } + } +} + +return firmware_version \ No newline at end of file diff --git a/drivers/SmartThings/zwave-sensor/src/init.lua b/drivers/SmartThings/zwave-sensor/src/init.lua index 8ad801de9e..213aa8c389 100644 --- a/drivers/SmartThings/zwave-sensor/src/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/init.lua @@ -152,7 +152,8 @@ local driver_template = { lazy_load_if_possible("v1-contact-event"), lazy_load_if_possible("timed-tamper-clear"), lazy_load_if_possible("wakeup-no-poll"), - lazy_load_if_possible("apiv6_bugfix") + lazy_load_if_possible("firmware-version"), + lazy_load_if_possible("apiv6_bugfix"), }, lifecycle_handlers = { added = added_handler, diff --git a/drivers/SmartThings/zwave-sensor/src/preferences.lua b/drivers/SmartThings/zwave-sensor/src/preferences.lua index 48ee75d78e..9585b6ffe9 100644 --- a/drivers/SmartThings/zwave-sensor/src/preferences.lua +++ b/drivers/SmartThings/zwave-sensor/src/preferences.lua @@ -148,7 +148,33 @@ local devices = { ledLowBrightness = {parameter_number = 82, size = 2}, ledHighBrightness = {parameter_number = 83, size = 2} } - } + }, + SHELLY_WAVE_MOTION_SENSOR = { + MATCHING_MATRIX = { + mfrs = 0x0460, + product_types = 0x0100, + product_ids = {0x0082} + }, + PARAMETERS = { + ledOpnClsChangeStat = {parameter_number = 157, size = 1}, + sensitivity = {parameter_number = 158, size = 1}, + blindTime = {parameter_number = 159, size = 2}, + motionNotdetRepT = {parameter_number = 160, size = 2}, + }, + }, + ZOOZ_ZSE42_WATER_LEAK = { + MATCHING_MATRIX = { + mfrs = 0x027A, + product_types = 0x7000, + product_ids = 0xE002 + }, + PARAMETERS = { + ledIndicator = { parameter_number = 1, size = 1 }, + leakAlertClearDelay = { parameter_number = 2, size = 4 }, + batteryThreshold = { parameter_number = 3, size = 1 }, + lowBatteryAlert = { parameter_number = 4, size = 1 }, + }, + }, } local preferences = {} diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua index 2d83fd6e25..59435aa68a 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua @@ -15,7 +15,9 @@ local test = require "integration_test" local zw = require "st.zwave" local zw_test_utils = require "integration_test.zwave_test_utils" +local capabilities = require "st.capabilities" local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 1 }) +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 2 }) local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ version = 5 }) local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) @@ -92,4 +94,26 @@ test.register_coroutine_test( end ) +test.register_message_test( + "Notification HOME_SECURITY MOTION_DETECTION should be handled as motion active", + { + { + channel = "zwave", + direction = "receive", + message = { + mock_sensor.id, + zw_test_utils.zwave_test_build_receive_command(Notification:Report({ + notification_type = Notification.notification_type.HOME_SECURITY, + event = Notification.event.home_security.MOTION_DETECTION + })) + } + }, + { + channel = "capability", + direction = "send", + message = mock_sensor:generate_test_message("main", capabilities.motionSensor.motion.active()) + } + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_6.lua b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_6.lua index 02c2adf751..f858438d9a 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_6.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_6.lua @@ -413,4 +413,26 @@ test.register_coroutine_test( end ) +test.register_message_test( + "Notification HOME_SECURITY MOTION_DETECTION should be handled as motion active", + { + { + channel = "zwave", + direction = "receive", + message = { + mock_sensor.id, + zw_test_utils.zwave_test_build_receive_command(Notification:Report({ + notification_type = Notification.notification_type.HOME_SECURITY, + event = Notification.event.home_security.MOTION_DETECTION + })) + } + }, + { + channel = "capability", + direction = "send", + message = mock_sensor:generate_test_message("main", capabilities.motionSensor.motion.active()) + } + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_1.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_1.lua index 4cd8d0e905..6904f318a9 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_1.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_1.lua @@ -58,6 +58,7 @@ test.set_test_init_function(test_init) test.register_message_test( "Device should be polled with refresh right after inclusion", { + -- The initial tamperAlert and contactSensor event should be send during the device's first time onboarding { channel = "device_lifecycle", direction = "receive", @@ -96,6 +97,36 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor1:generate_test_message("main", capabilities.contactSensor.contact.open()) + }, + -- Avoid sending the initial tamperAlert and contactSensor event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_fibaro_door_window_sensor1.id, "added" } + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_fibaro_door_window_sensor1, + Battery:Get({}) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_fibaro_door_window_sensor1, + SensorBinary:Get({}) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_fibaro_door_window_sensor1, + SensorAlarm:Get({}) + ) } }, { @@ -344,6 +375,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor1:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_fibaro_door_window_sensor1.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -363,6 +402,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor1:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 70.7, unit = 'F' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_fibaro_door_window_sensor1.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_2.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_2.lua index fccd88ad97..bc78bd02ca 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_2.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_2.lua @@ -339,6 +339,7 @@ test.register_coroutine_test( test.register_message_test( "device_added should be handled", { + -- The initial tamperAlert, contactSensor & temperatureAlarm event should be send during the device's first time onboarding { channel = "device_lifecycle", direction = "receive", @@ -358,6 +359,12 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared()) + }, + -- Avoid sending the initial tamperAlert, contactSensor & temperatureAlarm event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + { + channel = "device_lifecycle", + direction = "receive", + message = {mock_fibaro_door_window_sensor.id, "added"} } } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_with_temperature.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_with_temperature.lua index b100af6f1a..2b27d66868 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_with_temperature.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_with_temperature.lua @@ -232,6 +232,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_fibaro_door_window_sensor.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -251,6 +259,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 70.7, unit = 'F' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_fibaro_door_window_sensor.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor.lua index 909beb6b1d..2d93278b47 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor.lua @@ -20,6 +20,8 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) local SensorAlarm = (require "st.zwave.CommandClass.SensorAlarm")({ version = 1 }) local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 2 }) local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ version = 5 }) +local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 4 }) +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) local t_utils = require "integration_test.utils" local sensor_endpoints = { @@ -130,6 +132,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_sensor:generate_test_message("main", capabilities.temperatureMeasurement.temperature({value = 25, unit = 'C'})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_sensor.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -269,4 +279,31 @@ test.register_coroutine_test( ) +test.register_coroutine_test( + "doConfigure should call initial_configuration and preferences for non-wakeup device", + function () + test.socket.zwave:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_sensor.id, "doConfigure" }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_sensor, + Configuration:Set({ parameter_number = 74, configuration_value = 3, size = 1 }) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_sensor, + Association:Set({ grouping_identifier = 2, node_ids = {} }) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_sensor, + Association:Set({ grouping_identifier = 3, node_ids = {} }) + ) + ) + mock_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor.lua index 25a7b6ee83..24a7f31eaf 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor.lua @@ -312,6 +312,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -331,6 +339,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 70.7, unit = 'F' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_firmware_version.lua b/drivers/SmartThings/zwave-sensor/src/test/test_firmware_version.lua new file mode 100644 index 0000000000..d4903fc30d --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/test/test_firmware_version.lua @@ -0,0 +1,124 @@ +-- Copyright 2026 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" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +local zw_test_utils = require "integration_test.zwave_test_utils" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass.Version +local Version = (require "st.zwave.CommandClass.Version")({ version = 1 }) +--- @type st.zwave.CommandClass.WakeUp +local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) +--- @type st.zwave.CommandClass.Battery +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) + +local sensor_endpoints = { + { + command_classes = { + { value = cc.WAKE_UP }, + { value = cc.BATTERY }, + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("zooz-zse42-water.yml"), + zwave_endpoints = sensor_endpoints, + zwave_manufacturer_id = 0x027A, + zwave_product_type = 0x7000, + zwave_product_id = 0xE002, +}) + +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "Wakeup notification should not poll binary sensor if device has contact state", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(WakeUp:Notification({ })) } + }, + --This is sent by the sub-driver + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_device, + Version:Get({}) + ) + }, + --This is sent by the main driver + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_device, + WakeUp:IntervalGetV1({}) + ) + }, + --This is sent by the main driver + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_device, + Battery:Get({ }) + ) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_message_test( + "Version:Report should emit firmwareUpdate.currentVersion", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Version:Report({ + application_version = 1, + application_sub_version = 5, + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.firmwareUpdate.currentVersion({ value = "1.05" })) + } + } +) + +test.register_coroutine_test( + "added lifecycle event should emit initial state and request firmware version", + function () + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.waterSensor.water.dry()) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command(mock_device, Version:Get({})) + ) + end +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua index aa7d1ff60c..e6fff57b0d 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua @@ -173,7 +173,7 @@ test.register_message_test( ) test.register_message_test( - "SensorMultilevel report temperature should be handled as temperature", + "SensorMultilevel report temperature (C) should be handled as temperature", { { channel = "zwave", @@ -188,12 +188,20 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 30, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) test.register_message_test( - "SensorMultilevel report temperature should be handled as temperature", + "SensorMultilevel report temperature (F) should be handled as temperature", { { channel = "zwave", @@ -208,6 +216,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 70, unit = "F" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -266,6 +282,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 50, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -286,6 +310,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 50, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -1516,6 +1548,14 @@ test.register_message_test( mock_device, Meter:Get({scale = 0}) ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_device, + Meter:Get({scale = 4}) + ) } }, { diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_smartthings_water_leak_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_smartthings_water_leak_sensor.lua index e97c97029b..2321b5bd09 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_smartthings_water_leak_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_smartthings_water_leak_sensor.lua @@ -200,6 +200,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -220,6 +228,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 37, unit = 'F' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_zooz_4_in_1_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_zooz_4_in_1_sensor.lua index 1bef24c5d6..66cca4afe4 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_zooz_4_in_1_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_zooz_4_in_1_sensor.lua @@ -228,4 +228,22 @@ test.register_message_test( } ) +test.register_message_test( + "Sensor multilevel luminance report with value=0 uses default lux conversion", + { + { + channel = "zwave", + direction = "receive", + message = { mock_sensor.id, zw_test_utils.zwave_test_build_receive_command(SensorMultilevel:Report({ + sensor_type = SensorMultilevel.sensor_type.LUMINANCE, + sensor_value = 0 })) } + }, + { + channel = "capability", + direction = "send", + message = mock_sensor:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance({value = 0, unit = "lux"})) + } + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua index a390c35db9..c15daeb188 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua @@ -735,4 +735,40 @@ test.register_message_test( } ) +test.register_message_test( + "Basic Set value=0 for contact sensor should emit contact.closed", + { + { + channel = "zwave", + direction = "receive", + message = { mock_contact_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({ + value = 0 + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_contact_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + } + } +) + +test.register_message_test( + "Basic Set value=0 for motion sensor should emit motion.inactive", + { + { + channel = "zwave", + direction = "receive", + message = { mock_motion_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({ + value = 0 + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_motion_device:generate_test_message("main", capabilities.motionSensor.motion.inactive()) + } + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-siren/src/aeon-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/aeon-siren/can_handle.lua new file mode 100644 index 0000000000..2aae744ccc --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/aeon-siren/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeon_siren(opts, driver, device, ...) + local AEON_MFR = 0x0086 + local AEON_SIREN_PRODUCT_ID = 0x0050 + + if device.zwave_manufacturer_id == AEON_MFR and device.zwave_product_id == AEON_SIREN_PRODUCT_ID then + return true, require("aeon-siren") + end + return false +end + +return can_handle_aeon_siren diff --git a/drivers/SmartThings/zwave-siren/src/aeon-siren/init.lua b/drivers/SmartThings/zwave-siren/src/aeon-siren/init.lua index f98ba06d83..1c1a2bb0b1 100644 --- a/drivers/SmartThings/zwave-siren/src/aeon-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/aeon-siren/init.lua @@ -1,32 +1,16 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local Basic = (require "st.zwave.CommandClass.Basic")({ version=1 }) local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=1 }) -local AEON_MFR = 0x0086 -local AEON_SIREN_PRODUCT_ID = 0x0050 - local SOUND_TYPE_AND_VOLUME_PARAMETER_NUMBER = 37 local CONFIGURE_SOUND_TYPE = "type" local SOUND_TYPE_DEFAULT = 1 local CONFIGURE_VOLUME = "volume" local VOLUME_DEFAULT = 3 -local function can_handle_aeon_siren(opts, driver, device, ...) - return device.zwave_manufacturer_id == AEON_MFR and device.zwave_product_id == AEON_SIREN_PRODUCT_ID -end local function configure_sound(device, sound_type, volume) if sound_type == nil then sound_type = SOUND_TYPE_DEFAULT end @@ -64,7 +48,7 @@ end local aeon_siren = { NAME = "aeon-siren", - can_handle = can_handle_aeon_siren, + can_handle = require("aeon-siren.can_handle"), lifecycle_handlers = { doConfigure = do_configure, infoChanged = info_changed diff --git a/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/can_handle.lua new file mode 100644 index 0000000000..31304e8688 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeotec_doorbell_siren(opts, driver, device, ...) + local FINGERPRINTS = require("aeotec-doorbell-siren.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("aeotec-doorbell-siren") + end + end + return false +end + +return can_handle_aeotec_doorbell_siren diff --git a/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/fingerprints.lua b/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/fingerprints.lua new file mode 100644 index 0000000000..782dfbc0c9 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/fingerprints.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local AEOTEC_DOORBELL_SIREN_FINGERPRINTS = { + { manufacturerId = 0x0371, productType = 0x0003, productId = 0x00A2}, -- Aeotec Doorbell 6 (EU) + { manufacturerId = 0x0371, productType = 0x0103, productId = 0x00A2}, -- Aeotec Doorbell 6 (US) + { manufacturerId = 0x0371, productType = 0x0203, productId = 0x00A2}, -- Aeotec Doorbell 6 (AU) + { manufacturerId = 0x0371, productType = 0x0003, productId = 0x00A4}, -- Aeotec Siren 6 (EU) + { manufacturerId = 0x0371, productType = 0x0103, productId = 0x00A4}, -- Aeotec Siren 6 (US) + { manufacturerId = 0x0371, productType = 0x0203, productId = 0x00A4}, -- Aeotec Siren 6 (AU) +} + +return AEOTEC_DOORBELL_SIREN_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/init.lua b/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/init.lua index bab6909235..716c46186e 100644 --- a/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -20,14 +10,6 @@ local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) local SoundSwitch = (require "st.zwave.CommandClass.SoundSwitch")({version=1}) local preferencesMap = require "preferences" -local AEOTEC_DOORBELL_SIREN_FINGERPRINTS = { - { manufacturerId = 0x0371, productType = 0x0003, productId = 0x00A2}, -- Aeotec Doorbell 6 (EU) - { manufacturerId = 0x0371, productType = 0x0103, productId = 0x00A2}, -- Aeotec Doorbell 6 (US) - { manufacturerId = 0x0371, productType = 0x0203, productId = 0x00A2}, -- Aeotec Doorbell 6 (AU) - { manufacturerId = 0x0371, productType = 0x0003, productId = 0x00A4}, -- Aeotec Siren 6 (EU) - { manufacturerId = 0x0371, productType = 0x0103, productId = 0x00A4}, -- Aeotec Siren 6 (US) - { manufacturerId = 0x0371, productType = 0x0203, productId = 0x00A4}, -- Aeotec Siren 6 (AU) -} local COMPONENT_NAME = "componentName" local TONE = "tone" @@ -51,15 +33,6 @@ local BUTTON_BATTERY_NORMAL = 99 local DEVICE_PROFILE_CHANGE_IN_PROGRESS = "device_profile_change_in_progress" local NEXT_BUTTON_BATTERY_EVENT_DETAILS = "next_button_battery_event_details" -local function can_handle_aeotec_doorbell_siren(opts, driver, device, ...) - for _, fingerprint in ipairs(AEOTEC_DOORBELL_SIREN_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end - local function querySoundStatus(device) for endpoint = 2, NUMBER_OF_SOUND_COMPONENTS do device:send_to_component(Basic:Get({}), "sound"..endpoint) @@ -234,6 +207,7 @@ local function changeDeviceProfileIfNeeded(device, endpoint) end end +-- Note that endpoint should be a number not a dst_channels table. local function setActiveEndpoint(device, endpoint) if (endpoint) then device:set_field(LAST_TRIGGERED_ENDPOINT, endpoint, {persist = true}) @@ -297,26 +271,28 @@ end local function alarmChimeOnOff(device, command, newValue) if (device and command and newValue) then + -- Note that zwave/device.lua send_to_component expects the component_to_endpoint function to + -- return a dst_channels table, not a single endpoint number local endpoint = component_to_endpoint(device, command.component) - device:send(Basic:Set({value = newValue})):to_endpoint(endpoint) + device:send_to_component(Basic:Set({value = newValue}), command.component) if (newValue == ON) then - setActiveEndpoint(endpoint) + setActiveEndpoint(device, endpoint[1]) end end end -local function alarm_chime_on(device, command) +local function alarm_chime_on(self, device, command) resetActiveEndpoint(device) alarmChimeOnOff(device, command, ON) end -local function alarm_chime_off(device, command) +local function alarm_chime_off(self, device, command) alarmChimeOnOff(device, command, OFF) end local aeotec_doorbell_siren = { NAME = "aeotec-doorbell-siren", - can_handle = can_handle_aeotec_doorbell_siren, + can_handle = require("aeotec-doorbell-siren.can_handle"), lifecycle_handlers = { added = device_added, @@ -332,6 +308,9 @@ local aeotec_doorbell_siren = { [Notification.REPORT] = notification_report_handler } }, + -- This typo is a bug. There are many unit tests that fail, when + -- it is enabled, and it is not clear what the correct functionality is + -- without real device testing. capabilities_handlers = { [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = do_refresh diff --git a/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/can_handle.lua new file mode 100644 index 0000000000..3f4b44c1e0 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, cmd, ...) + local cc = require "st.zwave.CommandClass" + local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) + local version = require "version" + if version.api == 6 and + cmd.cmd_class == cc.WAKE_UP and + cmd.cmd_id == WakeUp.NOTIFICATION + then + return true, require("apiv6_bugfix") + end + return false +end + +return can_handle diff --git a/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/init.lua index 0204b7b2d5..2e7e3ca3b8 100644 --- a/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/init.lua +++ b/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/init.lua @@ -1,13 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - return version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION -end local function wakeup_notification(driver, device, cmd) device:refresh() @@ -20,7 +17,7 @@ local apiv6_bugfix = { } }, NAME = "apiv6_bugfix", - can_handle = can_handle + can_handle = require("apiv6_bugfix.can_handle"), } return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-siren/src/configurations.lua b/drivers/SmartThings/zwave-siren/src/configurations.lua index b17b3cbc0f..5165ee4551 100644 --- a/drivers/SmartThings/zwave-siren/src/configurations.lua +++ b/drivers/SmartThings/zwave-siren/src/configurations.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local devices = { YALE_SIREN = { diff --git a/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/can_handle.lua new file mode 100644 index 0000000000..5803087913 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_ecolink_wireless_siren(opts, driver, device, ...) + local FINGERPRINTS = require("ecolink-wireless-siren.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("ecolink-wireless-siren") + end + end + return false +end + +return can_handle_ecolink_wireless_siren diff --git a/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/fingerprints.lua b/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/fingerprints.lua new file mode 100644 index 0000000000..9edaef0374 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ECOLINK_WIRELESS_SIREN_FINGERPRINTS = { + { manufacturerId = 0x014A, productType = 0x0005, productId = 0x000A }, -- Ecolink Siren +} + +return ECOLINK_WIRELESS_SIREN_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/init.lua b/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/init.lua index 0413221185..6b5a2a25ad 100644 --- a/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -20,18 +10,7 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) --- @type st.zwave.CommandClass.SwitchBinary local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = 2 }) -local ECOLINK_WIRELESS_SIREN_FINGERPRINTS = { - { manufacturerId = 0x014A, productType = 0x0005, productId = 0x000A }, -- Ecolink Siren -} -local function can_handle_ecolink_wireless_siren(opts, driver, device, ...) - for _, fingerprint in ipairs(ECOLINK_WIRELESS_SIREN_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local function basic_set_handler(driver, device, cmd) local value = cmd.args.target_value and cmd.args.target_value or cmd.args.value @@ -103,7 +82,7 @@ local ecolink_wireless_siren = { lifecycle_handlers = { init = device_init }, - can_handle = can_handle_ecolink_wireless_siren, + can_handle = require("ecolink-wireless-siren.can_handle"), } return ecolink_wireless_siren diff --git a/drivers/SmartThings/zwave-siren/src/fortrezz/can_handle.lua b/drivers/SmartThings/zwave-siren/src/fortrezz/can_handle.lua new file mode 100644 index 0000000000..83289ab86a --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/fortrezz/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fortrezz_siren(opts, self, device, ...) + if device.zwave_manufacturer_id == 0x0084 and + device.zwave_product_type == 0x0313 and + device.zwave_product_id == 0x010B then + return true, require("fortrezz") + end + return false +end + +return can_handle_fortrezz_siren diff --git a/drivers/SmartThings/zwave-siren/src/fortrezz/init.lua b/drivers/SmartThings/zwave-siren/src/fortrezz/init.lua index 73a3e6e0d4..8bf31178f6 100644 --- a/drivers/SmartThings/zwave-siren/src/fortrezz/init.lua +++ b/drivers/SmartThings/zwave-siren/src/fortrezz/init.lua @@ -1,26 +1,11 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local capabilities = require "st.capabilities" local Basic = (require "st.zwave.CommandClass.Basic")({version=1}) -local function can_handle_fortrezz_siren(opts, self, device, ...) - return device.zwave_manufacturer_id == 0x0084 and - device.zwave_product_type == 0x0313 and - device.zwave_product_id == 0x010B -end local function set_and_get(value) return function (self, device, command) @@ -47,7 +32,7 @@ end local fortrezz_siren = { NAME = "fortrezz-siren", - can_handle = can_handle_fortrezz_siren, + can_handle = require("fortrezz.can_handle"), capability_handlers = { [capabilities.alarm.ID] = { [capabilities.alarm.commands.siren.NAME] = set_and_get(0x42), @@ -67,4 +52,4 @@ local fortrezz_siren = { } } -return fortrezz_siren \ No newline at end of file +return fortrezz_siren diff --git a/drivers/SmartThings/zwave-siren/src/init.lua b/drivers/SmartThings/zwave-siren/src/init.lua index a725e8b79c..b682ea77b7 100644 --- a/drivers/SmartThings/zwave-siren/src/init.lua +++ b/drivers/SmartThings/zwave-siren/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cap_defaults = require "st.capabilities.defaults" @@ -92,19 +82,7 @@ local driver_template = { capabilities.relativeHumidityMeasurement, capabilities.chime }, - sub_drivers = { - require("multifunctional-siren"), - require("zwave-sound-sensor"), - require("ecolink-wireless-siren"), - require("philio-sound-siren"), - require("aeotec-doorbell-siren"), - require("aeon-siren"), - require("yale-siren"), - require("zipato-siren"), - require("utilitech-siren"), - require("fortrezz"), - require("apiv6_bugfix"), - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { infoChanged = info_changed, doConfigure = do_configure, diff --git a/drivers/SmartThings/zwave-siren/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-siren/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-siren/src/multifunctional-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/multifunctional-siren/can_handle.lua new file mode 100644 index 0000000000..a3c62d02aa --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/multifunctional-siren/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_multifunctional_siren(opts, driver, device, ...) + local FINGERPRINTS = require("multifunctional-siren.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("multifunctional-siren") + end + end + return false +end + +return can_handle_multifunctional_siren diff --git a/drivers/SmartThings/zwave-siren/src/multifunctional-siren/fingerprints.lua b/drivers/SmartThings/zwave-siren/src/multifunctional-siren/fingerprints.lua new file mode 100644 index 0000000000..d2bcf5402d --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/multifunctional-siren/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local MULTIFUNCTIONAL_SIREN_FINGERPRINTS = { + { manufacturerId = 0x027A, productType = 0x000C, productId = 0x0003 }, -- Zooz S2 Multisiren ZSE19 + { manufacturerId = 0x0060, productType = 0x000C, productId = 0x0003 } -- Everspring Indoor Voice Siren +} + +return MULTIFUNCTIONAL_SIREN_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-siren/src/multifunctional-siren/init.lua b/drivers/SmartThings/zwave-siren/src/multifunctional-siren/init.lua index 2c8af3bb26..b1bca996d5 100644 --- a/drivers/SmartThings/zwave-siren/src/multifunctional-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/multifunctional-siren/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -20,24 +10,12 @@ local Basic = (require "st.zwave.CommandClass.Basic")({version=1}) --- @type st.zwave.CommandClass.Battery local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local MULTIFUNCTIONAL_SIREN_FINGERPRINTS = { - { manufacturerId = 0x027A, productType = 0x000C, productId = 0x0003 }, -- Zooz S2 Multisiren ZSE19 - { manufacturerId = 0x0060, productType = 0x000C, productId = 0x0003 } -- Everspring Indoor Voice Siren -} --- Determine whether the passed device is multifunctional siren --- --- @param driver Driver driver instance --- @param device Device device isntance --- @return boolean true if the device proper, else false -local function can_handle_multifunctional_siren(opts, driver, device, ...) - for _, fingerprint in ipairs(MULTIFUNCTIONAL_SIREN_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end --- Default handler for notification command class reports --- @@ -74,7 +52,7 @@ local multifunctional_siren = { doConfigure = do_configure }, NAME = "multifunctional siren", - can_handle = can_handle_multifunctional_siren, + can_handle = require("multifunctional-siren.can_handle"), } return multifunctional_siren diff --git a/drivers/SmartThings/zwave-siren/src/philio-sound-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/philio-sound-siren/can_handle.lua new file mode 100644 index 0000000000..23e614a2e2 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/philio-sound-siren/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_philio_sound_siren(opts, driver, device, ...) + local FINGERPRINTS = require("philio-sound-siren.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("philio-sound-siren") + end + end + return false +end + +return can_handle_philio_sound_siren diff --git a/drivers/SmartThings/zwave-siren/src/philio-sound-siren/fingerprints.lua b/drivers/SmartThings/zwave-siren/src/philio-sound-siren/fingerprints.lua new file mode 100644 index 0000000000..e1f34617fc --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/philio-sound-siren/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local PHILIO_SOUND_SIREN = { + { manufacturerId = 0x013C, productType = 0x0004, productId = 0x000A } +} + +return PHILIO_SOUND_SIREN diff --git a/drivers/SmartThings/zwave-siren/src/philio-sound-siren/init.lua b/drivers/SmartThings/zwave-siren/src/philio-sound-siren/init.lua index 6397022dd8..23e310bf14 100644 --- a/drivers/SmartThings/zwave-siren/src/philio-sound-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/philio-sound-siren/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local capabilities = require "st.capabilities" @@ -19,9 +9,6 @@ local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({version=2}) local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) local preferencesMap = require "preferences" -local PHILIO_SOUND_SIREN = { - { manufacturerId = 0x013C, productType = 0x0004, productId = 0x000A } -} local PARAMETER_SOUND = "sound" local SMOKE = 0 @@ -43,14 +30,6 @@ local sounds = { [SMOKE] = {notificationType = Notification.notification_type.SMOKE, event = Notification.event.smoke.DETECTED_LOCATION_PROVIDED} } -local function can_handle_philio_sound_siren(opts, driver, device, ...) - for _, fingerprint in ipairs(PHILIO_SOUND_SIREN) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local function device_added(self, device) device:refresh() @@ -157,7 +136,7 @@ end local philio_sound_siren = { NAME = "Philio sound siren", - can_handle = can_handle_philio_sound_siren, + can_handle = require("philio-sound-siren.can_handle"), lifecycle_handlers = { added = device_added }, diff --git a/drivers/SmartThings/zwave-siren/src/preferences.lua b/drivers/SmartThings/zwave-siren/src/preferences.lua index 3e9f8333ab..2c10de6fcb 100644 --- a/drivers/SmartThings/zwave-siren/src/preferences.lua +++ b/drivers/SmartThings/zwave-siren/src/preferences.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local devices = { PHILIO_SOUND_SIREN = { diff --git a/drivers/SmartThings/zwave-siren/src/sub_drivers.lua b/drivers/SmartThings/zwave-siren/src/sub_drivers.lua new file mode 100644 index 0000000000..a20d559e44 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/sub_drivers.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("multifunctional-siren"), + lazy_load_if_possible("zwave-sound-sensor"), + lazy_load_if_possible("ecolink-wireless-siren"), + lazy_load_if_possible("philio-sound-siren"), + lazy_load_if_possible("aeotec-doorbell-siren"), + lazy_load_if_possible("aeon-siren"), + lazy_load_if_possible("yale-siren"), + lazy_load_if_possible("zipato-siren"), + lazy_load_if_possible("utilitech-siren"), + lazy_load_if_possible("fortrezz"), + lazy_load_if_possible("apiv6_bugfix"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-siren/src/test/test_aeon_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_aeon_siren.lua index 714dc17a10..34cdc4ceea 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_aeon_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_aeon_siren.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-siren/src/test/test_aeotec_doorbell_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_aeotec_doorbell_siren.lua index c2dc708cfe..924ef4e4f2 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_aeotec_doorbell_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_aeotec_doorbell_siren.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-siren/src/test/test_ecolink_wireless_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_ecolink_wireless_siren.lua index f20e5c6074..54b4243331 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_ecolink_wireless_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_ecolink_wireless_siren.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-siren/src/test/test_fortrezz_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_fortrezz_siren.lua index e23b6d3caf..e99cfce77c 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_fortrezz_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_fortrezz_siren.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" @@ -163,4 +153,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-siren/src/test/test_philio_sound_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_philio_sound_siren.lua index 23ad20a1fd..d825be6cec 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_philio_sound_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_philio_sound_siren.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-siren/src/test/test_utilitech_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_utilitech_siren.lua index 4c24c069ad..e62141ebcf 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_utilitech_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_utilitech_siren.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-siren/src/test/test_yale_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_yale_siren.lua index 072a55c3d3..a7b7123f38 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_yale_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_yale_siren.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-siren/src/test/test_zipato_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_zipato_siren.lua index fbfbdd2db7..6b912be8bb 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_zipato_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_zipato_siren.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-siren/src/test/test_zwave_multifunctional-siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_zwave_multifunctional-siren.lua index c0d58bb952..a40b510b49 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_zwave_multifunctional-siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_zwave_multifunctional-siren.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-siren/src/test/test_zwave_notification_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_zwave_notification_siren.lua index 73de507946..0e0f407523 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_zwave_notification_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_zwave_notification_siren.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-siren/src/test/test_zwave_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_zwave_siren.lua index 75713b89b7..121f6170e2 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_zwave_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_zwave_siren.lua @@ -1,4 +1,4 @@ ----@diagnostic disable: param-type-mismatch, undefined-field +-- Copyright 2022 SmartThings, Inc. -- Copyright 2022 SmartThings -- -- Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/drivers/SmartThings/zwave-siren/src/test/test_zwave_sound_sensor.lua b/drivers/SmartThings/zwave-siren/src/test/test_zwave_sound_sensor.lua index e88b1e1852..10595c7137 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_zwave_sound_sensor.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_zwave_sound_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-siren/src/utilitech-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/utilitech-siren/can_handle.lua new file mode 100644 index 0000000000..72f6a963b9 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/utilitech-siren/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_utilitech_siren(opts, driver, device, ...) + local UTILITECH_MFR = 0x0060 + local UTILITECH_SIREN_PRODUCT_ID = 0x0001 + if device.zwave_manufacturer_id == UTILITECH_MFR and device.zwave_product_id == UTILITECH_SIREN_PRODUCT_ID then + return true, require("utilitech-siren") + end + return false +end + +return can_handle_utilitech_siren diff --git a/drivers/SmartThings/zwave-siren/src/utilitech-siren/init.lua b/drivers/SmartThings/zwave-siren/src/utilitech-siren/init.lua index 9e6818c6e4..0088fa6dd8 100644 --- a/drivers/SmartThings/zwave-siren/src/utilitech-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/utilitech-siren/init.lua @@ -1,29 +1,12 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local Basic = (require "st.zwave.CommandClass.Basic")({version=1,strict=true}) local Battery = (require "st.zwave.CommandClass.Battery")({version=1}) local BatteryDefaults = require "st.zwave.defaults.battery" -local UTILITECH_MFR = 0x0060 -local UTILITECH_SIREN_PRODUCT_ID = 0x0001 - -local function can_handle_utilitech_siren(opts, driver, device, ...) - return device.zwave_manufacturer_id == UTILITECH_MFR and device.zwave_product_id == UTILITECH_SIREN_PRODUCT_ID -end - local function device_added(self, device) device:send(Basic:Get({})) device:send(Battery:Get({})) @@ -39,7 +22,7 @@ end local utilitech_siren = { NAME = "utilitech-siren", - can_handle = can_handle_utilitech_siren, + can_handle = require("utilitech-siren.can_handle"), zwave_handlers = { [cc.BATTERY] = { [Battery.REPORT] = battery_report_handler diff --git a/drivers/SmartThings/zwave-siren/src/yale-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/yale-siren/can_handle.lua new file mode 100644 index 0000000000..1e29867936 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/yale-siren/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_yale_siren(opts, self, device, ...) + local YALE_MFR = 0x0129 + if device.zwave_manufacturer_id == YALE_MFR then + return true, require("yale-siren") + end + return false +end + +return can_handle_yale_siren diff --git a/drivers/SmartThings/zwave-siren/src/yale-siren/init.lua b/drivers/SmartThings/zwave-siren/src/yale-siren/init.lua index de548aee95..3757a2c3a9 100644 --- a/drivers/SmartThings/zwave-siren/src/yale-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/yale-siren/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local capabilities = require "st.capabilities" @@ -20,12 +10,6 @@ local Configuration = (require "st.zwave.CommandClass.Configuration")({version=1 local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({version=1}) local preferencesMap = require "preferences" -local YALE_MFR = 0x0129 - -local function can_handle_yale_siren(opts, self, device, ...) - return device.zwave_manufacturer_id == YALE_MFR -end - local function siren_set_helper(device, value) device:send(Basic:Set({value = value})) local query_device = function() @@ -98,7 +82,7 @@ end local yale_siren = { NAME = "yale-siren", - can_handle = can_handle_yale_siren, + can_handle = require("yale-siren.can_handle"), capability_handlers = { [capabilities.alarm.ID] = { [capabilities.alarm.commands.both.NAME] = siren_on, diff --git a/drivers/SmartThings/zwave-siren/src/zipato-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/zipato-siren/can_handle.lua new file mode 100644 index 0000000000..cb1170db2e --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/zipato-siren/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zipato_siren(opts, driver, device, ...) + local ZIPATO_MFR = 0x0131 + if device.zwave_manufacturer_id == ZIPATO_MFR then + return true, require("zipato-siren") + end + return false +end + +return can_handle_zipato_siren diff --git a/drivers/SmartThings/zwave-siren/src/zipato-siren/init.lua b/drivers/SmartThings/zwave-siren/src/zipato-siren/init.lua index e22e643ae1..2af57daa19 100644 --- a/drivers/SmartThings/zwave-siren/src/zipato-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/zipato-siren/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -18,13 +8,9 @@ local AlarmDefaults = require "st.zwave.defaults.alarm" local Basic = (require "st.zwave.CommandClass.Basic")({version=1}) local Battery = (require "st.zwave.CommandClass.Battery")({version=1}) -local ZIPATO_MFR = 0x0131 local BASIC_AND_SWITCH_BINARY_REPORT_STROBE_LIMIT = 33 local BASIC_AND_SWITCH_BINARY_REPORT_SIREN_LIMIT = 66 -local function can_handle_zipato_siren(opts, driver, device, ...) - return device.zwave_manufacturer_id == ZIPATO_MFR -end local function basic_report_handler(driver, device, cmd) local value = cmd.args.value @@ -64,7 +50,7 @@ end local zipato_siren = { NAME = "zipato-siren", - can_handle = can_handle_zipato_siren, + can_handle = require("zipato-siren.can_handle"), capability_handlers = { [capabilities.alarm.ID] = { [capabilities.alarm.commands.both.NAME] = siren_on, diff --git a/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/can_handle.lua b/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/can_handle.lua new file mode 100644 index 0000000000..c3fb8b343e --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zwave_sound_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-sound-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("zwave-sound-sensor") + end + end + return false +end + +return can_handle_zwave_sound_sensor diff --git a/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/fingerprints.lua b/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/fingerprints.lua new file mode 100644 index 0000000000..e0c8ef4762 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZWAVE_SOUND_SENSOR_FINGERPRINTS = { + { manufacturerId = 0x014A, productType = 0x0005, productId = 0x000F } --Ecolink Firefighter +} + +return ZWAVE_SOUND_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/init.lua b/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/init.lua index c591f8a89b..c50153ec26 100644 --- a/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/init.lua +++ b/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -18,23 +8,12 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Alarm local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 2 }) -local ZWAVE_SOUND_SENSOR_FINGERPRINTS = { - { manufacturerId = 0x014A, productType = 0x0005, productId = 0x000F } --Ecolink Firefighter -} --- Determine whether the passed device is zwave-sound-sensor --- --- @param driver Driver driver instance --- @param device Device device isntance --- @return boolean true if the device proper, else false -local function can_handle_zwave_sound_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(ZWAVE_SOUND_SENSOR_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end --- Default handler for alarm command class reports --- @@ -73,7 +52,7 @@ local zwave_sound_sensor = { added = added_handler, }, NAME = "zwave sound sensor", - can_handle = can_handle_zwave_sound_sensor, + can_handle = require("zwave-sound-sensor.can_handle"), } return zwave_sound_sensor diff --git a/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml b/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml index 6a73e78428..39b541becb 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml +++ b/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml @@ -63,3 +63,9 @@ zwaveManufacturer: productType: 0x0004 productId: 0x0003 deviceProfileName: co-battery + - id: "1051/1/1040" + deviceLabel: SMCO410 + manufacturerId: 0x041B + productId: 0x0410 + productType: 0x0001 + deviceProfileName: smoke-co-battery diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/can_handle.lua new file mode 100644 index 0000000000..8a9b8cc6cc --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, cmd, ...) + local cc = require "st.zwave.CommandClass" + local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) + local version = require "version" + if version.api == 6 and + cmd.cmd_class == cc.WAKE_UP and + cmd.cmd_id == WakeUp.NOTIFICATION then + return true, require("apiv6_bugfix") + end + return false +end + +return can_handle diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/init.lua index 0204b7b2d5..94dc5975ab 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/init.lua @@ -1,14 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - return version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION -end - local function wakeup_notification(driver, device, cmd) device:refresh() end @@ -20,7 +15,7 @@ local apiv6_bugfix = { } }, NAME = "apiv6_bugfix", - can_handle = can_handle + can_handle = require("apiv6_bugfix.can_handle"), } return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/can_handle.lua b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/can_handle.lua new file mode 100644 index 0000000000..4a3f51183b --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_smoke_sensor(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("fibaro-smoke-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("fibaro-smoke-sensor") + end + end + return false +end + +return can_handle_fibaro_smoke_sensor diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/fingerprints.lua b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/fingerprints.lua new file mode 100644 index 0000000000..81c04eccfc --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_SMOKE_SENSOR_FINGERPRINTS = { + { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x1002 }, -- Fibaro Smoke Sensor + { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x1003 }, -- Fibaro Smoke Sensor + { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x3002 }, -- Fibaro Smoke Sensor + { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x4002 } -- Fibaro Smoke Sensor +} + +return FIBARO_SMOKE_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/init.lua index a4d62fa1a4..bf7d554613 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -24,26 +14,12 @@ local WakeUp = (require "st.zwave.CommandClass.WakeUp")({version=1}) local FIBARO_SMOKE_SENSOR_WAKEUP_INTERVAL = 21600 --seconds -local FIBARO_SMOKE_SENSOR_FINGERPRINTS = { - { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x1002 }, -- Fibaro Smoke Sensor - { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x1003 }, -- Fibaro Smoke Sensor - { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x3002 }, -- Fibaro Smoke Sensor - { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x4002 } -- Fibaro Smoke Sensor -} --- Determine whether the passed device is fibaro smoke sensro --- --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is fibaro smoke sensor -local function can_handle_fibaro_smoke_sensor(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(FIBARO_SMOKE_SENSOR_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local function device_added(self, device) device:send(WakeUp:IntervalSet({node_id = self.environment_info.hub_zwave_id, seconds = FIBARO_SMOKE_SENSOR_WAKEUP_INTERVAL})) @@ -76,7 +52,7 @@ local fibaro_smoke_sensor = { added = device_added }, NAME = "fibaro smoke sensor", - can_handle = can_handle_fibaro_smoke_sensor, + can_handle = require("fibaro-smoke-sensor.can_handle"), health_check = false, } diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/init.lua index 0d0a5d1bfd..7968983f63 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -83,12 +73,7 @@ local driver_template = { capabilities.temperatureAlarm, capabilities.temperatureMeasurement }, - sub_drivers = { - require("zwave-smoke-co-alarm-v1"), - require("zwave-smoke-co-alarm-v2"), - require("fibaro-smoke-sensor"), - require("apiv6_bugfix"), - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { init = device_init, infoChanged = info_changed, diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-smoke-alarm/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/preferences.lua b/drivers/SmartThings/zwave-smoke-alarm/src/preferences.lua index 25606a5255..55fa70d48b 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/preferences.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/preferences.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local devices = { FIBARO = { diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/sub_drivers.lua b/drivers/SmartThings/zwave-smoke-alarm/src/sub_drivers.lua new file mode 100644 index 0000000000..f0a2d96412 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/sub_drivers.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("zwave-smoke-co-alarm-v1"), + lazy_load_if_possible("zwave-smoke-co-alarm-v2"), + lazy_load_if_possible("fibaro-smoke-sensor"), + lazy_load_if_possible("apiv6_bugfix"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_co_sensor_zw5.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_co_sensor_zw5.lua index 4f3b08d3f3..dc14f1c51b 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_co_sensor_zw5.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_co_sensor_zw5.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua index ebb50eaa6e..0ec85f00f2 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_alarm_v1.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_alarm_v1.lua index fd2996b9d1..aa1ebb27f0 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_alarm_v1.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_alarm_v1.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_co_detector.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_co_detector.lua index e42edd4828..1523bf2420 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_co_detector.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_co_detector.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_smoke_detector.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_smoke_detector.lua index 95121b1673..4464a10fda 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_smoke_detector.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_smoke_detector.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/can_handle.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/can_handle.lua new file mode 100644 index 0000000000..dc78d0e4ac --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_v1_alarm(opts, driver, device, cmd, ...) + -- The default handlers for the Alarm/Notification command class(es) for the + -- Smoke Detector and Carbon Monoxide Detector only handles V3 and up. + if opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version < 3 then + return true, require("zwave-smoke-co-alarm-v1") + end + return false +end + +return can_handle_v1_alarm diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/init.lua index 923c516167..8e3cc4e267 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -23,17 +12,6 @@ local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 1 }) -- manufacturerId = 0x0138, productType = 0x0001, productId = 0x0002 -- First Alert Smoke & CO Detector -- manufacturerId = 0x0138, productType = 0x0001, productId = 0x0003 -- First Alert Smoke & CO Detector ---- Determine whether the passed device only supports V1 or V2 of the Alarm command class ---- ---- @param driver st.zwave.Driver ---- @param device st.zwave.Device ---- @return boolean true if the device is smoke co alarm -local function can_handle_v1_alarm(opts, driver, device, cmd, ...) - -- The default handlers for the Alarm/Notification command class(es) for the - -- Smoke Detector and Carbon Monoxide Detector only handles V3 and up. - return opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version < 3 -end - --- Default handler for alarm command class reports --- --- This converts alarm V1 reports to correct smoke events @@ -75,7 +53,7 @@ local zwave_alarm = { } }, NAME = "Z-Wave smoke and CO alarm V1", - can_handle = can_handle_v1_alarm, + can_handle = require("zwave-smoke-co-alarm-v1.can_handle"), } return zwave_alarm diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/can_handle.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/can_handle.lua new file mode 100644 index 0000000000..6666e7abc2 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_v2_alarm(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("zwave-smoke-co-alarm-v2.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("zwave-smoke-co-alarm-v2") + end + end + return false +end + +return can_handle_v2_alarm diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/can_handle.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/can_handle.lua new file mode 100644 index 0000000000..baa0d7bbf0 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_co_sensor(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("zwave-smoke-co-alarm-v2.fibaro-co-sensor-zw5.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("zwave-smoke-co-alarm-v2.fibaro-co-sensor-zw5") + end + end + return false +end + +return can_handle_fibaro_co_sensor diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/fingerprints.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/fingerprints.lua new file mode 100644 index 0000000000..37fb1f508c --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_CO_SENSORS_FINGERPRINTS = { + { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1000 }, -- Fibaro CO Sensor + { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1001 } -- Fibaro CO Sensor +} + +return FIBARO_CO_SENSORS_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/init.lua index bde1cbc877..0559b83983 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) @@ -21,10 +11,6 @@ local TAMPERING_AND_EXCEEDING_THE_TEMPERATURE = 3 local ACOUSTIC_SIGNALS = 4 local EXCEEDING_THE_TEMPERATURE = 2 -local FIBARO_CO_SENSORS_FINGERPRINTS = { - { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1000 }, -- Fibaro CO Sensor - { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1001 } -- Fibaro CO Sensor -} local function parameterNumberToParameterName(preferences,parameterNumber) for id, parameter in pairs(preferences) do @@ -40,14 +26,6 @@ end --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is smoke co alarm -local function can_handle_fibaro_co_sensor(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(FIBARO_CO_SENSORS_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local function update_preferences(self, device, args) local preferences = preferencesMap.get_device_parameters(device) @@ -108,7 +86,7 @@ local fibaro_co_sensor = { init = device_init, infoChanged = info_changed }, - can_handle = can_handle_fibaro_co_sensor + can_handle = require("zwave-smoke-co-alarm-v2.fibaro-co-sensor-zw5.can_handle"), } return fibaro_co_sensor diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fingerprints.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fingerprints.lua new file mode 100644 index 0000000000..8dc021cc28 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SMOKE_CO_ALARM_V2_FINGERPRINTS = { + { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1000 }, -- Fibaro CO Sensor + { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1001 } -- Fibaro CO Sensor +} + +return SMOKE_CO_ALARM_V2_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/init.lua index b49b010cdb..5e69f7108d 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -20,24 +11,12 @@ local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 2 }) --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local SMOKE_CO_ALARM_V2_FINGERPRINTS = { - { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1000 }, -- Fibaro CO Sensor - { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1001 } -- Fibaro CO Sensor -} --- Determine whether the passed device is Smoke Alarm --- --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is smoke co alarm -local function can_handle_v2_alarm(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(SMOKE_CO_ALARM_V2_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local device_added = function(self, device) device:emit_event(capabilities.carbonMonoxideDetector.carbonMonoxide.clear()) @@ -94,13 +73,11 @@ local zwave_alarm = { } }, NAME = "Z-Wave smoke and CO alarm V2", - can_handle = can_handle_v2_alarm, + can_handle = require("zwave-smoke-co-alarm-v2.can_handle"), lifecycle_handlers = { added = device_added }, - sub_drivers = { - require("zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5") - } + sub_drivers = require("zwave-smoke-co-alarm-v2.sub_drivers"), } return zwave_alarm diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/sub_drivers.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/sub_drivers.lua new file mode 100644 index 0000000000..0699743371 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("zwave-smoke-co-alarm-v2.fibaro-co-sensor-zw5"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-switch/fingerprints.yml b/drivers/SmartThings/zwave-switch/fingerprints.yml index 7154dc11c7..d90a0c6b76 100644 --- a/drivers/SmartThings/zwave-switch/fingerprints.yml +++ b/drivers/SmartThings/zwave-switch/fingerprints.yml @@ -45,17 +45,23 @@ zwaveManufacturer: productId: 0x0000 deviceProfileName: switch-level - id: "Inovelli/Dimmer/Power/Energy" - deviceLabel: Inovelli Dimmer Switch + deviceLabel: Inovelli Dimmer Red Series manufacturerId: 0x031E productType: 0x0001 productId: 0x0001 deviceProfileName: inovelli-dimmer-power-energy - id: "Inovelli/Dimmer" - deviceLabel: Inovelli Dimmer Switch + deviceLabel: Inovelli Dimmer Black Series manufacturerId: 0x031E productType: 0x0003 productId: 0x0001 deviceProfileName: inovelli-dimmer + - id: "Inovelli/VZW32-SN" + deviceLabel: Inovelli mmWave Dimmer Red Series + manufacturerId: 0x031E + productType: 0x0017 + productId: 0x0001 + deviceProfileName: inovelli-mmwave-dimmer-vzw32-sn - id: "010F/0403" deviceLabel: Fibaro Single Switch manufacturerId: 0x010F diff --git a/drivers/SmartThings/zwave-switch/profiles/inovelli-dimmer-power-energy.yml b/drivers/SmartThings/zwave-switch/profiles/inovelli-dimmer-power-energy.yml index c3ee571c10..077c80e3a3 100644 --- a/drivers/SmartThings/zwave-switch/profiles/inovelli-dimmer-power-energy.yml +++ b/drivers/SmartThings/zwave-switch/profiles/inovelli-dimmer-power-energy.yml @@ -27,24 +27,48 @@ components: categories: - name: Light - 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: + "0": "Clear" + "1": "Solid" + "2": "Chase" + "3": "Fast Blink" + "4": "Slow Blink" + "5": "Pulse" + default: 1 - name: "dimmingSpeed" title: "Dimming Speed" description: "How fast or slow the light changes state when you hold the switch. diff --git a/drivers/SmartThings/zwave-switch/profiles/inovelli-mmwave-dimmer-vzw32-sn.yml b/drivers/SmartThings/zwave-switch/profiles/inovelli-mmwave-dimmer-vzw32-sn.yml new file mode 100644 index 0000000000..d85a661839 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/profiles/inovelli-mmwave-dimmer-vzw32-sn.yml @@ -0,0 +1,400 @@ +name: inovelli-mmwave-dimmer-vzw32-sn +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + - id: motionSensor + version: 1 + - id: illuminanceMeasurement + version: 1 + config: + values: + - key: "illuminance.value" + range: [0, 5000] + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: refresh + 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: "parameter158" + title: "158. Switch Mode" + description: "Use as a Dimmer or an On/Off switch" + required: true + preferenceType: enumeration + definition: + options: + "0": "Dimmer (default)" + "1": "On/Off" + 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: true + 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: 255 + 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=255 (Sync with parameter 1)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 255 + default: 255 + - 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=255 (Sync with parameter 1)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 255 + default: 255 + - 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=255 (Sync with parameter 3)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 255 + default: 255 + - 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: true + 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: true + preferenceType: number + definition: + minimum: 2 + maximum: 100 + default: 100 + - 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: true + preferenceType: number + definition: + minimum: 0 + maximum: 101 + default: 101 + - name: "parameter18" + title: "18. Active Power Reports" + description: "Power level change that will result in a new power report being sent. + 0 = Disabled + 1-32767 = 0.1W-3276.7W." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 32767 + default: 100 + - name: "parameter19" + title: "19. Periodic Power & Energy Reports" + description: "Time period between consecutive power & energy reports being sent (in seconds). The timer is reset after each report is sent." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 32767 + default: 3600 + - name: "parameter20" + title: "20. Active Energy Reports" + description: "Energy level change that will result in a new energy report being sent. + 0 = Disabled + 1-32767 = 0.01kWh-327.67kWh." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 32767 + default: 100 + - name: "parameter50" + title: "50. Button Press Delay" + description: "Adjust the delay used in scene control. 0=no delay (disables multi-tap scenes), 1=100ms, 2=200ms, 3=300ms, etc." + required: true + preferenceType: enumeration + definition: + options: + "0": "0ms" + "1": "100ms" + "2": "200ms" + "3": "300ms" + "4": "400ms" + "5": "500ms (default)" + "6": "600ms" + "7": "700ms" + "8": "800ms" + "9": "900ms" + default: 5 + - name: "parameter95" + title: "95. LED Indicator Color (w/On)" + description: "Set the color of the Full LED Indicator when the load is on." + required: true + 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: true + 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: true + 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: true + preferenceType: number + definition: + minimum: 0 + maximum: 100 + default: 5 + - name: "parameter101" + title: "101. mmWave Height Minimum (Floor)" + description: "Minimum range of the Z-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: -300 + - name: "parameter102" + title: "102. mmWave Height Maximum (Ceiling)" + description: "Maximum range of the Z-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: 300 + - name: "parameter103" + title: "103. mmWave Width Minimum (Left)" + description: "Minimum range of the X-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: -600 + - name: "parameter104" + title: "104. mmWave Width Maximum (Right)" + description: "Maximum range of the X-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: 600 + - name: "parameter105" + title: "105. mmWave Depth Minimum (Near)" + description: "Minimum range of the Y-Axis in cm" + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 600 + default: 0 + - name: "parameter106" + title: "106. mmWave Depth Maximum (Far)" + description: "Maximum range of the Y-Axis in cm" + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 600 + default: 600 + - name: "parameter108" + title: "108. mmWave Stay Life" + description: "Optimize detection in areas where user may be still for a long time. The delay time of the stay area is set to 50ms when it is set to 1, to 1 second when it is set to 20, and the default value is 300, that is, 15 seconds" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 4294967295 + default: 300 + - name: "parameter110" + title: "Light On Presence Behavior" + description: "When presence is detected, choose how to control the light load" + required: true + preferenceType: enumeration + definition: + options: + "0": "Disabled" + "1": "Auto On/Off when occupied (default)" + "2": "Auto Off when vacant" + "3": "Auto On when occupied" + "4": "Auto On/Off when Vacant" + "5": "Auto On when Vacant" + "6": "Auto Off when Occupied" + default: 1 + - name: "parameter111" + title: "111. mmWave Control Commands" + description: "Advanced commands to send to the mmWave Module (Please see documentation)" + required: false + preferenceType: enumeration + definition: + options: + "1": "Set Interference Area" + "3": "Clear Interference Area" + "0": "Factory Reset Module" + default: 3 + - name: "parameter112" + title: "112. mmWave Sensitivity" + description: "Adjust the sensitivity of the mmWave sensor. 0-Low, 1-Medium, 2-High." + required: false + preferenceType: enumeration + definition: + options: + "0": "Low" + "1": "Medium" + "2": "High (default)" + default: 2 + - name: "parameter113" + title: "113. mmWave Detection Delay" + description: "The time from detecting a person to triggering an action. 0-Low (5s), 1-Medium (1s), 2-Fast (0.2s)." + required: false + preferenceType: enumeration + definition: + options: + "0": "5 seconds" + "1": "1 second" + "2": "0.2 seconds (default)" + default: 2 + - name: "parameter114" + title: "mmWave Detection Timeout" + description: "Adjust the timeout after presence is no longer detected. After the timeout the load will turn off." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 4294967296 + default: 30 \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/profiles/leviton-zw6hd.yml b/drivers/SmartThings/zwave-switch/profiles/leviton-zw6hd.yml index 53aa313718..3791a4422e 100644 --- a/drivers/SmartThings/zwave-switch/profiles/leviton-zw6hd.yml +++ b/drivers/SmartThings/zwave-switch/profiles/leviton-zw6hd.yml @@ -10,7 +10,7 @@ components: config: values: - key: "level.value" - range: [1, 99] + range: [1, 100] - id: refresh version: 1 categories: diff --git a/drivers/SmartThings/zwave-switch/profiles/rgbw-bulb.yml b/drivers/SmartThings/zwave-switch/profiles/rgbw-bulb.yml new file mode 100644 index 0000000000..d87d32adc6 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/profiles/rgbw-bulb.yml @@ -0,0 +1,16 @@ +name: rgbw-bulb +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + - id: colorTemperature + version: 1 + - id: colorControl + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/can_handle.lua b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/can_handle.lua new file mode 100644 index 0000000000..5efc0d1df0 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/can_handle.lua @@ -0,0 +1,24 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +--- Determine whether the passed device is Aeon smart strip +--- +--- @param driver Driver driver instance +--- @param device Device device isntance +--- @return boolean true if the device proper, else false +local function can_handle_aeon_smart_strip(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x0086, prod = 0x0003, model = 0x000B}, -- Aeon Smart Strip DSC11-ZWUS + } + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("aeon-smart-strip") + return true, subdriver + end + end + return false +end + + +return can_handle_aeon_smart_strip diff --git a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/init.lua b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/init.lua index f3bafc0815..4bc16ba392 100644 --- a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/init.lua +++ b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -24,29 +13,10 @@ local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) --- @type st.zwave.CommandClass.SwitchBinary local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = 2 }) -local AEON_SMART_STRIP_FINGERPRINTS = { - {mfr = 0x0086, prod = 0x0003, model = 0x000B}, -- Aeon Smart Strip DSC11-ZWUS -} - local ENERGY_UNIT_KWH = "kWh" local ENERGY_UNIT_KVAH = "kVAh" local POWER_UNIT_WATT = "W" ---- Determine whether the passed device is Aeon smart strip ---- ---- @param driver Driver driver instance ---- @param device Device device isntance ---- @return boolean true if the device proper, else false -local function can_handle_aeon_smart_strip(opts, driver, device, ...) - for _, fingerprint in ipairs(AEON_SMART_STRIP_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("aeon-smart-strip") - return true, subdriver - end - end - return false -end - local function binary_event_helper(self, device, cmd) local value = cmd.args.value and cmd.args.value or cmd.args.target_value local event = value == SwitchBinary.value.OFF_DISABLE and capabilities.switch.switch.off() or capabilities.switch.switch.on() @@ -108,7 +78,7 @@ local aeon_smart_strip = { [Meter.REPORT] = meter_report_handler } }, - can_handle = can_handle_aeon_smart_strip, + can_handle = require("aeon-smart-strip.can_handle"), } return aeon_smart_strip diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/can_handle.lua b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/can_handle.lua new file mode 100644 index 0000000000..7c5995ea76 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/can_handle.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, ...) + local fingerprints = { + { mfr = 0x0086, model = 0x004E } + } + + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, nil, fingerprint.model) then + local subdriver = require("aeotec-heavy-duty") + return true, subdriver + end + end + return false +end + +return can_handle diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua index 9a10b32c64..2a47c4e122 100644 --- a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua +++ b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua @@ -1,18 +1,5 @@ --- Author: CommanderQ --- --- Copyright 2021 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.Meter @@ -29,20 +16,6 @@ local LAST_REPORT_TIME = "LAST_REPORT_TIME" local POWER_UNIT_WATT = "W" local ENERGY_UNIT_KWH = "kWh" -local FINGERPRINTS = { - { mfr = 0x0086, model = 0x004E } -} - -local function can_handle(opts, driver, device, ...) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:id_match(fingerprint.mfr, nil, fingerprint.model) then - local subdriver = require("aeotec-heavy-duty") - return true, subdriver - end - end - return false -end - local function emit_power_consumption_report_event(device, value, channel) -- powerConsumptionReport report interval local current_time = os.time() @@ -128,7 +101,7 @@ local driver_template = { lifecycle_handlers = { infoChanged = info_changed }, - can_handle = can_handle + can_handle = require("aeotec-heavy-duty.can_handle") } -return driver_template; \ No newline at end of file +return driver_template; diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/can_handle.lua new file mode 100644 index 0000000000..b8a5c5587c --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/can_handle.lua @@ -0,0 +1,21 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local function can_handle(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x0086, prodId = 0x0060}, + {mfr = 0x0371, prodId = 0x00AF}, -- Smart Switch 7 EU + {mfr = 0x0371, prodId = 0x0017} -- Smart Switch 7 US + } + + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, nil, fingerprint.prodId) then + local subdriver = require("aeotec-smart-switch") + return true, subdriver + end + end + return false +end + +return can_handle diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/init.lua b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/init.lua index d014188b46..0b34f1ba85 100644 --- a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local Basic = (require "st.zwave.CommandClass.Basic")({ version=1 }) @@ -28,22 +17,6 @@ local LAST_REPORT_TIME = "LAST_REPORT_TIME" local POWER_UNIT_WATT = "W" local ENERGY_UNIT_KWH = "kWh" -local FINGERPRINTS = { - {mfr = 0x0086, prodId = 0x0060}, - {mfr = 0x0371, prodId = 0x00AF}, -- Smart Switch 7 EU - {mfr = 0x0371, prodId = 0x0017} -- Smart Switch 7 US -} - -local function can_handle(opts, driver, device, ...) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:id_match(fingerprint.mfr, nil, fingerprint.prodId) then - local subdriver = require("aeotec-smart-switch") - return true, subdriver - end - end - return false -end - local function emit_power_consumption_report_event(device, value, channel) -- powerConsumptionReport report interval local current_time = os.time() @@ -144,7 +117,7 @@ local aeotec_smart_switch = { [Meter.REPORT] = meter_report_handler } }, - can_handle = can_handle + can_handle = require("aeotec-smart-switch.can_handle") } return aeotec_smart_switch diff --git a/drivers/SmartThings/zwave-switch/src/configurations.lua b/drivers/SmartThings/zwave-switch/src/configurations.lua index b2c283bd7c..199cf03237 100644 --- a/drivers/SmartThings/zwave-switch/src/configurations.lua +++ b/drivers/SmartThings/zwave-switch/src/configurations.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local devices = { AEOTEC_METERING_SWITCH = { diff --git a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/can_handle.lua b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/can_handle.lua new file mode 100644 index 0000000000..86fb4d7e68 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/can_handle.lua @@ -0,0 +1,24 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +--- Determine whether the passed device is Dawon smart plug +--- +--- @param driver Driver driver instance +--- @param device Device device isntance +--- @return boolean true if the device proper, else false +local function can_handle_dawon_smart_plug(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x018C, prod = 0x0042, model = 0x0005}, -- Dawon Smart Plug + {mfr = 0x018C, prod = 0x0042, model = 0x0008} -- Dawon Smart Multitab + } + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("dawon-smart-plug") + return true, subdriver + end + end + return false +end + +return can_handle_dawon_smart_plug diff --git a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/init.lua b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/init.lua index 6280842f6b..84643a500f 100644 --- a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/init.lua +++ b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -18,25 +7,7 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) -local DAWON_SMART_PLUG_FINGERPRINTS = { - {mfr = 0x018C, prod = 0x0042, model = 0x0005}, -- Dawon Smart Plug - {mfr = 0x018C, prod = 0x0042, model = 0x0008} -- Dawon Smart Multitab -} ---- Determine whether the passed device is Dawon smart plug ---- ---- @param driver Driver driver instance ---- @param device Device device isntance ---- @return boolean true if the device proper, else false -local function can_handle_dawon_smart_plug(opts, driver, device, ...) - for _, fingerprint in ipairs(DAWON_SMART_PLUG_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("dawon-smart-plug") - return true, subdriver - end - end - return false -end --- Default handler for notification reports --- @@ -60,7 +31,7 @@ local dawon_smart_plug = { [Notification.REPORT] = notification_report_handler } }, - can_handle = can_handle_dawon_smart_plug + can_handle = require("dawon-smart-plug.can_handle") } return dawon_smart_plug diff --git a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/can_handle.lua new file mode 100644 index 0000000000..cad81076d1 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/can_handle.lua @@ -0,0 +1,21 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +--- Determine whether the passed device is Dawon wall smart switch +--- +--- @param driver Driver driver instance +--- @param device Device device isntance +--- @return boolean true if the device proper, else false +local function can_handle_dawon_wall_smart_switch(opts, driver, device, ...) + local fingerprints = require("dawon-wall-smart-switch.fingerprints") + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("dawon-wall-smart-switch") + return true, subdriver + end + end + return false +end + +return can_handle_dawon_wall_smart_switch diff --git a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/fingerprints.lua new file mode 100644 index 0000000000..ab6633bac4 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + {mfr = 0x018C, prod = 0x0061, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 1 KR + {mfr = 0x018C, prod = 0x0062, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 2 KR + {mfr = 0x018C, prod = 0x0063, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 3 KR + {mfr = 0x018C, prod = 0x0064, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 1 US + {mfr = 0x018C, prod = 0x0065, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 2 US + {mfr = 0x018C, prod = 0x0066, model = 0x0001} -- Dawon Multipurpose Sensor + Smart Switch endpoint 3 US +} diff --git a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/init.lua b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/init.lua index f7978ad54d..4f4278c057 100644 --- a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -24,30 +13,6 @@ local Notification = (require "st.zwave.CommandClass.Notification")({ version = --- @type st.zwave.CommandClass.SensorMultilevel local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ version = 5 }) -local DAWON_WALL_SMART_SWITCH_FINGERPRINTS = { - {mfr = 0x018C, prod = 0x0061, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 1 KR - {mfr = 0x018C, prod = 0x0062, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 2 KR - {mfr = 0x018C, prod = 0x0063, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 3 KR - {mfr = 0x018C, prod = 0x0064, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 1 US - {mfr = 0x018C, prod = 0x0065, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 2 US - {mfr = 0x018C, prod = 0x0066, model = 0x0001} -- Dawon Multipurpose Sensor + Smart Switch endpoint 3 US -} - ---- Determine whether the passed device is Dawon wall smart switch ---- ---- @param driver Driver driver instance ---- @param device Device device isntance ---- @return boolean true if the device proper, else false -local function can_handle_dawon_wall_smart_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(DAWON_WALL_SMART_SWITCH_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("dawon-wall-smart-switch") - return true, subdriver - end - end - return false -end - --- Default handler for notification reports --- --- @param self st.zwave.Driver @@ -97,7 +62,7 @@ local dawon_wall_smart_switch = { doConfigure = do_configure, infoChanged = info_changed }, - can_handle = can_handle_dawon_wall_smart_switch, + can_handle = require("dawon-wall-smart-switch.can_handle"), } return dawon_wall_smart_switch diff --git a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/can_handle.lua b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/can_handle.lua new file mode 100644 index 0000000000..274676cbed --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + + +local function can_handle_eaton_5_scene_keypad(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x001A, prod = 0x574D, model = 0x0000}, -- Eaton 5-Scene Keypad + } + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("eaton-5-scene-keypad") + return true, subdriver + end + end + return false +end + +return can_handle_eaton_5_scene_keypad diff --git a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/init.lua b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/init.lua index 29fccc9261..f8315135fe 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/init.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 --- @type st.capabilities local capabilities = require "st.capabilities" @@ -29,10 +18,6 @@ local SceneControllerConf = (require "st.zwave.CommandClass.SceneControllerConf" local INDICATOR_SWITCH_STATES = "Indicator_switch_states" -local EATON_5_SCENE_KEYPAD_FINGERPRINT = { - {mfr = 0x001A, prod = 0x574D, model = 0x0000}, -- Eaton 5-Scene Keypad -} - local function upsert_after_bit_update_at_index(device, bit_position, new_bit) local old_value = device:get_field(INDICATOR_SWITCH_STATES) or 0 local mask = ~(0x1 << (bit_position - 1)) @@ -108,16 +93,6 @@ local function do_configure(self, device) device:set_field(INDICATOR_SWITCH_STATES, 0, { persist = true}) end -local function can_handle_eaton_5_scene_keypad(opts, driver, device, ...) - for _, fingerprint in ipairs(EATON_5_SCENE_KEYPAD_FINGERPRINT) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("eaton-5-scene-keypad") - return true, subdriver - end - end - return false -end - local eaton_5_scene_keypad = { NAME = "Eaton 5-Scene Keypad", zwave_handlers = { @@ -146,7 +121,7 @@ local eaton_5_scene_keypad = { lifecycle_handlers = { doConfigure = do_configure, }, - can_handle = can_handle_eaton_5_scene_keypad, + can_handle = require("eaton-5-scene-keypad.can_handle"), } return eaton_5_scene_keypad diff --git a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/can_handle.lua b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/can_handle.lua new file mode 100644 index 0000000000..6b608b042b --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/can_handle.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local function can_handle_eaton_accessory_dimmer(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x001A, prod = 0x4441, model = 0x0000} -- Eaton Dimmer Switch + } + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("eaton-accessory-dimmer") + return true, subdriver + end + end + return false +end + +return can_handle_eaton_accessory_dimmer diff --git a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/init.lua b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/init.lua index 3a9d5c9a47..bb49665075 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/init.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.utils @@ -24,20 +13,6 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) --- @type st.zwave.CommandClass.SwitchMultilevel local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version = 4 }) -local EATON_ACCESSORY_DIMMER_FINGERPRINTS = { - {mfr = 0x001A, prod = 0x4441, model = 0x0000} -- Eaton Dimmer Switch -} - -local function can_handle_eaton_accessory_dimmer(opts, driver, device, ...) - for _, fingerprint in ipairs(EATON_ACCESSORY_DIMMER_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("eaton-accessory-dimmer") - return true, subdriver - end - end - return false -end - local function dimmer_event(driver, device, cmd) local level = cmd.args.value and cmd.args.value or cmd.args.target_value @@ -110,7 +85,7 @@ local eaton_accessory_dimmer = { [capabilities.switchLevel.commands.setLevel.NAME] = switch_level_set } }, - can_handle = can_handle_eaton_accessory_dimmer, + can_handle = require("eaton-accessory-dimmer.can_handle"), } return eaton_accessory_dimmer diff --git a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/can_handle.lua new file mode 100644 index 0000000000..67aadecb8b --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + + +local function can_handle_eaton_anyplace_switch(opts, driver, device, ...) + local fingerprints = { + { manufacturerId = 0x001A, productType = 0x4243, productId = 0x0000 } -- Eaton Anyplace Switch + } + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subdriver = require("eaton-anyplace-switch") + return true, subdriver + end + end + return false +end + +return can_handle_eaton_anyplace_switch diff --git a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua index 720f88b56f..488babc276 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -18,19 +7,7 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Basic local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) -local EATON_ANYPLACE_SWITCH_FINGERPRINTS = { - { manufacturerId = 0x001A, productType = 0x4243, productId = 0x0000 } -- Eaton Anyplace Switch -} - -local function can_handle_eaton_anyplace_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(EATON_ANYPLACE_SWITCH_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subdriver = require("eaton-anyplace-switch") - return true, subdriver - end - end - return false -end +local switch_utils = require "switch_utils" local function basic_set_handler(self, device, cmd) if cmd.args.value == 0xFF then @@ -46,7 +23,7 @@ local function basic_get_handler(self, device, cmd) end local function device_added(driver, device) - device:emit_event(capabilities.switch.switch.off()) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.switch, capabilities.switch.switch.NAME, capabilities.switch.switch.off()) end local function switch_on_handler(driver, device) @@ -74,7 +51,7 @@ local eaton_anyplace_switch = { lifecycle_handlers = { added = device_added }, - can_handle = can_handle_eaton_anyplace_switch + can_handle = require("eaton-anyplace-switch.can_handle") } return eaton_anyplace_switch diff --git a/drivers/SmartThings/zwave-switch/src/ecolink-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/ecolink-switch/can_handle.lua new file mode 100644 index 0000000000..87771168f4 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/ecolink-switch/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local function can_handle_ecolink(opts, driver, device, ...) + local fingerprints = require("ecolink-switch.fingerprints") + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("ecolink-switch") + return true, subdriver + end + end + return false +end + +return can_handle_ecolink diff --git a/drivers/SmartThings/zwave-switch/src/ecolink-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/ecolink-switch/fingerprints.lua new file mode 100644 index 0000000000..742590dc1d --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/ecolink-switch/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + {mfr = 0x014A, prod = 0x0006, model = 0x0002}, + {mfr = 0x014A, prod = 0x0006, model = 0x0003}, + {mfr = 0x014A, prod = 0x0006, model = 0x0004}, + {mfr = 0x014A, prod = 0x0006, model = 0x0005}, + {mfr = 0x014A, prod = 0x0006, model = 0x0006} +} diff --git a/drivers/SmartThings/zwave-switch/src/ecolink-switch/init.lua b/drivers/SmartThings/zwave-switch/src/ecolink-switch/init.lua index 3e69399801..081cf113dd 100644 --- a/drivers/SmartThings/zwave-switch/src/ecolink-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/ecolink-switch/init.lua @@ -1,39 +1,10 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" local Basic = (require "st.zwave.CommandClass.Basic")({ version=1 }) -local ECOLINK_FINGERPRINTS = { - {mfr = 0x014A, prod = 0x0006, model = 0x0002}, - {mfr = 0x014A, prod = 0x0006, model = 0x0003}, - {mfr = 0x014A, prod = 0x0006, model = 0x0004}, - {mfr = 0x014A, prod = 0x0006, model = 0x0005}, - {mfr = 0x014A, prod = 0x0006, model = 0x0006} -} - -local function can_handle_ecolink(opts, driver, device, ...) - for _, fingerprint in ipairs(ECOLINK_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("ecolink-switch") - return true, subdriver - end - end - return false -end - local function basic_set_handler(driver, device, cmd) if cmd.args.value == 0xFF then device:emit_event(capabilities.switch.switch.on()) @@ -49,7 +20,7 @@ local ecolink_switch = { [Basic.SET] = basic_set_handler } }, - can_handle = can_handle_ecolink + can_handle = require("ecolink-switch.can_handle") } return ecolink_switch diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/can_handle.lua new file mode 100644 index 0000000000..5645551e3b --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/can_handle.lua @@ -0,0 +1,21 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local function can_handle_fibaro_double_switch(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x010F, prod = 0x0203, model = 0x1000}, -- Fibaro Switch + {mfr = 0x010F, prod = 0x0203, model = 0x2000}, -- Fibaro Switch + {mfr = 0x010F, prod = 0x0203, model = 0x3000} -- Fibaro Switch + } + + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("fibaro-double-switch") + return true, subdriver + end + end + return false +end + +return can_handle_fibaro_double_switch diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/init.lua b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/init.lua index 8bccc75464..e02d986b91 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local st_device = require "st.device" local capabilities = require "st.capabilities" @@ -34,22 +23,6 @@ local ENDPOINTS = { child = 2 } -local FIBARO_DOUBLE_SWITCH_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x0203, model = 0x1000}, -- Fibaro Switch - {mfr = 0x010F, prod = 0x0203, model = 0x2000}, -- Fibaro Switch - {mfr = 0x010F, prod = 0x0203, model = 0x3000} -- Fibaro Switch -} - -local function can_handle_fibaro_double_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(FIBARO_DOUBLE_SWITCH_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("fibaro-double-switch") - return true, subdriver - end - end - return false -end - local function do_refresh(driver, device, command) local component = command and command.component and command.component or "main" device:send_to_component(SwitchBinary:Get({}), component) @@ -140,7 +113,7 @@ local fibaro_double_switch = { init = device_init, added = device_added }, - can_handle = can_handle_fibaro_double_switch, + can_handle = require("fibaro-double-switch.can_handle") } return fibaro_double_switch diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/can_handle.lua new file mode 100644 index 0000000000..80d41e924b --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/can_handle.lua @@ -0,0 +1,20 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local function can_handle_fibaro_single_switch(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x010F, prod = 0x0403, model = 0x1000}, -- Fibaro Switch + {mfr = 0x010F, prod = 0x0403, model = 0x2000}, -- Fibaro Switch + {mfr = 0x010F, prod = 0x0403, model = 0x3000} -- Fibaro Switch + } + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("fibaro-single-switch") + return true, subdriver + end + end + return false +end + +return can_handle_fibaro_single_switch diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/init.lua b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/init.lua index e36703ccd6..8a3252cc32 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local ButtonDefaults = require "st.zwave.defaults.button" @@ -28,22 +17,6 @@ local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = --- @type st.zwave.CommandClass.Basic local Basic = (require "st.zwave.CommandClass.Basic")({ version=1 }) -local FIBARO_SINGLE_SWITCH_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x0403, model = 0x1000}, -- Fibaro Switch - {mfr = 0x010F, prod = 0x0403, model = 0x2000}, -- Fibaro Switch - {mfr = 0x010F, prod = 0x0403, model = 0x3000} -- Fibaro Switch -} - -local function can_handle_fibaro_single_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(FIBARO_SINGLE_SWITCH_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("fibaro-single-switch") - return true, subdriver - end - end - return false -end - local function central_scene_notification_handler(self, device, cmd) if cmd.src_channel == nil or cmd.src_channel == 0 then ButtonDefaults.zwave_handlers[cc.CENTRAL_SCENE][CentralScene.NOTIFICATION](self, device, cmd) @@ -94,7 +67,7 @@ local fibaro_single_switch = { [capabilities.switch.commands.off.NAME] = switch_handler_factory(0x00), } }, - can_handle = can_handle_fibaro_single_switch, + can_handle = require("fibaro-single-switch.can_handle") } return fibaro_single_switch diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/can_handle.lua b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/can_handle.lua new file mode 100644 index 0000000000..2a4c8f5b40 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local function can_handle_fibaro_wall_plug(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x010F, prod = 0x1401, model = 0x1001}, -- Fibaro Outlet + {mfr = 0x010F, prod = 0x1401, model = 0x2000}, -- Fibaro Outlet + } + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("fibaro-wall-plug-us") + return true, subdriver + end + end + return false +end + +return can_handle_fibaro_wall_plug diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/init.lua b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/init.lua index 6224421c75..8800e029ab 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/init.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/init.lua @@ -1,31 +1,5 @@ --- Copyright 2022 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 FIBARO_WALL_PLUG_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x1401, model = 0x1001}, -- Fibaro Outlet - {mfr = 0x010F, prod = 0x1401, model = 0x2000}, -- Fibaro Outlet -} - -local function can_handle_fibaro_wall_plug(opts, driver, device, ...) - for _, fingerprint in ipairs(FIBARO_WALL_PLUG_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("fibaro-wall-plug-us") - return true, subdriver - end - end - return false -end +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local function component_to_endpoint(device, component_id) if component_id == "main" then @@ -54,7 +28,7 @@ local fibaro_wall_plug = { lifecycle_handlers = { init = device_init }, - can_handle = can_handle_fibaro_wall_plug, + can_handle = require("fibaro-wall-plug-us.can_handle"), } return fibaro_wall_plug diff --git a/drivers/SmartThings/zwave-switch/src/init.lua b/drivers/SmartThings/zwave-switch/src/init.lua index 3acae8ffe0..26cca570a7 100644 --- a/drivers/SmartThings/zwave-switch/src/init.lua +++ b/drivers/SmartThings/zwave-switch/src/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.defaults @@ -103,19 +92,6 @@ local function switch_multilevel_stop_level_change_handler(driver, device, cmd) device:send(SwitchMultilevel:Get({})) end -local function lazy_load_if_possible(sub_driver_name) - -- gets the current lua libs api version - local version = require "version" - - -- version 9 will include the lazy loading functions - if version.api >= 9 then - return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) - else - return require(sub_driver_name) - end - -end - ------------------------------------------------------------------------------------------- -- Register message handlers and run driver ------------------------------------------------------------------------------------------- @@ -142,28 +118,7 @@ local driver_template = { [SwitchMultilevel.STOP_LEVEL_CHANGE] = switch_multilevel_stop_level_change_handler } }, - sub_drivers = { - lazy_load_if_possible("eaton-accessory-dimmer"), - lazy_load_if_possible("inovelli-LED"), - lazy_load_if_possible("dawon-smart-plug"), - lazy_load_if_possible("inovelli-2-channel-smart-plug"), - lazy_load_if_possible("zwave-dual-switch"), - lazy_load_if_possible("eaton-anyplace-switch"), - lazy_load_if_possible("fibaro-wall-plug-us"), - lazy_load_if_possible("dawon-wall-smart-switch"), - lazy_load_if_possible("zooz-power-strip"), - lazy_load_if_possible("aeon-smart-strip"), - lazy_load_if_possible("qubino-switches"), - lazy_load_if_possible("fibaro-double-switch"), - lazy_load_if_possible("fibaro-single-switch"), - lazy_load_if_possible("eaton-5-scene-keypad"), - lazy_load_if_possible("ecolink-switch"), - lazy_load_if_possible("multi-metering-switch"), - lazy_load_if_possible("zooz-zen-30-dimmer-relay"), - lazy_load_if_possible("multichannel-device"), - lazy_load_if_possible("aeotec-smart-switch"), - lazy_load_if_possible("aeotec-heavy-duty") - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { init = device_init, infoChanged = info_changed, diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/can_handle.lua new file mode 100644 index 0000000000..2bc88d57c5 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local function can_handle_inovelli_2_channel_smart_plug(opts, driver, device, ...) + local fingerprints = require("inovelli-2-channel-smart-plug.fingerprints") + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("inovelli-2-channel-smart-plug") + return true, subdriver + end + end + return false +end + +return can_handle_inovelli_2_channel_smart_plug diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/fingerprints.lua new file mode 100644 index 0000000000..ab2150468d --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/fingerprints.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + {mfr = 0x015D, prod = 0x0221, model = 0x251C}, -- Show Home Outlet + {mfr = 0x0312, prod = 0x0221, model = 0x251C}, -- Inovelli Outlet + {mfr = 0x0312, prod = 0xB221, model = 0x251C}, -- Inovelli Outlet + {mfr = 0x0312, prod = 0x0221, model = 0x611C}, -- Inovelli Outlet + {mfr = 0x015D, prod = 0x0221, model = 0x611C}, -- Inovelli Outlet + {mfr = 0x015D, prod = 0x6100, model = 0x6100}, -- Inovelli Outlet + {mfr = 0x0312, prod = 0x6100, model = 0x6100}, -- Inovelli Outlet + {mfr = 0x015D, prod = 0x2500, model = 0x2500}, -- Inovelli Outlet +} diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/init.lua index d97f85063c..313280d131 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/init.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -24,27 +13,6 @@ local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = --- @type st.zwave.CommandClass.Association local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) -local INOVELLI_2_CHANNEL_SMART_PLUG_FINGERPRINTS = { - {mfr = 0x015D, prod = 0x0221, model = 0x251C}, -- Show Home Outlet - {mfr = 0x0312, prod = 0x0221, model = 0x251C}, -- Inovelli Outlet - {mfr = 0x0312, prod = 0xB221, model = 0x251C}, -- Inovelli Outlet - {mfr = 0x0312, prod = 0x0221, model = 0x611C}, -- Inovelli Outlet - {mfr = 0x015D, prod = 0x0221, model = 0x611C}, -- Inovelli Outlet - {mfr = 0x015D, prod = 0x6100, model = 0x6100}, -- Inovelli Outlet - {mfr = 0x0312, prod = 0x6100, model = 0x6100}, -- Inovelli Outlet - {mfr = 0x015D, prod = 0x2500, model = 0x2500}, -- Inovelli Outlet -} - -local function can_handle_inovelli_2_channel_smart_plug(opts, driver, device, ...) - for _, fingerprint in ipairs(INOVELLI_2_CHANNEL_SMART_PLUG_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("inovelli-2-channel-smart-plug") - return true, subdriver - end - end - return false -end - local function handle_main_switch_event(device, value) if value == SwitchBinary.value.ON_ENABLE then device:emit_event(capabilities.switch.switch.on()) @@ -125,7 +93,7 @@ local inovelli_2_channel_smart_plug = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_inovelli_2_channel_smart_plug, + can_handle = require("inovelli-2-channel-smart-plug.can_handle") } return inovelli_2_channel_smart_plug diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua deleted file mode 100644 index 36bf1e0716..0000000000 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua +++ /dev/null @@ -1,111 +0,0 @@ --- Copyright 2022 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 capabilities = require "st.capabilities" ---- @type st.utils -local utils = require "st.utils" ---- @type st.zwave.constants -local constants = require "st.zwave.constants" ---- @type st.zwave.CommandClass -local cc = require "st.zwave.CommandClass" ---- @type st.zwave.CommandClass.Configuration -local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) - -local LED_COLOR_CONTROL_PARAMETER_NUMBER = 13 -local LED_GENERIC_SATURATION = 100 -local INOVELLI_MANUFACTURER_ID = 0x031E -local INOVELLI_LZW31SN_PRODUCT_TYPE = 0x0001 -local INOVELLI_LZW31_PRODUCT_TYPE = 0x0003 -local INOVELLI_DIMMER_PRODUCT_ID = 0x0001 -local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" - -local function huePercentToZwaveValue(value) - if value <= 2 then - return 0 - elseif value >= 98 then - return 255 - else - return utils.round(value / 100 * 255) - end -end - -local function zwaveValueToHuePercent(value) - if value <= 2 then - return 0 - elseif value >= 254 then - return 100 - else - return utils.round(value / 255 * 100) - end -end - -local function configuration_report(driver, device, cmd) - if cmd.args.parameter_number == LED_COLOR_CONTROL_PARAMETER_NUMBER then - local hue = zwaveValueToHuePercent(cmd.args.configuration_value) - - local ledBarComponent = device.profile.components[LED_BAR_COMPONENT_NAME] - if ledBarComponent ~= nil then - device:emit_component_event(ledBarComponent, capabilities.colorControl.hue(hue)) - device:emit_component_event(ledBarComponent, capabilities.colorControl.saturation(LED_GENERIC_SATURATION)) - end - end -end - -local function set_color(driver, device, cmd) - local value = huePercentToZwaveValue(cmd.args.color.hue) - local config = Configuration:Set({ - parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER, - configuration_value=value, - size=2 - }) - device:send(config) - - local query_configuration = function() - device:send(Configuration:Get({ parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER })) - end - - device.thread:call_with_delay(constants.DEFAULT_GET_STATUS_DELAY, query_configuration) -end - -local function can_handle_inovelli_led(opts, driver, device, ...) - if device:id_match( - INOVELLI_MANUFACTURER_ID, - {INOVELLI_LZW31SN_PRODUCT_TYPE, INOVELLI_LZW31_PRODUCT_TYPE}, - INOVELLI_DIMMER_PRODUCT_ID - ) then - local subdriver = require("inovelli-LED") - return true, subdriver - end - return false -end - -local inovelli_led = { - NAME = "Inovelli LED", - zwave_handlers = { - [cc.CONFIGURATION] = { - [Configuration.REPORT] = configuration_report - } - }, - capability_handlers = { - [capabilities.colorControl.ID] = { - [capabilities.colorControl.commands.setColor.NAME] = set_color - } - }, - can_handle = can_handle_inovelli_led, - sub_drivers = { - require("inovelli-LED/inovelli-lzw31sn") - } -} - -return inovelli_led diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua deleted file mode 100644 index 307ee9b46a..0000000000 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua +++ /dev/null @@ -1,112 +0,0 @@ --- Copyright 2022 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 capabilities = require "st.capabilities" ---- @type st.zwave.CommandClass -local cc = require "st.zwave.CommandClass" ---- @type st.zwave.CommandClass.CentralScene -local CentralScene = (require "st.zwave.CommandClass.CentralScene")({version=3}) - -local INOVELLI_MANUFACTURER_ID = 0x031E -local INOVELLI_LZW31SN_PRODUCT_TYPE = 0x0001 -local INOVELLI_DIMMER_PRODUCT_ID = 0x0001 -local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" - -local supported_button_values = { - ["button1"] = {"pushed", "pushed_2x", "pushed_3x", "pushed_4x", "pushed_5x"}, - ["button2"] = {"pushed", "pushed_2x", "pushed_3x", "pushed_4x", "pushed_5x"}, - ["button3"] = {"pushed"} -} - -local function device_added(driver, device) - for _, component in pairs(device.profile.components) do - if component.id ~= "main" and component.id ~= LED_BAR_COMPONENT_NAME 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 - device:refresh() -end - -local map_scene_number_to_component = { - [1] = "button2", - [2] = "button1", - [3] = "button3" -} - - -local map_key_attribute_to_capability = { - [CentralScene.key_attributes.KEY_PRESSED_1_TIME] = capabilities.button.button.pushed, - [CentralScene.key_attributes.KEY_PRESSED_2_TIMES] = capabilities.button.button.pushed_2x, - [CentralScene.key_attributes.KEY_PRESSED_3_TIMES] = capabilities.button.button.pushed_3x, - [CentralScene.key_attributes.KEY_PRESSED_4_TIMES] = capabilities.button.button.pushed_4x, - [CentralScene.key_attributes.KEY_PRESSED_5_TIMES] = capabilities.button.button.pushed_5x, -} - -local function central_scene_notification_handler(self, device, cmd) - if ( cmd.args.scene_number ~= nil and cmd.args.scene_number ~= 0 ) then - local capability_attribute = map_key_attribute_to_capability[cmd.args.key_attributes] - local additional_fields = { - state_change = true - } - - local event - if capability_attribute ~= nil then - event = capability_attribute(additional_fields) - end - - if event ~= nil then - -- device reports scene notifications from endpoint 0 (main) but central scene events have to be emitted for button components: 1,2,3 - local comp = device.profile.components[map_scene_number_to_component[cmd.args.scene_number]] - if comp ~= nil then - device:emit_component_event(comp, event) - end - end - end -end - -local function can_handle_inovelli_lzw31sn(opts, driver, device, ...) - if device:id_match( - INOVELLI_MANUFACTURER_ID, - INOVELLI_LZW31SN_PRODUCT_TYPE, - INOVELLI_DIMMER_PRODUCT_ID - ) then - return true - end - return false -end - -local inovelli_led_lzw31sn = { - NAME = "Inovelli LED LZW 31SN", - zwave_handlers = { - [cc.CENTRAL_SCENE] = { - [CentralScene.NOTIFICATION] = central_scene_notification_handler - } - }, - lifecycle_handlers = { - added = device_added - }, - can_handle = can_handle_inovelli_lzw31sn -} - -return inovelli_led_lzw31sn diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli/can_handle.lua new file mode 100644 index 0000000000..7c0f6be77b --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/can_handle.lua @@ -0,0 +1,20 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local INOVELLI_FINGERPRINTS = { + { mfr = 0x031E, prod = 0x0017, model = 0x0001 }, -- Inovelli VZW32-SN + { mfr = 0x031E, prod = 0x0001, model = 0x0001 }, -- Inovelli LZW31SN + { mfr = 0x031E, prod = 0x0003, model = 0x0001 }, -- Inovelli LZW31 +} + +local function can_handle_inovelli(opts, driver, device, ...) + for _, fingerprint in ipairs(INOVELLI_FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("inovelli") + return true, subdriver + end + end + return false +end + +return can_handle_inovelli diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli/init.lua new file mode 100644 index 0000000000..84ed2c7df7 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/init.lua @@ -0,0 +1,501 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass.Configuration +local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) +--- @type st.zwave.CommandClass.Association +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) +--- @type st.zwave.CommandClass.SwitchBinary +local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = 2 }) +--- @type st.zwave.CommandClass.Basic +local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) +--- @type st.zwave.CommandClass.SwitchMultilevel +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) +--- @type st.zwave.CommandClass.Meter +local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) +local preferencesMap = require "preferences" + +--- @type st.utils +local utils = require "st.utils" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +local log = require "log" +local st_device = require "st.device" + +--- @type st.zwave.CommandClass.CentralScene +local CentralScene = (require "st.zwave.CommandClass.CentralScene")({version=3}) +--- @type st.zwave.constants +local constants = require "st.zwave.constants" + +local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" + +local GEN3_NOTIFICATION_PARAMETER_NUMBER = 99 +local GEN2_NOTIFICATION_PARAMETER_NUMBER = 16 +local LED_COLOR_CONTROL_PARAMETER_NUMBER = 13 +local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" +local LED_GENERIC_SATURATION = 100 + +-- TODO: Remove after transition period - supportedButtonValues initialization +-- This table defines the supported button values for each button component. +-- Used to initialize supportedButtonValues on device_added and update devices with old values. +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"} +} + +-- Device type detection helpers +local function is_gen2(device) + return device:id_match(0x031E, {0x0001, 0x0003}, 0x0001) +end + +local function is_gen3(device) + return device:id_match(0x031E, {0x0015, 0x0017}, 0x0001) +end + +-- Helper function to get the correct notification parameter number based on device type +local function get_notification_parameter_number(device) + -- For child devices, check the parent device type + local device_to_check = device + if device.network_type == st_device.NETWORK_TYPE_CHILD then + device_to_check = device:get_parent_device() + end + + if is_gen3(device_to_check) then + return GEN3_NOTIFICATION_PARAMETER_NUMBER + else + return GEN2_NOTIFICATION_PARAMETER_NUMBER + end +end + +local function button_to_component(buttonId) + if buttonId > 0 then + return string.format("button%d", buttonId) + end +end + +local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return utils.round(value / 100 * 255) + end +end + +local function valueToHuePercent(value) + if value <= 2 then + return 0 + elseif value >= 254 then + return 100 + else + return utils.round(value / 255 * 100) + end +end + +local preferences_to_numeric_value = function(new_value) + local numeric = tonumber(new_value) + if numeric == nil then -- in case the value is boolean + numeric = new_value and 1 or 0 + end + return numeric +end + +local preferences_calculate_parameter = function(new_value, type, number) + if type == 4 and new_value > 2147483647 then + return ((4294967296 - new_value) * -1) + elseif type == 2 and new_value > 32767 then + return ((65536 - new_value) * -1) + elseif type == 1 and new_value > 127 then + return ((256 - new_value) * -1) + else + return new_value + end +end + +local function add_child(driver,parent,profile,child_type) + local child_metadata = { + type = "EDGE_CHILD", + label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)), + profile = profile, + parent_device_id = parent.id, + parent_assigned_child_key = child_type, + vendor_provided_label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)) + } + driver:try_create_device(child_metadata) +end + +local function getNotificationValue(device, value) + local level = device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) or 100 + local color = utils.round(device:get_latest_state("main", capabilities.colorControl.ID, capabilities.colorControl.hue.NAME) or 100) + local effect = device:get_parent_device().preferences.notificationType or 1 + local duration = 255 -- Default duration + + -- Get the parent device to check generation for child devices + local device_to_check = device + if device.network_type == st_device.NETWORK_TYPE_CHILD then + device_to_check = device:get_parent_device() + end + + local colorValue = huePercentToValue(value or color) + local notificationValue = 0 + + if is_gen3(device_to_check) then + -- Gen3 order: duration, level, color, effect (bytes 0-3 from low to high) + notificationValue = notificationValue + (effect * 16777216) -- byte 3 (highest) + notificationValue = notificationValue + (colorValue * 65536) -- byte 2 + notificationValue = notificationValue + (level * 256) -- byte 1 + notificationValue = notificationValue + (duration * 1) -- byte 0 (lowest) + else + -- Gen2 order: color, level, duration, effect (bytes 0-3 from low to high) + notificationValue = notificationValue + (effect * 16777216) -- byte 3 (highest) + notificationValue = notificationValue + (duration * 65536) -- byte 2 + notificationValue = notificationValue + (level * 256) -- byte 1 + notificationValue = notificationValue + (colorValue * 1) -- byte 0 (lowest) + end + + return notificationValue +end + +local function set_color(driver, device, command) + if device.network_type == st_device.NETWORK_TYPE_CHILD then + device:emit_event(capabilities.colorControl.hue(command.args.color.hue)) + device:emit_event(capabilities.colorControl.saturation(command.args.color.saturation)) + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + local config = Configuration:Set({ + parameter_number=get_notification_parameter_number(device), + configuration_value=getNotificationValue(device), + size=4 + }) + local send_configuration = function() + dev:send(config) + end + device.thread:call_with_delay(1,send_configuration) + else + local value = huePercentToValue(command.args.color.hue) + local config = Configuration:Set({ + parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER, + configuration_value=value, + size=2 + }) + device:send(config) + + local query_configuration = function() + device:send(Configuration:Get({ parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER })) + end + + device.thread:call_with_delay(constants.DEFAULT_GET_STATUS_DELAY, query_configuration) + end +end + +local function set_color_temperature(driver, device, command) + if device.network_type == st_device.NETWORK_TYPE_CHILD then + device:emit_event(capabilities.colorControl.hue(100)) + device:emit_event(capabilities.colorTemperature.colorTemperature(command.args.temperature)) + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + local config = Configuration:Set({ + parameter_number=get_notification_parameter_number(device), + configuration_value=getNotificationValue(device, 100), + size=4 + }) + local send_configuration = function() + dev:send(config) + end + device.thread:call_with_delay(1,send_configuration) + else + local value = huePercentToValue(100) + local config = Configuration:Set({ + parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER, + configuration_value=value, + size=2 + }) + device:send(config) + + local query_configuration = function() + device:send(Configuration:Get({ parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER })) + end + + device.thread:call_with_delay(constants.DEFAULT_GET_STATUS_DELAY, query_configuration) + end +end + +local function switch_level_set(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + local level = utils.round(command.args.level) + level = utils.clamp_value(level, 0, 99) + + device:send(SwitchMultilevel:Set({ value=level, duration=command.args.rate or "default" })) + + device.thread:call_with_delay(3, function(d) + device:send(SwitchMultilevel:Get({})) + end) + else + device:emit_event(capabilities.switchLevel.level(command.args.level)) + device:emit_event(capabilities.switch.switch(command.args.level ~= 0 and "on" or "off")) + local dev = device:get_parent_device() + local config = Configuration:Set({ + parameter_number=get_notification_parameter_number(device), + configuration_value=getNotificationValue(device), + size=4 + }) + local send_configuration = function() + dev:send(config) + end + device.thread:call_with_delay(1,send_configuration) + end +end + +local function refresh_handler(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(SwitchMultilevel:Get({})) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.WATTS })) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS })) + end +end + +local function device_added(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(Association:Set({grouping_identifier = 1, node_ids = {driver.environment_info.hub_zwave_id}})) + refresh_handler(driver, device) + if is_gen2(device) then + local ledBarComponent = device.profile.components[LED_BAR_COMPONENT_NAME] + if ledBarComponent ~= nil then + device:emit_component_event(ledBarComponent, capabilities.colorControl.hue(1)) + device:emit_component_event(ledBarComponent, capabilities.colorControl.saturation(1)) + end + end + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +end + +local function info_changed(driver, device, event, args) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + local time_diff = 3 + local last_clock_set_time = device:get_field(LATEST_CLOCK_SET_TIMESTAMP) + if last_clock_set_time ~= nil then + time_diff = os.difftime(os.time(), last_clock_set_time) + end + device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time(), {persist = true}) + if time_diff > 2 then + local preferences = preferencesMap.get_device_parameters(device) + if args.old_st_store.preferences["notificationChild"] ~= device.preferences.notificationChild and args.old_st_store.preferences["notificationChild"] == false and device.preferences.notificationChild == true then + if not device:get_child_by_parent_assigned_key('notification') then + add_child(driver,device,'rgbw-bulb','notification') + end + end + + for id, value in pairs(device.preferences) do + if args.old_st_store.preferences[id] ~= value and preferences and preferences[id] then + local new_parameter_value = preferences_calculate_parameter(preferences_to_numeric_value(device.preferences[id]), preferences[id].size, id) + device:send(Configuration:Set({parameter_number = preferences[id].parameter_number, size = preferences[id].size, configuration_value = new_parameter_value})) + end + end + else + log.info("info_changed running more than once. Cancelling this run. Time diff: " .. time_diff) + end + end +end + +local function switch_set_on_off_handler(value) + return function(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(Basic:Set({ value = value })) + device.thread:call_with_delay(3, function(d) + device:send(SwitchMultilevel:Get({})) + end) + else + device:emit_event(capabilities.switch.switch(value == 0 and "off" or "on")) + local dev = device:get_parent_device() + local config = Configuration:Set({ + parameter_number=get_notification_parameter_number(device), + configuration_value=(value == 0 and 0 or getNotificationValue(device)), + size=4 + }) + local send_configuration = function() + dev:send(config) + end + device.thread:call_with_delay(1,send_configuration) + end + end +end + +local function configuration_report(driver, device, cmd) + if cmd.args.parameter_number == LED_COLOR_CONTROL_PARAMETER_NUMBER and is_gen2(device) then + local hue = valueToHuePercent(cmd.args.configuration_value) + + local ledBarComponent = device.profile.components[LED_BAR_COMPONENT_NAME] + if ledBarComponent ~= nil then + device:emit_component_event(ledBarComponent, capabilities.colorControl.hue(hue)) + device:emit_component_event(ledBarComponent, capabilities.colorControl.saturation(LED_GENERIC_SATURATION)) + end + end +end + +local map_key_attribute_to_capability = { + [CentralScene.key_attributes.KEY_PRESSED_1_TIME] = capabilities.button.button.pushed, + [CentralScene.key_attributes.KEY_RELEASED] = capabilities.button.button.held, + [CentralScene.key_attributes.KEY_HELD_DOWN] = capabilities.button.button.down_hold, + [CentralScene.key_attributes.KEY_PRESSED_2_TIMES] = capabilities.button.button.pushed_2x, + [CentralScene.key_attributes.KEY_PRESSED_3_TIMES] = capabilities.button.button.pushed_3x, + [CentralScene.key_attributes.KEY_PRESSED_4_TIMES] = capabilities.button.button.pushed_4x, + [CentralScene.key_attributes.KEY_PRESSED_5_TIMES] = capabilities.button.button.pushed_5x, +} + +-- Map key attributes to their button value strings for support checking +-- TODO: This mapping and the support check below can likely be removed after a transition period. +-- Once users have interacted with their devices and the supportedButtonValues gets properly +-- set during device initialization, the driver will know which values are supported and +-- won't attempt to emit unsupported events. This code is a temporary safeguard to prevent +-- errors during the transition period. +local map_key_attribute_to_value = { + [CentralScene.key_attributes.KEY_RELEASED] = "held", + [CentralScene.key_attributes.KEY_HELD_DOWN] = "down_hold", +} + +-- TODO: Remove after transition period - button value support checking +-- Helper function to check if a button value is supported. +-- This function can likely be removed after a transition period once devices have +-- their supportedButtonValues properly set. See comment above map_key_attribute_to_value. +local function is_button_value_supported(device, component, value) + if value == nil then + return true -- If no value to check, assume supported + end + + local supported_values_state = device:get_latest_state( + component.id, + capabilities.button.ID, + capabilities.button.supportedButtonValues.NAME + ) + + -- Check multiple possible structures for supportedButtonValues + -- In SmartThings Edge, get_latest_state returns a state object + -- For supportedButtonValues, the array could be in: state.value, or state itself IS the array + local supported_values = nil + if supported_values_state ~= nil then + -- First check .value property (most common structure) + if supported_values_state.value ~= nil then + supported_values = supported_values_state.value + -- Check if state itself is an array (the state IS the array) + -- Check if index 1 exists - if it does and .value doesn't exist, the state itself is the array + elseif type(supported_values_state) == "table" and supported_values_state[1] ~= nil then + supported_values = supported_values_state + end + + -- Check .state.value structure (nested structure) + if supported_values == nil and supported_values_state.state ~= nil and supported_values_state.state.value ~= nil then + supported_values = supported_values_state.state.value + end + end + + if supported_values == nil then + return true -- If no supported values set, assume all are supported (backward compatibility) + end + + -- Check if the value is in the supported values array + if type(supported_values) == "table" then + for _, supported_value in ipairs(supported_values) do + if supported_value == value then + return true + end + end + end + + return false +end + +local function central_scene_notification_handler(self, device, cmd) + if ( cmd.args.scene_number ~= nil and cmd.args.scene_number ~= 0 ) then + local button_number = cmd.args.scene_number + local capability_attribute = map_key_attribute_to_capability[cmd.args.key_attributes] + local additional_fields = { + state_change = true + } + + local event + if capability_attribute ~= nil then + event = capability_attribute(additional_fields) + end + + if event ~= nil then + -- device reports scene notifications from endpoint 0 (main) but central scene events have to be emitted for button components: 1,2,3 + local component_name = button_to_component(button_number) + local comp = device.profile.components[component_name] + if comp ~= nil then + -- TODO: Remove after transition period - button value support checking + -- Check if held or down_hold are supported before emitting. + -- This support check can likely be removed after a transition period once devices + -- have their supportedButtonValues properly set. The driver will then only emit events + -- for values that are actually supported, preventing errors. See comment above map_key_attribute_to_value. + local button_value = map_key_attribute_to_value[cmd.args.key_attributes] + local is_supported = is_button_value_supported(device, comp, button_value) + if button_value == nil or is_supported then + device:emit_component_event(comp, event) + else + -- TODO: Remove after transition period - supportedButtonValues update for old devices + -- Update supportedButtonValues for devices with old values from previous driver versions. + -- After updating, emit the event since the value is now supported. + if supported_button_values[comp.id] ~= nil then + device:emit_component_event( + comp, + capabilities.button.supportedButtonValues( + supported_button_values[comp.id], + { visibility = { displayed = false } } + ) + ) + device:emit_component_event(comp, event) + end + end + end + end + end +end + +------------------------------------------------------------------------------------------- +-- Register message handlers and run driver +------------------------------------------------------------------------------------------- +local inovelli = { + NAME = "Inovelli Z-Wave Switch", + lifecycle_handlers = { + infoChanged = info_changed, + added = device_added, + }, + zwave_handlers = { + [cc.CENTRAL_SCENE] = { + [CentralScene.NOTIFICATION] = central_scene_notification_handler + }, + [cc.CONFIGURATION] = { + [Configuration.REPORT] = configuration_report + } + }, + capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.switch.on.NAME] = switch_set_on_off_handler(SwitchBinary.value.ON_ENABLE), + [capabilities.switch.switch.off.NAME] = switch_set_on_off_handler(SwitchBinary.value.OFF_DISABLE) + }, + [capabilities.colorControl.ID] = { + [capabilities.colorControl.commands.setColor.NAME] = set_color + }, + [capabilities.colorTemperature.ID] = { + [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature + }, + [capabilities.switchLevel.ID] = { + [capabilities.switchLevel.commands.setLevel.NAME] = switch_level_set + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh_handler + } + }, + can_handle = require("inovelli.can_handle"), + sub_drivers = require("inovelli.sub_drivers"), +} + +return inovelli \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/can_handle.lua new file mode 100644 index 0000000000..6b70eb3c27 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local INOVELLI_MANUFACTURER_ID = 0x031E +local INOVELLI_LZW31SN_PRODUCT_TYPE = 0x0001 +local INOVELLI_DIMMER_PRODUCT_ID = 0x0001 + +local function can_handle_lzw31sn(opts, driver, device, ...) + if device:id_match( + INOVELLI_MANUFACTURER_ID, + INOVELLI_LZW31SN_PRODUCT_TYPE, + INOVELLI_DIMMER_PRODUCT_ID + ) then + return true, require("inovelli.lzw31-sn") + end + return false +end + +return can_handle_lzw31sn diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/init.lua new file mode 100644 index 0000000000..77a5430681 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/init.lua @@ -0,0 +1,74 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass.SwitchMultilevel +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) +--- @type st.zwave.CommandClass.Meter +local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) +--- @type st.zwave.CommandClass.Association +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) +--- @type st.device +local st_device = require "st.device" + +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"} +} + +local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" + +local function refresh_handler(driver, device) + device:send(SwitchMultilevel:Get({})) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.WATTS })) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS })) +end + +local function device_added(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(Association:Set({grouping_identifier = 1, node_ids = {driver.environment_info.hub_zwave_id}})) + for _, component in pairs(device.profile.components) do + if component.id ~= "main" and component.id ~= LED_BAR_COMPONENT_NAME 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 + refresh_handler(driver, device) + local ledBarComponent = device.profile.components[LED_BAR_COMPONENT_NAME] + if ledBarComponent ~= nil then + device:emit_component_event(ledBarComponent, capabilities.colorControl.hue(1)) + device:emit_component_event(ledBarComponent, capabilities.colorControl.saturation(1)) + end + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +end + +local lzw31_sn = { + NAME = "Inovelli LZW31-SN Z-Wave Dimmer", + lifecycle_handlers = { + added = device_added, + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh_handler + } + }, + can_handle = require("inovelli.lzw31-sn.can_handle") +} + +return lzw31_sn \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/inovelli/sub_drivers.lua new file mode 100644 index 0000000000..e182120ece --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("inovelli.lzw31-sn"), + lazy_load("inovelli.vzw32-sn") +} diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/can_handle.lua new file mode 100644 index 0000000000..5ed4e272e0 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local INOVELLI_MANUFACTURER_ID = 0x031E +local INOVELLI_VZW32_SN_PRODUCT_TYPE = 0x0017 +local INOVELLI_DIMMER_PRODUCT_ID = 0x0001 + +local function can_handle_vzw32_sn(opts, driver, device, ...) + if device:id_match( + INOVELLI_MANUFACTURER_ID, + INOVELLI_VZW32_SN_PRODUCT_TYPE, + INOVELLI_DIMMER_PRODUCT_ID + ) then + return true, require("inovelli.vzw32-sn") + end + return false +end + +return can_handle_vzw32_sn diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/init.lua new file mode 100644 index 0000000000..0ad1d31430 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/init.lua @@ -0,0 +1,73 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass.SwitchMultilevel +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) +--- @type st.zwave.CommandClass.SensorMultilevel +local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ version = 7 }) +--- @type st.zwave.CommandClass.Meter +local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +--- @type st.zwave.CommandClass.Association +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) +--- @type st.device +local st_device = require "st.device" + +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"} +} + +local function refresh_handler(driver, device) + device:send(SwitchMultilevel:Get({})) + device:send(SensorMultilevel:Get({sensor_type = SensorMultilevel.sensor_type.ILLUMINANCE})) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.WATTS })) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS })) + device:send(Notification:Get({notification_type = Notification.notification_type.HOME_SECURITY, event = Notification.event.home_security.MOTION_DETECTION})) +end + +local function device_added(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(Association:Set({grouping_identifier = 1, node_ids = {driver.environment_info.hub_zwave_id}})) + for _, component in pairs(device.profile.components) do + if component.id ~= "main" and component.id ~= "LEDColorConfiguration" 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 + refresh_handler(driver, device) + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +end + +local vzw32_sn = { + NAME = "Inovelli VZW32-SN mmWave Dimmer", + lifecycle_handlers = { + added = device_added, + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh_handler + } + }, + can_handle = require("inovelli.vzw32-sn.can_handle") +} + +return vzw32_sn \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-switch/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/can_handle.lua new file mode 100644 index 0000000000..89fd6ef17f --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local function can_handle_multi_metering_switch(opts, driver, device, ...) + local fingerprints = require("multi-metering-switch.fingerprints") + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("multi-metering-switch") + return true, subdriver + end + end + return false +end + +return can_handle_multi_metering_switch diff --git a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/fingerprints.lua new file mode 100644 index 0000000000..d35e06a5f0 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/fingerprints.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + {mfr = 0x0086, prod = 0x0003, model = 0x0084}, -- Aeotec Nano Switch 1 + {mfr = 0x0086, prod = 0x0103, model = 0x0084}, -- Aeotec Nano Switch 1 + {mfr = 0x0086, prod = 0x0203, model = 0x0084}, -- AU Aeotec Nano Switch 1 + {mfr = 0x027A, prod = 0xA000, model = 0xA004}, -- Zooz ZEN Power Strip 1 + {mfr = 0x015F, prod = 0x3102, model = 0x0201}, -- WYFY Touch 1-button Switch + {mfr = 0x015F, prod = 0x3102, model = 0x0202}, -- WYFY Touch 2-button Switch + {mfr = 0x015F, prod = 0x3102, model = 0x0204}, -- WYFY Touch 4-button Switch + {mfr = 0x015F, prod = 0x3111, model = 0x5102}, -- WYFY Touch 1-button Switch + {mfr = 0x015F, prod = 0x3121, model = 0x5102}, -- WYFY Touch 2-button Switch + {mfr = 0x015F, prod = 0x3141, model = 0x5102}, -- WYFY Touch 4-button Switch + {mfr = 0x0460, prod = 0x0002, model = 0x0081}, -- Shelly Wave 2PM + {mfr = 0x0460, prod = 0x0002, model = 0x008C}, -- Shelly Wave Pro 2 + {mfr = 0x0460, prod = 0x0002, model = 0x008D}, -- Shelly Wave Pro 2PM +} diff --git a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/init.lua b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/init.lua index efe7d35893..ca4c8951d9 100644 --- a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local st_device = require "st.device" local utils = require "st.utils" @@ -31,32 +20,6 @@ local MULTI_METERING_SWITCH_CONFIGURATION_MAP = require "multi-metering-switch/m local PARENT_ENDPOINT = 1 -local MULTI_METERING_SWITCH_FINGERPRINTS = { - {mfr = 0x0086, prod = 0x0003, model = 0x0084}, -- Aeotec Nano Switch 1 - {mfr = 0x0086, prod = 0x0103, model = 0x0084}, -- Aeotec Nano Switch 1 - {mfr = 0x0086, prod = 0x0203, model = 0x0084}, -- AU Aeotec Nano Switch 1 - {mfr = 0x027A, prod = 0xA000, model = 0xA004}, -- Zooz ZEN Power Strip 1 - {mfr = 0x015F, prod = 0x3102, model = 0x0201}, -- WYFY Touch 1-button Switch - {mfr = 0x015F, prod = 0x3102, model = 0x0202}, -- WYFY Touch 2-button Switch - {mfr = 0x015F, prod = 0x3102, model = 0x0204}, -- WYFY Touch 4-button Switch - {mfr = 0x015F, prod = 0x3111, model = 0x5102}, -- WYFY Touch 1-button Switch - {mfr = 0x015F, prod = 0x3121, model = 0x5102}, -- WYFY Touch 2-button Switch - {mfr = 0x015F, prod = 0x3141, model = 0x5102}, -- WYFY Touch 4-button Switch - {mfr = 0x0460, prod = 0x0002, model = 0x0081}, -- Shelly Wave 2PM - {mfr = 0x0460, prod = 0x0002, model = 0x008C}, -- Shelly Wave Pro 2 - {mfr = 0x0460, prod = 0x0002, model = 0x008D}, -- Shelly Wave Pro 2PM -} - -local function can_handle_multi_metering_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(MULTI_METERING_SWITCH_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("multi-metering-switch") - return true, subdriver - end - end - return false -end - local function find_child(parent, ep_id) if ep_id == PARENT_ENDPOINT then return parent @@ -199,7 +162,7 @@ local multi_metering_switch = { init = device_init, added = device_added }, - can_handle = can_handle_multi_metering_switch, + can_handle = require("multi-metering-switch.can_handle"), } return multi_metering_switch diff --git a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/multi_metering_switch_configurations.lua b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/multi_metering_switch_configurations.lua index dfb773efc1..af047646d5 100644 --- a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/multi_metering_switch_configurations.lua +++ b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/multi_metering_switch_configurations.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local devices = { AEOTEC_NANO_SWITCH_1 = { diff --git a/drivers/SmartThings/zwave-switch/src/multichannel-device/can_handle.lua b/drivers/SmartThings/zwave-switch/src/multichannel-device/can_handle.lua new file mode 100644 index 0000000000..49464a3240 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/multichannel-device/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" + +local function can_handle_multichannel_device(opts, driver, device, ...) + if device:supports_capability(capabilities.zwMultichannel) then + local subdriver = require("multichannel-device") + return true, subdriver + end + return false +end + +return can_handle_multichannel_device diff --git a/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua b/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua index 54349673e8..82cba1330f 100644 --- a/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua +++ b/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua @@ -1,18 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local cc = require "st.zwave.CommandClass" -local capabilities = require "st.capabilities" local st_device = require "st.device" local MultiChannel = (require "st.zwave.CommandClass.MultiChannel")({ version = 3 }) local utils = require "st.utils" @@ -27,14 +15,6 @@ local map_device_class_to_profile = { [0xA1] = "generic-sensor" } -local function can_handle_multichannel_device(opts, driver, device, ...) - if device:supports_capability(capabilities.zwMultichannel) then - local subdriver = require("multichannel-device") - return true, subdriver - end - return false -end - local function find_child(device, src_channel) if src_channel == 0 then return device @@ -91,7 +71,7 @@ local multichannel_device = { [MultiChannel.CAPABILITY_REPORT] = capability_get_report_handler } }, - can_handle = can_handle_multichannel_device + can_handle = require("multichannel-device.can_handle") } -return multichannel_device \ No newline at end of file +return multichannel_device diff --git a/drivers/SmartThings/zwave-switch/src/preferences.lua b/drivers/SmartThings/zwave-switch/src/preferences.lua index 73ed5e52f3..824e570070 100644 --- a/drivers/SmartThings/zwave-switch/src/preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local AEOTEC_HEAVY_DUTY_SWITCH = { PARAMETERS = { @@ -70,6 +59,44 @@ local devices = { switchType = {parameter_number = 22, size = 1} } }, + INOVELLI_VZW32_SN = { + MATCHING_MATRIX = { + mfrs = 0x031E, + product_types = {0x0017}, + product_ids = 0x0001 + }, + PARAMETERS = { + parameter158 = {parameter_number = 158, size = 1}, + parameter52 = {parameter_number = 52, size = 1}, + parameter1 = {parameter_number = 1, size = 1}, + parameter2 = {parameter_number = 2, size = 1}, + parameter3 = {parameter_number = 3, size = 1}, + parameter4 = {parameter_number = 4, size = 1}, + parameter9 = {parameter_number = 9, size = 1}, + parameter10 = {parameter_number = 10, size = 1}, + parameter15 = {parameter_number = 15, size = 1}, + parameter18 = {parameter_number = 18, size = 1}, + parameter19 = {parameter_number = 19, size = 2}, + parameter20 = {parameter_number = 20, size = 2}, + parameter50 = {parameter_number = 50, size = 1}, + parameter95 = {parameter_number = 95, size = 1}, + parameter96 = {parameter_number = 96, size = 1}, + parameter97 = {parameter_number = 97, size = 1}, + parameter98 = {parameter_number = 98, size = 1}, + parameter101 = {parameter_number = 101, size = 2}, + parameter102 = {parameter_number = 102, size = 2}, + parameter103 = {parameter_number = 103, size = 2}, + parameter104 = {parameter_number = 104, size = 2}, + parameter105 = {parameter_number = 105, size = 2}, + parameter106 = {parameter_number = 106, size = 2}, + parameter108 = {parameter_number = 108, size = 4}, + parameter110 = {parameter_number = 110, size = 2}, + parameter111 = {parameter_number = 111, size = 1}, + parameter112 = {parameter_number = 112, size = 1}, + parameter113 = {parameter_number = 113, size = 1}, + parameter114 = {parameter_number = 114, size = 4} + } + }, QUBINO_FLUSH_DIMMER = { MATCHING_MATRIX = { mfrs = 0x0159, @@ -428,4 +455,4 @@ preferences.to_numeric_value = function(new_value) return numeric end -return preferences \ No newline at end of file +return preferences diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/can_handle.lua new file mode 100644 index 0000000000..3312c13838 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local constants = require "qubino-switches.constants.qubino-constants" + +local function can_handle_qubino_flush_relay(opts, driver, device, cmd, ...) + if device:id_match(constants.QUBINO_MFR) then + local subdriver = require("qubino-switches") + return true, subdriver + end + return false +end + +return can_handle_qubino_flush_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/constants/qubino-constants.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/constants/qubino-constants.lua index 31613f40b9..8395eb78fe 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/constants/qubino-constants.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/constants/qubino-constants.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 return { TEMP_SENSOR_WORK_THRESHOLD = -20, -- Celsius degrees diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/fingerprints.lua new file mode 100644 index 0000000000..056b743e8f --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/fingerprints.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + {mfr = 0x0159, prod = 0x0001, model = 0x0051, deviceProfile = "qubino-flush-dimmer"}, -- Qubino Flush Dimmer + {mfr = 0x0159, prod = 0x0001, model = 0x0052, deviceProfile = "qubino-din-dimmer"}, -- Qubino DIN Dimmer + {mfr = 0x0159, prod = 0x0001, model = 0x0053, deviceProfile = "qubino-flush-dimmer-0-10V"}, -- Qubino Flush Dimmer 0-10V + {mfr = 0x0159, prod = 0x0001, model = 0x0055, deviceProfile = "qubino-mini-dimmer"}, -- Qubino Mini Dimmer + {mfr = 0x0159, prod = 0x0002, model = 0x0051, deviceProfile = "qubino-flush2-relay"}, -- Qubino Flush 2 Relay + {mfr = 0x0159, prod = 0x0002, model = 0x0052, deviceProfile = "qubino-flush1-relay"}, -- Qubino Flush 1 Relay + {mfr = 0x0159, prod = 0x0002, model = 0x0053, deviceProfile = "qubino-flush1d-relay"} -- Qubino Flush 1D Relay +} diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/init.lua index d883aa4821..15d541366a 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -26,19 +15,11 @@ local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({version=1}) local constants = require "qubino-switches/constants/qubino-constants" -local QUBINO_FINGERPRINTS = { - {mfr = 0x0159, prod = 0x0001, model = 0x0051, deviceProfile = "qubino-flush-dimmer"}, -- Qubino Flush Dimmer - {mfr = 0x0159, prod = 0x0001, model = 0x0052, deviceProfile = "qubino-din-dimmer"}, -- Qubino DIN Dimmer - {mfr = 0x0159, prod = 0x0001, model = 0x0053, deviceProfile = "qubino-flush-dimmer-0-10V"}, -- Qubino Flush Dimmer 0-10V - {mfr = 0x0159, prod = 0x0001, model = 0x0055, deviceProfile = "qubino-mini-dimmer"}, -- Qubino Mini Dimmer - {mfr = 0x0159, prod = 0x0002, model = 0x0051, deviceProfile = "qubino-flush2-relay"}, -- Qubino Flush 2 Relay - {mfr = 0x0159, prod = 0x0002, model = 0x0052, deviceProfile = "qubino-flush1-relay"}, -- Qubino Flush 1 Relay - {mfr = 0x0159, prod = 0x0002, model = 0x0053, deviceProfile = "qubino-flush1d-relay"} -- Qubino Flush 1D Relay -} +local fingerprints = require("qubino-switches.fingerprints") local function getDeviceProfile(device, isTemperatureSensorOnboard) local newDeviceProfile - for _, fingerprint in ipairs(QUBINO_FINGERPRINTS) do + for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then newDeviceProfile = fingerprint.deviceProfile if(isTemperatureSensorOnboard) then @@ -51,14 +32,6 @@ local function getDeviceProfile(device, isTemperatureSensorOnboard) return nil end -local function can_handle_qubino_flush_relay(opts, driver, device, cmd, ...) - if device:id_match(constants.QUBINO_MFR) then - local subdriver = require("qubino-switches") - return true, subdriver - end - return false -end - local function add_temperature_sensor_if_needed(device) if not (device:supports_capability_by_id(capabilities.temperatureMeasurement.ID)) then local new_profile = getDeviceProfile(device, true) @@ -112,7 +85,7 @@ end local qubino_relays = { NAME = "Qubino Relays", - can_handle = can_handle_qubino_flush_relay, + can_handle = require("qubino-switches.can_handle"), zwave_handlers = { [cc.SENSOR_MULTILEVEL] = { [SensorMultilevel.REPORT] = sensor_multilevel_report @@ -126,10 +99,7 @@ local qubino_relays = { lifecycle_handlers = { added = device_added }, - sub_drivers = { - require("qubino-switches/qubino-relays"), - require("qubino-switches/qubino-dimmer") - } + sub_drivers = require("qubino-switches.sub_drivers"), } return qubino_relays diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/can_handle.lua new file mode 100644 index 0000000000..9588fdccae --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local function can_handle_qubino_dimmer(opts, driver, device, ...) + local fingerprints = require("qubino-switches.qubino-dimmer.fingerprints") + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("qubino-switches.qubino-dimmer") + end + end + return false +end + +return can_handle_qubino_dimmer diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/fingerprints.lua new file mode 100644 index 0000000000..32946fe973 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + {mfr = 0x0159, prod = 0x0001, model = 0x0051}, -- Qubino Flush Dimmer + {mfr = 0x0159, prod = 0x0001, model = 0x0052}, -- Qubino DIN Dimmer + {mfr = 0x0159, prod = 0x0001, model = 0x0053}, -- Qubino Flush Dimmer 0-10V + {mfr = 0x0159, prod = 0x0001, model = 0x0055} -- Qubino Mini Dimmer +} diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/init.lua index 03023adffa..d0f0e607a5 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/init.lua @@ -1,35 +1,8 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local MultichannelAssociation = (require "st.zwave.CommandClass.MultiChannelAssociation")({ version = 3 }) -local QUBINO_DIMMER_FINGERPRINTS = { - {mfr = 0x0159, prod = 0x0001, model = 0x0051}, -- Qubino Flush Dimmer - {mfr = 0x0159, prod = 0x0001, model = 0x0052}, -- Qubino DIN Dimmer - {mfr = 0x0159, prod = 0x0001, model = 0x0053}, -- Qubino Flush Dimmer 0-10V - {mfr = 0x0159, prod = 0x0001, model = 0x0055} -- Qubino Mini Dimmer -} - -local function can_handle_qubino_dimmer(opts, driver, device, ...) - for _, fingerprint in ipairs(QUBINO_DIMMER_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end - local function do_configure(self, device) device:send(MultichannelAssociation:Remove({grouping_identifier = 1, node_ids = {}})) device:send(MultichannelAssociation:Set({grouping_identifier = 1, node_ids = {self.environment_info.hub_zwave_id}})) @@ -40,10 +13,8 @@ local qubino_dimmer = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_qubino_dimmer, - sub_drivers = { - require("qubino-switches/qubino-dimmer/qubino-din-dimmer") - } + can_handle = require("qubino-switches.qubino-dimmer.can_handle"), + sub_drivers = require("qubino-switches.qubino-dimmer.sub_drivers"), } return qubino_dimmer diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/can_handle.lua new file mode 100644 index 0000000000..b0da891a74 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_qubino_din_dimmer(opts, driver, device, ...) + -- Qubino Din Dimmer: mfr = 0x0159, prod = 0x0001, model = 0x0052 + if device:id_match(0x0159, 0x0001, 0x0052) then + return true, require("qubino-switches.qubino-dimmer.qubino-din-dimmer") + end + return false +end + +return can_handle_qubino_din_dimmer diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/init.lua index f763c36c34..e47d4b149f 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/init.lua @@ -1,28 +1,9 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) local MultichannelAssociation = (require "st.zwave.CommandClass.MultiChannelAssociation")({ version = 3 }) -local function can_handle_qubino_din_dimmer(opts, driver, device, ...) - -- Qubino Din Dimmer: mfr = 0x0159, prod = 0x0001, model = 0x0052 - if device:id_match(0x0159, 0x0001, 0x0052) then - return true - end - return false -end - local function do_configure(self, device) device:send(MultichannelAssociation:Remove({grouping_identifier = 1, node_ids = {}})) device:send(MultichannelAssociation:Set({grouping_identifier = 1, node_ids = {self.environment_info.hub_zwave_id}})) @@ -34,7 +15,7 @@ local qubino_din_dimmer = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_qubino_din_dimmer + can_handle = require("qubino-switches.qubino-dimmer.qubino-din-dimmer.can_handle") } return qubino_din_dimmer diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/sub_drivers.lua new file mode 100644 index 0000000000..96da43c3e6 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("qubino-switches.qubino-dimmer.qubino-din-dimmer"), +} diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/can_handle.lua new file mode 100644 index 0000000000..4eb7c04595 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local function can_handle_qubino_flush_relay(opts, driver, device, cmd, ...) + local fingerprints = { + {mfr = 0x0159, prod = 0x0002, model = 0x0051}, -- Qubino Flush 2 Relay + {mfr = 0x0159, prod = 0x0002, model = 0x0052}, -- Qubino Flush 1 Relay + {mfr = 0x0159, prod = 0x0002, model = 0x0053} -- Qubino Flush 1D Relay + } + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("qubino-switches.qubino-relays") + end + end + return false +end + +return can_handle_qubino_flush_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/init.lua index b6e3918cfb..8199235310 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/init.lua @@ -1,34 +1,8 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local Association = (require "st.zwave.CommandClass.Association")({ version = 2 }) -local QUBINO_FLUSH_RELAY_FINGERPRINT = { - {mfr = 0x0159, prod = 0x0002, model = 0x0051}, -- Qubino Flush 2 Relay - {mfr = 0x0159, prod = 0x0002, model = 0x0052}, -- Qubino Flush 1 Relay - {mfr = 0x0159, prod = 0x0002, model = 0x0053} -- Qubino Flush 1D Relay -} - -local function can_handle_qubino_flush_relay(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(QUBINO_FLUSH_RELAY_FINGERPRINT) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end - local function do_configure(self, device) local association_cmd = Association:Set({grouping_identifier = 2, node_ids = {self.environment_info.hub_zwave_id}}) -- This command needs to be sent before creating component @@ -40,12 +14,8 @@ end local qubino_relays = { NAME = "Qubino Relays", - can_handle = can_handle_qubino_flush_relay, - sub_drivers = { - require("qubino-switches/qubino-relays/qubino-flush-2-relay"), - require("qubino-switches/qubino-relays/qubino-flush-1-relay"), - require("qubino-switches/qubino-relays/qubino-flush-1d-relay") - }, + can_handle = require("qubino-switches.qubino-relays.can_handle"), + sub_drivers = require("qubino-switches.qubino-relays.sub_drivers"), lifecycle_handlers = { doConfigure = do_configure }, diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/can_handle.lua new file mode 100644 index 0000000000..f91589bce9 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local QUBINO_FLUSH_1_RELAY_FINGERPRINT = {mfr = 0x0159, prod = 0x0002, model = 0x0052} + +local function can_handle_qubino_flush_1_relay(opts, driver, device, ...) + if device:id_match(QUBINO_FLUSH_1_RELAY_FINGERPRINT.mfr, QUBINO_FLUSH_1_RELAY_FINGERPRINT.prod, QUBINO_FLUSH_1_RELAY_FINGERPRINT.model) then + return true, require("qubino-switches.qubino-relays.qubino-flush-1-relay") + end + return false +end + +return can_handle_qubino_flush_1_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/init.lua index 7fef39539b..61cf89394e 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/init.lua @@ -1,28 +1,11 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 --- @type st.zwave.CommandClass.Association local Association = (require "st.zwave.CommandClass.Association")({version=2}) --- @type st.zwave.CommandClass.MultiChannelAssociation local MultiChannelAssociation = (require "st.zwave.CommandClass.MultiChannelAssociation")({version=3}) -local QUBINO_FLUSH_1_RELAY_FINGERPRINT = {mfr = 0x0159, prod = 0x0002, model = 0x0052} - -local function can_handle_qubino_flush_1_relay(opts, driver, device, ...) - return device:id_match(QUBINO_FLUSH_1_RELAY_FINGERPRINT.mfr, QUBINO_FLUSH_1_RELAY_FINGERPRINT.prod, QUBINO_FLUSH_1_RELAY_FINGERPRINT.model) -end - local function do_configure(self, device) -- Hub automatically adds device to multiChannelAssosciationGroup and this needs to be removed device:send(MultiChannelAssociation:Remove({grouping_identifier = 1, node_ids = {}})) @@ -40,7 +23,7 @@ local qubino_flush_1_relay = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_qubino_flush_1_relay + can_handle = require("qubino-switches.qubino-relays.qubino-flush-1-relay.can_handle") } return qubino_flush_1_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/can_handle.lua new file mode 100644 index 0000000000..9d5f69bb34 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local QUBINO_FLUSH_1D_RELAY_FINGERPRINT = {mfr = 0x0159, prod = 0x0002, model = 0x0053} + +local function can_handle_qubino_flush_1d_relay(opts, driver, device, ...) + if device:id_match(QUBINO_FLUSH_1D_RELAY_FINGERPRINT.mfr, QUBINO_FLUSH_1D_RELAY_FINGERPRINT.prod, QUBINO_FLUSH_1D_RELAY_FINGERPRINT.model) then + return true, require("qubino-switches.qubino-relays.qubino-flush-1d-relay") + end + return false +end + +return can_handle_qubino_flush_1d_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/init.lua index 55b900920c..8803a85af2 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/init.lua @@ -1,25 +1,8 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local MultichannelAssociation = (require "st.zwave.CommandClass.MultiChannelAssociation")({ version = 3 }) -local QUBINO_FLUSH_1D_RELAY_FINGERPRINT = {mfr = 0x0159, prod = 0x0002, model = 0x0053} - -local function can_handle_qubino_flush_1d_relay(opts, driver, device, ...) - return device:id_match(QUBINO_FLUSH_1D_RELAY_FINGERPRINT.mfr, QUBINO_FLUSH_1D_RELAY_FINGERPRINT.prod, QUBINO_FLUSH_1D_RELAY_FINGERPRINT.model) -end - local function do_configure(self, device) device:send(MultichannelAssociation:Set({grouping_identifier = 2, node_ids = {self.environment_info.hub_zwave_id}})) device:refresh() @@ -30,7 +13,7 @@ local qubino_flush_1d_relay = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_qubino_flush_1d_relay + can_handle = require("qubino-switches.qubino-relays.qubino-flush-1d-relay.can_handle") } return qubino_flush_1d_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/can_handle.lua new file mode 100644 index 0000000000..f3cb5f83e9 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local QUBINO_FLUSH_2_RELAY_FINGERPRINT = { mfr = 0x0159, prod = 0x0002, model = 0x0051 } + +local function can_handle_qubino_flush_2_relay(opts, driver, device, ...) + if device:id_match(QUBINO_FLUSH_2_RELAY_FINGERPRINT.mfr, QUBINO_FLUSH_2_RELAY_FINGERPRINT.prod, QUBINO_FLUSH_2_RELAY_FINGERPRINT.model) then + return true, require("qubino-switches.qubino-relays.qubino-flush-2-relay") + end + return false +end + +return can_handle_qubino_flush_2_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/init.lua index c0a8d0c967..8f60cc1e58 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local st_device = require "st.device" local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -30,12 +19,6 @@ local utils = require "st.utils" local CHILD_SWITCH_EP = 2 local CHILD_TEMP_SENSOR_EP = 3 -local QUBINO_FLUSH_2_RELAY_FINGERPRINT = { mfr = 0x0159, prod = 0x0002, model = 0x0051 } - -local function can_handle_qubino_flush_2_relay(opts, driver, device, ...) - return device:id_match(QUBINO_FLUSH_2_RELAY_FINGERPRINT.mfr, QUBINO_FLUSH_2_RELAY_FINGERPRINT.prod, QUBINO_FLUSH_2_RELAY_FINGERPRINT.model) -end - local function component_to_endpoint(device, component_id) return { 1 } end @@ -178,7 +161,7 @@ local qubino_flush_2_relay = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } }, - can_handle = can_handle_qubino_flush_2_relay + can_handle = require("qubino-switches.qubino-relays.qubino-flush-2-relay.can_handle") } return qubino_flush_2_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/sub_drivers.lua new file mode 100644 index 0000000000..27319d943b --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load = require "lazy_load_subdriver" +return { + lazy_load("qubino-switches.qubino-relays.qubino-flush-1-relay"), + lazy_load("qubino-switches.qubino-relays.qubino-flush-1d-relay"), + lazy_load("qubino-switches.qubino-relays.qubino-flush-2-relay"), +} diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/sub_drivers.lua new file mode 100644 index 0000000000..09a60bf335 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("qubino-switches.qubino-relays"), + lazy_load("qubino-switches.qubino-dimmer"), +} diff --git a/drivers/SmartThings/zwave-switch/src/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/sub_drivers.lua new file mode 100644 index 0000000000..b4cd3d57e9 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/sub_drivers.lua @@ -0,0 +1,28 @@ + +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" + +return { + lazy_load_if_possible("eaton-accessory-dimmer"), + lazy_load_if_possible("inovelli"), + lazy_load_if_possible("dawon-smart-plug"), + lazy_load_if_possible("inovelli-2-channel-smart-plug"), + lazy_load_if_possible("zwave-dual-switch"), + lazy_load_if_possible("eaton-anyplace-switch"), + lazy_load_if_possible("fibaro-wall-plug-us"), + lazy_load_if_possible("dawon-wall-smart-switch"), + lazy_load_if_possible("zooz-power-strip"), + lazy_load_if_possible("aeon-smart-strip"), + lazy_load_if_possible("qubino-switches"), + lazy_load_if_possible("fibaro-double-switch"), + lazy_load_if_possible("fibaro-single-switch"), + lazy_load_if_possible("eaton-5-scene-keypad"), + lazy_load_if_possible("ecolink-switch"), + lazy_load_if_possible("multi-metering-switch"), + lazy_load_if_possible("zooz-zen-30-dimmer-relay"), + lazy_load_if_possible("multichannel-device"), + lazy_load_if_possible("aeotec-smart-switch"), + lazy_load_if_possible("aeotec-heavy-duty") +} diff --git a/drivers/SmartThings/zwave-switch/src/switch_utils.lua b/drivers/SmartThings/zwave-switch/src/switch_utils.lua new file mode 100644 index 0000000000..66ad4715f9 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/switch_utils.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local switch_utils = {} + +switch_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + +return switch_utils diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeon_smart_strip.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeon_smart_strip.lua index 0985633cdd..f11a379666 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeon_smart_strip.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeon_smart_strip.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dimmer_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dimmer_switch.lua index f172ba9fde..6fb6c9c5ef 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dimmer_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dimmer_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -296,6 +285,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dual_nano_switch_configuration.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dual_nano_switch_configuration.lua index c5c3331ba6..272321f53f 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dual_nano_switch_configuration.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dual_nano_switch_configuration.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_heavy_duty_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_heavy_duty_switch.lua index 857411c120..a11fe3abcb 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_heavy_duty_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_heavy_duty_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -479,4 +468,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_metering_switch_configuration.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_metering_switch_configuration.lua index d0d87cce8e..2ebeb664bb 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_metering_switch_configuration.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_metering_switch_configuration.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer.lua index 11f4707f8a..db1e4c2498 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -320,6 +309,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer_preferences.lua index dba043703f..a8206a0052 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer_preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch.lua index d5878b6fbc..fdba32d9cb 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_eu.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_eu.lua index 50c1a98a57..079616dba4 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_eu.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_eu.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_us.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_us.lua index 6e65c5ffab..0ecb71e59d 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_us.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_us.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_gen5.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_gen5.lua index a1e3607ce3..4c2b76fdf6 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_gen5.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_gen5.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_dawon_smart_plug.lua b/drivers/SmartThings/zwave-switch/src/test/test_dawon_smart_plug.lua index 543d3b0b97..cbfe5f42a0 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_dawon_smart_plug.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_dawon_smart_plug.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_dawon_wall_smart_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_dawon_wall_smart_switch.lua index bfa8073280..ee48f99494 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_dawon_wall_smart_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_dawon_wall_smart_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_eaton_5_scene_keypad.lua b/drivers/SmartThings/zwave-switch/src/test/test_eaton_5_scene_keypad.lua index 5157669f72..d50b7cdaa5 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_eaton_5_scene_keypad.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_eaton_5_scene_keypad.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_eaton_accessory_dimmer.lua b/drivers/SmartThings/zwave-switch/src/test/test_eaton_accessory_dimmer.lua index 2ff605d146..8b5e96b161 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_eaton_accessory_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_eaton_accessory_dimmer.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_eaton_anyplace_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_eaton_anyplace_switch.lua index 93783aa1a4..6815a408da 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_eaton_anyplace_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_eaton_anyplace_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -45,6 +34,7 @@ test.set_test_init_function(test_init) test.register_message_test( "Basic SET 0x00 should be handled as switch off", { + -- The initial switch event should be send during the device's first time onboarding { channel = "device_lifecycle", direction = "receive", @@ -60,6 +50,22 @@ test.register_message_test( direction = "receive", message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({value=0x00})) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.off()) + }, + -- Avoid sending the initial switch event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + { + channel = "device_lifecycle", + direction = "receive", + message = {mock_device.id, "added"} + }, + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({value=0x00})) } + }, { channel = "capability", direction = "send", @@ -71,6 +77,7 @@ test.register_message_test( test.register_message_test( "Basic SET 0xFF should be handled as switch on", { + -- The initial switch event should be send during the device's first time onboarding { channel = "device_lifecycle", direction = "receive", @@ -86,6 +93,22 @@ test.register_message_test( direction = "receive", message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({value=0xFF})) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + }, + -- Avoid sending the initial switch event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + { + channel = "device_lifecycle", + direction = "receive", + message = {mock_device.id, "added"} + }, + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({value=0xFF})) } + }, { channel = "capability", direction = "send", diff --git a/drivers/SmartThings/zwave-switch/src/test/test_eaton_rf_dimmer.lua b/drivers/SmartThings/zwave-switch/src/test/test_eaton_rf_dimmer.lua index d939e78d12..99c6ae4068 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_eaton_rf_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_eaton_rf_dimmer.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_ecolink_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_ecolink_switch.lua index c208be9849..4f5557f9a3 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_ecolink_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_ecolink_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_double_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_double_switch.lua index 86b830be19..59934e6439 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_double_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_double_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_single_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_single_switch.lua index cafd5ee79b..0b40d6be33 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_single_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_single_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_eu.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_eu.lua index 94ccbdb384..58ef03fc3f 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_eu.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_eu.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_uk_configuration.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_uk_configuration.lua index 65636f6c22..44aae0f2fa 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_uk_configuration.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_uk_configuration.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_us.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_us.lua index f0ed1f2520..8aa815e829 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_us.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_us.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_dimmer_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_dimmer_preferences.lua index fc42034137..db69dc9845 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_dimmer_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_dimmer_preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch.lua index 11746559a5..f4e41b5bee 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -214,6 +203,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_parent:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -387,6 +384,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_child:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_child.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch_preferences.lua index 3dbdd66622..2771cbc3a7 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch_preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_generic_zwave_device1.lua b/drivers/SmartThings/zwave-switch/src/test/test_generic_zwave_device1.lua index 89056f13e9..309d2c79b0 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_generic_zwave_device1.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_generic_zwave_device1.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_go_control_plug_in_switch_configuraton.lua b/drivers/SmartThings/zwave-switch/src/test/test_go_control_plug_in_switch_configuraton.lua index abbce9082b..9b72af072e 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_go_control_plug_in_switch_configuraton.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_go_control_plug_in_switch_configuraton.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_honeywell_dimmer.lua b/drivers/SmartThings/zwave-switch/src/test/test_honeywell_dimmer.lua index cde284936a..4125fc6f00 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_honeywell_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_honeywell_dimmer.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_2_channel_smart_plug.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_2_channel_smart_plug.lua index 12753b2879..bc752ec6f1 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_2_channel_smart_plug.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_2_channel_smart_plug.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua index 59fa304000..0863f9eb43 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua @@ -1,23 +1,14 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local zw = require "st.zwave" local zw_test_utils = require "integration_test.zwave_test_utils" -local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) local CentralScene = (require "st.zwave.CommandClass.CentralScene")({version=1}) +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) +local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) local t_utils = require "integration_test.utils" local INOVELLI_MANUFACTURER_ID = 0x031E @@ -66,8 +57,8 @@ end test.set_test_init_function(test_init) local supported_button_values = { - ["button1"] = {"pushed", "pushed_2x", "pushed_3x", "pushed_4x", "pushed_5x"}, - ["button2"] = {"pushed", "pushed_2x", "pushed_3x", "pushed_4x", "pushed_5x"}, + ["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"} } @@ -100,9 +91,44 @@ test.register_coroutine_test( test.socket.zwave:__expect_send( zw_test_utils.zwave_test_build_send_command( mock_inovelli_dimmer, - Basic:Get({}) + Association:Set({ grouping_identifier = 1, node_ids = {}, payload = "\x01" }) ) ) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_dimmer, + SwitchMultilevel:Get({}) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_dimmer, + Meter:Get({ scale = Meter.scale.electric_meter.WATTS }) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_dimmer, + Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS }) + ) + ) + + local ledBarComponent = mock_inovelli_dimmer.profile.components[LED_BAR_COMPONENT_NAME] + if ledBarComponent ~= nil then + test.socket.capability:__expect_send( + mock_inovelli_dimmer:generate_test_message( + LED_BAR_COMPONENT_NAME, + capabilities.colorControl.hue(1) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_dimmer:generate_test_message( + LED_BAR_COMPONENT_NAME, + capabilities.colorControl.saturation(1) + ) + ) + end end ) @@ -126,7 +152,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed({state_change = true})) + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed({state_change = true})) } } ) @@ -150,7 +176,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_4x({ state_change = true })) } } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer.lua index 250182526d..d7dfe35b24 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_led.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_led.lua index 3141e5021f..6df8b97048 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_led.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_led.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_power_energy.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_power_energy.lua index 7605ada316..eab58c99f9 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_power_energy.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_power_energy.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_preferences.lua index 6b4e978981..43dd38ee7f 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_scenes.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_scenes.lua index 2f575d8e06..316b9b1254 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_scenes.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_scenes.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -65,7 +54,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed({ + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed({ state_change = true })) } } @@ -85,7 +74,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_2x({ + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_2x({ state_change = true })) } } @@ -105,7 +94,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_3x({ + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_3x({ state_change = true })) } } @@ -125,7 +114,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_4x({ + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) } } @@ -145,7 +134,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_5x({ + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_5x({ state_change = true })) } } @@ -165,7 +154,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed({ + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) } } @@ -185,7 +174,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_2x({ + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_2x({ state_change = true })) } } @@ -205,7 +194,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_3x({ + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_3x({ state_change = true })) } } @@ -225,7 +214,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_4x({ + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_4x({ state_change = true })) } } @@ -245,7 +234,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_5x({ + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_5x({ state_change = true })) } } diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn.lua new file mode 100644 index 0000000000..5593ced043 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn.lua @@ -0,0 +1,325 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +local zw_test_utils = require "integration_test.zwave_test_utils" +local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({version=2}) +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) +local Basic = (require "st.zwave.CommandClass.Basic")({version=1}) +local CentralScene = (require "st.zwave.CommandClass.CentralScene")({version=3}) +local Association = (require "st.zwave.CommandClass.Association")({version=1}) +local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({version=7}) +local Meter = (require "st.zwave.CommandClass.Meter")({version=3}) +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local t_utils = require "integration_test.utils" + +-- Inovelli VZW32-SN device identifiers +local INOVELLI_MANUFACTURER_ID = 0x031E +local INOVELLI_VZW32_SN_PRODUCT_TYPE = 0x0017 +local INOVELLI_VZW32_SN_PRODUCT_ID = 0x0001 +local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" + +-- Device endpoints with supported command classes +local inovelli_vzw32_sn_endpoints = { + { + command_classes = { + {value = zw.SWITCH_BINARY}, + {value = zw.SWITCH_MULTILEVEL}, + {value = zw.BASIC}, + {value = zw.CONFIGURATION}, + {value = zw.CENTRAL_SCENE}, + {value = zw.ASSOCIATION}, + {value = zw.SENSOR_MULTILEVEL}, + {value = zw.METER}, + {value = zw.NOTIFICATION}, + } + } +} + +-- Create mock device +local mock_inovelli_vzw32_sn = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("inovelli-mmwave-dimmer-vzw32-sn.yml"), + zwave_endpoints = inovelli_vzw32_sn_endpoints, + zwave_manufacturer_id = INOVELLI_MANUFACTURER_ID, + zwave_product_type = INOVELLI_VZW32_SN_PRODUCT_TYPE, + zwave_product_id = INOVELLI_VZW32_SN_PRODUCT_ID +}) + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzw32_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_coroutine_test( + "Device should initialize properly on added lifecycle event", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzw32_sn.id, "added" }) + + for button_name, _ in pairs(mock_inovelli_vzw32_sn.profile.components) do + if button_name ~= "main" and button_name ~= LED_BAR_COMPONENT_NAME then + test.socket.capability:__expect_send( + mock_inovelli_vzw32_sn:generate_test_message( + button_name, + capabilities.button.supportedButtonValues( + supported_button_values[button_name], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzw32_sn:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + end + end + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Association:Set({ + grouping_identifier = 1, + node_ids = {}, -- Mock hub Z-Wave ID + payload = "\x01", -- Should contain grouping_identifier = 1 + }) + ) + ) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SwitchMultilevel:Get({}) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SensorMultilevel:Get({sensor_type = SensorMultilevel.sensor_type.ILLUMINANCE}) + ) + ) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Meter:Get({ scale = Meter.scale.electric_meter.WATTS }) + ) + ) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS }) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Notification:Get({notification_type = Notification.notification_type.HOME_SECURITY, event = Notification.event.home_security.MOTION_DETECTION}) + ) + ) + end +) + +-- Test switch on command +test.register_coroutine_test( + "Switch on command should send Basic Set with ON value", + function() + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + test.socket.capability:__queue_receive({ + mock_inovelli_vzw32_sn.id, + { capability = "switch", command = "on", args = {} } + }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Basic:Set({ value = SwitchBinary.value.ON_ENABLE }) + ) + ) + test.wait_for_events() + test.mock_time.advance_time(3) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SwitchMultilevel:Get({}) + ) + ) + end +) + +-- Test switch off command +test.register_coroutine_test( + "Switch off command should send Basic Set with OFF value", + function() + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + test.socket.capability:__queue_receive({ + mock_inovelli_vzw32_sn.id, + { capability = "switch", command = "off", args = {} } + }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Basic:Set({ value = SwitchBinary.value.OFF_DISABLE }) + ) + ) + test.wait_for_events() + test.mock_time.advance_time(3) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SwitchMultilevel:Get({}) + ) + ) + end +) + +-- Test switch level command +test.register_coroutine_test( + "Switch level command should send SwitchMultilevel Set", + function() + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + + test.socket.capability:__queue_receive({ + mock_inovelli_vzw32_sn.id, + { capability = "switchLevel", command = "setLevel", args = { 50 } } + }) + + local expected_command = SwitchMultilevel:Set({ value = 50, duration = "default" }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + expected_command + ) + ) + + test.wait_for_events() + test.mock_time.advance_time(3) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SwitchMultilevel:Get({}) + ) + ) + end +) + +-- Test central scene notifications +test.register_message_test( + "Central scene notification should emit button events", + { + { + channel = "zwave", + direction = "receive", + message = { mock_inovelli_vzw32_sn.id, zw_test_utils.zwave_test_build_receive_command(CentralScene:Notification({ + scene_number = 1, + key_attributes=CentralScene.key_attributes.KEY_PRESSED_1_TIME + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzw32_sn:generate_test_message("button1", capabilities.button.button.pushed({ + state_change = true + })) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test central scene notifications - button2 pressed 4 times +test.register_message_test( + "Central scene notification button2 pressed 4 times should emit button events", + { + { + channel = "zwave", + direction = "receive", + message = { mock_inovelli_vzw32_sn.id, zw_test_utils.zwave_test_build_receive_command(CentralScene:Notification({ + scene_number = 2, + key_attributes=CentralScene.key_attributes.KEY_PRESSED_4_TIMES + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzw32_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ + state_change = true + })) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test refresh capability +test.register_message_test( + "Refresh capability should request switch level", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzw32_sn.id, + { capability = "refresh", command = "refresh", args = {} } + } + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SwitchMultilevel:Get({}) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SensorMultilevel:Get({sensor_type = SensorMultilevel.sensor_type.ILLUMINANCE}) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Meter:Get({ scale = Meter.scale.electric_meter.WATTS }) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS }) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Notification:Get({notification_type = Notification.notification_type.HOME_SECURITY, event = Notification.event.home_security.MOTION_DETECTION}) + ) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_child.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_child.lua new file mode 100644 index 0000000000..40213851c7 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_child.lua @@ -0,0 +1,335 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +local zw_test_utils = require "integration_test.zwave_test_utils" +local Configuration = (require "st.zwave.CommandClass.Configuration")({version=4}) +local t_utils = require "integration_test.utils" +local st_device = require "st.device" + +-- Inovelli VZW32-SN device identifiers +local INOVELLI_MANUFACTURER_ID = 0x031E +local INOVELLI_VZW32_SN_PRODUCT_TYPE = 0x0017 +local INOVELLI_VZW32_SN_PRODUCT_ID = 0x0001 + +-- Device endpoints with supported command classes +local inovelli_vzw32_sn_endpoints = { + { + command_classes = { + {value = zw.SWITCH_BINARY}, + {value = zw.SWITCH_MULTILEVEL}, + {value = zw.BASIC}, + {value = zw.CONFIGURATION}, + {value = zw.CENTRAL_SCENE}, + {value = zw.ASSOCIATION}, + } + } +} + +-- Create mock parent device +local mock_parent_device = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("inovelli-mmwave-dimmer-vzw32-sn.yml"), + zwave_endpoints = inovelli_vzw32_sn_endpoints, + zwave_manufacturer_id = INOVELLI_MANUFACTURER_ID, + zwave_product_type = INOVELLI_VZW32_SN_PRODUCT_TYPE, + zwave_product_id = INOVELLI_VZW32_SN_PRODUCT_ID +}) + +-- Create mock child device (notification device) +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" +}) + +-- Set child device network type +mock_child_device.network_type = st_device.NETWORK_TYPE_CHILD + +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.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.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_parent_device, + Configuration:Set({ + parameter_number = 99, + configuration_value = notificationValue, + size = 4 + }) + ) + ) + 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.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_parent_device, + Configuration:Set({ + parameter_number = 99, + configuration_value = 0, -- Switch off sends 0 + size = 4 + }) + ) + ) + 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.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_parent_device, + Configuration:Set({ + parameter_number = 99, + configuration_value = notificationValue, + size = 4 + }) + ) + ) + end +) + +-- Test child device color command +test.register_coroutine_test( + "Child device color 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 = math.random(0, 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 = "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.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_parent_device, + Configuration:Set({ + parameter_number = 99, + configuration_value = notificationValue, + size = 4 + }) + ) + ) + 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() + local temp = math.random(2700, 6500) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorTemperature", command = "setColorTemperature", args = { temp } } + }) + + 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(temp)) + ) + + 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.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_parent_device, + Configuration:Set({ + parameter_number = 99, + configuration_value = 33514751, -- Calculated: effect(1)*16777216 + hue(255)*65536 + level(100)*256 + 255 + size = 4 + }) + ) + ) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_preferences.lua new file mode 100644 index 0000000000..8cccb5726e --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_preferences.lua @@ -0,0 +1,151 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local zw = require "st.zwave" +local zw_test_utils = require "integration_test.zwave_test_utils" +local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) +local t_utils = require "integration_test.utils" + +-- Inovelli VZW32-SN device identifiers +local INOVELLI_MANUFACTURER_ID = 0x031E +local INOVELLI_VZW32_SN_PRODUCT_TYPE = 0x0017 +local INOVELLI_VZW32_SN_PRODUCT_ID = 0x0001 + +-- Device endpoints with supported command classes +local inovelli_vzw32_sn_endpoints = { + { + command_classes = { + { value = zw.SWITCH_BINARY }, + { value = zw.SWITCH_MULTILEVEL }, + { value = zw.BASIC }, + { value = zw.CONFIGURATION }, + { value = zw.CENTRAL_SCENE }, + { value = zw.ASSOCIATION }, + } + } +} + +-- Create mock device +local mock_inovelli_vzw32_sn = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("inovelli-mmwave-dimmer-vzw32-sn.yml"), + zwave_endpoints = inovelli_vzw32_sn_endpoints, + zwave_manufacturer_id = INOVELLI_MANUFACTURER_ID, + zwave_product_type = INOVELLI_VZW32_SN_PRODUCT_TYPE, + zwave_product_id = INOVELLI_VZW32_SN_PRODUCT_ID +}) + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzw32_sn) +end +test.set_test_init_function(test_init) + +-- Test parameter 1 (example preference) +do + local new_param_value = 10 + test.register_coroutine_test( + "Parameter 1 should be updated in the device configuration after change", + function() + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzw32_sn:generate_info_changed({preferences = {parameter1 = new_param_value}})) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Configuration:Set({ + parameter_number = 1, + configuration_value = new_param_value, + size = 1 + }) + ) + ) + end + ) +end + +-- Test parameter 52 (example preference) +do + local new_param_value = 25 + test.register_coroutine_test( + "Parameter 52 should be updated in the device configuration after change", + function() + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzw32_sn:generate_info_changed({preferences = {parameter52 = new_param_value}})) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Configuration:Set({ + parameter_number = 52, + configuration_value = new_param_value, + size = 1 + }) + ) + ) + end + ) +end + +-- Test parameter 158 (example preference) +do + local new_param_value = 5 + test.register_coroutine_test( + "Parameter 158 should be updated in the device configuration after change", + function() + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzw32_sn:generate_info_changed({preferences = {parameter158 = new_param_value}})) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Configuration:Set({ + parameter_number = 158, + configuration_value = new_param_value, + size = 1 + }) + ) + ) + end + ) +end + +-- Test parameter 101 (2-byte parameter) +do + local new_param_value = -400 + test.register_coroutine_test( + "Parameter 101 should be updated in the device configuration after change", + function() + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzw32_sn:generate_info_changed({preferences = {parameter101 = new_param_value}})) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Configuration:Set({ + parameter_number = 101, + configuration_value = new_param_value, + size = 2 + }) + ) + ) + end + ) +end + +-- Test notificationChild preference (special case for child device creation) +do + local new_param_value = true + test.register_coroutine_test( + "notificationChild preference should create child device when enabled", + function() + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzw32_sn:generate_info_changed({preferences = {notificationChild = new_param_value}})) + + -- Expect child device creation + mock_inovelli_vzw32_sn:expect_device_create({ + type = "EDGE_CHILD", + label = "nil Notification", -- This will be the parent label + "Notification" + profile = "rgbw-bulb", + parent_device_id = mock_inovelli_vzw32_sn.id, + parent_assigned_child_key = "notification" + }) + end + ) +end + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/test/test_multi_metering_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_multi_metering_switch.lua index 9373383093..11a79bc12d 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_multi_metering_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_multi_metering_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua b/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua index a49bd678d9..7f584e3125 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" local zw = require "st.zwave" @@ -2046,6 +2035,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_child_5:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 20, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -2517,4 +2514,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-switch/src/test/test_popp_outdoor_plug_configuration.lua b/drivers/SmartThings/zwave-switch/src/test/test_popp_outdoor_plug_configuration.lua index 20b0eed8f6..165dab059e 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_popp_outdoor_plug_configuration.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_popp_outdoor_plug_configuration.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer.lua index e38df9c0f5..9cec4ecd4e 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -323,6 +312,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer_preferences.lua index 21e7fbba3f..920c6c5b2f 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer_preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1_relay_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1_relay_preferences.lua index d9e6b82d63..d44eb8f7cb 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1_relay_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1_relay_preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1d_relay_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1d_relay_preferences.lua index ba758717e8..3436ccba26 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1d_relay_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1d_relay_preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay.lua index 49f1117d41..61a62ef0d7 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -335,6 +324,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_parent_device:generate_test_message("main", capabilities.powerMeter.power({ value = 5, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -359,6 +356,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_child_2_device:generate_test_message("main", capabilities.powerMeter.power({ value = 5, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay_preferences.lua index 4fc60d1927..3ac1fda4b4 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay_preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer.lua index d9588faa15..cfe8dccde7 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -314,6 +303,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_0_10V_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_0_10V_preferences.lua index 48fa00fa38..db28991f2d 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_0_10V_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_0_10V_preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_preferences.lua index cd07b64151..d88d6c39b1 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_mini_dimmer_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_mini_dimmer_preferences.lua index a4f60c6a50..f385e0be33 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_mini_dimmer_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_mini_dimmer_preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_with_power.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_with_power.lua index 3e3aca24f2..ccaf9c7aec 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_with_power.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_with_power.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -163,6 +152,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_without_power.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_without_power.lua index 83f6674852..c06f516249 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_without_power.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_without_power.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_shelly_multi_metering_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_shelly_multi_metering_switch.lua index b5606f6ab9..331640b1be 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_shelly_multi_metering_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_shelly_multi_metering_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch.lua b/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch.lua index cc34fe0737..9bc1387f2b 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch_configuration.lua b/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch_configuration.lua index 0a295fbd91..010607fc2d 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch_configuration.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch_configuration.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zooz_double_plug.lua b/drivers/SmartThings/zwave-switch/src/test/test_zooz_double_plug.lua index 9f7f7d5b86..abf41946cc 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zooz_double_plug.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zooz_double_plug.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -493,6 +482,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_parent:generate_test_message("main", capabilities.powerMeter.power({ value = 89, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -514,6 +511,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_parent:generate_test_message("main", capabilities.powerMeter.power({ value = 89, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zooz_power_strip.lua b/drivers/SmartThings/zwave-switch/src/test/test_zooz_power_strip.lua index d63c644040..477886d76e 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zooz_power_strip.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zooz_power_strip.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay.lua b/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay.lua index 926733ab34..b3a18291e7 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local zw = require "st.zwave" local test = require "integration_test" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay_preferences.lua index 7ebf45335a..c186f9d36b 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay_preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_dimmer_power_energy.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_dimmer_power_energy.lua index 5f7217472e..215cc0675d 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_dimmer_power_energy.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_dimmer_power_energy.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -234,6 +223,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch.lua index 4b69b063a7..84502e9835 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch_migration.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch_migration.lua index db799e9451..ac396439f6 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch_migration.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch_migration.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch.lua index d1d73cd496..effb5845b4 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_battery.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_battery.lua index acba87f120..26a72eb4e6 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_battery.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_battery.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_electric_meter.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_electric_meter.lua index abfaa48ffd..935e4b63e8 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_electric_meter.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_electric_meter.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -57,6 +46,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_switch:generate_test_message("main", capabilities.powerMeter.power({ value = 27, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_switch.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_energy_meter.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_energy_meter.lua index 8b526a9984..016cb56ef1 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_energy_meter.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_energy_meter.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -64,6 +53,14 @@ test.register_message_test( meter_value = 27}) )} }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_switch.id, capability_id = "powerMeter", capability_attr_id = "power" } + } + } } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_level.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_level.lua index b8e834cffe..c8cca92dfb 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_level.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_level.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_power_meter.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_power_meter.lua index 56ffdd939d..edec7aefba 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_power_meter.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_power_meter.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -68,6 +57,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_switch:generate_test_message("main", capabilities.powerMeter.power({ value = 27, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_switch.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/can_handle.lua b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/can_handle.lua new file mode 100644 index 0000000000..a55c1120e6 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/can_handle.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local function can_handle_zooz_power_strip(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x015D, prod = 0x0651, model = 0xF51C} -- Zooz ZEN 20 Power Strip + } + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("zooz-power-strip") + return true, subdriver + end + end + return false +end + +return can_handle_zooz_power_strip diff --git a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/init.lua b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/init.lua index 24b969d13e..8bf3ad9dc4 100644 --- a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/init.lua +++ b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -20,20 +9,6 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) --- @type st.zwave.CommandClass.SwitchBinary local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = 2 }) -local ZOOZ_POWER_STRIP_FINGERPRINTS = { - {mfr = 0x015D, prod = 0x0651, model = 0xF51C} -- Zooz ZEN 20 Power Strip -} - -local function can_handle_zooz_power_strip(opts, driver, device, ...) - for _, fingerprint in ipairs(ZOOZ_POWER_STRIP_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("zooz-power-strip") - return true, subdriver - end - end - return false -end - local function binary_event_helper(driver, device, cmd) if cmd.src_channel > 0 then local value = cmd.args.value and cmd.args.value or cmd.args.target_value @@ -124,7 +99,7 @@ local zooz_power_strip = { [capabilities.switch.commands.off.NAME] = switch_off_handler } }, - can_handle = can_handle_zooz_power_strip, + can_handle = require("zooz-power-strip.can_handle"), } return zooz_power_strip diff --git a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/can_handle.lua new file mode 100644 index 0000000000..19d44bc623 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zooz_zen_30_dimmer_relay_double_switch(opts, driver, device, ...) + local fingerprints = { + { mfr = 0x027A, prod = 0xA000, model = 0xA008 } -- Zooz Zen 30 Dimmer Relay Double Switch + } + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("zooz-zen-30-dimmer-relay") + return true, subdriver + end + end + return false +end + +return can_handle_zooz_zen_30_dimmer_relay_double_switch diff --git a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/init.lua b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/init.lua index c92c485e43..6bb9bd4e1b 100644 --- a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/init.lua +++ b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local st_device = require "st.device" local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -70,20 +59,6 @@ local map_key_attribute_to_capability = { } } -local ZOOZ_ZEN_30_DIMMER_RELAY_FINGERPRINTS = { - { mfr = 0x027A, prod = 0xA000, model = 0xA008 } -- Zooz Zen 30 Dimmer Relay Double Switch -} - -local function can_handle_zooz_zen_30_dimmer_relay_double_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(ZOOZ_ZEN_30_DIMMER_RELAY_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("zooz-zen-30-dimmer-relay") - return true, subdriver - end - end - return false -end - local function find_child(parent, src_channel) if src_channel == 0 then return parent @@ -205,7 +180,7 @@ local zooz_zen_30_dimmer_relay_double_switch = { init = device_init, added = device_added }, - can_handle = can_handle_zooz_zen_30_dimmer_relay_double_switch + can_handle = require("zooz-zen-30-dimmer-relay.can_handle") } return zooz_zen_30_dimmer_relay_double_switch diff --git a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/can_handle.lua new file mode 100644 index 0000000000..416c3dfa67 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local function can_handle_zwave_dual_switch(opts, driver, device, ...) + local fingerprints = require("zwave-dual-switch.fingerprints") + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("zwave-dual-switch") + return true, subdriver + end + end + return false +end + +return can_handle_zwave_dual_switch diff --git a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/dual_switch_configurations.lua b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/dual_switch_configurations.lua index d0b2f83373..f5c01ecf6b 100644 --- a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/dual_switch_configurations.lua +++ b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/dual_switch_configurations.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local devices = { FIBARO_WALLI_DOUBLE_SWITCH = { diff --git a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/fingerprints.lua new file mode 100644 index 0000000000..9640f5c4d2 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/fingerprints.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = 0x0086, prod = 0x0103, model = 0x008C }, -- Aeotec Switch 1 + { mfr = 0x0086, prod = 0x0003, model = 0x008C }, -- Aeotec Switch 1 + { mfr = 0x0258, prod = 0x0003, model = 0x008B }, -- NEO Coolcam Switch 1 + { mfr = 0x0258, prod = 0x0003, model = 0x108B }, -- NEO Coolcam Switch 1 + { mfr = 0x0312, prod = 0xC000, model = 0xC004 }, -- EVA Switch 1 + { mfr = 0x0312, prod = 0xFF00, model = 0xFF05 }, -- Minoston Switch 1 + { mfr = 0x0312, prod = 0xC000, model = 0xC007 }, -- Evalogik Switch 1 + { mfr = 0x010F, prod = 0x1B01, model = 0x1000 }, -- Fibaro Walli Double Switch + { mfr = 0x027A, prod = 0xA000, model = 0xA003 } -- Zooz Double Plug +} diff --git a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/init.lua b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/init.lua index 60bf54e4e0..4bea42c584 100644 --- a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local st_device = require "st.device" local capabilities = require "st.capabilities" --- @type st.zwave.defaults.switch @@ -26,28 +15,6 @@ local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) local dualSwitchConfigurationsMap = require "zwave-dual-switch/dual_switch_configurations" local utils = require "st.utils" -local ZWAVE_DUAL_SWITCH_FINGERPRINTS = { - { mfr = 0x0086, prod = 0x0103, model = 0x008C }, -- Aeotec Switch 1 - { mfr = 0x0086, prod = 0x0003, model = 0x008C }, -- Aeotec Switch 1 - { mfr = 0x0258, prod = 0x0003, model = 0x008B }, -- NEO Coolcam Switch 1 - { mfr = 0x0258, prod = 0x0003, model = 0x108B }, -- NEO Coolcam Switch 1 - { mfr = 0x0312, prod = 0xC000, model = 0xC004 }, -- EVA Switch 1 - { mfr = 0x0312, prod = 0xFF00, model = 0xFF05 }, -- Minoston Switch 1 - { mfr = 0x0312, prod = 0xC000, model = 0xC007 }, -- Evalogik Switch 1 - { mfr = 0x010F, prod = 0x1B01, model = 0x1000 }, -- Fibaro Walli Double Switch - { mfr = 0x027A, prod = 0xA000, model = 0xA003 } -- Zooz Double Plug -} - -local function can_handle_zwave_dual_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(ZWAVE_DUAL_SWITCH_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("zwave-dual-switch") - return true, subdriver - end - end - return false -end - local function find_child(parent, src_channel) if src_channel == 1 then return parent @@ -166,7 +133,7 @@ local zwave_dual_switch = { added = device_added, init = device_init }, - can_handle = can_handle_zwave_dual_switch + can_handle = require("zwave-dual-switch.can_handle") } return zwave_dual_switch diff --git a/drivers/SmartThings/zwave-thermostat/src/aeotec-radiator-thermostat/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/aeotec-radiator-thermostat/can_handle.lua new file mode 100644 index 0000000000..9feb63e1fa --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/aeotec-radiator-thermostat/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeotec_radiator_thermostat(opts, driver, device, ...) + local AEOTEC_THERMOSTAT_FINGERPRINT = {mfr = 0x0371, prod = 0x0002, model = 0x0015} + + if device:id_match(AEOTEC_THERMOSTAT_FINGERPRINT.mfr, AEOTEC_THERMOSTAT_FINGERPRINT.prod, AEOTEC_THERMOSTAT_FINGERPRINT.model) then + return true, require "aeotec-radiator-thermostat" + else + return false + end +end + +return can_handle_aeotec_radiator_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/aeotec-radiator-thermostat/init.lua b/drivers/SmartThings/zwave-thermostat/src/aeotec-radiator-thermostat/init.lua index b101a43d38..cdb90e7f44 100755 --- a/drivers/SmartThings/zwave-thermostat/src/aeotec-radiator-thermostat/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/aeotec-radiator-thermostat/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -24,12 +14,6 @@ local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ver --- @type st.zwave.CommandClass.ThermostatSetpoint local ThermostatSetpoint = (require "st.zwave.CommandClass.ThermostatSetpoint")({version=1}) -local AEOTEC_THERMOSTAT_FINGERPRINT = {mfr = 0x0371, prod = 0x0002, model = 0x0015} - -local function can_handle_aeotec_radiator_thermostat(opts, driver, device, ...) - return device:id_match(AEOTEC_THERMOSTAT_FINGERPRINT.mfr, AEOTEC_THERMOSTAT_FINGERPRINT.prod, AEOTEC_THERMOSTAT_FINGERPRINT.model) -end - local function thermostat_mode_report_handler(self, device, cmd) local event = nil if (cmd.args.mode == ThermostatMode.mode.OFF) then @@ -114,7 +98,7 @@ local aeotec_radiator_thermostat = { } }, lifecycle_handlers = {added = device_added}, - can_handle = can_handle_aeotec_radiator_thermostat + can_handle = require("aeotec-radiator-thermostat.can_handle"), } return aeotec_radiator_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/can_handle.lua new file mode 100644 index 0000000000..079eeee5d3 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/can_handle.lua @@ -0,0 +1,25 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, cmd, ...) + local version = require "version" + local cc = require "st.zwave.CommandClass" + local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) + local DANFOSS_LC13_THERMOSTAT_FPS = require "apiv6_bugfix.fingerprints" + + if version.api == 6 and + cmd.cmd_class == cc.WAKE_UP and + cmd.cmd_id == WakeUp.NOTIFICATION and not + (device:id_match(DANFOSS_LC13_THERMOSTAT_FPS[1].manufacturerId, + DANFOSS_LC13_THERMOSTAT_FPS[1].productType, + DANFOSS_LC13_THERMOSTAT_FPS[1].productId) or + device:id_match(DANFOSS_LC13_THERMOSTAT_FPS[2].manufacturerId, + DANFOSS_LC13_THERMOSTAT_FPS[2].productType, + DANFOSS_LC13_THERMOSTAT_FPS[2].productId)) then + return true, require "apiv6_bugfix" + else + return false + end +end + +return can_handle diff --git a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/fingerprints.lua b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/fingerprints.lua new file mode 100644 index 0000000000..e87a5990e2 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local DANFOSS_LC13_THERMOSTAT_FPS = { + { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0003 }, -- Danfoss LC13 Thermostat + { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0004 } -- Danfoss LC13 Thermostat +} + +return DANFOSS_LC13_THERMOSTAT_FPS diff --git a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/init.lua index 4994710970..52c419a590 100644 --- a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/init.lua @@ -1,23 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) -local DANFOSS_LC13_THERMOSTAT_FPS = { - { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0003 }, -- Danfoss LC13 Thermostat - { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0004 } -- Danfoss LC13 Thermostat -} - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - return version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION and not - (device:id_match(DANFOSS_LC13_THERMOSTAT_FPS[1].manufacturerId, - DANFOSS_LC13_THERMOSTAT_FPS[1].productType, - DANFOSS_LC13_THERMOSTAT_FPS[1].productId) or - device:id_match(DANFOSS_LC13_THERMOSTAT_FPS[2].manufacturerId, - DANFOSS_LC13_THERMOSTAT_FPS[2].productType, - DANFOSS_LC13_THERMOSTAT_FPS[2].productId)) -end local function wakeup_notification(driver, device, cmd) device:refresh() @@ -30,7 +16,7 @@ local apiv6_bugfix = { } }, NAME = "apiv6_bugfix", - can_handle = can_handle + can_handle = require("apiv6_bugfix.can_handle"), } return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/can_handle.lua new file mode 100644 index 0000000000..6483fc2393 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_ct100_thermostat(opts, driver, device) + local CT100_THERMOSTAT_FINGERPRINTS = require "ct100-thermostat.fingerprints" + for _, fingerprint in ipairs(CT100_THERMOSTAT_FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require "ct100-thermostat" + end + end + + return false +end + +return can_handle_ct100_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/fingerprints.lua b/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/fingerprints.lua new file mode 100644 index 0000000000..c45eb5e7af --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local CT100_THERMOSTAT_FINGERPRINTS = { + { manufacturerId = 0x0098, productType = 0x6401, productId = 0x0107 }, -- 2Gig CT100 Programmable Thermostat + { manufacturerId = 0x0098, productType = 0x6501, productId = 0x000C }, -- Iris Thermostat +} + +return CT100_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/init.lua b/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/init.lua index e44da0ca36..d5859fe777 100644 --- a/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -33,11 +23,6 @@ local cooling_setpoint_defaults = require "st.zwave.defaults.thermostatCoolingSe local constants = require "st.zwave.constants" local utils = require "st.utils" -local CT100_THERMOSTAT_FINGERPRINTS = { - { manufacturerId = 0x0098, productType = 0x6401, productId = 0x0107 }, -- 2Gig CT100 Programmable Thermostat - { manufacturerId = 0x0098, productType = 0x6501, productId = 0x000C }, -- Iris Thermostat -} - -- This old device uses separate endpoints to get values of temp and humidity -- DTH actually uses the old mutliInstance encap, but multichannel should be back-compat local TEMPERATURE_ENDPOINT = 1 @@ -75,16 +60,6 @@ local function set_setpoint_factory(setpoint_type) end end -local function can_handle_ct100_thermostat(opts, driver, device) - for _, fingerprint in ipairs(CT100_THERMOSTAT_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - - return false -end - local function thermostat_mode_report_handler(self, device, cmd) local event = nil @@ -210,7 +185,7 @@ local ct100_thermostat = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } }, - can_handle = can_handle_ct100_thermostat, + can_handle = require("ct100-thermostat.can_handle"), } return ct100_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/can_handle.lua new file mode 100644 index 0000000000..6ece30f897 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_heat_controller(opts, driver, device, ...) + local FINGERPRINTS = require("fibaro-heat-controller.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("fibaro-heat-controller") + end + end + + return false +end + +return can_handle_fibaro_heat_controller diff --git a/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/fingerprints.lua b/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/fingerprints.lua new file mode 100644 index 0000000000..31cbd7ff0e --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_HEAT_FINGERPRINTS = { + {mfr = 0x010F, prod = 0x1301, model = 0x1000}, -- Fibaro Heat Controller + {mfr = 0x010F, prod = 0x1301, model = 0x1001} -- Fibaro Heat Controller +} + +return FIBARO_HEAT_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/init.lua b/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/init.lua index be5365edf0..ab7ef51f30 100644 --- a/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -30,22 +20,9 @@ local ApplicationStatus = (require "st.zwave.CommandClass.ApplicationStatus")({v local utils = require "st.utils" -local FIBARO_HEAT_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x1301, model = 0x1000}, -- Fibaro Heat Controller - {mfr = 0x010F, prod = 0x1301, model = 0x1001} -- Fibaro Heat Controller -} local FORCED_REFRESH_THREAD = "forcedRefreshThread" -local function can_handle_fibaro_heat_controller(opts, driver, device, ...) - for _, fingerprint in ipairs(FIBARO_HEAT_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - - return false -end local function thermostat_mode_report_handler(self, device, cmd) local event = nil @@ -189,7 +166,7 @@ local fibaro_heat_controller = { added = device_added, init = map_components }, - can_handle = can_handle_fibaro_heat_controller + can_handle = require("fibaro-heat-controller.can_handle"), } return fibaro_heat_controller diff --git a/drivers/SmartThings/zwave-thermostat/src/init.lua b/drivers/SmartThings/zwave-thermostat/src/init.lua index 4fb3eff09e..3668085b1a 100755 --- a/drivers/SmartThings/zwave-thermostat/src/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.Driver @@ -115,16 +105,7 @@ local driver_template = { lifecycle_handlers = { added = device_added }, - sub_drivers = { - require("aeotec-radiator-thermostat"), - require("popp-radiator-thermostat"), - require("ct100-thermostat"), - require("fibaro-heat-controller"), - require("stelpro-ki-thermostat"), - require("qubino-flush-thermostat"), - require("thermostat-heating-battery"), - require("apiv6_bugfix"), - } + sub_drivers = require("sub_drivers"), } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities, {native_capability_attrs_enabled = true}) diff --git a/drivers/SmartThings/zwave-thermostat/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-thermostat/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-thermostat/src/popp-radiator-thermostat/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/popp-radiator-thermostat/can_handle.lua new file mode 100644 index 0000000000..19e5f39cf3 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/popp-radiator-thermostat/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_popp_radiator_thermostat(opts, driver, device, ...) + local POPP_THERMOSTAT_FINGERPRINT = {mfr = 0x0002, prod = 0x0115, model = 0xA010} + + if device:id_match(POPP_THERMOSTAT_FINGERPRINT.mfr, POPP_THERMOSTAT_FINGERPRINT.prod, POPP_THERMOSTAT_FINGERPRINT.model) then + return true, require "popp-radiator-thermostat" + else + return false + end +end + +return can_handle_popp_radiator_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/popp-radiator-thermostat/init.lua b/drivers/SmartThings/zwave-thermostat/src/popp-radiator-thermostat/init.lua index f6ba9955b0..46234ea2ac 100755 --- a/drivers/SmartThings/zwave-thermostat/src/popp-radiator-thermostat/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/popp-radiator-thermostat/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local utils = require "st.utils" @@ -29,8 +19,6 @@ local LATEST_WAKEUP = "latest_wakeup" local CACHED_SETPOINT = "cached_setpoint" local POPP_WAKEUP_INTERVAL = 600 --seconds -local POPP_THERMOSTAT_FINGERPRINT = {mfr = 0x0002, prod = 0x0115, model = 0xA010} - local function get_latest_wakeup_timestamp(device) return device:get_field(LATEST_WAKEUP) end @@ -48,10 +36,6 @@ local function seconds_since_latest_wakeup(device) end end -local function can_handle_popp_radiator_thermostat(opts, driver, device, ...) - return device:id_match(POPP_THERMOSTAT_FINGERPRINT.mfr, POPP_THERMOSTAT_FINGERPRINT.prod, POPP_THERMOSTAT_FINGERPRINT.model) -end - -- POPP is a sleepy device, therefore it won't accept setpoint commands rightaway. -- That's why driver waits for a device to wake up and then sends cached setpoint command. -- Driver assumes that wakeUps come in reguraly every 10 minutes. @@ -116,7 +100,7 @@ local popp_radiator_thermostat = { lifecycle_handlers = { added = added_handler }, - can_handle = can_handle_popp_radiator_thermostat + can_handle = require("popp-radiator-thermostat.can_handle"), } return popp_radiator_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/can_handle.lua new file mode 100644 index 0000000000..e5a6a8474b --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_qubino_thermostat(opts, driver, device, ...) + local FINGERPRINTS = require("qubino-flush-thermostat.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("qubino-flush-thermostat") + end + end + return false +end + +return can_handle_qubino_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/fingerprints.lua b/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/fingerprints.lua new file mode 100644 index 0000000000..d640f7e1ce --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local QUBINO_FINGERPRINTS = { + {mfr = 0x0159, prod = 0x0005, model = 0x0054}, -- Qubino Flush On/Off Thermostat 2 +} + +return QUBINO_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/init.lua b/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/init.lua index d80e58e2da..fd9ff2aacb 100644 --- a/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + --- @type st.zwave.defaults.switch local TemperatureMeasurementDefaults = require "st.zwave.defaults.temperatureMeasurement" @@ -31,9 +21,6 @@ local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ ve --- @type st.zwave.CommandClass.ThermostatOperatingState local ThermostatOperatingState = (require "st.zwave.CommandClass.ThermostatOperatingState")({version=1}) -local QUBINO_FINGERPRINTS = { - {mfr = 0x0159, prod = 0x0005, model = 0x0054}, -- Qubino Flush On/Off Thermostat 2 -} -- parameter which tells whether device is configured heat or cool thermostat mode local DEVICE_MODE_PARAMETER = 59 @@ -47,14 +34,6 @@ local CONFIGURED_MODE = "configured_mode" local COOL_MODE = "cool" local HEAT_MODE = "heat" -local function can_handle_qubino_thermostat(opts, driver, device, ...) - for _, fingerprint in ipairs(QUBINO_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function info_changed(self, device, event, args) local new_parameter_value @@ -137,7 +116,7 @@ local qubino_thermostat = { infoChanged = info_changed }, NAME = "qubino thermostat", - can_handle = can_handle_qubino_thermostat + can_handle = require("qubino-flush-thermostat.can_handle"), } return qubino_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/can_handle.lua new file mode 100644 index 0000000000..65af08df62 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_stelpro_ki_thermostat(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("stelpro-ki-thermostat.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("stelpro-ki-thermostat") + end + end + + return false +end + +return can_handle_stelpro_ki_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/fingerprints.lua b/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/fingerprints.lua new file mode 100644 index 0000000000..00a43a739f --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local STELPRO_KI_THERMOSTAT_FINGERPRINTS = { + { manufacturerId = 0x0239, productType = 0x0001, productId = 0x0001 } -- Stelpro Ki Thermostat +} + +return STELPRO_KI_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/init.lua b/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/init.lua index 3520e35325..7b6acbe3e7 100644 --- a/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local log = require "log" local capabilities = require "st.capabilities" @@ -21,19 +11,7 @@ local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ ve --- @type st.zwave.CommandClass.ThermostatMode local ThermostatMode = (require "st.zwave.CommandClass.ThermostatMode")({ version = 2 }) -local STELPRO_KI_THERMOSTAT_FINGERPRINTS = { - { manufacturerId = 0x0239, productType = 0x0001, productId = 0x0001 } -- Stelpro Ki Thermostat -} - -local function can_handle_stelpro_ki_thermostat(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(STELPRO_KI_THERMOSTAT_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local function sensor_multilevel_report_handler(self, device, cmd) if (cmd.args.sensor_type == SensorMultilevel.sensor_type.TEMPERATURE) then @@ -138,7 +116,7 @@ local stelpro_ki_thermostat = { lifecycle_handlers = { added = device_added }, - can_handle = can_handle_stelpro_ki_thermostat, + can_handle = require("stelpro-ki-thermostat.can_handle"), } return stelpro_ki_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua b/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua new file mode 100644 index 0000000000..dc30091b79 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("aeotec-radiator-thermostat"), + lazy_load_if_possible("popp-radiator-thermostat"), + lazy_load_if_possible("ct100-thermostat"), + lazy_load_if_possible("fibaro-heat-controller"), + lazy_load_if_possible("stelpro-ki-thermostat"), + lazy_load_if_possible("qubino-flush-thermostat"), + lazy_load_if_possible("thermostat-heating-battery"), + lazy_load_if_possible("apiv6_bugfix"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-thermostat/src/test/test_aeotec_radiator_thermostat.lua b/drivers/SmartThings/zwave-thermostat/src/test/test_aeotec_radiator_thermostat.lua index 6393613025..69034f9b5a 100755 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_aeotec_radiator_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_aeotec_radiator_thermostat.lua @@ -146,6 +146,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zwave-thermostat/src/test/test_fibaro_heat_controller.lua b/drivers/SmartThings/zwave-thermostat/src/test/test_fibaro_heat_controller.lua index 7cbc6e38e5..cac8b883e4 100644 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_fibaro_heat_controller.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_fibaro_heat_controller.lua @@ -244,6 +244,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zwave-thermostat/src/test/test_popp_radiator_thermostat.lua b/drivers/SmartThings/zwave-thermostat/src/test/test_popp_radiator_thermostat.lua index 7ff628f974..3671cd4029 100755 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_popp_radiator_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_popp_radiator_thermostat.lua @@ -95,8 +95,16 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } } } + } ) test.register_message_test( diff --git a/drivers/SmartThings/zwave-thermostat/src/test/test_qubino_flush_thermostat.lua b/drivers/SmartThings/zwave-thermostat/src/test/test_qubino_flush_thermostat.lua index 00056c753a..2c9b04d82d 100644 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_qubino_flush_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_qubino_flush_thermostat.lua @@ -471,6 +471,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 5, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) diff --git a/drivers/SmartThings/zwave-thermostat/src/test/test_zwave_thermostat.lua b/drivers/SmartThings/zwave-thermostat/src/test/test_zwave_thermostat.lua index b332713319..ea14555a0a 100644 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_zwave_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_zwave_thermostat.lua @@ -256,6 +256,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/can_handle.lua new file mode 100644 index 0000000000..f02798191a --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_thermostat_heating_battery(opts, driver, device, cmd, ...) + local DANFOSS_LC13_THERMOSTAT_FINGERPRINTS = require "thermostat-heating-battery.fingerprints" + for _, fingerprint in ipairs(DANFOSS_LC13_THERMOSTAT_FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require "thermostat-heating-battery" + end + end + + return false +end + +return can_handle_thermostat_heating_battery diff --git a/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/fingerprints.lua b/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/fingerprints.lua new file mode 100644 index 0000000000..bc32cac629 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local DANFOSS_LC13_THERMOSTAT_FINGERPRINTS = { + { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0003 }, -- Danfoss LC13 Thermostat + { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0004 } -- Danfoss LC13 Thermostat +} + +return DANFOSS_LC13_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/init.lua b/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/init.lua index 4cf3c22f24..60a9a2055f 100644 --- a/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local utils = require "st.utils" @@ -42,11 +32,6 @@ local CLAMP = { CELSIUS_MAX = 28 } -local DANFOSS_LC13_THERMOSTAT_FINGERPRINTS = { - { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0003 }, -- Danfoss LC13 Thermostat - { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0004 } -- Danfoss LC13 Thermostat -} - local WEEK = {6, 0, 1, 2, 3, 4, 5} --[[ Danfoss LC13 (Living Connect) @@ -62,16 +47,6 @@ Note: https://idency.com/products/idencyhome/smarthome/sensors/danfoss-z-wave-li to the Z-Wave network, it only allows a one-way communication to change its setpoint. --]] -local function can_handle_thermostat_heating_battery(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(DANFOSS_LC13_THERMOSTAT_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - - return false -end - local function adjust_temperature_if_exceeded_min_max_limit (degree, scale) if scale == ThermostatSetpoint.scale.CELSIUS then return utils.clamp_value(degree, CLAMP.CELSIUS_MIN, CLAMP.CELSIUS_MAX) @@ -283,7 +258,7 @@ local thermostat_heating_battery = { init = device_init, added = added_handler, }, - can_handle = can_handle_thermostat_heating_battery + can_handle = require("thermostat-heating-battery.can_handle"), } return thermostat_heating_battery diff --git a/drivers/SmartThings/zwave-valve/src/init.lua b/drivers/SmartThings/zwave-valve/src/init.lua index 43de12dcc4..cea5b877c1 100644 --- a/drivers/SmartThings/zwave-valve/src/init.lua +++ b/drivers/SmartThings/zwave-valve/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.defaults @@ -26,10 +16,7 @@ local driver_template = { supported_capabilities = { capabilities.valve, }, - sub_drivers = { - -- Fortrezz and Zooz valves treat open as "off" and close as "on" - require("inverse_valve") - }, + sub_drivers = require("sub_drivers"), } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-valve/src/inverse_valve/can_handle.lua b/drivers/SmartThings/zwave-valve/src/inverse_valve/can_handle.lua new file mode 100644 index 0000000000..b36334f2c9 --- /dev/null +++ b/drivers/SmartThings/zwave-valve/src/inverse_valve/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function inverse_valve_can_handle(opts, driver, device, ...) + if device.zwave_manufacturer_id == 0x0084 or device.zwave_manufacturer_id == 0x027A then + return true, require("inverse_valve") + end + return false +end + +return inverse_valve_can_handle diff --git a/drivers/SmartThings/zwave-valve/src/inverse_valve/init.lua b/drivers/SmartThings/zwave-valve/src/inverse_valve/init.lua index 510e79019b..853bada351 100644 --- a/drivers/SmartThings/zwave-valve/src/inverse_valve/init.lua +++ b/drivers/SmartThings/zwave-valve/src/inverse_valve/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -55,9 +45,7 @@ local inverse_valve = { [capabilities.valve.commands.close.NAME] = close_handler } }, - can_handle = function(opts, driver, device, ...) - return device.zwave_manufacturer_id == 0x0084 or device.zwave_manufacturer_id == 0x027A - end + can_handle = require("inverse_valve.can_handle"), } return inverse_valve diff --git a/drivers/SmartThings/zwave-valve/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-valve/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-valve/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-valve/src/sub_drivers.lua b/drivers/SmartThings/zwave-valve/src/sub_drivers.lua new file mode 100644 index 0000000000..b43856e0cd --- /dev/null +++ b/drivers/SmartThings/zwave-valve/src/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("inverse_valve"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-valve/src/test/test_inverse.valve.lua b/drivers/SmartThings/zwave-valve/src/test/test_inverse.valve.lua index db7d6be656..68c563061b 100644 --- a/drivers/SmartThings/zwave-valve/src/test/test_inverse.valve.lua +++ b/drivers/SmartThings/zwave-valve/src/test/test_inverse.valve.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-valve/src/test/test_zwave_valve.lua b/drivers/SmartThings/zwave-valve/src/test/test_zwave_valve.lua index 4a21568119..efa0c42599 100644 --- a/drivers/SmartThings/zwave-valve/src/test/test_zwave_valve.lua +++ b/drivers/SmartThings/zwave-valve/src/test/test_zwave_valve.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-window-treatment/profiles/window-treatment-preset-reverse.yml b/drivers/SmartThings/zwave-window-treatment/profiles/window-treatment-preset-reverse.yml index a786609ae2..b1b46c51ef 100644 --- a/drivers/SmartThings/zwave-window-treatment/profiles/window-treatment-preset-reverse.yml +++ b/drivers/SmartThings/zwave-window-treatment/profiles/window-treatment-preset-reverse.yml @@ -15,7 +15,5 @@ components: categories: - name: Blind preferences: - - preferenceId: presetPosition - explicit: true - preferenceId: reverse explicit: true diff --git a/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/can_handle.lua b/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/can_handle.lua new file mode 100644 index 0000000000..7caf966d19 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeotec_nano_shutter(opts, driver, device, ...) + local FINGERPRINTS = require("aeotec-nano-shutter.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("aeotec-nano-shutter") + end + end + return false +end + +return can_handle_aeotec_nano_shutter diff --git a/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/fingerprints.lua b/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/fingerprints.lua new file mode 100644 index 0000000000..dcbcf2f872 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local AEOTEC_NANO_SHUTTER_FINGERPRINTS = { + {mfr = 0x0086, prod = 0x0003, model = 0x008D}, -- Aeotec nano shutter EU + {mfr = 0x0086, prod = 0x0103, model = 0x008D}, -- Aeotec nano shutter US + {mfr = 0x0371, prod = 0x0003, model = 0x008D}, -- Aeotec nano shutter EU + {mfr = 0x0371, prod = 0x0103, model = 0x008D} -- Aeotec nano shutter US +} + +return AEOTEC_NANO_SHUTTER_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/init.lua b/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/init.lua index 6ca6fd7837..82977a7cfb 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -28,26 +18,12 @@ local SET_BUTTON_TO_CLOSE = "close" local SET_BUTTON_TO_PAUSE = "pause" local SHADE_STATE = "shade_state" -local AEOTEC_NANO_SHUTTER_FINGERPRINTS = { - {mfr = 0x0086, prod = 0x0003, model = 0x008D}, -- Aeotec nano shutter EU - {mfr = 0x0086, prod = 0x0103, model = 0x008D}, -- Aeotec nano shutter US - {mfr = 0x0371, prod = 0x0003, model = 0x008D}, -- Aeotec nano shutter EU - {mfr = 0x0371, prod = 0x0103, model = 0x008D} -- Aeotec nano shutter US -} --- Determine whether the passed device is proper --- --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is proper, else false -local function can_handle_aeotec_nano_shutter(opts, driver, device, ...) - for _, fingerprint in ipairs(AEOTEC_NANO_SHUTTER_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end --- Default handler for basic reports for the devices --- @@ -140,7 +116,7 @@ local aeotec_nano_shutter = { added = added_handler }, NAME = "Aeotec nano shutter", - can_handle = can_handle_aeotec_nano_shutter + can_handle = require("aeotec-nano-shutter.can_handle"), } return aeotec_nano_shutter diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/can_handle.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/can_handle.lua new file mode 100644 index 0000000000..881ebd36a3 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_iblinds_window_treatment(opts, driver, device, ...) + local FINGERPRINTS = require("iblinds-window-treatment.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("iblinds-window-treatment") + end + end + return false +end + +return can_handle_iblinds_window_treatment diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/fingerprints.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/fingerprints.lua new file mode 100644 index 0000000000..d4f652f8b9 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local IBLINDS_WINDOW_TREATMENT_FINGERPRINTS = { + {mfr = 0x0287, prod = 0x0003, model = 0x000D}, -- iBlinds Window Treatment v1 / v2 + {mfr = 0x0287, prod = 0x0004, model = 0x0071}, -- iBlinds Window Treatment v3 + {mfr = 0x0287, prod = 0x0004, model = 0x0072} -- iBlinds Window Treatment v3.1 +} + +return IBLINDS_WINDOW_TREATMENT_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/init.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/init.lua index 2b981cd7ab..9c3d2d9970 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/init.lua @@ -1,40 +1,18 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.SwitchMultilevel local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version=3 }) +local window_preset_defaults = require "window_preset_defaults" -local IBLINDS_WINDOW_TREATMENT_FINGERPRINTS = { - {mfr = 0x0287, prod = 0x0003, model = 0x000D}, -- iBlinds Window Treatment v1 / v2 - {mfr = 0x0287, prod = 0x0004, model = 0x0071}, -- iBlinds Window Treatment v3 - {mfr = 0x0287, prod = 0x0004, model = 0x0072} -- iBlinds Window Treatment v3.1 -} --- Determine whether the passed device is iblinds window treatment --- --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is iblinds window treatment, else false -local function can_handle_iblinds_window_treatment(opts, driver, device, ...) - for _, fingerprint in ipairs(IBLINDS_WINDOW_TREATMENT_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local capability_handlers = {} @@ -70,8 +48,11 @@ function capability_handlers.set_shade_level(driver, device, command) set_shade_level_helper(driver, device, command.args.shadeLevel) end -function capability_handlers.preset_position(driver, device) - set_shade_level_helper(driver, device, device.preferences.presetPosition or 50) +function capability_handlers.preset_position(driver, device, command) + local level = device:get_latest_state(command.component, "windowShadePreset", "position") or + device:get_field(window_preset_defaults.PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or 50 + set_shade_level_helper(driver, device, level) end local iblinds_window_treatment = { @@ -87,11 +68,9 @@ local iblinds_window_treatment = { [capabilities.windowShadePreset.commands.presetPosition.NAME] = capability_handlers.preset_position } }, - sub_drivers = { - require("iblinds-window-treatment.v3") - }, + sub_drivers = require("iblinds-window-treatment.sub_drivers"), NAME = "iBlinds window treatment", - can_handle = can_handle_iblinds_window_treatment + can_handle = require("iblinds-window-treatment.can_handle"), } return iblinds_window_treatment diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/sub_drivers.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/sub_drivers.lua new file mode 100644 index 0000000000..187ef0ee04 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require("lazy_load_subdriver") + +return { + lazy_load_if_possible("iblinds-window-treatment.v3") +} diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3.lua deleted file mode 100644 index abc16b678c..0000000000 --- a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3.lua +++ /dev/null @@ -1,83 +0,0 @@ --- Copyright 2022 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 capabilities = require "st.capabilities" ---- @type st.zwave.CommandClass.SwitchMultilevel -local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version=3 }) - -local IBLINDS_WINDOW_TREATMENT_FINGERPRINTS_V3 = { - {mfr = 0x0287, prod = 0x0004, model = 0x0071}, -- iBlinds Window Treatment v3 - {mfr = 0x0287, prod = 0x0004, model = 0x0072} -- iBlinds Window Treatment v3 -} - ---- Determine whether the passed device is iblinds window treatment v3 ---- ---- @param driver st.zwave.Driver ---- @param device st.zwave.Device ---- @return boolean true if the device is iblinds window treatment, else false -local function can_handle_iblinds_window_treatment_v3(opts, driver, device, ...) - for _, fingerprint in ipairs(IBLINDS_WINDOW_TREATMENT_FINGERPRINTS_V3) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end - -local capability_handlers = {} - -function capability_handlers.close(driver, device) - device:emit_event(capabilities.windowShade.windowShade.closed()) - device:emit_event(capabilities.windowShadeLevel.shadeLevel(0)) - device:send(SwitchMultilevel:Set({value = 0})) -end - -local function set_shade_level_helper(driver, device, value) - value = math.max(math.min(value, 99), 0) - if value == 0 or value == 99 then - device:emit_event(capabilities.windowShade.windowShade.closed()) - elseif value == (device.preferences.defaultOnValue or 50) then - device:emit_event(capabilities.windowShade.windowShade.open()) - else - device:emit_event(capabilities.windowShade.windowShade.partially_open()) - end - device:emit_event(capabilities.windowShadeLevel.shadeLevel(value)) - device:send(SwitchMultilevel:Set({value = value})) -end - -function capability_handlers.set_shade_level(driver, device, command) - set_shade_level_helper(driver, device, command.args.shadeLevel) -end - -function capability_handlers.preset_position(driver, device) - set_shade_level_helper(driver, device, device.preferences.defaultOnValue or 50) -end - -local iblinds_window_treatment_v3 = { - capability_handlers = { - [capabilities.windowShade.ID] = { - [capabilities.windowShade.commands.close.NAME] = capability_handlers.close - }, - [capabilities.windowShadeLevel.ID] = { - [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = capability_handlers.set_shade_level - }, - [capabilities.windowShadePreset.ID] = { - [capabilities.windowShadePreset.commands.presetPosition.NAME] = capability_handlers.preset_position - } - }, - NAME = "iBlinds window treatment v3", - can_handle = can_handle_iblinds_window_treatment_v3 -} - -return iblinds_window_treatment_v3 diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/can_handle.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/can_handle.lua new file mode 100644 index 0000000000..ababe18ac6 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +--- Determine whether the passed device is iblinds window treatment v3 +--- +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @return boolean true if the device is iblinds window treatment, else false +local function can_handle_iblinds_window_treatment_v3(opts, driver, device, ...) + local FINGERPRINTS = require("iblinds-window-treatment.v3.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("iblinds-window-treatment.v3") + end + end + return false +end + +return can_handle_iblinds_window_treatment_v3 diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/fingerprints.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/fingerprints.lua new file mode 100644 index 0000000000..ee256ea18a --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local IBLINDS_WINDOW_TREATMENT_FINGERPRINTS_V3 = { + {mfr = 0x0287, prod = 0x0004, model = 0x0071}, -- iBlinds Window Treatment v3 + {mfr = 0x0287, prod = 0x0004, model = 0x0072} -- iBlinds Window Treatment v3 +} + +return IBLINDS_WINDOW_TREATMENT_FINGERPRINTS_V3 diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/init.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/init.lua new file mode 100644 index 0000000000..c0bb8bc363 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/init.lua @@ -0,0 +1,85 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass.SwitchMultilevel +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version=3 }) + +local IBLINDS_WINDOW_TREATMENT_FINGERPRINTS_V3 = { + {mfr = 0x0287, prod = 0x0004, model = 0x0071}, -- iBlinds Window Treatment v3 + {mfr = 0x0287, prod = 0x0004, model = 0x0072} -- iBlinds Window Treatment v3 +} + +--- Determine whether the passed device is iblinds window treatment v3 +--- +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @return boolean true if the device is iblinds window treatment, else false +local function can_handle_iblinds_window_treatment_v3(opts, driver, device, ...) + for _, fingerprint in ipairs(IBLINDS_WINDOW_TREATMENT_FINGERPRINTS_V3) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true + end + end + return false +end + +local function init_handler(self, device) + if device:supports_capability_by_id(capabilities.windowShadePreset.ID) and + device:get_latest_state("main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.supportedCommands.NAME) == nil then + + -- setPresetPosition is not supported (device uses a separate preference) + device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition"}, { visibility = { displayed = false }})) + end +end + +local capability_handlers = {} + +function capability_handlers.close(driver, device) + device:emit_event(capabilities.windowShade.windowShade.closed()) + device:emit_event(capabilities.windowShadeLevel.shadeLevel(0)) + device:send(SwitchMultilevel:Set({value = 0})) +end + +local function set_shade_level_helper(driver, device, value) + value = math.max(math.min(value, 99), 0) + if value == 0 or value == 99 then + device:emit_event(capabilities.windowShade.windowShade.closed()) + elseif value == (device.preferences.defaultOnValue or 50) then + device:emit_event(capabilities.windowShade.windowShade.open()) + else + device:emit_event(capabilities.windowShade.windowShade.partially_open()) + end + device:emit_event(capabilities.windowShadeLevel.shadeLevel(value)) + device:send(SwitchMultilevel:Set({value = value})) +end + +function capability_handlers.set_shade_level(driver, device, command) + set_shade_level_helper(driver, device, command.args.shadeLevel) +end + +function capability_handlers.preset_position(driver, device) + set_shade_level_helper(driver, device, device.preferences.defaultOnValue or 50) +end + +local iblinds_window_treatment_v3 = { + lifecycle_handlers = { + init = init_handler + }, + capability_handlers = { + [capabilities.windowShade.ID] = { + [capabilities.windowShade.commands.close.NAME] = capability_handlers.close + }, + [capabilities.windowShadeLevel.ID] = { + [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = capability_handlers.set_shade_level + }, + [capabilities.windowShadePreset.ID] = { + [capabilities.windowShadePreset.commands.presetPosition.NAME] = capability_handlers.preset_position + } + }, + NAME = "iBlinds window treatment v3", + can_handle = can_handle_iblinds_window_treatment_v3 +} + +return iblinds_window_treatment_v3 diff --git a/drivers/SmartThings/zwave-window-treatment/src/init.lua b/drivers/SmartThings/zwave-window-treatment/src/init.lua index 1cc9c34141..b2597d9f61 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.defaults @@ -20,6 +10,24 @@ local ZwaveDriver = require "st.zwave.driver" --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) local preferencesMap = require "preferences" +local window_preset_defaults = require "window_preset_defaults" + +local function init_handler(self, device) + if device:supports_capability_by_id(capabilities.windowShadePreset.ID) and + device:get_latest_state("main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME) == nil then + + -- These should only ever be nil once (and at the same time) for already-installed devices + -- It can be relocated to `added` after migration is complete + device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, { visibility = { displayed = false }})) + + local preset_position = device:get_field(window_preset_defaults.PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + window_preset_defaults.PRESET_LEVEL + + device:emit_event(capabilities.windowShadePreset.position(preset_position, { visibility = {displayed = false}})) + device:set_field(window_preset_defaults.PRESET_LEVEL_KEY, preset_position, {persist = true}) + end +end local function added_handler(self, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, { visibility = { displayed = false } })) @@ -56,15 +64,17 @@ local driver_template = { capabilities.battery }, lifecycle_handlers = { + init = init_handler, added = added_handler, infoChanged = info_changed }, - sub_drivers = { - require("springs-window-fashion-shade"), - require("iblinds-window-treatment"), - require("window-treatment-venetian"), - require("aeotec-nano-shutter") - } + capability_handlers = { + [capabilities.windowShadePreset.ID] = { + [capabilities.windowShadePreset.commands.setPresetPosition.NAME] = window_preset_defaults.set_preset_position_cmd, + [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_preset_defaults.window_shade_preset_cmd, + } + }, + sub_drivers = require("sub_drivers"), } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-window-treatment/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-window-treatment/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-window-treatment/src/preferences.lua b/drivers/SmartThings/zwave-window-treatment/src/preferences.lua index e8854c0bc0..54bad4c0ec 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/preferences.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/preferences.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local devices = { QUBINO = { diff --git a/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/can_handle.lua b/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/can_handle.lua new file mode 100644 index 0000000000..12d85394cf --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_springs_window_fashion_shade(opts, driver, device, ...) + local FINGERPRINTS = require("springs-window-fashion-shade.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("springs-window-fashion-shade") + end + end + return false +end + +return can_handle_springs_window_fashion_shade diff --git a/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/fingerprints.lua b/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/fingerprints.lua new file mode 100644 index 0000000000..7730b633e8 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SPRINGS_WINDOW_FINGERPRINTS = { + {mfr = 0x026E, prod = 0x4353, model = 0x5A31}, -- Springs Window Shade + {mfr = 0x026E, prod = 0x5253, model = 0x5A31}, -- Springs Roller Shade +} + +return SPRINGS_WINDOW_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/init.lua b/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/init.lua index 6e8ef08b09..2f4d3c38e6 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.constants @@ -18,23 +8,21 @@ local constants = require "st.zwave.constants" --- @type st.zwave.CommandClass.SwitchMultilevel local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) -local SPRINGS_WINDOW_FINGERPRINTS = { - {mfr = 0x026E, prod = 0x4353, model = 0x5A31}, -- Springs Window Shade - {mfr = 0x026E, prod = 0x5253, model = 0x5A31}, -- Springs Roller Shade -} --- Determine whether the passed device is springs window fashion shade --- --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is springs window fashion shade, else false -local function can_handle_springs_window_fashion_shade(opts, driver, device, ...) - for _, fingerprint in ipairs(SPRINGS_WINDOW_FINGERPRINTS) do - if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end + +local function init_handler(self, device) + -- This device has a preset position set in hardware, so we need to override the base driver + if device:supports_capability_by_id(capabilities.windowShadePreset.ID) and + device:get_latest_state("main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.supportedCommands.NAME) == nil then + + -- setPresetPosition is not supported + device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition"}, { visibility = { displayed = false }})) end - return false end local capability_handlers = {} @@ -58,13 +46,16 @@ function capability_handlers.preset_position(driver, device) end local springs_window_fashion_shade = { + lifecycle_handlers = { + init = init_handler + }, capability_handlers = { [capabilities.windowShadePreset.ID] = { [capabilities.windowShadePreset.commands.presetPosition.NAME] = capability_handlers.preset_position } }, NAME = "Springs window fashion shade", - can_handle = can_handle_springs_window_fashion_shade, + can_handle = require("springs-window-fashion-shade.can_handle"), } return springs_window_fashion_shade diff --git a/drivers/SmartThings/zwave-window-treatment/src/sub_drivers.lua b/drivers/SmartThings/zwave-window-treatment/src/sub_drivers.lua new file mode 100644 index 0000000000..6db8853e1a --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/sub_drivers.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("springs-window-fashion-shade"), + lazy_load_if_possible("iblinds-window-treatment"), + lazy_load_if_possible("window-treatment-venetian"), + lazy_load_if_possible("aeotec-nano-shutter"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_fibaro_roller_shutter.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_fibaro_roller_shutter.lua index 9c800e7070..c53e314f3a 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_fibaro_roller_shutter.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_fibaro_roller_shutter.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" @@ -303,7 +293,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_fibaro_roller_shutter.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.zwave:__expect_send( @@ -750,4 +740,30 @@ do end ) end +test.register_coroutine_test( + "Configuration:Report for OPERATING_MODE=1 should update to roller shutter profile", + function() + test.socket.zwave:__queue_receive({ + mock_fibaro_roller_shutter_venetian.id, + zw_test_utils.zwave_test_build_receive_command( + Configuration:Report({ parameter_number = 151, configuration_value = 1 }) + ) + }) + mock_fibaro_roller_shutter_venetian:expect_metadata_update({ profile = "fibaro-roller-shutter" }) + end +) + +test.register_coroutine_test( + "Configuration:Report for OPERATING_MODE=2 should update to venetian profile", + function() + test.socket.zwave:__queue_receive({ + mock_fibaro_roller_shutter_venetian.id, + zw_test_utils.zwave_test_build_receive_command( + Configuration:Report({ parameter_number = 151, configuration_value = 2 }) + ) + }) + mock_fibaro_roller_shutter_venetian:expect_metadata_update({ profile = "fibaro-roller-shutter-venetian" }) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_qubino_flush_shutter.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_qubino_flush_shutter.lua index ce5ed9fb81..d9bd11e01f 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_qubino_flush_shutter.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_qubino_flush_shutter.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" @@ -233,7 +223,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_qubino_flush_shutter.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.zwave:__expect_send( @@ -520,122 +510,113 @@ test.register_coroutine_test( end ) -do - test.register_coroutine_test( - "Mode should be changed to venetian blinds after receiving configuration report with value 1", - function() - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_qubino_flush_shutter.id, - Configuration:Report({ - parameter_number = 71, - size = 1, - configuration_value = 1 - }) +test.register_coroutine_test( + "Mode should be changed to venetian blinds after receiving configuration report with value 1", + function() + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_qubino_flush_shutter.id, + Configuration:Report({ + parameter_number = 71, + size = 1, + configuration_value = 1 }) - mock_qubino_flush_shutter:expect_metadata_update({ profile = "qubino-flush-shutter-venetian" }) - end - ) -end + }) + mock_qubino_flush_shutter:expect_metadata_update({ profile = "qubino-flush-shutter-venetian" }) + end +) -do - test.register_coroutine_test( - "Mode should be changed to shutter after receiving configuration report with value 0", - function() - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_qubino_flush_shutter.id, - Configuration:Report({ - parameter_number = 71, - size = 1, - configuration_value = 0 - }) +test.register_coroutine_test( + "Mode should be changed to shutter after receiving configuration report with value 0", + function() + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_qubino_flush_shutter.id, + Configuration:Report({ + parameter_number = 71, + size = 1, + configuration_value = 0 }) - mock_qubino_flush_shutter:expect_metadata_update({ profile = "qubino-flush-shutter" }) - end - ) -end + }) + mock_qubino_flush_shutter:expect_metadata_update({ profile = "qubino-flush-shutter" }) + end +) -do - test.register_coroutine_test( - "SwitchMultilevel:Set() should be correctly interpreted by the driver", - function() - local targetValue = 50 - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_qubino_flush_shutter.id, - SwitchMultilevel:Set({ - value = targetValue - }) +test.register_coroutine_test( + "SwitchMultilevel:Set() should be correctly interpreted by the driver", + function() + local targetValue = 50 + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_qubino_flush_shutter.id, + SwitchMultilevel:Set({ + value = targetValue }) - local expectedCachedEvent = utils.stringify_table(capabilities.windowShade.windowShade.opening()) - test.wait_for_events() - local actualCachedEvent = utils.stringify_table(mock_qubino_flush_shutter.transient_store.blinds_last_command) - assert(expectedCachedEvent == actualCachedEvent, "driver should cache 'opening' event when targetLevel > currentLevel") - assert(targetValue == mock_qubino_flush_shutter.transient_store.shade_target, "driver should chache correct level value") - end - ) -end + }) + local expectedCachedEvent = utils.stringify_table(capabilities.windowShade.windowShade.opening()) + test.wait_for_events() + local actualCachedEvent = utils.stringify_table(mock_qubino_flush_shutter.transient_store.blinds_last_command) + assert(expectedCachedEvent == actualCachedEvent, "driver should cache 'opening' event when targetLevel > currentLevel") + assert(targetValue == mock_qubino_flush_shutter.transient_store.shade_target, "driver should chache correct level value") + end +) -do - test.register_coroutine_test( - "Meter:Report() with meter_value > 0 should be correctly interpreted by the driver", - function() - local cachedShadesEvent = capabilities.windowShade.windowShade.opening() - local targetValue = 50 - mock_qubino_flush_shutter:set_field("blinds_last_command", cachedShadesEvent) - mock_qubino_flush_shutter:set_field("shade_target", targetValue) - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_qubino_flush_shutter.id, - Meter:Report({ - scale = Meter.scale.electric_meter.WATTS, - meter_value = 10 - }) +test.register_coroutine_test( + "Meter:Report() with meter_value > 0 should be correctly interpreted by the driver", + function() + local cachedShadesEvent = capabilities.windowShade.windowShade.opening() + local targetValue = 50 + mock_qubino_flush_shutter:set_field("blinds_last_command", cachedShadesEvent) + mock_qubino_flush_shutter:set_field("shade_target", targetValue) + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_qubino_flush_shutter.id, + Meter:Report({ + scale = Meter.scale.electric_meter.WATTS, + meter_value = 10 }) - test.socket.capability:__expect_send( - mock_qubino_flush_shutter:generate_test_message("main", cachedShadesEvent) - ) - test.socket.capability:__expect_send( - mock_qubino_flush_shutter:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(targetValue)) - ) - test.socket.capability:__expect_send( - mock_qubino_flush_shutter:generate_test_message("main", capabilities.powerMeter.power({value = 10, unit = "W"})) - ) - end - ) -end + }) + test.socket.capability:__expect_send( + mock_qubino_flush_shutter:generate_test_message("main", cachedShadesEvent) + ) + test.socket.capability:__expect_send( + mock_qubino_flush_shutter:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(targetValue)) + ) + test.socket.capability:__expect_send( + mock_qubino_flush_shutter:generate_test_message("main", capabilities.powerMeter.power({value = 10, unit = "W"})) + ) + end +) -do - test.register_coroutine_test( - "Meter:Report() with meter_value == 0 should be correctly interpreted by the driver", - function() - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_qubino_flush_shutter.id, - Meter:Report({ - scale = Meter.scale.electric_meter.WATTS, - meter_value = 0 - }) + +test.register_coroutine_test( + "Meter:Report() with meter_value == 0 should be correctly interpreted by the driver", + function() + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_qubino_flush_shutter.id, + Meter:Report({ + scale = Meter.scale.electric_meter.WATTS, + meter_value = 0 }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_qubino_flush_shutter, - SwitchMultilevel:Get({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_qubino_flush_shutter, - Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}) - ) + }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_qubino_flush_shutter, + SwitchMultilevel:Get({}) ) - test.socket.capability:__expect_send( - mock_qubino_flush_shutter:generate_test_message("main", capabilities.powerMeter.power({value = 0, unit = "W"})) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_qubino_flush_shutter, + Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}) ) - end - ) -end + ) + test.socket.capability:__expect_send( + mock_qubino_flush_shutter:generate_test_message("main", capabilities.powerMeter.power({value = 0, unit = "W"})) + ) + end +) test.register_message_test( "Energy meter reports should be generating events", @@ -661,4 +642,58 @@ test.register_message_test( } ) +test.register_coroutine_test( + "SwitchMultilevel:Set with lower target than current should cache closing command and fire Meter:Get after 4s", + function() + -- Pre-set state so currentLevel (80) > targetLevel (10) + mock_qubino_flush_shutter_venetian:update_state_cache_entry( + "main", capabilities.windowShadeLevel.ID, "shadeLevel", { value = 80 } + ) + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.zwave:__queue_receive({ + mock_qubino_flush_shutter_venetian.id, + SwitchMultilevel:Set({ value = 10 }) + }) + test.wait_for_events() + test.mock_time.advance_time(4) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_qubino_flush_shutter_venetian, + Meter:Get({ scale = Meter.scale.electric_meter.WATTS }) + ) + ) + end +) + +do + local new_param_value = 1 + test.register_coroutine_test( + "infoChanged on venetian qubino should send Configuration:Set then Configuration:Get after 1s", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + local device_data = utils.deep_copy(mock_qubino_flush_shutter_venetian.raw_st_data) + device_data.preferences["operatingModes"] = new_param_value + local device_data_json = dkjson.encode(device_data) + test.socket.device_lifecycle:__queue_receive({ mock_qubino_flush_shutter_venetian.id, "infoChanged", device_data_json }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_qubino_flush_shutter_venetian, + Configuration:Set({ + parameter_number = 71, + configuration_value = new_param_value, + size = 1 + }) + ) + ) + test.mock_time.advance_time(1) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_qubino_flush_shutter_venetian, + Configuration:Get({ parameter_number = 71 }) + ) + ) + end + ) +end + test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_aeotec_nano_shutter.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_aeotec_nano_shutter.lua index c11b578b07..089fc29bac 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_aeotec_nano_shutter.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_aeotec_nano_shutter.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua index bdc41956ec..8913a42e53 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" @@ -49,6 +39,15 @@ local mock_blind_v3 = test.mock_device.build_test_zwave_device({ local function test_init() test.mock_device.add_test_device(mock_blind) test.mock_device.add_test_device(mock_blind_v3) + test.socket.capability:__expect_send( + mock_blind:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_blind:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_blind_v3:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition"}, {visibility = {displayed=false}})) + ) end test.set_test_init_function(test_init) @@ -214,7 +213,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_blind.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.capability:__expect_send( @@ -240,18 +239,20 @@ test.register_coroutine_test( test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") test.socket.zwave:__set_channel_ordering("relaxed") test.socket.device_lifecycle():__queue_receive({mock_blind.id, "init"}) - test.socket.device_lifecycle():__queue_receive(mock_blind:generate_info_changed( - { - preferences = { - presetPosition = 35 - } - } - )) + test.socket.capability:__queue_receive( + { + mock_blind.id, + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {35} } + } + ) + test.socket.capability:__expect_send( + mock_blind:generate_test_message("main", capabilities.windowShadePreset.position(35)) + ) test.wait_for_events() test.socket.capability:__queue_receive( { mock_blind.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.capability:__expect_send( @@ -374,7 +375,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_blind_v3.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.capability:__expect_send( @@ -415,7 +416,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_blind_v3.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.capability:__expect_send( @@ -474,4 +475,52 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Setting window shade level to 0 on iblinds v1 should emit windowShade.closed", + function() + test.socket.capability:__queue_receive( + { + mock_blind.id, + { capability = "windowShadeLevel", command = "setShadeLevel", args = { 0 } } + } + ) + test.socket.capability:__expect_send( + mock_blind:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + ) + test.socket.capability:__expect_send( + mock_blind:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(0)) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_blind, + SwitchMultilevel:Set({ value = 0 }) + ) + ) + end +) + +test.register_coroutine_test( + "Setting window shade level to 0 on iblinds v3 should emit windowShade.closed", + function() + test.socket.capability:__queue_receive( + { + mock_blind_v3.id, + { capability = "windowShadeLevel", command = "setShadeLevel", args = { 0 } } + } + ) + test.socket.capability:__expect_send( + mock_blind_v3:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + ) + test.socket.capability:__expect_send( + mock_blind_v3:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(0)) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_blind_v3, + SwitchMultilevel:Set({ value = 0 }) + ) + ) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua index 4335ae3b41..843b26e407 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local constants = require "st.zwave.constants" @@ -18,6 +8,7 @@ local zw = require "st.zwave" local zw_test_utils = require "integration_test.zwave_test_utils" local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version=4 }) local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" -- supported comand classes: SWITCH_MULTILEVEL local window_shade_switch_multilevel_endpoints = { @@ -38,6 +29,9 @@ local mock_springs_window_fashion_shade = test.mock_device.build_test_zwave_devi local function test_init() test.mock_device.add_test_device(mock_springs_window_fashion_shade) + test.socket.capability:__expect_send( + mock_springs_window_fashion_shade:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition"}, {visibility = {displayed=false}})) + ) end test.set_test_init_function(test_init) @@ -48,7 +42,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_springs_window_fashion_shade.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.zwave:__expect_send( diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua index bf60c1797b..be25647509 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" @@ -54,6 +44,18 @@ local mock_window_shade_switch_multilevel = test.mock_device.build_test_zwave_de local function test_init() test.mock_device.add_test_device(mock_window_shade_basic) test.mock_device.add_test_device(mock_window_shade_switch_multilevel) + test.socket.capability:__expect_send( + mock_window_shade_basic:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_window_shade_basic:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_window_shade_switch_multilevel:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_window_shade_switch_multilevel:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) end test.set_test_init_function(test_init) @@ -304,7 +306,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_window_shade_switch_multilevel.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.zwave:__expect_send( @@ -529,4 +531,43 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Setting window shade preset on basic-only device should generate Basic:Set and Basic:Get", + function() + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + test.socket.capability:__queue_receive( + { + mock_window_shade_basic.id, + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } + } + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_window_shade_basic, + Basic:Set({ value = 50 }) + ) + ) + test.wait_for_events() + test.mock_time.advance_time(5) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_window_shade_basic, + Basic:Get({}) + ) + ) + end +) + +test.register_coroutine_test( + "Adding a window treatment device should emit supportedWindowShadeCommands", + function() + test.socket.device_lifecycle():__queue_receive({ mock_window_shade_basic.id, "added" }) + test.socket.capability:__expect_send( + mock_window_shade_basic:generate_test_message("main", capabilities.windowShade.supportedWindowShadeCommands( + {"open", "close", "pause"}, { visibility = { displayed = false } } + )) + ) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/can_handle.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/can_handle.lua new file mode 100644 index 0000000000..f2eb62ad27 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_window_treatment_venetian(opts, driver, device, ...) + local FINGERPRINTS = require("window-treatment-venetian.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("window-treatment-venetian") + end + end + return false +end + +return can_handle_window_treatment_venetian diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/can_handle.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/can_handle.lua new file mode 100644 index 0000000000..cc9ca62b1c --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_roller_shutter(opts, driver, device, ...) + local FINGERPRINTS = require("window-treatment-venetian.fibaro-roller-shutter.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("window-treatment-venetian.fibaro-roller-shutter") + end + end + return false +end + +return can_handle_fibaro_roller_shutter diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/fingerprints.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/fingerprints.lua new file mode 100644 index 0000000000..925685157f --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_ROLLER_SHUTTER_FINGERPRINTS = { + {mfr = 0x010F, prod = 0x1D01, model = 0x1000}, -- Fibaro Walli Roller Shutter +} + +return FIBARO_ROLLER_SHUTTER_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/init.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/init.lua index dbdde5b87c..5581e3a5af 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/init.lua @@ -1,25 +1,12 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + --- @type st.zwave.CommandClass local cc = (require "st.zwave.CommandClass") --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({version=1}) -local FIBARO_ROLLER_SHUTTER_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x1D01, model = 0x1000}, -- Fibaro Walli Roller Shutter -} -- configuration parameters local CALIBRATION_CONFIGURATION = 150 @@ -33,14 +20,6 @@ local CLB_NOT_STARTED = "not_started" local CLB_DONE = "done" local CLB_PENDING = "pending" -local function can_handle_fibaro_roller_shutter(opts, driver, device, ...) - for _, fingerprint in ipairs(FIBARO_ROLLER_SHUTTER_FINGERPRINTS) do - if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function configuration_report(driver, device, cmd) local parameter_number = cmd.args.parameter_number @@ -79,7 +58,7 @@ local fibaro_roller_shutter = { } }, NAME = "fibaro roller shutter", - can_handle = can_handle_fibaro_roller_shutter, + can_handle = require("window-treatment-venetian.fibaro-roller-shutter.can_handle"), lifecycle_handlers = { add = device_added } diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fingerprints.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fingerprints.lua new file mode 100644 index 0000000000..91dc96c5b8 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local WINDOW_TREATMENT_VENETIAN_FINGERPRINTS = { + {mfr = 0x010F, prod = 0x1D01, model = 0x1000}, -- Fibaro Walli Roller Shutter + {mfr = 0x0159, prod = 0x0003, model = 0x0052}, -- Qubino Flush Shutter AC + {mfr = 0x0159, prod = 0x0003, model = 0x0053}, -- Qubino Flush Shutter DC +} + +return WINDOW_TREATMENT_VENETIAN_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/init.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/init.lua index 2ff27250a8..c201ed32eb 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cc = (require "st.zwave.CommandClass") local Basic = (require "st.zwave.CommandClass.Basic")({ version=1 }) @@ -19,20 +10,7 @@ local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ver local WindowShadeDefaults = require "st.zwave.defaults.windowShade" local WindowShadeLevelDefaults = require "st.zwave.defaults.windowShadeLevel" -local WINDOW_TREATMENT_VENETIAN_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x1D01, model = 0x1000}, -- Fibaro Walli Roller Shutter - {mfr = 0x0159, prod = 0x0003, model = 0x0052}, -- Qubino Flush Shutter AC - {mfr = 0x0159, prod = 0x0003, model = 0x0053}, -- Qubino Flush Shutter DC -} -local function can_handle_window_treatment_venetian(opts, driver, device, ...) - for _, fingerprint in ipairs(WINDOW_TREATMENT_VENETIAN_FINGERPRINTS) do - if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function shade_event_handler(self, device, cmd) WindowShadeDefaults.zwave_handlers[cc.SWITCH_MULTILEVEL][SwitchMultilevel.REPORT](self, device, cmd) @@ -70,14 +48,11 @@ local window_treatment_venetian = { [SwitchMultilevel.REPORT] = shade_event_handler } }, - can_handle = can_handle_window_treatment_venetian, + can_handle = require("window-treatment-venetian.can_handle"), lifecycle_handlers = { init = map_components }, - sub_drivers = { - require("window-treatment-venetian/fibaro-roller-shutter"), - require("window-treatment-venetian/qubino-flush-shutter") - } + sub_drivers = require("window-treatment-venetian.sub_drivers"), } return window_treatment_venetian diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/can_handle.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/can_handle.lua new file mode 100644 index 0000000000..e601800084 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_qubino_flush_shutter(opts, self, device, ...) + local FINGERPRINTS = require("window-treatment-venetian.qubino-flush-shutter.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("window-treatment-venetian.qubino-flush-shutter") + end + end + return false +end + +return can_handle_qubino_flush_shutter diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/fingerprints.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/fingerprints.lua new file mode 100644 index 0000000000..098879f313 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local QUBINO_FLUSH_SHUTTER_FINGERPRINTS = { + {mfr = 0x0159, prod = 0x0003, model = 0x0052}, -- Qubino Flush Shutter AC + {mfr = 0x0159, prod = 0x0003, model = 0x0053}, -- Qubino Flush Shutter DC +} + +return QUBINO_FLUSH_SHUTTER_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/init.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/init.lua index fafc6d5026..56ba50f516 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -41,19 +31,7 @@ local SHADE_TARGET = "shade_target" local ENERGY_UNIT_KWH = "kWh" local POWER_UNIT_WATT = "W" -local QUBINO_FLUSH_SHUTTER_FINGERPRINTS = { - {mfr = 0x0159, prod = 0x0003, model = 0x0052}, -- Qubino Flush Shutter AC - {mfr = 0x0159, prod = 0x0003, model = 0x0053}, -- Qubino Flush Shutter DC -} -local function can_handle_qubino_flush_shutter(opts, self, device, ...) - for _, fingerprint in ipairs(QUBINO_FLUSH_SHUTTER_FINGERPRINTS) do - if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function configuration_report(self, device, cmd) local parameter_number = cmd.args.parameter_number @@ -190,7 +168,7 @@ local qubino_flush_shutter = { [capabilities.windowShade.commands.close.NAME] = close }, }, - can_handle = can_handle_qubino_flush_shutter, + can_handle = require("window-treatment-venetian.qubino-flush-shutter.can_handle"), lifecycle_handlers = { added = device_added, infoChanged = info_changed diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/sub_drivers.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/sub_drivers.lua new file mode 100644 index 0000000000..41600cffef --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("window-treatment-venetian/fibaro-roller-shutter"), + lazy_load_if_possible("window-treatment-venetian/qubino-flush-shutter"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-window-treatment/src/window_preset_defaults.lua b/drivers/SmartThings/zwave-window-treatment/src/window_preset_defaults.lua new file mode 100644 index 0000000000..3d5056abc5 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window_preset_defaults.lua @@ -0,0 +1,55 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + + +-- These were added to scripting engine, but this file is to make sure drivers +-- runing on older versions of scripting engine can still access these values +local capabilities = require "st.capabilities" +local constants = require "st.zwave.constants" + +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.SwitchMultilevel +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version = 4 }) +--- @type st.zwave.CommandClass.Basic +local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) + +local defaults = {} + +--- WINDOW SHADE PRESET CONSTANTS +defaults.PRESET_LEVEL = 50 +defaults.PRESET_LEVEL_KEY = "_presetLevel" + +defaults.set_preset_position_cmd = function(driver, device, command) + device:emit_component_event({id = command.component}, capabilities.windowShadePreset.position(command.args.position)) + device:set_field(defaults.PRESET_LEVEL_KEY, command.args.position, {persist = true}) +end + +defaults.window_shade_preset_cmd = function(driver, device, command) + local set + local get + local preset_level = device:get_latest_state(command.component, "windowShadePreset", "position") or + device:get_field(constants.PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + defaults.PRESET_LEVEL + if device:is_cc_supported(cc.SWITCH_MULTILEVEL) then + set = SwitchMultilevel:Set({ + value = preset_level, + duration = constants.DEFAULT_DIMMING_DURATION + }) + get = SwitchMultilevel:Get({}) + else + set = Basic:Set({ + value = preset_level + }) + get = Basic:Get({}) + end + device:send_to_component(set, command.component) + local query_device = function() + device:send_to_component(get, command.component) + end + device.thread:call_with_delay(constants.MIN_DIMMING_GET_STATUS_DELAY, query_device) +end + +return defaults diff --git a/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml b/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml index 9712eb6849..a6bbc35a10 100644 --- a/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml +++ b/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml @@ -4,6 +4,8 @@ components: capabilities: - id: windowShade version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: windowShadeLevel version: 1 - id: windowShadePreset diff --git a/drivers/Unofficial/tuya-zigbee/src/button/init.lua b/drivers/Unofficial/tuya-zigbee/src/button/init.lua index a3df25a7f8..61599e3d5a 100644 --- a/drivers/Unofficial/tuya-zigbee/src/button/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/button/init.lua @@ -17,6 +17,7 @@ local clusters = require "st.zigbee.zcl.clusters" local OnOff = clusters.OnOff local device_management = require "st.zigbee.device_management" local PRESENT_ATTRIBUTE_ID = 0x00fd +local tuya_utils = require "tuya_utils" local FINGERPRINTS = { { mfr = "_TZ3000_ja5osu5g", model = "TS004F"}, @@ -36,7 +37,7 @@ end local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local tuya_private_cluster_button_handler = function(driver, device, zb_rx) diff --git a/drivers/Unofficial/tuya-zigbee/src/button/meian-button/init.lua b/drivers/Unofficial/tuya-zigbee/src/button/meian-button/init.lua index f0281f91f5..887826a9f0 100644 --- a/drivers/Unofficial/tuya-zigbee/src/button/meian-button/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/button/meian-button/init.lua @@ -21,6 +21,7 @@ local data_types = require "st.zigbee.data_types" local messages = require "st.zigbee.messages" local defaults = require "st.zigbee.defaults" local PowerConfiguration = clusters.PowerConfiguration +local tuya_utils = require "tuya_utils" local IASACE = clusters.IASACE @@ -63,7 +64,7 @@ end local function added_handler(driver, device, event, args) device:emit_event(capabilities.button.supportedButtonValues({"pushed"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) local magic_spell = {0x0004, 0x0000, 0x0001, 0x0005, 0x0007, 0xfffe} device:send(read_attribute_function(device, clusters.Basic.ID, magic_spell)) diff --git a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua index 1eb2e94340..76446005d7 100644 --- a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua @@ -17,9 +17,12 @@ local clusters = require "st.zigbee.zcl.clusters" local utils = require "st.utils" local device_management = require "st.zigbee.device_management" local tuya_utils = require "tuya_utils" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" local Basic = clusters.Basic local packet_id = 0 +local log = require "log" + +local PRESET_LEVEL = 50 +local PRESET_LEVEL_KEY = "_presetLevel" local FINGERPRINTS = { { mfr = "_TZE284_nladmfvf", model = "TS0601"} @@ -34,6 +37,23 @@ local function is_tuya_curtain(opts, driver, device) return false end +local function init_handler(self, device) + if device:supports_capability_by_id(capabilities.windowShadePreset.ID) and + device:get_latest_state("main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME) == nil then + + -- These should only ever be nil once (and at the same time) for already-installed devices + -- It can be removed after migration is complete + device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, { visibility = { displayed = false }})) + + local preset_position = device:get_field(PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + PRESET_LEVEL + + device:emit_event(capabilities.windowShadePreset.position(preset_position, { visibility = {displayed = false}})) + device:set_field(PRESET_LEVEL_KEY, preset_position, {persist = true}) + end +end + local do_configure = function(driver, device) -- configure ApplicationVersion to keep device online, tuya hub also uses this attribute tuya_utils.send_magic_spell(device) @@ -43,8 +63,10 @@ end local function device_added(driver, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) - device:emit_event(capabilities.windowShadeLevel.shadeLevel(0)) - device:emit_event(capabilities.windowShade.windowShade.closed()) + tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) + tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) + device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, { visibility = { displayed = false }})) + tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadePreset, capabilities.windowShadePreset.position.NAME, PRESET_LEVEL) end local function increase_packet_id(packet_id) @@ -88,6 +110,7 @@ local function window_shade_level(driver, device, command) if level > 100 then level = 100 end + log.info("capability handler level ------------->", level) level = utils.round(level) if device:get_manufacturer() == "_TZE284_nladmfvf" then level = 100 - level -- specific for _TZE284_nladmfvf @@ -97,11 +120,19 @@ local function window_shade_level(driver, device, command) end local function window_shade_preset(driver, device) - local level = device.preferences and device.preferences.presetPosition or window_preset_defaults.PRESET_LEVEL + local level = device:get_latest_state("main", "windowShadePreset", "position") or + device:get_field(PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + PRESET_LEVEL tuya_utils.send_tuya_command(device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00'..string.pack(">I2", level), packet_id) packet_id = increase_packet_id(packet_id) end +local function set_preset_position_cmd(driver, device, command) + device:emit_component_event({id = command.component}, capabilities.windowShadePreset.position(command.args.position)) + device:set_field(PRESET_LEVEL_KEY, command.args.position, {persist = true}) +end + local function tuya_cluster_handler(driver, device, zb_rx) local window_shade_level_event, window_shade_val_event local raw = zb_rx.body.zcl_body.body_bytes @@ -124,9 +155,32 @@ local function tuya_cluster_handler(driver, device, zb_rx) end end +local function knob_to_window_shade_step_cmd(driver, device, command) + -- step1: get the rotateAmount + local step = command.args.stepSize + -- step2: get the current_level + local current_level = device:get_latest_state("main", capabilities.windowShadeLevel.ID, capabilities.windowShadeLevel.shadeLevel.NAME) or 0 + -- calcultate the target_level + -- Tuya curtain devices use INVERTED position logic, + -- if we want to set to "target level" = "current_level" + "step" + -- we should send (100 - target level) to device + local target_level = 100-(current_level + step) + if target_level > 100 then + target_level = 100 + elseif target_level < 0 then + target_level = 0 + end + target_level = utils.round(target_level) + tuya_utils.send_tuya_command(device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00'..string.pack(">I2", target_level), packet_id) + packet_id = increase_packet_id(packet_id) + log.info("-------------------target_level", 100-target_level) + device:emit_event(capabilities.windowShadeLevel.shadeLevel(100-target_level)) +end + local tuya_curtain_driver = { NAME = "tuya curtain", lifecycle_handlers = { + init = init_handler, added = device_added, infoChanged = device_info_changed, doConfigure = do_configure @@ -141,7 +195,11 @@ local tuya_curtain_driver = { [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = window_shade_level }, [capabilities.windowShadePreset.ID] = { - [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_preset + [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_preset, + [capabilities.windowShadePreset.commands.setPresetPosition.NAME] = set_preset_position_cmd + }, + [capabilities.statelessSwitchLevelStep.ID] = { + [capabilities.statelessSwitchLevelStep.commands.stepLevel.NAME] = knob_to_window_shade_step_cmd } }, zigbee_handlers = { diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua index a97ff0af10..c3610f55ea 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua @@ -136,6 +136,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Configure should configure all necessary attributes", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device_meian_button.id, "added" }) test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( @@ -162,6 +163,26 @@ test.register_coroutine_test( mock_device_meian_button.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_meian_button) }) + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device_meian_button.id, "added" }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device_meian_button:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device_meian_button:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + test.socket.zigbee:__expect_send({ mock_device_meian_button.id, tuya_utils.build_tuya_magic_spell_message(mock_device_meian_button) }) + test.socket.zigbee:__expect_send({ + mock_device_meian_button.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_meian_button) + }) end ) diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua index 431cbc2c48..73567c24e0 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua @@ -47,6 +47,7 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send( mock_device:generate_test_message( @@ -63,6 +64,20 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", button.button.pushed({ state_change = false })) ) + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) end ) diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua index d6beda0de8..ca09608347 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua @@ -39,7 +39,14 @@ local mock_simple_device = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() - test.mock_device.add_test_device(mock_simple_device)end + test.mock_device.add_test_device(mock_simple_device) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) +end test.set_test_init_function(test_init) @@ -88,6 +95,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial window shade event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added" }) test.socket.capability:__expect_send( mock_simple_device:generate_test_message( @@ -107,6 +115,22 @@ test.register_coroutine_test( capabilities.windowShade.windowShade.closed() ) ) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.wait_for_events() + -- Avoid sending the initial window shade event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added" }) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message( + "main", + capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}}) + ) + ) + + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) end ) @@ -121,7 +145,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x00', 2) } + message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x00', 0) } }, { channel = "capability", @@ -142,7 +166,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x02', 3) } + message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x02', 0) } }, { channel = "capability", @@ -163,7 +187,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x01', 4) } + message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x01', 0) } } } ) @@ -179,7 +203,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x3c', 5) } + message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x3c', 0) } } } ) @@ -195,7 +219,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x32', 6) } + message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x32', 0) } } } ) diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_switch.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_switch.lua index 4da9d411d7..97537c36ad 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_switch.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_switch.lua @@ -252,7 +252,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x02', tuya_utils.DP_TYPE_BOOL, '\x01', 1) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x02', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } } ) @@ -268,7 +268,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x03', tuya_utils.DP_TYPE_BOOL, '\x01', 2) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x03', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } } ) @@ -284,7 +284,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x04', tuya_utils.DP_TYPE_BOOL, '\x01', 3) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x04', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } } ) @@ -300,7 +300,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x05', tuya_utils.DP_TYPE_BOOL, '\x01', 4) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x05', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } } ) @@ -316,7 +316,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x06', tuya_utils.DP_TYPE_BOOL, '\x01', 5) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x06', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } } ) @@ -332,7 +332,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x01', tuya_utils.DP_TYPE_BOOL, '\x00', 6) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x01', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) @@ -348,7 +348,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x02', tuya_utils.DP_TYPE_BOOL, '\x00', 7) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x02', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) @@ -364,7 +364,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x03', tuya_utils.DP_TYPE_BOOL, '\x00', 8) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x03', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) @@ -380,7 +380,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x04', tuya_utils.DP_TYPE_BOOL, '\x00', 9) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x04', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) @@ -396,7 +396,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x05', tuya_utils.DP_TYPE_BOOL, '\x00', 10) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x05', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) @@ -412,7 +412,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x06', tuya_utils.DP_TYPE_BOOL, '\x00', 11) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x06', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) diff --git a/drivers/Unofficial/tuya-zigbee/src/tuya_utils.lua b/drivers/Unofficial/tuya-zigbee/src/tuya_utils.lua index 8e3c707b7d..f28a6a87c1 100644 --- a/drivers/Unofficial/tuya-zigbee/src/tuya_utils.lua +++ b/drivers/Unofficial/tuya-zigbee/src/tuya_utils.lua @@ -157,6 +157,12 @@ tuya_utils.build_tuya_magic_spell_message = function(device) }) end +tuya_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + tuya_utils.TUYA_PRIVATE_CLUSTER = TUYA_PRIVATE_CLUSTER tuya_utils.DP_TYPE_BOOL = DP_TYPE_BOOL tuya_utils.DP_TYPE_ENUM = DP_TYPE_ENUM diff --git a/tools/deploy.py b/tools/deploy.py index c9d92448f5..2c550149c7 100644 --- a/tools/deploy.py +++ b/tools/deploy.py @@ -5,27 +5,29 @@ CHANGED_DRIVERS = os.environ.get('CHANGED_DRIVERS') # configurable from Jenkins to override and manually set the drivers to be uploaded DRIVERS_OVERRIDE = os.environ.get('DRIVERS_OVERRIDE') or "[]" +DRY_RUN = os.environ.get("DRY_RUN") == True or os.environ.get("DRY_RUN") == "True" print(BRANCH) print(ENVIRONMENT) print(CHANGED_DRIVERS) -branch_environment = "{}_{}_".format(BRANCH, ENVIRONMENT) -ENVIRONMENT_URL = os.environ.get(ENVIRONMENT+'_ENVIRONMENT_URL') +ENVIRONMENT_URL = os.environ.get('ENVIRONMENT_URL') if not ENVIRONMENT_URL: print("No environment url specified, aborting.") exit(0) UPLOAD_URL = ENVIRONMENT_URL+"/drivers/package" -CHANNEL_ID = os.environ.get(branch_environment+'CHANNEL_ID') +CHANNEL_ID = os.environ.get(BRANCH+'_CHANNEL_ID') if not CHANNEL_ID: - print("No channel id specified, aborting.") + print("No channel id specified for "+BRANCH+", aborting.") exit(0) UPDATE_URL = ENVIRONMENT_URL+"/channels/"+CHANNEL_ID+"/drivers/bulk" -TOKEN = os.environ.get(ENVIRONMENT+'_TOKEN') +TOKEN = os.environ.get('TOKEN') DRIVERID = "driverId" VERSION = "version" PACKAGEKEY = "packageKey" +FAILURE_FILE = "failures.log" + BOSE_APPKEY = os.environ.get("BOSE_AUDIONOTIFICATION_APPKEY") SONOS_API_KEY = os.environ.get("SONOS_API_KEY") or "N/A" @@ -91,7 +93,8 @@ headers = { "Accept": "application/vnd.smartthings+json;v=20200810", "Authorization": "Bearer "+TOKEN, - "X-ST-LOG-LEVEL": "DEBUG" + "X-ST-LOG-LEVEL": "DEBUG", + "X-ST-CORRELATION": "driver-deployment-"+BRANCH+"-"+ENVIRONMENT+"-"+str(time.time()) }, json = { DRIVERID: driver[DRIVERID], @@ -147,7 +150,7 @@ print(error.stderr) retries += 1 if retries >= 5: - print("5 zip failires, skipping "+package_key+" and continuing.") + print("5 zip failures, skipping "+package_key+" and continuing.") continue with open(driver+".zip", 'rb') as driver_package: data = driver_package.read() @@ -169,6 +172,11 @@ if response.status_code == 500 or response.status_code == 429: retries = retries + 1 if retries > 3: + with open("../../"+FAILURE_FILE, 'a') as f: # go up to the root directory to output, since we've changed dirs to the partner directory + f.write("Failed to upload driver to "+ENVIRONMENT+": "+driver) + f.write("Error code: "+str(response.status_code)) + f.write("Error response: "+response.text) + f.write('\n') break # give up if response.status_code == 429: time.sleep(10) @@ -187,23 +195,27 @@ print("Uploading package: {} driver id: {} version: {}".format(package_key, driver_info[DRIVERID], driver_info[VERSION])) driver_updates.append({DRIVERID: driver_info[DRIVERID], VERSION: driver_info[VERSION]}) -response = requests.put( - UPDATE_URL, - headers={ - "Accept": "application/vnd.smartthings+json;v=20200810", - "Authorization": "Bearer "+TOKEN, - "Content-Type": "application/json", - "X-ST-LOG-LEVEL": "DEBUG" - }, - data=json.dumps(driver_updates) -) -if response.status_code != 204: - print("Failed to bulk update drivers") - print("Error code: "+str(response.status_code)) - print("Error response: "+response.text) - exit(1) +if DRY_RUN: + print("Dry Run, skipping bulk upload to " + UPDATE_URL) +else: + response = requests.put( + UPDATE_URL, + headers={ + "Accept": "application/vnd.smartthings+json;v=20200810", + "Authorization": "Bearer "+TOKEN, + "Content-Type": "application/json", + "X-ST-LOG-LEVEL": "DEBUG", + "X-ST-CORRELATION": "driver-deployment-"+BRANCH+"-"+ENVIRONMENT+"-"+str(time.time()) + }, + data=json.dumps(driver_updates) + ) + if response.status_code != 204: + print("Failed to bulk update drivers") + print("Error code: "+str(response.status_code)) + print("Error response: "+response.text) + exit(1) print("Update drivers: ") print(drivers_updated) -print("\nDrivers currently deplpyed: ") +print("\nDrivers currently deployed: ") print(uploaded_drivers.keys()) diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index 80067bc5f0..952b4a5c44 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -99,6 +99,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "ThirdReality Smart Watering Kit",树实智能浇灌套装 "Zemismart ZM24A Smart Curtain", Zemismart ZM24A 智能窗帘 "Greentown Lock(SG20)",绿城门锁(SG20) +"MultiIR Air Quality Detector",MultiIR空气质量检测仪 "Essentials Indoor Lights",基础款室内照明 "Sleepone Ai SX-1",智能床垫 "Smart Mechanical Keyboard MK1",树实智能机械键盘MK1 @@ -114,4 +115,19 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "WISTAR WSERD50-B Smart Tubular Motor",威仕达智能管状电机 WSERD50-B "WISTAR WSERD50-L Smart Tubular Motor",威仕达智能管状电机 WSERD50-L "WISTAR WSERD50-T Smart Tubular Motor",威仕达智能管状电机 WSERD50-T -"WISTAR WSER60 Smart Tubular Motor",威仕达智能管状电机 WSER60 \ No newline at end of file +"WISTAR WSER60 Smart Tubular Motor",威仕达智能管状电机 WSER60 +"WISTAR WSCMXH Smart Vertical Blind Motor",威仕达智能梦幻帘电机 WSCMXH +"WISTAR WSCMXF Smart Vertical Blind Motor",威仕达智能梦幻帘电机 WSCMXF +"WISTAR WSCMXF-LED Smart Vertical Blind Motor",威仕达智能梦幻帘电机 WSCMXF-LED +"VIVIDSTORM Smart Screen VWSDSTUST120H",VIVIDSTORM智能幕布 VWSDSTUST120H +"HOPOsmart Window Opener A2230011",HOPOsmart链式开窗器 A2230011 +"Yanmi Switch (3 Way)",岩米三位智能开关面板 +"Onvis Smart Plug S4EU",Onvis 智能插座S4EU +"Yanmi Switch (2 Way)",岩米二位智能开关面板 +"Yanmi Switch (1 Way)",岩米一位智能开关面板 +"WISTAR WSCMQ Smart Curtain Motor",威仕达智能开合帘电机 WSCMQ +"WISTAR WSCMXI Smart Curtain Motor",威仕达智能开合帘电机 WSCMXI +"WISTAR WSCMT Smart Curtain Motor",威仕达智能开合帘电机 WSCMT +"WISTAR WSCMXB Smart Curtain Motor",威仕达智能开合帘电机 WSCMXB +"WISTAR WSCMXC Smart Curtain Motor",威仕达智能开合帘电机 WSCMXC +"WISTAR WSCMXJ Smart Curtain Motor",威仕达智能开合帘电机 WSCMXJ diff --git a/tools/pre-commit b/tools/pre-commit new file mode 100755 index 0000000000..41a9406bf5 --- /dev/null +++ b/tools/pre-commit @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# +# Copyright 2025 SmartThings, Inc. +# Licensed under the Apache License, Version 2.0 +# +# pre-commit +# +# - Soft link into .git/hooks/pre-commit +# - $ ln -s ../../tools/pre-commit .git/hooks/pre-commit +# - Ensure executable + +set -e + +# Get changed files that are Added or Modified +if [[ "$1" ]]; then + files=$(find $1 -type f -follow -print) +else + files=$(git diff --staged --name-only --diff-filter=AM) +fi + +fail=0 +for file in $files; do + # Only process .lua files + case "$file" in + *.lua) + # Skip if not a regular file + [ -f "$file" ] || continue + + # Check if file is not empty, is a SmartThings driver and has the copyright header + if [ -s "$file" ] && [[ "$file" =~ "drivers/SmartThings" ]] \ + && [[ ! $(grep $file --text -P -e "-{2,} Copyright (?:© )?\d{4}(?:-\d{4})? SmartThings") ]]; then + echo "$file: SmartThings Copyright missing from file" + fail=1 + fi + + # Check if file is non-empty and does NOT end with a newline + if [ -s "$file" ] && [ -n "$(tail -c 1 "$file")" ]; then + echo "$file: Missing newline at end of file" + fail=1 + fi + ;; + esac +done + +exit $fail diff --git a/tools/run_driver_tests.py b/tools/run_driver_tests.py index c797ef5320..e3e58f2154 100755 --- a/tools/run_driver_tests.py +++ b/tools/run_driver_tests.py @@ -7,6 +7,7 @@ import argparse from pathlib import Path import junit_xml +import shutil VERBOSITY_TOTALS_ONLY = 0 VERBOSITY_TEST_STATUS_ONLY = 1 @@ -14,7 +15,7 @@ VERBOSITY_ALL_TEST_LOGS = 3 DRIVER_DIR = Path(os.path.abspath(__file__)).parents[1].joinpath("drivers") -LUACOV_CONFIG = DRIVER_DIR.parent.joinpath(".circleci", "config.luacov") +LUACOV_CONFIG = DRIVER_DIR.parent.joinpath("tools", "config.luacov") def find_affected_tests(working_dir, changed_files): affected_tests = [] @@ -29,13 +30,14 @@ def find_affected_tests(working_dir, changed_files): affected_tests = set(affected_tests) return affected_tests -def run_tests(verbosity_level, filter, junit, coverage_files): +def run_tests(verbosity_level, filter, junit, coverage_files, html): owd = os.getcwd() coverage_files = find_affected_tests(owd, coverage_files) failure_files = defaultdict(list) ts = [] total_tests = 0 total_passes = 0 + drivers_needing_html = {} for test_file in DRIVER_DIR.glob("*" + os.path.sep + "*" + os.path.sep + "src" + os.path.sep + "test" + os.path.sep + "test_*.lua"): if filter != None and re.search(filter, str(test_file)) is None: continue @@ -137,7 +139,29 @@ def run_tests(verbosity_level, filter, junit, coverage_files): test_suite.test_cases = test_cases ts.append(test_suite) if test_file in coverage_files: - subprocess.run("luacov -c={}".format(LUACOV_CONFIG), shell=True) + if html: + driver_name = test_file.parts[-4] + src_path = test_file.parents[1] + drivers_needing_html[driver_name] = src_path + else: + subprocess.run("luacov -c={}".format(LUACOV_CONFIG), shell=True) + + if drivers_needing_html: + coverage_html_dir = DRIVER_DIR.parent.joinpath("tools/coverage_output_html") + try: + os.mkdir(coverage_html_dir) + except FileExistsError: + pass + + for driver_name, src_path in drivers_needing_html.items(): + os.chdir(src_path) + subprocess.run("luacov -c {} --reporter html".format(LUACOV_CONFIG), shell=True) + html_source = Path("luacov.report.out") + html_dest = coverage_html_dir.joinpath(driver_name+"_luacov.report.html") + if html_source.exists(): + shutil.copy(html_source, html_dest) + else: + print(f"Warning: HTML coverage file for {driver_name} not found") total_test_info = "Total unit tests passes: {}/{}".format(total_passes, total_tests) print("#" * len(total_test_info)) @@ -165,6 +189,7 @@ def run_tests(verbosity_level, filter, junit, coverage_files): parser.add_argument("--filter", "-f", type=str, nargs="?", help="only run tests containing the filter value in the path") parser.add_argument("--junit", "-j", type=str, nargs="?", help="output test results in JUnit XML to the specified file") parser.add_argument("--coverage", "-c", nargs="*", help="run code tests with coverage (luacov must be installed) OPTIONAL: restrict files to run coverage tests for") + parser.add_argument("--html", action="store_true", help="Generate HTML coverage reports for the files specified by the coverage argument") args = parser.parse_args() verbosity_level = 0 if args.verbose: @@ -173,5 +198,4 @@ def run_tests(verbosity_level, filter, junit, coverage_files): verbosity_level = 2 elif args.superextraverbose: verbosity_level = 3 - run_tests(verbosity_level, args.filter, args.junit, args.coverage) - + run_tests(verbosity_level, args.filter, args.junit, args.coverage, args.html)