diff --git a/drivers/SmartThings/frient-smoke-siren/config.yml b/drivers/SmartThings/frient-smoke-siren/config.yml new file mode 100644 index 0000000000..34e0e1cedd --- /dev/null +++ b/drivers/SmartThings/frient-smoke-siren/config.yml @@ -0,0 +1,5 @@ +name: 'frient Water leak_Heat_Smoke_Siren' +description: 'frient EDGE driver for Intelligent smoke alarm, Intelligent heat alarm, Water leak detector and Smart Siren' +packageKey: 'frient-smoke-siren' +permissions: + zigbee: {} diff --git a/drivers/SmartThings/frient-smoke-siren/fingerprints.yml b/drivers/SmartThings/frient-smoke-siren/fingerprints.yml new file mode 100644 index 0000000000..dc116c608f --- /dev/null +++ b/drivers/SmartThings/frient-smoke-siren/fingerprints.yml @@ -0,0 +1,11 @@ +zigbeeManufacturer: + - id: "frient/SMSZB-120" + deviceLabel: Intelligent Smoke Alarm + manufacturer: frient A/S + model: SMSZB-120 + deviceProfileName: smoke-siren-temperature-battery + - id: "frient/SIRZB-110" + deviceLabel: Smart Siren + manufacturer: frient A/S + model: SIRZB-110 + deviceProfileName: siren-battery-source-tamper diff --git a/drivers/SmartThings/frient-smoke-siren/profiles/siren-battery-source-tamper.yml b/drivers/SmartThings/frient-smoke-siren/profiles/siren-battery-source-tamper.yml new file mode 100644 index 0000000000..1278c166ab --- /dev/null +++ b/drivers/SmartThings/frient-smoke-siren/profiles/siren-battery-source-tamper.yml @@ -0,0 +1,70 @@ +name: 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 +metadata: + vid: 656d48d0-0c37-3573-8ba5-03122a5b4c5c + mnmn: SmartThingsCommunity +preferences: + - title: "Alarm sound" + name: warningSound + description: "Type to sound to play when alarm is triggered" + required: false + preferenceType: enumeration + definition: + options: + BURGLAR: "Burglar" + FIRE: "Fire" + EMERGENCY: "Emergency" + POLICE_PANIC: "Panic" + FIRE_PANIC: "Panic Fire" + EMERGENCY_PANIC: "Panic Emergency" + default: "BURGLAR" + - title: "Alarm duration (s)" + name: warningDuration + description: "After how many seconds should the alarm turn off" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 240 + - title: "Find sensor sound" + name: squawkSound + description: "Squawk sound to play when beep tone is triggered" + required: false + preferenceType: enumeration + definition: + options: + SOUND_FOR_SYSTEM_IS_ARMED: "Armed" + SOUND_FOR_SYSTEM_IS_DISARMED: "Disarmed" + default: "SOUND_FOR_SYSTEM_IS_ARMED" + - title: "Volume" + name: warningLevel + description: "Alarm/Beep volume level" + required: false + preferenceType: enumeration + definition: + options: + LOW_LEVEL: "Low" + MEDIUM_LEVEL: "Medium" + HIGH_LEVEL: "High" + VERY_HIGH_LEVEL: "Very High" + default: "VERY_HIGH_LEVEL" + diff --git a/drivers/SmartThings/frient-smoke-siren/profiles/smoke-siren-temperature-battery.yml b/drivers/SmartThings/frient-smoke-siren/profiles/smoke-siren-temperature-battery.yml new file mode 100644 index 0000000000..1d774764e8 --- /dev/null +++ b/drivers/SmartThings/frient-smoke-siren/profiles/smoke-siren-temperature-battery.yml @@ -0,0 +1,42 @@ +name: smoke-siren-temperature-battery +components: +- id: main + capabilities: + - id: smokeDetector + version: 1 + - id: alarm + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmokeDetector +metadata: + vid: 45c81942-30bb-38b9-9e66-3343e2a8c330 + mnmn: SmartThingsCommunity +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: "Alarm duration (s)" + name: warningDuration + description: "After how many seconds should the alarm turn off" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 240 diff --git a/drivers/SmartThings/frient-smoke-siren/src/device_base_functions.lua b/drivers/SmartThings/frient-smoke-siren/src/device_base_functions.lua new file mode 100644 index 0000000000..c16bec6065 --- /dev/null +++ b/drivers/SmartThings/frient-smoke-siren/src/device_base_functions.lua @@ -0,0 +1,101 @@ +local capabilities = require "st.capabilities" +local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local cluster_base = require "st.zigbee.cluster_base" +local zcl_clusters = require "st.zigbee.zcl.clusters" +local log = require "log" + +local PowerConfiguration = zcl_clusters.PowerConfiguration +local IASZone = zcl_clusters.IASZone +local IASWD = zcl_clusters.IASWD +local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement +local Basic = zcl_clusters.Basic + +local FRIENT_DEVICE_FINGERPRINTS = require "device_config" + +local BASE_FUNCTIONS = {} + +-- Constants +BASE_FUNCTIONS.DEVELCO_MANUFACTURER_CODE = 0x1015 +BASE_FUNCTIONS.DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR = 0x8000 + +BASE_FUNCTIONS.SIREN_ENDIAN = "siren_endian" +BASE_FUNCTIONS.PRIMARY_SW_VERSION = "primary_sw_version" + +BASE_FUNCTIONS.ALARM_COMMAND = "alarmCommand" +BASE_FUNCTIONS.ALARM_LAST_DURATION = "lastDuration" +BASE_FUNCTIONS.ALARM_MAX_DURATION = "maxDuration" + +BASE_FUNCTIONS.ALARM_DEFAULT_MAX_DURATION = 240 + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +function BASE_FUNCTIONS.added(driver, device) + for _, fingerprint in ipairs(FRIENT_DEVICE_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + if device:supports_capability(capabilities.tamperAlert) then + device:emit_event(capabilities.tamperAlert.tamper.clear()) + end + if device:supports_capability(capabilities.smokeDetector) then + device:emit_event(capabilities.smokeDetector.smoke.clear()) + end + if device:supports_capability(capabilities.temperatureAlarm) then + device:emit_event(capabilities.temperatureAlarm.temperatureAlarm.cleared()) + end + if device:supports_capability(capabilities.waterSensor) then + device:emit_event(capabilities.waterSensor.water.dry()) + end + if device:supports_capability(capabilities.switch) then + device:emit_event(capabilities.switch.switch.off()) + end + if device:supports_capability(capabilities.alarm) then + device:emit_event(capabilities.alarm.alarm.off()) + device:set_field(BASE_FUNCTIONS.ALARM_MAX_DURATION, BASE_FUNCTIONS.ALARM_DEFAULT_MAX_DURATION, { persist = true }) + end + + device:send(cluster_base.read_manufacturer_specific_attribute(device, Basic.ID, BASE_FUNCTIONS.DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, BASE_FUNCTIONS.DEVELCO_MANUFACTURER_CODE)) -- Read the firmware version + end + end +end + +function BASE_FUNCTIONS.init(driver, device) + for _, fingerprint in ipairs(FRIENT_DEVICE_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + if device:supports_capability(capabilities.battery) then + battery_defaults.build_linear_voltage_init(2.3, 3.0)(driver, device) + end + end + end +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +function BASE_FUNCTIONS.do_refresh(driver, device) + device:refresh() + + -- Check if we have the software version + local sw_version = device:get_field(BASE_FUNCTIONS.PRIMARY_SW_VERSION) + if ((sw_version == nil) or (sw_version == "")) then + log.warn("Refresh: Firmware version not detected, checking software version") + device:send(cluster_base.read_manufacturer_specific_attribute(device, Basic.ID, BASE_FUNCTIONS.DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, BASE_FUNCTIONS.DEVELCO_MANUFACTURER_CODE)) + else + log.trace("Refresh: Firmware version: 0x" .. sw_version) + end +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param event string The lifecycle event name +--- @param args table Table containing information relevant to the lifecycle event +function BASE_FUNCTIONS.do_configure(driver, device, event, args) + device:configure() + for _, fingerprint in ipairs(FRIENT_DEVICE_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + if fingerprint.ENDPOINT_SIREN then + device:set_field(BASE_FUNCTIONS.ALARM_MAX_DURATION, device.preferences.warningDuration == nil and BASE_FUNCTIONS.ALARM_DEFAULT_MAX_DURATION or device.preferences.warningDuration, { persist = true }) + device:send(IASWD.attributes.MaxDuration:write(device, device.preferences.warningDuration == nil and BASE_FUNCTIONS.ALARM_DEFAULT_MAX_DURATION or device.preferences.warningDuration):to_endpoint(fingerprint.ENDPOINT_SIREN)) + end + end + end +end + +return BASE_FUNCTIONS diff --git a/drivers/SmartThings/frient-smoke-siren/src/device_config.lua b/drivers/SmartThings/frient-smoke-siren/src/device_config.lua new file mode 100644 index 0000000000..8272ff5bbe --- /dev/null +++ b/drivers/SmartThings/frient-smoke-siren/src/device_config.lua @@ -0,0 +1,7 @@ +local FRIENT_DEVICE_FINGERPRINTS = { + { mfr = "frient A/S", model = "SMSZB-120", subdriver = "smoke", ENDPOINT_SIREN = 0x23, ENDPOINT_TEMPERATURE = 0x26, + ENDPOINT_TAMPER = 0x23 }, -- Siren, Temperature, Smoke + { mfr = "frient A/S", model = "SIRZB-110", subdriver = "siren", ENDPOINT_SIREN = 0x2B, ENDPOINT_TAMPER = 0x2B } -- Siren, Tamper +} + +return FRIENT_DEVICE_FINGERPRINTS diff --git a/drivers/SmartThings/frient-smoke-siren/src/frient-siren/init.lua b/drivers/SmartThings/frient-smoke-siren/src/frient-siren/init.lua new file mode 100644 index 0000000000..6d83e787ca --- /dev/null +++ b/drivers/SmartThings/frient-smoke-siren/src/frient-siren/init.lua @@ -0,0 +1,100 @@ +local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local zcl_clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local log = require "log" + +local IASZone = zcl_clusters.IASZone +local Basic = zcl_clusters.Basic + +local FRIENT_DEVICE_FINGERPRINTS = require "device_config" +local BASE_FUNCTIONS = require "device_base_functions" + +local SIREN_FIXED_ENDIAN_SW_VERSION = "010903" + +--- @param opts table A table containing optional arguments that can be used to determine if something is handleable +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +local function can_handle_frient(opts, driver, device, ...) + for _, fingerprint in ipairs(FRIENT_DEVICE_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model and fingerprint.subdriver == "siren" then + return true + end + end + return false +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +local function device_init(driver, device) + --battery_defaults.build_linear_voltage_init(3.3, 4.0)(driver, device) -- Update the battery threholds for this device (the device never reaches 4.1) +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param zone_status st.zigbee.zcl.types.IasZoneStatus 2 byte bitmap zoneStatus attribute value of the IAS Zone cluster +--- @param zigbee_message st.zigbee.ZigbeeMessageRx the full message this report came in +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_tamper_set() and capabilities.tamperAlert.tamper.detected() or capabilities.tamperAlert.tamper.clear() + ) + 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 + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param zone_status st.zigbee.zcl.types.IasZoneStatus the value of the attribute +--- @param zb_rx st.zigbee.ZigbeeMessageRx the full message this report came in +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 + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param zb_rx st.zigbee.ZigbeeMessageRx the full message this report came in +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 + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param value st.zigbee.data_types.StringABC the value of the Attribute +--- @param zb_rx st.zigbee.ZigbeeMessageRx the full message this report came in +local function primary_sw_version_attr_handler(driver, device, value, zb_rx) + --log.warn("Siren Primary Software Version Attribute report: 0x"..string.format("%x", zb_rx.body.zcl_body.attr_records[1].attr_id.value).."=0x"..value.value) + local primary_sw_version = value.value:gsub('.', function(c) return string.format('%02x', string.byte(c)) end) + log.debug("Siren Primary Software Version firmware: 0x" .. primary_sw_version) + device:set_field(BASE_FUNCTIONS.PRIMARY_SW_VERSION, primary_sw_version, { persist = true }) + if (primary_sw_version < SIREN_FIXED_ENDIAN_SW_VERSION) then + log.warn("Device has reverse Siren endian firmware") + device:set_field(BASE_FUNCTIONS.SIREN_ENDIAN, "reverse", { persist = true }) + end +end + +local frient_siren = { + NAME = "frient Siren", + lifecycle_handlers = { + init = device_init, + }, + zigbee_handlers = { + cluster = { + [IASZone.ID] = { + [IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler + } + }, + attr = { + [Basic.ID] = { + [BASE_FUNCTIONS.DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR] = primary_sw_version_attr_handler, + }, + [IASZone.ID] = { + [IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler + } + } + }, + can_handle = can_handle_frient +} + +return frient_siren diff --git a/drivers/SmartThings/frient-smoke-siren/src/frient-smoke/init.lua b/drivers/SmartThings/frient-smoke-siren/src/frient-smoke/init.lua new file mode 100644 index 0000000000..b08caa6b25 --- /dev/null +++ b/drivers/SmartThings/frient-smoke-siren/src/frient-smoke/init.lua @@ -0,0 +1,97 @@ +local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local zcl_clusters = require "st.zigbee.zcl.clusters" +local device_management = require "st.zigbee.device_management" +local capabilities = require "st.capabilities" +local util = require "st.utils" +local log = require "log" + +local IASZone = zcl_clusters.IASZone +local Basic = zcl_clusters.Basic + +local FRIENT_DEVICE_FINGERPRINTS = require "device_config" +local BASE_FUNCTIONS = require "device_base_functions" + +local SMOKE_FIXED_ENDIAN_SW_VERSION = "040005" + +--- @param opts table A table containing optional arguments that can be used to determine if something is handleable +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +local function can_handle_frient(opts, driver, device, ...) + for _, fingerprint in ipairs(FRIENT_DEVICE_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model and fingerprint.subdriver == "smoke" then + return true + end + end + return false +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +local function device_init(driver, device) + battery_defaults.build_linear_voltage_init(2.3, 3.0)(driver, device) +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param zone_status st.zigbee.zcl.types.IasZoneStatus 2 byte bitmap zoneStatus attribute value of the IAS Zone cluster +--- @param zigbee_message st.zigbee.ZigbeeMessageRx the full message this report came in +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_alarm1_set() and capabilities.smokeDetector.smoke.detected() or zone_status:is_test_set() and capabilities.smokeDetector.smoke.tested() or capabilities.smokeDetector.smoke.clear()) +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param zone_status st.zigbee.zcl.types.IasZoneStatus the value of the attribute +--- @param zb_rx st.zigbee.ZigbeeMessageRx the full message this report came in +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 + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param zb_rx st.zigbee.ZigbeeMessageRx the full message this report came in +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 + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param value st.zigbee.data_types.StringABC the value of the Attribute +--- @param zb_rx st.zigbee.ZigbeeMessageRx the full message this report came in +local function primary_sw_version_attr_handler(driver, device, value, zb_rx) + --log.warn("Siren Primary Software Version Attribute report: 0x"..string.format("%x", zb_rx.body.zcl_body.attr_records[1].attr_id.value).."=0x"..value.value) + local primary_sw_version = value.value:gsub('.', function(c) return string.format('%02x', string.byte(c)) end) + log.debug("Smoke Primary Software Version firmware: 0x" .. primary_sw_version) + device:set_field(BASE_FUNCTIONS.PRIMARY_SW_VERSION, primary_sw_version, { persist = true }) + if (primary_sw_version < SMOKE_FIXED_ENDIAN_SW_VERSION) then + log.warn("Device has reverse Siren endian firmware") + device:set_field(BASE_FUNCTIONS.SIREN_ENDIAN, "reverse", { persist = true }) + end +end + +local frient_smoke = { + NAME = "frient Smoke Detector", + lifecycle_handlers = { + init = device_init, + }, + zigbee_handlers = { + cluster = { + [IASZone.ID] = { + [IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler + } + }, + attr = { + [Basic.ID] = { + [BASE_FUNCTIONS.DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR] = primary_sw_version_attr_handler, + }, + [IASZone.ID] = { + [IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler + } + } + }, + can_handle = can_handle_frient +} + +return frient_smoke diff --git a/drivers/SmartThings/frient-smoke-siren/src/init.lua b/drivers/SmartThings/frient-smoke-siren/src/init.lua new file mode 100644 index 0000000000..7455cd3129 --- /dev/null +++ b/drivers/SmartThings/frient-smoke-siren/src/init.lua @@ -0,0 +1,334 @@ +local ZigbeeDriver = require "st.zigbee" +local defaults = require "st.zigbee.defaults" +local constants = require "st.zigbee.constants" +local data_types = require "st.zigbee.data_types" +local zcl_global_commands = require "st.zigbee.zcl.global_commands" +local log = require "log" +local cluster_base = require "st.zigbee.cluster_base" + +local zcl_clusters = require "st.zigbee.zcl.clusters" +local Status = require "st.zigbee.generated.types.ZclStatus" +local IASZone = zcl_clusters.IASZone +local IASWD = zcl_clusters.IASWD +local SirenConfiguration = IASWD.types.SirenConfiguration +local SquawkConfiguration = IASWD.types.SquawkConfiguration +local WarningMode = IASWD.types.WarningMode +local Strobe = IASWD.types.Strobe +local SquawkMode = IASWD.types.SquawkMode +local IaswdLevel = IASWD.types.IaswdLevel +local Basic = zcl_clusters.Basic + +local capabilities = require "st.capabilities" + +local BASE_FUNCTIONS = require "device_base_functions" + +log.trace("Initializing frient driver") + +-- Constants +local ALARM_STROBE_DUTY_CYCLE = 40 +local ALARM_STROBE_NO_DUTY_CYCLE = 0 + +local alarm_command = { + OFF = 0, + SIREN = 1, + STROBE = 2, + BOTH = 3 +} + +--- @param device st.zigbee.Device The device this message was received from containing identifying information +local function emit_alarm_event(device, cmd) + --log.trace("Updating alarm state:"..cmd) + if cmd == alarm_command.OFF then + device:emit_event(capabilities.alarm.alarm.off()) + device:emit_event(capabilities.switch.switch.off()) + else + if cmd == alarm_command.SIREN then + device:emit_event(capabilities.alarm.alarm.siren()) + elseif cmd == alarm_command.STROBE then + device:emit_event(capabilities.alarm.alarm.strobe()) + else + device:emit_event(capabilities.alarm.alarm.both()) + end + device:emit_event(capabilities.switch.switch.on()) + end +end + +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param warning_mode st.zigbee.zcl.clusters.IASWD.types.WarningMode Warning mode for siren +--- @param warning_siren_level st.zigbee.zcl.clusters.IASWD.types.IaswdLevel Siren level +--- @param strobe_level st.zigbee.zcl.clusters.IASWD.types.IaswdLevel Strobe level +local function send_siren_command(device, warning_mode, warning_siren_level, strobe_active, strobe_level) + -- Check if we have the software version first + local sw_version = device:get_field(BASE_FUNCTIONS.PRIMARY_SW_VERSION) + if ((sw_version == nil) or (sw_version == "")) then + log.warn("Siren: Firmware version not detected, checking software version") + device:send(cluster_base.read_manufacturer_specific_attribute(device, Basic.ID, BASE_FUNCTIONS.DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, BASE_FUNCTIONS.DEVELCO_MANUFACTURER_CODE)) + end + + local max_duration = device:get_field(BASE_FUNCTIONS.ALARM_MAX_DURATION) + local warning_duration = max_duration and max_duration or BASE_FUNCTIONS.ALARM_DEFAULT_MAX_DURATION + local duty_cycle = (strobe_active == Strobe.USE_STROBE) and ALARM_STROBE_DUTY_CYCLE or ALARM_STROBE_NO_DUTY_CYCLE + + device:set_field(BASE_FUNCTIONS.ALARM_LAST_DURATION, warning_duration, {persist = true}) + + local siren_configuration + + if (device:get_field(BASE_FUNCTIONS.SIREN_ENDIAN) == "reverse") then + -- Old frient firmware, the endian format is reversed + log.warn("Reverse endian format detected") + local siren_config_value = (warning_siren_level << 6) | (strobe_active << 4) | warning_mode + siren_configuration = SirenConfiguration(siren_config_value) + else + siren_configuration = SirenConfiguration(0x00) + siren_configuration:set_warning_mode(warning_mode) + siren_configuration:set_strobe(strobe_active) + 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(duty_cycle), + data_types.Enum8(strobe_level) + ) + ) +end + +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param squawk_mode st.zigbee.zcl.clusters.IASWD.types.SquawkMode Warning mode for siren +--- @param squawk_siren_level st.zigbee.zcl.clusters.IASWD.types.IaswdLevel Siren level +--- @param strobe_active st.zigbee.zcl.clusters.IASWD.types.Strobe Strobe level +local function send_squawk_command(device, squawk_mode, squawk_siren_level, strobe_active) + -- Check if we have the software version first + local sw_version = device:get_field(BASE_FUNCTIONS.PRIMARY_SW_VERSION) + if ((sw_version == nil) or (sw_version == "")) then + log.warn("Squawk: Firmware version not detected, checking software version") + device:send(cluster_base.read_manufacturer_specific_attribute(device, Basic.ID, BASE_FUNCTIONS.DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, BASE_FUNCTIONS.DEVELCO_MANUFACTURER_CODE)) + end + + local squawk_configuration + + if (device:get_field(BASE_FUNCTIONS.SIREN_ENDIAN) == "reverse") then + -- Old frient firmware, the endian format is reversed + log.warn("Reverse endian format detected") + local squawk_config_value = (squawk_siren_level << 6) | (strobe_active << 4) | squawk_mode + squawk_configuration = SquawkConfiguration(squawk_config_value) + else + squawk_configuration = SquawkConfiguration(0x00) + squawk_configuration:set_squawk_mode(squawk_mode) + squawk_configuration:set_squawk_strobe_active(strobe_active) + squawk_configuration:set_squawk_level(squawk_siren_level) + end + + device:send( + IASWD.server.commands.Squawk( + device, + squawk_configuration + ) + ) +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param zb_rx st.zigbee.ZigbeeMessageRx the full message this report came in +local function default_response_handler(driver, device, zb_rx) + local is_success = zb_rx.body.zcl_body.status.value + local command = zb_rx.body.zcl_body.cmd.value + local alarm_ev = device:get_field(BASE_FUNCTIONS.ALARM_COMMAND) + + if command == IASWD.server.commands.StartWarning.ID and is_success == Status.SUCCESS then + if alarm_ev ~= alarm_command.OFF then + emit_alarm_event(device, alarm_ev) + local lastDuration = device:get_field(BASE_FUNCTIONS.ALARM_LAST_DURATION) + device.thread:call_with_delay(lastDuration, function(d) + device:emit_event(capabilities.alarm.alarm.off()) + device:emit_event(capabilities.switch.switch.off()) + end) + else + emit_alarm_event(device,alarm_command.OFF) + end + end +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param max_duration st.zigbee.data_types.Uint8 the value of the attribute +local function attr_max_duration_handler(driver, device, max_duration) + device:set_field(BASE_FUNCTIONS.ALARM_MAX_DURATION, max_duration.value, {persist = true}) +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param command string Command parameters if applicable +local function siren_switch_both_handler(driver, device, command) + log.debug("Starting Siren + Strobe") + device:set_field(BASE_FUNCTIONS.ALARM_COMMAND, alarm_command.BOTH, {persist = true}) + send_siren_command(device, device.preferences.warningSound == nil and WarningMode.BURGLAR or WarningMode[device.preferences.warningSound], device.preferences.warningLevel == nil and IaswdLevel.VERY_HIGH_LEVEL or IaswdLevel[device.preferences.warningLevel], Strobe.USE_STROBE, IaswdLevel.VERY_HIGH_LEVEL) +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param command string Command parameters if applicable +local function siren_alarm_siren_handler(driver, device, command) + log.debug("Starting Siren") + device:set_field(BASE_FUNCTIONS.ALARM_COMMAND, alarm_command.SIREN, {persist = true}) + send_siren_command(device, device.preferences.warningSound == nil and WarningMode.BURGLAR or WarningMode[device.preferences.warningSound], device.preferences.warningLevel == nil and IaswdLevel.VERY_HIGH_LEVEL or IaswdLevel[device.preferences.warningLevel], Strobe.NO_STROBE, IaswdLevel.LOW_LEVEL) +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param command string Command parameters if applicable +local function siren_alarm_strobe_handler(driver, device, command) + log.debug("Starting Strobe") + device:set_field(BASE_FUNCTIONS.ALARM_COMMAND, alarm_command.STROBE, {persist = true}) + send_siren_command(device, WarningMode.STOP, IaswdLevel.LOW_LEVEL, Strobe.USE_STROBE, IaswdLevel.VERY_HIGH_LEVEL) +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param command string Command parameters if applicable +local function siren_tone_beep_handler(driver, device, command) + log.debug("Starting Squawk") + send_squawk_command(device, device.preferences.squawkSound == nil and SquawkMode.SOUND_FOR_SYSTEM_IS_ARMED or SquawkMode[device.preferences.squawkSound], device.preferences.warningLevel == nil and IaswdLevel.VERY_HIGH_LEVEL or IaswdLevel[device.preferences.warningLevel], Strobe.NO_STROBE) +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param command string Command parameters if applicable +local function siren_switch_on_handler(driver, device, command) + log.debug("Starting Switch On") + siren_switch_both_handler(driver, device, command) +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param command string Command parameters if applicable +local function siren_switch_off_handler(driver, device, command) + device:set_field(BASE_FUNCTIONS.ALARM_COMMAND, alarm_command.OFF, {persist = true}) + log.debug("Starting Switch Off") + send_siren_command(device, WarningMode.STOP, IaswdLevel.LOW_LEVEL, Strobe.NO_STROBE, IaswdLevel.LOW_LEVEL) +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param value st.zigbee.data_types.StringABC the value of the Attribute +--- @param zb_rx st.zigbee.ZigbeeMessageRx the full message this report came in +local function primary_sw_version_attr_handler(driver, device, value, zb_rx) + --log.warn("Manufacturer Primary Software Version Attribute report: 0x"..string.format("%x", zb_rx.body.zcl_body.attr_records[1].attr_id.value).."=0x"..value.value) + local primary_sw_version = value.value:gsub('.', function (c) return string.format('%02x', string.byte(c)) end) + log.debug("Manufacturer Primary Software Version firmware: 0x"..primary_sw_version) + device:set_field(BASE_FUNCTIONS.PRIMARY_SW_VERSION, primary_sw_version, {persist = true}) +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +local function do_refresh(driver, device) + BASE_FUNCTIONS.do_refresh(driver, device) +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +--- @param event string The lifecycle event name +--- @param args table Table containing information relevant to the lifecycle event +local function do_configure(driver, device, event, args) + log.trace("Configuring device:"..event)--..", "..util.stringify_table(args, nil, true)) + if ((event == "doConfigure") or (args and args.old_st_store)) then -- Only if we got a parameter update then reinitialize, infoChanged could be called periodically also + BASE_FUNCTIONS.do_configure(driver, device, event, args) + end + + device.thread:call_with_delay(5, function() + do_refresh(driver, device) + end) +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +local function device_added(driver, device) + log.trace "Added device" + BASE_FUNCTIONS.added(driver, device) +end + +--- @param driver ZigbeeDriver The current driver running containing necessary context for execution +--- @param device st.zigbee.Device The device this message was received from containing identifying information +local function device_init(driver, device) + log.trace "Initializing device" + BASE_FUNCTIONS.init(driver, device) +end + +local zigbee_smoke_siren_driver_template = { + NAME = "frient Smoke Siren driver", + supported_capabilities = { + capabilities.alarm, + capabilities.waterSensor, + capabilities.tone, + capabilities.switch, + capabilities.smokeDetector, + capabilities.temperatureAlarm, + capabilities.temperatureMeasurement, + capabilities.battery, + capabilities.tamperAlert, + capabilities.powerSource, + }, + sub_drivers = { + require("frient-smoke"), + require("frient-siren") + }, + ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, + lifecycle_handlers = { + added = device_added, + init = device_init, + doConfigure = do_configure, + infoChanged = do_configure, + }, + zigbee_handlers = { + global = { + [IASWD.ID] = { + [zcl_global_commands.DEFAULT_RESPONSE_ID] = default_response_handler + } + }, + attr = { + [Basic.ID] = { + [BASE_FUNCTIONS.DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR] = primary_sw_version_attr_handler, + }, + [IASWD.ID] = { + [IASWD.attributes.MaxDuration.ID] = attr_max_duration_handler + } + } + }, + capability_handlers = { + [capabilities.alarm.ID] = { + [capabilities.alarm.commands.both.NAME] = siren_switch_both_handler, + [capabilities.alarm.commands.off.NAME] = siren_switch_off_handler, + [capabilities.alarm.commands.siren.NAME] = siren_alarm_siren_handler, + [capabilities.alarm.commands.strobe.NAME] = siren_alarm_strobe_handler + }, + [capabilities.tone.ID] = { + [capabilities.tone.commands.beep.NAME] = siren_tone_beep_handler, + }, + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = siren_switch_on_handler, + [capabilities.switch.commands.off.NAME] = siren_switch_off_handler + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh, + }, + }, + cluster_configurations = { + [capabilities.alarm.ID] = { + { + cluster = IASZone.ID, + attribute = IASZone.attributes.ZoneStatus.ID, + minimum_interval = 0, + maximum_interval = 180, + data_type = IASZone.attributes.ZoneStatus.base_type + } + } + }, +} + +defaults.register_for_default_handlers(zigbee_smoke_siren_driver_template, zigbee_smoke_siren_driver_template.supported_capabilities) +local zigbee_smoke_siren_driver = ZigbeeDriver("frient-smoke-siren-detector", zigbee_smoke_siren_driver_template) +zigbee_smoke_siren_driver:run() + diff --git a/drivers/SmartThings/frient-smoke-siren/src/test/test_frient_siren.lua b/drivers/SmartThings/frient-smoke-siren/src/test/test_frient_siren.lua new file mode 100644 index 0000000000..8d1048ad99 --- /dev/null +++ b/drivers/SmartThings/frient-smoke-siren/src/test/test_frient_siren.lua @@ -0,0 +1,354 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT 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 t_utils = require "integration_test.utils" + +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" +local clusters = require "st.zigbee.zcl.clusters" +local Basic = clusters.Basic +local IASZone = clusters.IASZone +local IASWD = clusters.IASWD +local PowerConfiguration = clusters.PowerConfiguration +local SirenConfiguration = require "st.zigbee.generated.zcl_clusters.IASWD.types.SirenConfiguration" +local SquawkConfiguration = require "st.zigbee.generated.zcl_clusters.IASWD.types.SquawkConfiguration" + +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" + +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" +local base64 = require "st.base64" + +local PRIMARY_SW_VERSION_ATTRIBUTE_ID = 0x8000 +local MFG_CODE = 0x1015 + +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("siren-battery-source-tamper.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "SIRZB-110", + server_clusters = { 0x0005, 0x0006 } + }, + [0x2B] = { + id = 0x2B, + server_clusters = { 0x0000, 0x0001, 0x0003, 0x0004, 0x0500, 0x0502, 0xFC05 } + } + } + } +) + +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( + "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.tamperAlert.tamper.clear()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.alarm.alarm.off()) + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Basic.ID, PRIMARY_SW_VERSION_ATTRIBUTE_ID, + MFG_CODE, data_types.OctetString) + } + ) + end +) + +test.register_coroutine_test( + "Handle doConfigure lifecycle", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxd") + + 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, 180, 0) + }) + 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 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write(mock_device, 0x00F0) + }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "Refresh Capability Command should refresh device", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "refresh", command = "refresh", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, IASZone.attributes.ZoneStatus:read(mock_device) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, zigbee_test_utils.build_attribute_read(mock_device, Basic.ID, { PRIMARY_SW_VERSION_ATTRIBUTE_ID }, MFG_CODE):to_endpoint(0x2B) } + } + } +) + +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, zigbee_test_utils.build_attribute_read(mock_device, Basic.ID, { PRIMARY_SW_VERSION_ATTRIBUTE_ID }, MFG_CODE):to_endpoint(0x2B) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, + SirenConfiguration(0x17), + data_types.Uint16(0x00F0), + data_types.Uint8(0x28), + data_types.Enum8(0x03)) } + } + } +) + +test.register_message_test( + "Capability(alarm) command(off) on should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "alarm", component = "main", command = "off", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, zigbee_test_utils.build_attribute_read(mock_device, Basic.ID, { PRIMARY_SW_VERSION_ATTRIBUTE_ID }, MFG_CODE):to_endpoint(0x2B) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, + SirenConfiguration(00), + data_types.Uint16(0x00F0), + data_types.Uint8(00), + data_types.Enum8(00)) } + } + } +) + +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, zigbee_test_utils.build_attribute_read(mock_device, Basic.ID, { PRIMARY_SW_VERSION_ATTRIBUTE_ID }, MFG_CODE):to_endpoint(0x2B) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, + SirenConfiguration(0x13), + data_types.Uint16(0x00F0), + data_types.Uint8(00), + data_types.Enum8(00)) } + } + } +) + +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, zigbee_test_utils.build_attribute_read(mock_device, Basic.ID, { PRIMARY_SW_VERSION_ATTRIBUTE_ID }, MFG_CODE):to_endpoint(0x2B) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, + SirenConfiguration(0x04), + data_types.Uint16(0x00F0), + data_types.Uint8(0x28), + data_types.Enum8(0x03)) } + } + } +) + +test.register_message_test( + "Capability(tone) command(beep) on should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "tone", component = "main", command = "beep", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, zigbee_test_utils.build_attribute_read(mock_device, Basic.ID, { PRIMARY_SW_VERSION_ATTRIBUTE_ID }, MFG_CODE):to_endpoint(0x2B) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, IASWD.server.commands.Squawk(mock_device, + SquawkConfiguration(0x0B)) } + } + } +) + +test.register_message_test( + "Battery percentage remaining report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 30) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(15)) + } + } +) + +test.register_coroutine_test( + "Setting a max duration should be handled", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive({ mock_device.id, IASWD.attributes.MaxDuration:build_test_attr_report(mock_device, 50) }) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.capability:__queue_receive({ mock_device.id, { capability = "alarm", component = "main", command = "siren", args = { } } }) + + test.socket.zigbee:__expect_send({ mock_device.id, + zigbee_test_utils.build_attribute_read(mock_device, Basic.ID, { PRIMARY_SW_VERSION_ATTRIBUTE_ID }, MFG_CODE) + :to_endpoint(0x2B) }) + test.socket.zigbee:__expect_send({ mock_device.id, IASWD.server.commands.StartWarning(mock_device, + SirenConfiguration(0x13), + data_types.Uint16(0x0032), + data_types.Uint8(0), + data_types.Enum8(0)) }) + end +) + +test.register_coroutine_test( + "Health check should check all relevant attributes", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + 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.BatteryPercentageRemaining:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + 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.alarm.alarm.off()) + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Basic.ID, PRIMARY_SW_VERSION_ATTRIBUTE_ID, + MFG_CODE, data_types.OctetString) + } + ) + 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.run_registered_tests() diff --git a/drivers/SmartThings/frient-smoke-siren/src/test/test_frient_smoke_detector.lua b/drivers/SmartThings/frient-smoke-siren/src/test/test_frient_smoke_detector.lua new file mode 100644 index 0000000000..24418b63ae --- /dev/null +++ b/drivers/SmartThings/frient-smoke-siren/src/test/test_frient_smoke_detector.lua @@ -0,0 +1,203 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT 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 t_utils = require "integration_test.utils" + +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" +local clusters = require "st.zigbee.zcl.clusters" +local Basic = clusters.Basic +local IASZone = clusters.IASZone +local IASWD = clusters.IASWD +local PowerConfiguration = clusters.PowerConfiguration +local TemperatureMeasurement = clusters.TemperatureMeasurement + +local ZoneStatusAttribute = IASZone.attributes.ZoneStatus + +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" + +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" +local base64 = require "st.base64" + +local PRIMARY_SW_VERSION_ATTRIBUTE_ID = 0x8000 +local MFG_CODE = 0x1015 + +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("smoke-siren-temperature-battery.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "SMSZB-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) + zigbee_test_utils.init_noop_health_check_timer() +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.smokeDetector.smoke.clear()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.alarm.alarm.off()) + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Basic.ID, PRIMARY_SW_VERSION_ATTRIBUTE_ID, + MFG_CODE, data_types.OctetString) + } + ) + end +) + +test.register_coroutine_test( + "Handle doConfigure lifecycle", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxd") + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID, 0x23) + :to_endpoint(0x23) + }) + 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, IASZone.ID, 0x23) + :to_endpoint(0x23) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting(mock_device, 0, 180, 0) + }) + 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 + ) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, TemperatureMeasurement.ID, 0x26) + :to_endpoint(0x26) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 30, 300, 16) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write(mock_device, 0x00F0) + }) + 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, 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.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.mock_time.advance_time(50000) -- battery is 21600 for max reporting interval + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.smokeDetector.smoke.clear()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.alarm.alarm.off()) + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Basic.ID, PRIMARY_SW_VERSION_ATTRIBUTE_ID, + MFG_CODE, data_types.OctetString) + } + ) + 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, + TemperatureMeasurement.attributes.MeasuredValue: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.run_registered_tests()