diff --git a/drivers/SmartThings/matter-lock/profiles/lock-unlatch.yml b/drivers/SmartThings/matter-lock/profiles/lock-unlatch.yml new file mode 100644 index 0000000000..f479990ddf --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-unlatch.yml @@ -0,0 +1,93 @@ +name: lock-unlatch +components: +- label: Main + id: main + capabilities: + - id: lock + version: 1 + config: + values: + - key: "lock.value" + enabledValues: + - locked + - unlocked + - unlatched + - not fully locked + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - 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 + 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 + 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}}' + 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}}' \ No newline at end of file diff --git a/drivers/SmartThings/matter-lock/profiles/lock-user-pin-battery.yml b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-battery.yml new file mode 100644 index 0000000000..2401c4119e --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-battery.yml @@ -0,0 +1,29 @@ +name: lock-user-pin-battery +components: +- id: main + capabilities: + - id: lock + version: 1 + config: + values: + - key: "lock.value" + enabledValues: + - locked + - unlocked + - not fully locked + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + - id: lockCredentials + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock diff --git a/drivers/SmartThings/matter-lock/profiles/lock-user-pin-batteryLevel.yml b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-batteryLevel.yml new file mode 100644 index 0000000000..582fcd3975 --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-batteryLevel.yml @@ -0,0 +1,29 @@ +name: lock-user-pin-batteryLevel +components: +- id: main + capabilities: + - id: lock + version: 1 + config: + values: + - key: "lock.value" + enabledValues: + - locked + - unlocked + - not fully locked + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + - id: lockCredentials + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock diff --git a/drivers/SmartThings/matter-lock/profiles/lock-user-pin-schedule-battery.yml b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-schedule-battery.yml new file mode 100644 index 0000000000..ac52c839fa --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-schedule-battery.yml @@ -0,0 +1,31 @@ +name: lock-user-pin-schedule-battery +components: +- id: main + capabilities: + - id: lock + version: 1 + config: + values: + - key: "lock.value" + enabledValues: + - locked + - unlocked + - not fully locked + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + - id: lockCredentials + version: 1 + - id: lockSchedules + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock diff --git a/drivers/SmartThings/matter-lock/profiles/lock-user-pin-schedule-batteryLevel.yml b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-schedule-batteryLevel.yml new file mode 100644 index 0000000000..d1b8ce8a13 --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-schedule-batteryLevel.yml @@ -0,0 +1,31 @@ +name: lock-user-pin-schedule-batteryLevel +components: +- id: main + capabilities: + - id: lock + version: 1 + config: + values: + - key: "lock.value" + enabledValues: + - locked + - unlocked + - not fully locked + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + - id: lockCredentials + version: 1 + - id: lockSchedules + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock diff --git a/drivers/SmartThings/matter-lock/profiles/lock-user-pin-schedule-unlatch.yml b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-schedule-unlatch.yml new file mode 100644 index 0000000000..7ec2fa7518 --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-schedule-unlatch.yml @@ -0,0 +1,99 @@ +name: lock-user-pin-schedule-unlatch +components: +- label: Main + id: main + capabilities: + - id: lock + version: 1 + config: + values: + - key: "lock.value" + enabledValues: + - locked + - unlocked + - unlatched + - not fully locked + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + - id: lockCredentials + version: 1 + - id: lockSchedules + version: 1 + - 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 + 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 + 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}}' + 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}}' \ No newline at end of file diff --git a/drivers/SmartThings/matter-lock/profiles/lock-user-pin-unlatch.yml b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-unlatch.yml new file mode 100644 index 0000000000..e27a6bc0f4 --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-user-pin-unlatch.yml @@ -0,0 +1,97 @@ +name: lock-user-pin-unlatch +components: +- label: Main + id: main + capabilities: + - id: lock + version: 1 + config: + values: + - key: "lock.value" + enabledValues: + - locked + - unlocked + - unlatched + - not fully locked + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + - id: lockCredentials + version: 1 + - 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 + 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 + 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}}' + 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}}' \ No newline at end of file diff --git a/drivers/SmartThings/matter-lock/profiles/lock-user-schedule-unlatch.yml b/drivers/SmartThings/matter-lock/profiles/lock-user-schedule-unlatch.yml new file mode 100644 index 0000000000..10669ccaeb --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-user-schedule-unlatch.yml @@ -0,0 +1,97 @@ +name: lock-user-schedule-unlatch +components: +- label: Main + id: main + capabilities: + - id: lock + version: 1 + config: + values: + - key: "lock.value" + enabledValues: + - locked + - unlocked + - unlatched + - not fully locked + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + - id: lockSchedules + version: 1 + - 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 + 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 + 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}}' + 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}}' \ No newline at end of file diff --git a/drivers/SmartThings/matter-lock/profiles/lock-user-unlatch.yml b/drivers/SmartThings/matter-lock/profiles/lock-user-unlatch.yml new file mode 100644 index 0000000000..eaaf37ae63 --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-user-unlatch.yml @@ -0,0 +1,95 @@ +name: lock-user-unlatch +components: +- label: Main + id: main + capabilities: + - id: lock + version: 1 + config: + values: + - key: "lock.value" + enabledValues: + - locked + - unlocked + - unlatched + - not fully locked + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + - 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 + 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 + 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}}' + 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}}' \ No newline at end of file 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 c4cefed6cf..f7adcf40c6 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -25,15 +25,19 @@ if version.api < 10 then end local DoorLock = clusters.DoorLock +local PowerSource = clusters.PowerSource local INITIAL_COTA_INDEX = 1 local ALL_INDEX = 0xFFFE local NEW_MATTER_LOCK_PRODUCTS = { {0x115f, 0x2802}, -- AQARA, U200 {0x115f, 0x2801}, -- AQARA, U300 - {0x10E1, 0x2002} -- VDA + {0x147F, 0x0001}, -- U-tec + {0x10E1, 0x2002} -- VDA } +local PROFILE_BASE_NAME = "__profile_base_name" + local subscribed_attributes = { [capabilities.lock.ID] = { DoorLock.attributes.LockState @@ -53,6 +57,12 @@ local subscribed_attributes = { [capabilities.lockSchedules.ID] = { DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser, DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser + }, + [capabilities.battery.ID] = { + PowerSource.attributes.BatPercentRemaining + }, + [capabilities.batteryLevel.ID] = { + PowerSource.attributes.BatChargeLevel } } @@ -126,6 +136,8 @@ local function do_configure(driver, device) 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 @@ -137,8 +149,21 @@ local function do_configure(driver, device) profile_name = profile_name .. "-schedule" end end - device.log.info(string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) + if #unbolt_eps > 0 then + profile_name = profile_name .. "-unlatch" + device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + 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(string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) + end end local function info_changed(driver, device, event, args) @@ -185,6 +210,7 @@ local function lock_state_handler(driver, device, ib, response) [LockState.NOT_FULLY_LOCKED] = attr.not_fully_locked(), [LockState.LOCKED] = attr.locked(), [LockState.UNLOCKED] = attr.unlocked(), + [LockState.UNLATCHED] = attr.unlatched() } -- The lock state is usually updated in lock_state_handler and lock_op_event_handler, respectively. @@ -213,9 +239,14 @@ local function operating_modes_handler(driver, device, ib, response) [op_type.PASSAGE] = false, } local result = opMode_map[ib.data.value] + local unbolt_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.UNBOLT}) if result == true then device:emit_event(status("true", {visibility = {displayed = true}})) - device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + if #unbolt_eps > 0 then + device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + else + device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + end elseif result == false then device:emit_event(status("false", {visibility = {displayed = true}})) device:emit_event(capabilities.lock.supportedLockCommands({}, {visibility = {displayed = false}})) @@ -340,6 +371,55 @@ 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 +--------------------------------- +-- 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. + 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" + end + device.log.info(string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) + end +end + +------------------------------- +-- Battery Percent Remaining -- +------------------------------- +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 + +-------------------------- +-- Battery Charge Level -- +-------------------------- +local function handle_battery_charge_level(driver, device, ib, response) + if ib.data.value == PowerSource.types.BatChargeLevelEnum.OK then + device:emit_event(capabilities.batteryLevel.battery.normal()) + elseif ib.data.value == PowerSource.types.BatChargeLevelEnum.WARNING then + device:emit_event(capabilities.batteryLevel.battery.warning()) + elseif ib.data.value == PowerSource.types.BatChargeLevelEnum.CRITICAL then + device:emit_event(capabilities.batteryLevel.battery.critical()) + end +end + -- Capability Handler ----------------- -- Lock/Unlock -- @@ -357,6 +437,30 @@ local function handle_lock(driver, device, command) end local function handle_unlock(driver, device, command) + local unbolt_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.UNBOLT}) + local cota_cred = device:get_field(lock_utils.COTA_CRED) + local ep = device:component_to_endpoint(command.component) + + if #unbolt_eps > 0 then + if cota_cred then + device:send( + DoorLock.server.commands.UnboltDoor(device, ep, cota_cred) + ) + else + device:send(DoorLock.server.commands.UnboltDoor(device, ep)) + end + else + if cota_cred then + device:send( + DoorLock.server.commands.UnlockDoor(device, ep, cota_cred) + ) + else + device:send(DoorLock.server.commands.UnlockDoor(device, ep)) + end + end +end + +local function handle_unlatch(driver, device, command) local ep = device:component_to_endpoint(command.component) local cota_cred = device:get_field(lock_utils.COTA_CRED) if cota_cred then @@ -1568,7 +1672,7 @@ local function lock_op_event_handler(driver, device, ib, response) elseif opType.value == Type.UNLOCK then opType = Lock.unlocked elseif opType.value == Type.UNLATCH then - opType = Lock.locked + opType = Lock.unlatched else return end @@ -1634,6 +1738,11 @@ 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, + }, + [PowerSource.ID] = { + [PowerSource.attributes.AttributeList.ID] = handle_power_source_attribute_list, + [PowerSource.attributes.BatPercentRemaining.ID] = handle_battery_percent_remaining, + [PowerSource.attributes.BatChargeLevel.ID] = handle_battery_charge_level, } }, event = { @@ -1660,6 +1769,7 @@ local new_matter_lock_handler = { [capabilities.lock.ID] = { [capabilities.lock.commands.lock.NAME] = handle_lock, [capabilities.lock.commands.unlock.NAME] = handle_unlock, + [capabilities.lock.commands.unlatch.NAME] = handle_unlatch }, [capabilities.lockUsers.ID] = { [capabilities.lockUsers.commands.addUser.NAME] = handle_add_user, @@ -1685,7 +1795,9 @@ local new_matter_lock_handler = { capabilities.lock, capabilities.lockUsers, capabilities.lockCredentials, - capabilities.lockSchedules + capabilities.lockSchedules, + capabilities.battery, + capabilities.batteryLevel }, can_handle = is_new_matter_lock_products } 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 new file mode 100644 index 0000000000..2e390f4d34 --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_unlatch.lua @@ -0,0 +1,363 @@ +-- 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. + +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" +local DoorLock = clusters.DoorLock +local types = DoorLock.types + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock-unlatch.yml"), + manufacturer_info = { + vendor_id = 0x115f, + product_id = 0x2802, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.BasicInformation.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, -- UNBOLT + } + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + +local function test_init() + 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}) + test.mock_device.add_test_device(mock_device) +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() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.OperatingMode:build_test_report_data( + mock_device, 1, DoorLock.attributes.OperatingMode.NORMAL + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.remoteControlStatus.remoteControlEnabled("true", {visibility = {displayed = true}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.OperatingMode:build_test_report_data( + mock_device, 1, DoorLock.attributes.OperatingMode.VACATION + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.remoteControlStatus.remoteControlEnabled("true", {visibility = {displayed = true}})) + ) + 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(Privacy, No Remote Lock UnLock, Passage) from Matter device.", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.OperatingMode:build_test_report_data( + mock_device, 1, DoorLock.attributes.OperatingMode.PRIVACY + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.remoteControlStatus.remoteControlEnabled("false", {visibility = {displayed = true}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({}, {visibility = {displayed = false}})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.OperatingMode:build_test_report_data( + mock_device, 1, DoorLock.attributes.OperatingMode.NO_REMOTE_LOCK_UNLOCK + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.remoteControlStatus.remoteControlEnabled("false", {visibility = {displayed = true}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({}, {visibility = {displayed = false}})) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.OperatingMode:build_test_report_data( + mock_device, 1, DoorLock.attributes.OperatingMode.PASSAGE + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.remoteControlStatus.remoteControlEnabled("false", {visibility = {displayed = true}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({}, {visibility = {displayed = false}})) + ) + end +) + +test.register_message_test( + "Handle Lock command received from SmartThings.", { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + {capability = "lock", component = "main", command = "lock", args = {}}, + }, + }, + { + channel = "matter", + direction = "send", + message = {mock_device.id, DoorLock.server.commands.LockDoor(mock_device, 1)}, + }, + } +) + +test.register_message_test( + "Handle Unlock command received from SmartThings.", { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + {capability = "lock", component = "main", command = "unlock", args = {}}, + }, + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + DoorLock.server.commands.UnboltDoor(mock_device, 1), + }, + }, + } +) + +test.register_message_test( + "Handle Unlatch command received from SmartThings.", { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + {capability = "lock", component = "main", command = "unlatch", args = {}}, + }, + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + DoorLock.server.commands.UnlockDoor(mock_device, 1), + }, + }, + } +) + +test.register_coroutine_test( + "Handle received LockState.LOCKED from Matter device.", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.LockState:build_test_report_data( + mock_device, 1, DoorLock.attributes.LockState.LOCKED + ), + } + ) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.mock_time.advance_time(1) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.lock.locked()) + ) + end +) + +test.register_coroutine_test( + "Handle received LockState.UNLOCKED from Matter device.", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.LockState:build_test_report_data( + mock_device, 1, DoorLock.attributes.LockState.UNLOCKED + ), + } + ) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.mock_time.advance_time(1) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.lock.unlocked()) + ) + end +) + +test.register_coroutine_test( + "Handle received LockState.UNLATCHED from Matter device.", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.attributes.LockState:build_test_report_data( + mock_device, 1, DoorLock.attributes.LockState.UNLATCHED + ), + } + ) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.mock_time.advance_time(1) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.lock.unlatched()) + ) + end +) + +test.register_message_test( + "Handle Unlatch Operation event from Matter device.", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + DoorLock.events.LockOperation:build_test_event_report( + mock_device, 1, + { + lock_operation_type = types.LockOperationTypeEnum.UNLATCH, + operation_source = types.OperationSourceEnum.MANUAL, + user_index = 1, + fabric_index = 1, + source_node = 1 + } + ), + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message( + "main", + capabilities.lock.lock.unlatched( + {data = {method = "manual", userIndex = 1}, state_change = true} + ) + ), + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + DoorLock.events.LockOperation:build_test_event_report( + mock_device, 1, + { + lock_operation_type = types.LockOperationTypeEnum.UNLATCH, + operation_source = types.OperationSourceEnum.BUTTON, + user_index = 1, + fabric_index = 1, + source_node = 1 + } + ), + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message( + "main", + capabilities.lock.lock.unlatched( + {data = {method = "button", userIndex = 1}, state_change = true} + ) + ), + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + DoorLock.events.LockOperation:build_test_event_report( + mock_device, 1, + { + lock_operation_type = types.LockOperationTypeEnum.UNLATCH, + operation_source = types.OperationSourceEnum.RFID, + user_index = 1, + fabric_index = 1, + source_node = 1 + } + ), + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message( + "main", + capabilities.lock.lock.unlatched( + {data = {method = "rfid", userIndex = 1}, state_change = true} + ) + ), + } + } +) +test.run_registered_tests() 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 836ad6fb7e..7d8e92eeeb 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 @@ -79,6 +79,9 @@ test.register_coroutine_test( 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 ) @@ -733,33 +736,6 @@ test.register_message_test( ) ), }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - DoorLock.events.LockOperation:build_test_event_report( - mock_device, 1, - { - lock_operation_type = types.LockOperationTypeEnum.UNLATCH, - operation_source = types.OperationSourceEnum.MANUAL, - user_index = 1, - fabric_index = 1, - source_node = 1 - } - ), - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message( - "main", - capabilities.lock.lock.locked( - {data = {method = "manual", userIndex = 1}, state_change = true} - ) - ), - }, { channel = "matter", direction = "receive", @@ -814,33 +790,6 @@ test.register_message_test( ) ), }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - DoorLock.events.LockOperation:build_test_event_report( - mock_device, 1, - { - lock_operation_type = types.LockOperationTypeEnum.UNLATCH, - operation_source = types.OperationSourceEnum.BUTTON, - user_index = 1, - fabric_index = 1, - source_node = 1 - } - ), - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message( - "main", - capabilities.lock.lock.locked( - {data = {method = "button", userIndex = 1}, state_change = true} - ) - ), - }, { channel = "matter", direction = "receive", @@ -895,33 +844,6 @@ test.register_message_test( ) ), }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - DoorLock.events.LockOperation:build_test_event_report( - mock_device, 1, - { - lock_operation_type = types.LockOperationTypeEnum.UNLATCH, - operation_source = types.OperationSourceEnum.RFID, - user_index = 1, - fabric_index = 1, - source_node = 1 - } - ), - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message( - "main", - capabilities.lock.lock.locked( - {data = {method = "rfid", userIndex = 1}, state_change = true} - ) - ), - }, { channel = "matter", direction = "receive", 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 new file mode 100644 index 0000000000..1565b37799 --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua @@ -0,0 +1,189 @@ +-- 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. + +local test = require "integration_test" +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" + +local DoorLock = clusters.DoorLock + +local mock_device = 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 function test_init() + 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)) + subscribe_request:merge(DoorLock.attributes.NumberOfPINUsersSupported:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.MaxPINCodeLength:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.MinPINCodeLength:subscribe(mock_device)) + subscribe_request:merge(DoorLock.attributes.RequirePINforRemoteOperation:subscribe(mock_device)) + 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}) + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Test 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, + 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), + }) + } + ) + mock_device:expect_metadata_update({ profile = "lock-user-pin" }) + end +) + +test.register_coroutine_test( + "Test 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, + 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), + }) + } + ) + mock_device:expect_metadata_update({ profile = "lock-user-pin-batteryLevel" }) + end +) + +test.register_coroutine_test( + "Test 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, + 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), + }) + } + ) + mock_device:expect_metadata_update({ profile = "lock-user-pin-battery" }) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index ae96a9eef3..b3e87fa237 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -282,6 +282,11 @@ matterManufacturer: vendorId: 0x1189 productId: 0x0461 deviceProfileName: light-color-level + - id: "4489/1372" + deviceLabel: MATTER PLUG EU WH + vendorId: 0x1189 + productId: 0x055C + deviceProfileName: plug-binary #Innovation Matters - id: "4978/1" deviceLabel: M2D Bridge diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua similarity index 62% rename from drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua rename to drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua index 22befec642..3dc58635e1 100644 --- a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua +++ b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua @@ -2,19 +2,19 @@ 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", +local CumulativeEnergyImported = { + ID = 0x0001, + NAME = "CumulativeEnergyImported", base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", } -function CumulativeEnergyExported:new_value(...) +function CumulativeEnergyImported:new_value(...) local o = self.base_type(table.unpack({...})) self:augment_type(o) return o end -function CumulativeEnergyExported:read(device, endpoint_id) +function CumulativeEnergyImported:read(device, endpoint_id) return cluster_base.read( device, endpoint_id, @@ -24,7 +24,7 @@ function CumulativeEnergyExported:read(device, endpoint_id) ) end -function CumulativeEnergyExported:subscribe(device, endpoint_id) +function CumulativeEnergyImported:subscribe(device, endpoint_id) return cluster_base.subscribe( device, endpoint_id, @@ -34,12 +34,12 @@ function CumulativeEnergyExported:subscribe(device, endpoint_id) ) end -function CumulativeEnergyExported:set_parent_cluster(cluster) +function CumulativeEnergyImported:set_parent_cluster(cluster) self._cluster = cluster return self end -function CumulativeEnergyExported:build_test_report_data( +function CumulativeEnergyImported:build_test_report_data( device, endpoint_id, value, @@ -57,12 +57,12 @@ function CumulativeEnergyExported:build_test_report_data( ) end -function CumulativeEnergyExported:deserialize(tlv_buf) +function CumulativeEnergyImported: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 +setmetatable(CumulativeEnergyImported, {__call = CumulativeEnergyImported.new_value, __index = CumulativeEnergyImported.base_type}) +return CumulativeEnergyImported diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua similarity index 62% rename from drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua rename to drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua index 4c1ee29274..753b91ea2d 100644 --- a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua +++ b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua @@ -2,19 +2,19 @@ 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", +local PeriodicEnergyImported = { + ID = 0x0003, + NAME = "PeriodicEnergyImported", base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", } -function PeriodicEnergyExported:new_value(...) +function PeriodicEnergyImported:new_value(...) local o = self.base_type(table.unpack({...})) self:augment_type(o) return o end -function PeriodicEnergyExported:read(device, endpoint_id) +function PeriodicEnergyImported:read(device, endpoint_id) return cluster_base.read( device, endpoint_id, @@ -24,7 +24,7 @@ function PeriodicEnergyExported:read(device, endpoint_id) ) end -function PeriodicEnergyExported:subscribe(device, endpoint_id) +function PeriodicEnergyImported:subscribe(device, endpoint_id) return cluster_base.subscribe( device, endpoint_id, @@ -34,12 +34,12 @@ function PeriodicEnergyExported:subscribe(device, endpoint_id) ) end -function PeriodicEnergyExported:set_parent_cluster(cluster) +function PeriodicEnergyImported:set_parent_cluster(cluster) self._cluster = cluster return self end -function PeriodicEnergyExported:build_test_report_data( +function PeriodicEnergyImported:build_test_report_data( device, endpoint_id, value, @@ -57,12 +57,12 @@ function PeriodicEnergyExported:build_test_report_data( ) end -function PeriodicEnergyExported:deserialize(tlv_buf) +function PeriodicEnergyImported: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 +setmetatable(PeriodicEnergyImported, {__call = PeriodicEnergyImported.new_value, __index = PeriodicEnergyImported.base_type}) +return PeriodicEnergyImported diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index be192b9eeb..59ad021a54 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -162,12 +162,12 @@ local fingerprint_profile_overrides = { local detect_matter_thing local CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" -local FIRST_EXPORT_REPORT_TIMESTAMP = "__first_export_report_timestamp" -local EXPORT_POLL_TIMER_SETTING_ATTEMPTED = "__export_poll_timer_setting_attempted" -local EXPORT_REPORT_TIMEOUT = "__export_report_timeout" -local TOTAL_EXPORTED_ENERGY = "__total_exported_energy" -local LAST_EXPORTED_REPORT_TIMESTAMP = "__last_exported_report_timestamp" -local RECURRING_EXPORT_REPORT_POLL_TIMER = "__recurring_export_report_poll_timer" +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 @@ -187,26 +187,26 @@ local function iso8061Timestamp(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) end -local function delete_export_poll_schedule(device) - local export_poll_timer = device:get_field(RECURRING_EXPORT_REPORT_POLL_TIMER) - if export_poll_timer then - device.thread:cancel_timer(export_poll_timer) - device:set_field(RECURRING_EXPORT_REPORT_POLL_TIMER, nil) - device:set_field(EXPORT_POLL_TIMER_SETTING_ATTEMPTED, nil) +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 end -local function send_export_poll_report(device, latest_total_exported_energy_wh) +local function send_import_poll_report(device, latest_total_imported_energy_wh) local current_time = os.time() - local last_time = device:get_field(LAST_EXPORTED_REPORT_TIMESTAMP) or 0 - device:set_field(LAST_EXPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) + 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_exported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, + local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, capabilities.powerConsumptionReport.powerConsumption.NAME) - if previous_exported_report and previous_exported_report.energy then - energy_delta_wh = math.max(latest_total_exported_energy_wh - previous_exported_report.energy, 0.0) + 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' @@ -214,17 +214,17 @@ local function send_export_poll_report(device, latest_total_exported_energy_wh) start = iso8061Timestamp(last_time), ["end"] = iso8061Timestamp(current_time - 1), deltaEnergy = energy_delta_wh, - energy = latest_total_exported_energy_wh + energy = latest_total_imported_energy_wh })) end local function create_poll_report_schedule(device) - local export_timer = device.thread:call_on_schedule( - device:get_field(EXPORT_REPORT_TIMEOUT), - send_export_poll_report(device, device:get_field(TOTAL_EXPORTED_ENERGY)), - "polling_export_report_schedule_timer" + local import_timer = device.thread:call_on_schedule( + device:get_field(IMPORT_REPORT_TIMEOUT), + send_import_poll_report(device, device:get_field(TOTAL_IMPORTED_ENERGY)), + "polling_import_report_schedule_timer" ) - device:set_field(RECURRING_EXPORT_REPORT_POLL_TIMER, export_timer) + device:set_field(RECURRING_IMPORT_REPORT_POLL_TIMER, import_timer) end local function set_poll_report_timer_and_schedule(device, is_cumulative_report) @@ -238,18 +238,18 @@ local function set_poll_report_timer_and_schedule(device, is_cumulative_report) return elseif not device:get_field(SUBSCRIPTION_REPORT_OCCURRED) then device:set_field(SUBSCRIPTION_REPORT_OCCURRED, true) - elseif not device:get_field(FIRST_EXPORT_REPORT_TIMESTAMP) then - device:set_field(FIRST_EXPORT_REPORT_TIMESTAMP, os.time()) + 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_EXPORT_REPORT_TIMESTAMP) + 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(EXPORT_REPORT_TIMEOUT, math.max(report_interval_secs, MINIMUM_ST_ENERGY_REPORT_INTERVAL)) + 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 if device:supports_capability(capabilities.powerConsumptionReport) then create_poll_report_schedule(device) end - device:set_field(EXPORT_POLL_TIMER_SETTING_ATTEMPTED, true) + device:set_field(IMPORT_POLL_TIMER_SETTING_ATTEMPTED, true) end end @@ -723,7 +723,7 @@ end local function device_removed(driver, device) log.info("device removed") - delete_export_poll_schedule(device) + delete_import_poll_schedule(device) end local function handle_switch_on(driver, device, cmd) @@ -1002,33 +1002,33 @@ 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_exported_handler(driver, device, ib, response) +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_EXPORTED_ENERGY, watt_hour_value) + device:set_field(TOTAL_IMPORTED_ENERGY, watt_hour_value) device:emit_event(capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) end end -local function per_energy_exported_handler(driver, device, ib, response) +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_EXPORTED_ENERGY) or 0 + 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_EXPORTED_ENERGY, summed_energy_report) + device:set_field(TOTAL_IMPORTED_ENERGY, summed_energy_report) 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(EXPORT_POLL_TIMER_SETTING_ATTEMPTED) then + 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_exported_handler(driver, device, ib, response) + cumul_energy_imported_handler(driver, device, ib, response) elseif device:get_field(CUMULATIVE_REPORTS_NOT_SUPPORTED) then - per_energy_exported_handler(driver, device, ib, response) + per_energy_imported_handler(driver, device, ib, response) end end end @@ -1197,8 +1197,8 @@ local matter_driver_template = { [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = active_power_handler, }, [clusters.ElectricalEnergyMeasurement.ID] = { - [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported.ID] = energy_report_handler_factory(true), - [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported.ID] = energy_report_handler_factory(false), + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = energy_report_handler_factory(true), + [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = energy_report_handler_factory(false), }, [clusters.ValveConfigurationAndControl.ID] = { [clusters.ValveConfigurationAndControl.attributes.CurrentState.ID] = valve_state_attr_handler, @@ -1257,8 +1257,8 @@ local matter_driver_template = { clusters.PowerSource.attributes.BatPercentRemaining, }, [capabilities.energyMeter.ID] = { - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported }, [capabilities.powerMeter.ID] = { clusters.ElectricalPowerMeasurement.attributes.ActivePower diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua index e702362997..9dfaa15348 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -89,14 +89,14 @@ local mock_device_periodic = test.mock_device.build_test_matter_device({ }) local subscribed_attributes_periodic = { - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, } local subscribed_attributes = { clusters.OnOff.attributes.OnOff, clusters.ElectricalPowerMeasurement.attributes.ActivePower, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, } local cumulative_report_val_19 = { @@ -264,7 +264,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyExported:build_test_report_data(mock_device, 1, cumulative_report_val_19) + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, 1, cumulative_report_val_19) } }, { @@ -277,7 +277,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyExported:build_test_report_data(mock_device, 1, cumulative_report_val_19) + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, 1, cumulative_report_val_19) } }, { @@ -290,7 +290,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyExported:build_test_report_data(mock_device, 1, cumulative_report_val_29) + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, 1, cumulative_report_val_29) } }, { @@ -313,7 +313,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyExported:build_test_report_data(mock_device, 1, cumulative_report_val_39) + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, 1, cumulative_report_val_39) } }, { @@ -332,7 +332,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyExported:build_test_report_data(mock_device, 1, periodic_report_val_23) + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data(mock_device, 1, periodic_report_val_23) } }, { @@ -340,7 +340,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyExported:build_test_report_data(mock_device, 1, periodic_report_val_23) + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data(mock_device, 1, periodic_report_val_23) } }, } @@ -354,7 +354,7 @@ test.register_message_test( direction = "receive", message = { mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyExported:build_test_report_data(mock_device_periodic, 1, periodic_report_val_23) + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data(mock_device_periodic, 1, periodic_report_val_23) } }, { @@ -367,7 +367,7 @@ test.register_message_test( direction = "receive", message = { mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyExported:build_test_report_data(mock_device_periodic, 1, periodic_report_val_23) + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data(mock_device_periodic, 1, periodic_report_val_23) } }, { @@ -380,7 +380,7 @@ test.register_message_test( direction = "receive", message = { mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyExported:build_test_report_data(mock_device_periodic, 1, periodic_report_val_23) + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data(mock_device_periodic, 1, periodic_report_val_23) } }, { @@ -410,7 +410,7 @@ test.register_coroutine_test( test.socket["matter"]:__queue_receive( { mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:build_test_report_data( + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( mock_device, 1, cumulative_report_val_19 ) } @@ -421,7 +421,7 @@ test.register_coroutine_test( test.socket["matter"]:__queue_receive( { mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:build_test_report_data( + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( mock_device, 1, cumulative_report_val_19 ) } @@ -434,7 +434,7 @@ test.register_coroutine_test( test.socket["matter"]:__queue_receive( { mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:build_test_report_data( + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( mock_device, 1, cumulative_report_val_29 ) } @@ -451,11 +451,11 @@ test.register_coroutine_test( mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) ) test.wait_for_events() - local report_export_poll_timer = mock_device:get_field("__recurring_export_report_poll_timer") - local export_timer_length = mock_device:get_field("__export_report_timeout") - assert(report_export_poll_timer ~= nil, "report_export_poll_timer should exist") - assert(export_timer_length ~= nil, "export_timer_length should exist") - assert(export_timer_length == MINIMUM_ST_ENERGY_REPORT_INTERVAL, "export_timer should min_interval") + 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 ) @@ -465,7 +465,7 @@ test.register_coroutine_test( test.socket["matter"]:__queue_receive( { mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:build_test_report_data( + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( mock_device, 1, cumulative_report_val_19 ) } @@ -476,7 +476,7 @@ test.register_coroutine_test( test.socket["matter"]:__queue_receive( { mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:build_test_report_data( + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( mock_device, 1, cumulative_report_val_19 ) } @@ -489,7 +489,7 @@ test.register_coroutine_test( test.socket["matter"]:__queue_receive( { mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:build_test_report_data( + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( mock_device, 1, cumulative_report_val_29 ) } @@ -506,11 +506,11 @@ test.register_coroutine_test( mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) ) test.wait_for_events() - local report_export_poll_timer = mock_device:get_field("__recurring_export_report_poll_timer") - local export_timer_length = mock_device:get_field("__export_report_timeout") - assert(report_export_poll_timer ~= nil, "report_export_poll_timer should exist") - assert(export_timer_length ~= nil, "export_timer_length should exist") - assert(export_timer_length == 2000, "export_timer should min_interval") + 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 ) @@ -520,7 +520,7 @@ test.register_coroutine_test( test.socket["matter"]:__queue_receive( { mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:build_test_report_data( + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( mock_device, 1, cumulative_report_val_19 ) } @@ -531,7 +531,7 @@ test.register_coroutine_test( test.socket["matter"]:__queue_receive( { mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:build_test_report_data( + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( mock_device, 1, cumulative_report_val_19 ) } @@ -544,7 +544,7 @@ test.register_coroutine_test( test.socket["matter"]:__queue_receive( { mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:build_test_report_data( + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( mock_device, 1, cumulative_report_val_29 ) } @@ -561,28 +561,28 @@ test.register_coroutine_test( mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) ) test.wait_for_events() - local report_export_poll_timer = mock_device:get_field("__recurring_export_report_poll_timer") - local export_timer_length = mock_device:get_field("__export_report_timeout") - assert(report_export_poll_timer ~= nil, "report_export_poll_timer should exist") - assert(export_timer_length ~= nil, "export_timer_length should exist") - assert(export_timer_length == 2000, "export_timer should min_interval") + 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_export_poll_timer = mock_device:get_field("__recurring_export_report_poll_timer") - export_timer_length = mock_device:get_field("__export_report_timeout") - assert(report_export_poll_timer == nil, "report_export_poll_timer should exist") - assert(export_timer_length == nil, "export_timer_length should exist") + 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 export energy device poll timer (<15 minutes) gets correctly set", function() + "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.PeriodicEnergyExported:build_test_report_data( + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( mock_device_periodic, 1, periodic_report_val_23 ) } @@ -593,7 +593,7 @@ test.register_coroutine_test( test.socket["matter"]:__queue_receive( { mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported:build_test_report_data( + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( mock_device_periodic, 1, periodic_report_val_23 ) } @@ -606,7 +606,7 @@ test.register_coroutine_test( test.socket["matter"]:__queue_receive( { mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported:build_test_report_data( + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( mock_device_periodic, 1, periodic_report_val_23 ) } @@ -623,22 +623,22 @@ test.register_coroutine_test( mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 69.0, unit = "Wh" })) ) test.wait_for_events() - local report_export_poll_timer = mock_device_periodic:get_field("__recurring_export_report_poll_timer") - local export_timer_length = mock_device_periodic:get_field("__export_report_timeout") - assert(report_export_poll_timer ~= nil, "report_export_poll_timer should exist") - assert(export_timer_length ~= nil, "export_timer_length should exist") - assert(export_timer_length == MINIMUM_ST_ENERGY_REPORT_INTERVAL, "export_timer should min_interval") + 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 export energy device poll timer (>15 minutes) gets correctly set", function() + "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.PeriodicEnergyExported:build_test_report_data( + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( mock_device_periodic, 1, periodic_report_val_23 ) } @@ -649,7 +649,7 @@ test.register_coroutine_test( test.socket["matter"]:__queue_receive( { mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported:build_test_report_data( + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( mock_device_periodic, 1, periodic_report_val_23 ) } @@ -662,7 +662,7 @@ test.register_coroutine_test( test.socket["matter"]:__queue_receive( { mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported:build_test_report_data( + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( mock_device_periodic, 1, periodic_report_val_23 ) } @@ -679,11 +679,11 @@ test.register_coroutine_test( mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 69.0, unit = "Wh" })) ) test.wait_for_events() - local report_export_poll_timer = mock_device_periodic:get_field("__recurring_export_report_poll_timer") - local export_timer_length = mock_device_periodic:get_field("__export_report_timeout") - assert(report_export_poll_timer ~= nil, "report_export_poll_timer should exist") - assert(export_timer_length ~= nil, "export_timer_length should exist") - assert(export_timer_length == 2000, "export_timer should min_interval") + 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 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index 5571326297..20b6f2d64d 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -819,7 +819,7 @@ local function sequence_of_operation_handler(driver, device, ib, response) -- or not the device supports emergency heat or fan only local supported_modes = {capabilities.thermostatMode.thermostatMode.off.NAME} - local auto = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.auto}) + local auto = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}) if #auto > 0 then table.insert(supported_modes, capabilities.thermostatMode.thermostatMode.auto.NAME) end 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 96835c2f38..5d740cc47e 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua @@ -53,6 +53,40 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) +local mock_device_auto = 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=35, -- Heat, Cool, and Auto 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"}, + } + } + } +}) + local function test_init() local cluster_subscribe_list = { clusters.Thermostat.attributes.LocalTemperature, @@ -85,6 +119,38 @@ local function test_init() end test.set_test_init_function(test_init) +local function test_init_auto() + 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, + } + test.socket.matter:__set_channel_ordering("relaxed") + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_auto) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_auto)) + end + end + test.socket.matter:__expect_send({mock_device_auto.id, subscribe_request}) + 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 + test.register_message_test( "Relative humidity reports should generate correct messages", { @@ -295,7 +361,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "auto", "cool", "heat"}, {visibility={displayed=false}})) + message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat"}, {visibility={displayed=false}})) }, { channel = "matter", @@ -308,7 +374,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "auto", "heat"}, {visibility={displayed=false}})) + message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "heat"}, {visibility={displayed=false}})) }, { channel = "matter", @@ -321,11 +387,57 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "auto", "cool"}, {visibility={displayed=false}})) + message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "cool"}, {visibility={displayed=false}})) }, } ) +test.register_message_test( + "Thermostat control sequence reports should generate correct messages when auto feature is supported", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device_auto.id, + ControlSequenceOfOperation:build_test_report_data(mock_device_auto, 1, ControlSequenceOfOperation.COOLING_AND_HEATING_WITH_REHEAT) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_auto:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "auto", "cool", "heat"}, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device_auto.id, + ControlSequenceOfOperation:build_test_report_data(mock_device_auto, 1, ControlSequenceOfOperation.HEATING_WITH_REHEAT) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_auto:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "auto", "heat"}, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device_auto.id, + ControlSequenceOfOperation:build_test_report_data(mock_device_auto, 1, ControlSequenceOfOperation.COOLING_WITH_REHEAT) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_auto:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "auto", "cool"}, {visibility={displayed=false}})) + }, + }, + { test_init = test_init_auto } +) + test.register_message_test( "Additional mode reports should extend the supported modes", { @@ -340,7 +452,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "auto", "cool", "heat"}, {visibility={displayed=false}})) + message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat"}, {visibility={displayed=false}})) }, { channel = "matter", @@ -358,11 +470,49 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "auto", "cool", "heat", "emergency heat"}, {visibility={displayed=false}})) + message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "cool", "heat", "emergency heat"}, {visibility={displayed=false}})) } } ) +test.register_message_test( + "Additional mode reports should extend the supported modes when auto is supported", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device_auto.id, + clusters.Thermostat.server.attributes.ControlSequenceOfOperation:build_test_report_data(mock_device_auto, 1, 5) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_auto:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "auto", "cool", "heat"}, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device_auto.id, + clusters.Thermostat.server.attributes.SystemMode:build_test_report_data(mock_device_auto, 1, 5) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_auto:generate_test_message("main", capabilities.thermostatMode.thermostatMode.emergency_heat()) + }, + { + channel = "capability", + direction = "send", + message = mock_device_auto:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"off", "auto", "cool", "heat", "emergency heat"}, {visibility={displayed=false}})) + } + }, + { test_init = test_init_auto } +) + local FanMode = clusters.FanControl.attributes.FanMode test.register_message_test( "Thermostat fan mode reports should generate correct messages", 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 f0d4744ce3..556cc5ecb0 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 @@ -308,7 +308,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", - capabilities.thermostatMode.supportedThermostatModes({ "off", "auto", "cool", "heat" }, {visibility={displayed=false}})) + capabilities.thermostatMode.supportedThermostatModes({ "off", "cool", "heat" }, {visibility={displayed=false}})) }, { channel = "matter", @@ -322,7 +322,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", - capabilities.thermostatMode.supportedThermostatModes({ "off", "auto", "heat" }, {visibility={displayed=false}})) + capabilities.thermostatMode.supportedThermostatModes({ "off", "heat" }, {visibility={displayed=false}})) }, { channel = "matter", @@ -336,7 +336,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", - capabilities.thermostatMode.supportedThermostatModes({ "off", "auto", "cool" }, {visibility={displayed=false}})) + capabilities.thermostatMode.supportedThermostatModes({ "off", "cool" }, {visibility={displayed=false}})) }, } ) @@ -356,7 +356,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", - capabilities.thermostatMode.supportedThermostatModes({ "off", "auto", "cool", "heat" }, {visibility={displayed=false}})) + capabilities.thermostatMode.supportedThermostatModes({ "off", "cool", "heat" }, {visibility={displayed=false}})) }, { channel = "matter", @@ -375,7 +375,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", - capabilities.thermostatMode.supportedThermostatModes({ "off", "auto", "cool", "heat", "emergency heat" }, {visibility={displayed=false}})) + capabilities.thermostatMode.supportedThermostatModes({ "off", "cool", "heat", "emergency heat" }, {visibility={displayed=false}})) } } ) diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index 016f92af31..42ba1d914b 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -15,7 +15,7 @@ matterManufacturer: deviceLabel: Zemismart MT25B Roller Motor vendorId: 0x139C productId: 0xFF60 - deviceProfileName: window-covering + deviceProfileName: window-covering - id: "5020/65296" deviceLabel: Zemismart MT82 Smart Curtain vendorId: 0x139C @@ -30,12 +30,33 @@ matterManufacturer: deviceLabel: Zemismart ZM02 Smart Curtain vendorId: 0x139C productId: 0xFA32 - deviceProfileName: window-covering + deviceProfileName: window-covering - id: "5020/64017" deviceLabel: Zemismart ZM25C Smart Curtain vendorId: 0x139C productId: 0xFA11 deviceProfileName: window-covering + - id: "5020/64049" + deviceLabel: Zemismart ZM01 Smart Curtain + vendorId: 0x139C + productId: 0xFA31 + deviceProfileName: window-covering +#WISTAR + - id: "5207/17" + deviceLabel: WISTAR WSER40 Smart Tubular Motor + vendorId: 0x1457 + productId: 0x0011 + deviceProfileName: window-covering + - id: "5207/18" + deviceLabel: WISTAR WSER50 Smart Tubular Motor + vendorId: 0x1457 + productId: 0x0012 + deviceProfileName: window-covering + - id: "5207/2" + deviceLabel: WISTAR WSERD30-B Smart Tubular Motor + vendorId: 0x1457 + productId: 0x0002 + deviceProfileName: window-covering-battery matterGeneric: - id: "windowcovering" deviceLabel: Matter Window Covering diff --git a/drivers/SmartThings/philips-hue/src/consts.lua b/drivers/SmartThings/philips-hue/src/consts.lua index 4fb4f42763..1c35d92726 100644 --- a/drivers/SmartThings/philips-hue/src/consts.lua +++ b/drivers/SmartThings/philips-hue/src/consts.lua @@ -6,6 +6,6 @@ Consts.DEFAULT_MAX_MIREK = 500 Consts.MIN_TEMP_KELVIN_COLOR_AMBIANCE = 2000 Consts.MIN_TEMP_KELVIN_WHITE_AMBIANCE = 2200 Consts.MAX_TEMP_KELVIN = 6500 -Consts.DEFAULT_MIN_DIMMING = 2 +Consts.KELVIN_STEP_SIZE = 11 return Consts diff --git a/drivers/SmartThings/philips-hue/src/disco/button.lua b/drivers/SmartThings/philips-hue/src/disco/button.lua index fce804f0e7..e3f70f8464 100644 --- a/drivers/SmartThings/philips-hue/src/disco/button.lua +++ b/drivers/SmartThings/philips-hue/src/disco/button.lua @@ -1,6 +1,9 @@ local log = require "log" local socket = require "cosock".socket local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local HueDeviceTypes = require "hue_device_types" diff --git a/drivers/SmartThings/philips-hue/src/disco/contact.lua b/drivers/SmartThings/philips-hue/src/disco/contact.lua index 2a301b1e52..217e16d0b9 100644 --- a/drivers/SmartThings/philips-hue/src/disco/contact.lua +++ b/drivers/SmartThings/philips-hue/src/disco/contact.lua @@ -1,6 +1,9 @@ local log = require "log" local socket = require "cosock".socket local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local HueDeviceTypes = require "hue_device_types" diff --git a/drivers/SmartThings/philips-hue/src/disco/init.lua b/drivers/SmartThings/philips-hue/src/disco/init.lua index e26578ff5d..0746e13942 100644 --- a/drivers/SmartThings/philips-hue/src/disco/init.lua +++ b/drivers/SmartThings/philips-hue/src/disco/init.lua @@ -4,6 +4,9 @@ local socket = require "cosock.socket" local mdns = require "st.mdns" local net_utils = require "st.net_utils" local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local Fields = require "fields" local HueApi = require "hue.api" diff --git a/drivers/SmartThings/philips-hue/src/disco/light.lua b/drivers/SmartThings/philips-hue/src/disco/light.lua index 115c281787..ef5f9d9371 100644 --- a/drivers/SmartThings/philips-hue/src/disco/light.lua +++ b/drivers/SmartThings/philips-hue/src/disco/light.lua @@ -1,6 +1,9 @@ local log = require "log" local socket = require "cosock".socket local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local HueDeviceTypes = require "hue_device_types" diff --git a/drivers/SmartThings/philips-hue/src/disco/motion.lua b/drivers/SmartThings/philips-hue/src/disco/motion.lua index 8e556466b5..e4267a38d1 100644 --- a/drivers/SmartThings/philips-hue/src/disco/motion.lua +++ b/drivers/SmartThings/philips-hue/src/disco/motion.lua @@ -1,6 +1,9 @@ local log = require "log" local socket = require "cosock".socket local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local HueDeviceTypes = require "hue_device_types" diff --git a/drivers/SmartThings/philips-hue/src/fields.lua b/drivers/SmartThings/philips-hue/src/fields.lua index 6b88d59fbb..6a02307f53 100644 --- a/drivers/SmartThings/philips-hue/src/fields.lua +++ b/drivers/SmartThings/philips-hue/src/fields.lua @@ -7,7 +7,6 @@ --- @field BRIDGE_SW_VERSION string The SW Version of the bridge to determine if it supports CLIP V2 --- @field DEVICE_TYPE string Field on all Hue devices that indicates type, maps to a Hue service rtype. --- @field BRIDGE_API string Transient field that holds the HueAPI instance for the bridge ---- @field MIN_DIMMING string Minimum dimming/brightness value accepted by a light --- @field EVENT_SOURCE string Field on a bridge that stores a handle to the SSE EventSource client. local Fields = { _ADDED = "added", @@ -24,7 +23,6 @@ local Fields = { IPV4 = "ipv4", IS_ONLINE = "is_online", IS_MULTI_SERVICE = "is_multi_service", - MIN_DIMMING = "mindim", MIN_KELVIN = "mintemp", MAX_KELVIN = "maxtemp", MODEL_ID = "modelid", diff --git a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua index 9ad3ebf499..22943ef2e8 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua @@ -2,7 +2,7 @@ local capabilities = require "st.capabilities" local log = require "log" local st_utils = require "st.utils" -- trick to fix the VS Code Lua Language Server typechecking ----@type fun(val: table, name: string?, multi_line: boolean?): string +---@type fun(val: any?, name: string?, multi_line: boolean?): string st_utils.stringify_table = st_utils.stringify_table local Consts = require "consts" @@ -42,15 +42,7 @@ local function _emit_light_events_inner(light_device, light_repr) end if light_repr.dimming then - local min_dim = light_device:get_field(Fields.MIN_DIMMING) - local api_min_dim = st_utils.round(st_utils.clamp_value(light_repr.dimming.min_dim_level or Consts.DEFAULT_MIN_DIMMING, 1, 100)) - if min_dim ~= api_min_dim then - min_dim = api_min_dim - light_device:set_field(Fields.MIN_DIMMING, min_dim, { persist = true }) - log.info("EMITTING DIMMING CAP RANGE") - light_device:emit_event(capabilities.switchLevel.levelRange({ minimum = min_dim, maximum = 100 })) - end - local adjusted_level = st_utils.round(st_utils.clamp_value(light_repr.dimming.brightness, min_dim, 100)) + local adjusted_level = st_utils.round(st_utils.clamp_value(light_repr.dimming.brightness, 1, 100)) if utils.is_nan(adjusted_level) then light_device.log.warn( string.format( diff --git a/drivers/SmartThings/philips-hue/src/handlers/commands.lua b/drivers/SmartThings/philips-hue/src/handlers/commands.lua index 2b79a22e29..a0cf161081 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/commands.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/commands.lua @@ -1,5 +1,8 @@ local log = require "log" local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local Consts = require "consts" local Fields = require "fields" @@ -8,7 +11,7 @@ local HueColorUtils = require "utils.cie_utils" local utils = require "utils" -- trick to fix the VS Code Lua Language Server typechecking ----@type fun(val: table, name: string?, multi_line: boolean?): string +---@type fun(val: any?, name: string?, multi_line: boolean?): string st_utils.stringify_table = st_utils.stringify_table ---@class CommandHandlers 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 e1f60af2ab..3cc6f3094d 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua @@ -1,6 +1,9 @@ local capabilities = require "st.capabilities" local log = require "log" local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local refresh_handler = require("handlers.commands").refresh_handler 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 f263c6d7ac..c8f34d107d 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/contact.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/contact.lua @@ -1,5 +1,8 @@ local log = require "log" local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local refresh_handler = require("handlers.commands").refresh_handler 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 4b148a115a..c9d413841d 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua @@ -1,5 +1,9 @@ local log = require "log" local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table + local capabilities = require "st.capabilities" local Discovery = require "disco" 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 8f3b572651..e912204757 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua @@ -1,6 +1,10 @@ local log = require "log" +local capabilities = require "st.capabilities" local refresh_handler = require("handlers.commands").refresh_handler local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local Consts = require "consts" local Discovery = require "disco" @@ -137,13 +141,6 @@ function LightLifecycleHandlers.added(driver, device, parent_device_id, resource ---@type HueLightInfo local light_info = Discovery.device_state_disco_cache[device_light_resource_id] - local minimum_dimming = 2 - - if light_info.dimming and light_info.dimming.min_dim_level then - minimum_dimming = st_utils.round(st_utils.clamp_value(light_info.dimming.min_dim_level, 1, 100)) - end - - device:set_field(Fields.MIN_DIMMING, minimum_dimming, { persist = true }) -- Remembering that mirek are reciprocal to kelvin, note the following: -- ** Minimum Mirek -> _Maximum_ Kelvin @@ -218,6 +215,8 @@ function LightLifecycleHandlers.init(driver, device) svc_rids_for_device[device_light_resource_id] = HueDeviceTypes.LIGHT end device:set_field(Fields._INIT, true, { persist = false }) + device:emit_event(capabilities.switchLevel.levelRange({ minimum = 1, maximum = 100 })) + if device:get_field(Fields._REFRESH_AFTER_INIT) then refresh_handler(driver, device) device:set_field(Fields._REFRESH_AFTER_INIT, false, { persist = true }) 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 2a315775e8..9cbf51b9e4 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/motion.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/motion.lua @@ -1,5 +1,8 @@ local log = require "log" local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local refresh_handler = require("handlers.commands").refresh_handler diff --git a/drivers/SmartThings/philips-hue/src/handlers/migration_handlers/light.lua b/drivers/SmartThings/philips-hue/src/handlers/migration_handlers/light.lua index 0b33b5326c..27f97be58d 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/migration_handlers/light.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/migration_handlers/light.lua @@ -1,6 +1,9 @@ local capabilities = require "st.capabilities" local log = require "log" local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local Discovery = require "disco" local Fields = require "fields" diff --git a/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua b/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua index 8baeb1ec82..4347cbbd2a 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua @@ -1,6 +1,9 @@ local cosock = require "cosock" local log = require "log" local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local Fields = require "fields" local HueDeviceTypes = require "hue_device_types" @@ -123,6 +126,7 @@ function RefreshHandlers.do_refresh_all_for_bridge(driver, bridge_device) -- but only the first time we encounter a device type. We cache them since we're refreshing -- everything. if + device_type and type(device_type_refresh_handlers_map[device_type]) == "function" and statuses_by_device_type[device_type] == nil then diff --git a/drivers/SmartThings/philips-hue/src/hue/api.lua b/drivers/SmartThings/philips-hue/src/hue/api.lua index d5031c2aca..512961d4b9 100644 --- a/drivers/SmartThings/philips-hue/src/hue/api.lua +++ b/drivers/SmartThings/philips-hue/src/hue/api.lua @@ -5,11 +5,14 @@ local json = require "st.json" local log = require "log" local RestClient = require "lunchbox.rest" local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local HueDeviceTypes = require "hue_device_types" -- trick to fix the VS Code Lua Language Server typechecking ----@type fun(val: table, name: string?, multi_line: boolean?): string +---@type fun(val: any?, name: string?, multi_line: boolean?): string st_utils.stringify_table = st_utils.stringify_table local APPLICATION_KEY_HEADER = "hue-application-key" @@ -82,6 +85,7 @@ end ---@return table? tbl the table representation of the JSON response, nil on error ---@return string? err the error message, nil on success ---@return string? partial the partial response if the response was not complete +---@return ... local function process_rest_response(response, err, partial, err_callback) if err == nil and response == nil then log.error_with({ hub_logs = true }, @@ -208,6 +212,7 @@ end ---@param path string ---@return table|nil response REST response, nil if error ---@return nil|string error nil on success +---@return ... local function do_get(instance, path) local reply_tx, reply_rx = channel.new() reply_rx:settimeout(10) @@ -226,6 +231,7 @@ end ---@param payload string ---@return table|nil response REST response, nil if error ---@return nil|string error nil on success +---@return ... local function do_put(instance, path, payload) local reply_tx, reply_rx = channel.new() reply_rx:settimeout(10) @@ -244,6 +250,7 @@ end ---@return HueBridgeInfo|nil bridge_info nil on err ---@return nil|string error nil on success ---@return nil|string partial partial response if available, nil otherwise +---@return ... function PhilipsHueApi.get_bridge_info(bridge_ip, socket_builder) local tx, rx = channel.new() rx:settimeout(10) @@ -266,6 +273,7 @@ end ---@return HueApiKeyResponse[]? api_key_response nil on err ---@return string? error nil on success ---@return string? partial partial response if available, nil otherwise +---@return ... function PhilipsHueApi.request_api_key(bridge_ip, socket_builder) local tx, rx = channel.new() rx:settimeout(10) diff --git a/drivers/SmartThings/philips-hue/src/hue_debug/init.lua b/drivers/SmartThings/philips-hue/src/hue_debug/init.lua index 43b4001d42..a6ce0ebfee 100644 --- a/drivers/SmartThings/philips-hue/src/hue_debug/init.lua +++ b/drivers/SmartThings/philips-hue/src/hue_debug/init.lua @@ -1,4 +1,7 @@ local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local log = require "log" local utils = require "utils" diff --git a/drivers/SmartThings/philips-hue/src/init.lua b/drivers/SmartThings/philips-hue/src/init.lua index e726fbfaa7..860eaffd08 100644 --- a/drivers/SmartThings/philips-hue/src/init.lua +++ b/drivers/SmartThings/philips-hue/src/init.lua @@ -17,12 +17,16 @@ -- Improvements to be made: -- -- =============================================================================================== -local Driver = require "st.driver" +local logjam = require "logjam" +logjam.enable_passthrough() +logjam.inject_global() local log = require "log" + +local Driver = require "st.driver" local st_utils = require "st.utils" -- trick to fix the VS Code Lua Language Server typechecking ----@type fun(val: table, name: string?, multi_line: boolean?): string +---@type fun(val: any?, name: string?, multi_line: boolean?): string st_utils.stringify_table = st_utils.stringify_table local Discovery = require "disco" diff --git a/drivers/SmartThings/philips-hue/src/logjam.lua b/drivers/SmartThings/philips-hue/src/logjam.lua index 480b731986..bb2f0627aa 100644 --- a/drivers/SmartThings/philips-hue/src/logjam.lua +++ b/drivers/SmartThings/philips-hue/src/logjam.lua @@ -1,9 +1,84 @@ local log = require "log" +local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local logjam = {} + +logjam.real_log = log.log + +logjam.enabled_modules = {} + +function logjam.inject_global() + for field_key, level_key in pairs(log) do + if + string.find(field_key, "LOG_LEVEL_") and + type(level_key) == "string" and + type(log[level_key]) == "function" + then + log[level_key] = logjam[level_key] + local level_with_key = string.format("%s_with", level_key) + if type(log[level_with_key]) == "function" then + log[level_with_key] = logjam[level_with_key] + end + end + end + log.log = logjam.log +end + +function logjam.enable_passthrough() + logjam.passthrough = true +end + +function logjam.disable_passthrough() + logjam.passthrough = false +end + +function logjam.enable(module) + logjam.enabled_modules[module] = true +end + +function logjam.disable(module) + logjam.enabled_modules[module] = nil +end + function logjam.log(opts, level, ...) - if opts.on == true then - log.log(opts, level, ...) + local call_info + if not opts.call_info then + call_info = debug.getinfo(2) + else + call_info = opts.call_info + end + opts.call_info = nil + + local module_name = nil + if type(call_info.source) == "string" then + module_name = + call_info.source + :gsub("%.lua", "") + :gsub("/init", "") + :gsub("/", ".") + :gsub("^init$", "philips-hue") + end + + local module_enabled = false + local module_prefix = "" + if type(module_name) == "string" and module_name:len() > 0 then + module_enabled = logjam.enabled_modules[module_name] + module_prefix = string.format("[%s] ", module_name) + end + + -- explicit on/off log option takes precedence, so that we can allow + -- `false` to override passthrough/module_enabled flags. + if type(opts.on) == "boolean" then + if opts.on then + logjam.real_log(opts, level, module_prefix, ...) + end + return + end + if logjam.passthrough or module_enabled then + logjam.real_log(opts, level, module_prefix, ...) end end @@ -16,16 +91,35 @@ for field_key, level_key in pairs(log) do local level_with_key = string.format("%s_with", level_key) logjam[level_key] = function(...) local first_arg = select(1, ...) - if first_arg == true or (type(first_arg) == "table" and first_arg.on == true) then - log[level_key](select(2, ...)) + local opts = {} + local log_args_start_idx = 1 + if type(first_arg) == "boolean" then + opts.on = first_arg + log_args_start_idx = 2 + elseif type(first_arg) == "table" then + opts = first_arg + log_args_start_idx = 2 end + local info = debug.getinfo(2) + opts.call_info = info + logjam.log(opts, level_key, select(log_args_start_idx, ...)) end logjam[level_with_key] = function(opts, ...) - opts = opts or {} - if opts.on == true then - log[level_with_key](...) + local log_opts = {} + local log_args = table.pack(...) + if type(opts) == "table" then + for k, v in pairs(opts) do + log_opts[k] = v + end + elseif type(opts) == "boolean" then + log_opts.on = opts + elseif opts ~= nil then + log_args.insert(log_args, 1, opts) end + local info = debug.getinfo(2) + log_opts.call_info = info + logjam.log(log_opts, level_key, table.unpack(log_args)) end end end diff --git a/drivers/SmartThings/philips-hue/src/lunchbox/sse/eventsource.lua b/drivers/SmartThings/philips-hue/src/lunchbox/sse/eventsource.lua index d6d51018e1..e0f4d0caf8 100644 --- a/drivers/SmartThings/philips-hue/src/lunchbox/sse/eventsource.lua +++ b/drivers/SmartThings/philips-hue/src/lunchbox/sse/eventsource.lua @@ -5,6 +5,11 @@ local ssl = require "cosock.ssl" ---@type fun(sock: table, config: table?): table?, string? ssl.wrap = ssl.wrap +local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table + local log = require "log" local util = require "lunchbox.util" local Request = require "luncheon.request" @@ -474,7 +479,6 @@ function EventSource.new(url, extra_headers, sock_builder) }, EventSource) cosock.spawn(function() - local st_utils = require "st.utils" while true do if source.ready_state == EventSource.ReadyStates.CLOSED and not source._reconnect diff --git a/drivers/SmartThings/philips-hue/src/stray_device_helper.lua b/drivers/SmartThings/philips-hue/src/stray_device_helper.lua index 7a7ff2fde2..fb58e81725 100644 --- a/drivers/SmartThings/philips-hue/src/stray_device_helper.lua +++ b/drivers/SmartThings/philips-hue/src/stray_device_helper.lua @@ -1,6 +1,9 @@ local cosock = require "cosock" local log = require "log" local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local Discovery = require "disco" local Fields = require "fields" diff --git a/drivers/SmartThings/philips-hue/src/test/spec/device_faker.lua b/drivers/SmartThings/philips-hue/src/test/spec/device_faker.lua index 9e084de0c2..8afae0cfcf 100644 --- a/drivers/SmartThings/philips-hue/src/test/spec/device_faker.lua +++ b/drivers/SmartThings/philips-hue/src/test/spec/device_faker.lua @@ -2,6 +2,9 @@ local utils = require "utils" local lazy_fakers = utils.lazy_handler_loader("fakers") local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local test_helpers = require "test_helpers" diff --git a/drivers/SmartThings/philips-hue/src/test/spec/fakers/light_faker.lua b/drivers/SmartThings/philips-hue/src/test/spec/fakers/light_faker.lua index 4f357ffec6..189b0c9e33 100644 --- a/drivers/SmartThings/philips-hue/src/test/spec/fakers/light_faker.lua +++ b/drivers/SmartThings/philips-hue/src/test/spec/fakers/light_faker.lua @@ -1,4 +1,7 @@ local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local function make_migrated_device(faker_args, bridge_info) local device_network_id = faker_args.dni or st_utils.generate_uuid_v4() diff --git a/drivers/SmartThings/philips-hue/src/utils/cie_utils.lua b/drivers/SmartThings/philips-hue/src/utils/cie_utils.lua index c88869b76e..85a25b462d 100644 --- a/drivers/SmartThings/philips-hue/src/utils/cie_utils.lua +++ b/drivers/SmartThings/philips-hue/src/utils/cie_utils.lua @@ -1,4 +1,8 @@ local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table + local CieUtils = {} local DefaultGamut = { 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 b1894ee31a..ea67abea13 100644 --- a/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua +++ b/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua @@ -2,6 +2,9 @@ local cosock = require "cosock" local log = require "log" local json = require "st.json" local st_utils = require "st.utils" +-- trick to fix the VS Code Lua Language Server typechecking +---@type fun(val: any?, name: string?, multi_line: boolean?): string +st_utils.stringify_table = st_utils.stringify_table local Discovery = require "disco" local EventSource = require "lunchbox.sse.eventsource" @@ -234,7 +237,7 @@ function hue_bridge_utils.do_bridge_network_init(driver, bridge_device, bridge_u end bridge_device:set_field(Fields._INIT, true, { persist = false }) local ids_to_remove = {} - for id, device in ipairs(driver._devices_pending_refresh) do + for id, device in pairs(driver._devices_pending_refresh) do local parent_bridge = utils.get_hue_bridge_for_device(driver, device) local bridge_id = parent_bridge and parent_bridge.id if bridge_id == bridge_device.id then diff --git a/drivers/SmartThings/philips-hue/src/utils/init.lua b/drivers/SmartThings/philips-hue/src/utils/init.lua index c2ea955cef..082353c3e2 100644 --- a/drivers/SmartThings/philips-hue/src/utils/init.lua +++ b/drivers/SmartThings/philips-hue/src/utils/init.lua @@ -1,5 +1,6 @@ local log = require "log" +local Consts = require "consts" local Fields = require "fields" local HueDeviceTypes = require "hue_device_types" @@ -71,7 +72,10 @@ end function utils.kelvin_to_mirek(kelvin) return 1000000 / kelvin end -function utils.mirek_to_kelvin(mirek) return 1000000 / mirek end +function utils.mirek_to_kelvin(mirek) + local raw_kelvin = 1000000 / mirek + return Consts.KELVIN_STEP_SIZE * math.floor(raw_kelvin / Consts.KELVIN_STEP_SIZE) +end function utils.str_starts_with(str, start) return str:sub(1, #start) == start diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index 59c6705447..51f3839872 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -80,4 +80,8 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "WallHero Outlet",智能墙面五孔插座 "WallHero Remote Control (4-Inch)",语音场景(4寸)面板 "Zemismart ZM02 Smart Curtain",Zemismart ZM02 智能窗帘 -"Zemismart ZM25C Smart Curtain",Zemismart ZM25C 智能窗帘 \ No newline at end of file +"Zemismart ZM25C Smart Curtain",Zemismart ZM25C 智能窗帘 +"Zemismart ZM01 Smart Curtain", Zemismart ZM01 智能窗帘 +"WISTAR WSERD30-B Smart Tubular Motor",威仕达智能管状电机 WSERD30-B +"WISTAR WSER40 Smart Tubular Motor",威仕达智能管状电机 WSER40 +"WISTAR WSER50 Smart Tubular Motor",威仕达智能管状电机 WSER50