diff --git a/drivers/SmartThings/philips-hue/PROGRESS.md b/drivers/SmartThings/philips-hue/PROGRESS.md index 254cfb61c4..8ba23092cd 100644 --- a/drivers/SmartThings/philips-hue/PROGRESS.md +++ b/drivers/SmartThings/philips-hue/PROGRESS.md @@ -8,35 +8,35 @@ Tracking TODOs for this refactor in this file; this is mostly to allow for creat #### Tasks -- [x] Convert supported resources map to a map of handlers instead of boolean ✅ 2024-04-16 -- [x] Move handlers to their own file ✅ 2024-04-16 -- [x] Rename the "light_state_disco_cache" key to be service type agnostic ✅ 2024-04-16 -- [x] Update the `discovered_device_callback` function to allow for other device types ✅ 2024-04-16 +- [x] Convert supported resources map to a map of handlers instead of boolean +- [x] Move handlers to their own file +- [x] Rename the "light_state_disco_cache" key to be service type agnostic +- [x] Update the `discovered_device_callback` function to allow for other device types ### Capability Handlers #### Tasks -- [x] Create a table of handlers for dispatching refreshes by device type ✅ 2024-04-17 -- [x] Fix `refresh_all_for_bridge` to remove assumptions that all child devices are lights ✅ 2024-04-17 +- [x] Create a table of handlers for dispatching refreshes by device type +- [x] Fix `refresh_all_for_bridge` to remove assumptions that all child devices are lights ### Driver (init.lua) Refactors #### Tasks -- [x] Extract lifecycle handlers to their own module(s) ✅ 2024-04-22 - - [x] 2024-04-18 Update: Initial code review missed that `is_*_bridge` and `is_*_light` calls in `utils` were implemented such that the check for light was based on failing the check for bridge. So those need to be fixed as well. ✅ 2024-04-22 -- [x] Extract attribute event emitters to their own module(s) ✅ 2024-04-17 -- [ ] Refactor Stray Light Handler to be a general Stray Device Handler -- [ ] Refactor SSE `onmessage` callback to remove light-specific assumptions - - [ ] `update` messages are hard coded to emit light events with no checks - - [ ] `add` message handling rejects non-light devices instead of being written to be extensible +- [x] Extract lifecycle handlers to their own module(s) + - [x] 2024-04-18 Update: Initial code review missed that `is_*_bridge` and `is_*_light` calls in `utils` were implemented such that the check for light was based on failing the check for bridge. So those need to be fixed as well. +- [x] Extract attribute event emitters to their own module(s) +- [x] Refactor Stray Light Handler to be a general Stray Device Handler +- [x] Refactor SSE `onmessage` callback to remove light-specific assumptions + - [x] `update` messages are hard coded to emit light events with no checks + - [x] `add` message handling rejects non-light devices instead of being written to be extensible ### Miscellaneous/Custodial #### Tasks - [ ] Refactor fresh handlers to be a single generic refresh handler, which is only possible once all of the above is complete. -- [ ] Update all doc strings that claim we only support bridges and lights -- [ ] Update any dangling utility methods/variables/symbols that use "light" when they should use "device" -- [ ] Normalize modules to all use `/init.lua` instead of `.lua` as a sibling to ``. +- [x] Update all doc strings that claim we only support bridges and lights +- [x] Update any dangling utility methods/variables/symbols that use "light" when they should use "device" +- [x] Normalize modules to all use `/init.lua` instead of `.lua` as a sibling to ``. diff --git a/drivers/SmartThings/philips-hue/profiles/motion-sensor.yml b/drivers/SmartThings/philips-hue/profiles/motion-sensor.yml index a2e3a07499..c8067e0e89 100644 --- a/drivers/SmartThings/philips-hue/profiles/motion-sensor.yml +++ b/drivers/SmartThings/philips-hue/profiles/motion-sensor.yml @@ -4,12 +4,12 @@ components: capabilities: - id: motionSensor version: 1 - - id: battery - version: 1 - id: illuminanceMeasurement version: 1 - id: temperatureMeasurement version: 1 + - id: battery + version: 1 - id: refresh version: 1 categories: diff --git a/drivers/SmartThings/philips-hue/profiles/single-button.yml b/drivers/SmartThings/philips-hue/profiles/single-button.yml index ad0c0ddb02..49003127c4 100644 --- a/drivers/SmartThings/philips-hue/profiles/single-button.yml +++ b/drivers/SmartThings/philips-hue/profiles/single-button.yml @@ -9,4 +9,4 @@ components: - id: refresh version: 1 categories: - - name: RemoteController + - name: Button diff --git a/drivers/SmartThings/philips-hue/src/disco/button.lua b/drivers/SmartThings/philips-hue/src/disco/button.lua index 88af38f89d..fce804f0e7 100644 --- a/drivers/SmartThings/philips-hue/src/disco/button.lua +++ b/drivers/SmartThings/philips-hue/src/disco/button.lua @@ -7,6 +7,7 @@ local HueDeviceTypes = require "hue_device_types" ---@class DiscoveredButtonHandler: DiscoveredChildDeviceHandler local M = {} +-- TODO This should be generalizable to all "sensors", including buttons. ---@param driver HueDriver ---@param api_instance PhilipsHueApi ---@param device_service_info HueDeviceInfo @@ -15,6 +16,7 @@ local M = {} ---@return table? description nil on error ---@return string? err nil on success local function _do_update(driver, api_instance, device_service_info, bridge_network_id, cache) + log.debug("------------ _do_update") local rid_by_rtype = {} local button_services = {} local num_buttons = 0 @@ -34,7 +36,8 @@ local function _do_update(driver, api_instance, device_service_info, bridge_netw parent_device_id = bridge_device.id, hue_device_id = device_service_info.id, hue_device_data = device_service_info, - num_buttons = num_buttons + num_buttons = num_buttons, + sensor_list = { power_id = HueDeviceTypes.DEVICE_POWER } } for _, button_rid in ipairs(button_services) do @@ -51,6 +54,8 @@ local function _do_update(driver, api_instance, device_service_info, bridge_netw if control_id == 1 then button_remote_description.id = button_repr.data[1].id end + + button_remote_description.sensor_list[button_id_key] = HueDeviceTypes.BUTTON end end @@ -87,7 +92,6 @@ function M.update_state_for_all_device_services(driver, api_instance, device_ser return end - log.debug("------------ _do_update") return _do_update(driver, api_instance, device_service_info.data[1], bridge_network_id, cache) end @@ -122,7 +126,7 @@ function M.handle_discovered_device( return end - local button_profile_ref = "" + local button_profile_ref -- For Philips Hue Smart Button or single switch In-Wall Switch module which contains only 1 button if button_description.num_buttons == 1 then button_profile_ref = "single-button" @@ -132,6 +136,15 @@ function M.handle_discovered_device( -- For Philips Hue Dimmer Remote and Tap Dial, which contains 4 buttons elseif button_description.num_buttons == 4 then button_profile_ref = "4-button-remote" + else + log.error( + string.format( + "Do not currently have a profile for device %s with %s buttons, skipping device create", + device_service_info.metadata.name, + button_description.num_buttons + ) + ) + return end local bridge_device = driver:get_device_by_dni(bridge_network_id) or {} diff --git a/drivers/SmartThings/philips-hue/src/disco/contact.lua b/drivers/SmartThings/philips-hue/src/disco/contact.lua index b714838bd5..ff05439dea 100644 --- a/drivers/SmartThings/philips-hue/src/disco/contact.lua +++ b/drivers/SmartThings/philips-hue/src/disco/contact.lua @@ -1,4 +1,4 @@ -local log = require "log" +local log = require "logjam" local socket = require "cosock".socket local st_utils = require "st.utils" @@ -7,6 +7,7 @@ local HueDeviceTypes = require "hue_device_types" ---@class DiscoveredContactSensorHandler: DiscoveredChildDeviceHandler local M = {} +-- TODO This should be generalizable to all "sensors", including buttons. ---@param driver HueDriver ---@param api_instance PhilipsHueApi ---@param device_service_info HueDeviceInfo @@ -15,6 +16,7 @@ local M = {} ---@return table? description nil on error ---@return string? err nil on success local function _do_update(driver, api_instance, device_service_info, bridge_network_id, cache) + log.debug("------------ _do_update") local rid_by_rtype = {} for _, svc in ipairs(device_service_info.services) do rid_by_rtype[svc.rtype] = svc.rid @@ -55,6 +57,12 @@ local function _do_update(driver, api_instance, device_service_info, bridge_netw contact_sensor_description.tamper_reports = tamper.data[1].tamper_reports end + contact_sensor_description.sensor_list = { + id = HueDeviceTypes.CONTACT, + power_id = HueDeviceTypes.DEVICE_POWER, + tamper_id = HueDeviceTypes.TAMPER + } + if type(cache) == "table" then cache[resource_id] = contact_sensor_description if device_service_info.id_v1 then @@ -80,7 +88,6 @@ function M.update_state_for_all_device_services(driver, api_instance, device_ser return end - log.debug("------------ _do_update") return _do_update(driver, api_instance, device_service_info.data[1], bridge_network_id, cache) end diff --git a/drivers/SmartThings/philips-hue/src/disco/init.lua b/drivers/SmartThings/philips-hue/src/disco/init.lua index b798c04d1d..e26578ff5d 100644 --- a/drivers/SmartThings/philips-hue/src/disco/init.lua +++ b/drivers/SmartThings/philips-hue/src/disco/init.lua @@ -47,44 +47,6 @@ local function is_device_service_supported(svc_info) return discovered_device_handlers[svc_info.rtype or ""] ~= nil end ----@param driver HueDriver ----@param bridge_network_id string ----@param primary_services table array of services that *can* map to device records. Multi-buttons wouldn't do so, but compound lights would. ----@param device_info table -local function discovered_device_callback(driver, bridge_network_id, primary_services, device_info) - local v1_dni = bridge_network_id .. "/" .. (device_info.id_v1 or "UNKNOWN"):gsub("/lights/", "") - local primary_service_type = HueDeviceTypes.determine_main_service_rtype(device_info, primary_services) - if not primary_service_type then - log.error( - string.format( - "Couldn't determine primary service type for device %s, unable to join", - (device_info.metadata.name) - ) - ) - return - end - - for _, svc_info in ipairs(primary_services[primary_service_type]) do - local v2_resource_id = svc_info.rid or "" - if driver:get_device_by_dni(v1_dni) or driver.hue_identifier_to_device_record[v2_resource_id] then return end - end - - local api_instance = HueDiscovery.disco_api_instances[bridge_network_id] - if not api_instance then - log.warn("No API instance for bridge_network_id ", bridge_network_id) - return - end - - HueDiscovery.handle_discovered_child_device( - driver, - primary_service_type, - bridge_network_id, - api_instance, - primary_services, - device_info - ) -end - -- "forward declarations" ---@param driver HueDriver ---@param bridge_ip string @@ -114,9 +76,7 @@ local function discovered_bridge_callback(driver, bridge_ip, bridge_network_id) driver, bridge_network_id, HueDiscovery.disco_api_instances[bridge_network_id], - function(hue_driver, svc_info, device_info) - discovered_device_callback(hue_driver, bridge_network_id, svc_info, device_info) - end, + HueDiscovery.handle_discovered_child_device, "[Discovery: " .. (known_bridge_device.label or bridge_network_id or known_bridge_device.id or "unknown bridge") .. " bridge scan]" @@ -244,9 +204,7 @@ function HueDiscovery.scan_bridge_and_update_devices(driver, bridge_network_id) driver, bridge_network_id, HueDiscovery.disco_api_instances[bridge_network_id], - function(hue_driver, svc_info, device_info) - discovered_device_callback(hue_driver, bridge_network_id, svc_info, device_info) - end, + HueDiscovery.handle_discovered_child_device, "[Discovery: " .. (known_bridge_device.label or bridge_network_id or known_bridge_device.id or "unknown bridge") .. " bridge re-scan]", @@ -258,7 +216,7 @@ end ---@param driver HueDriver ---@param bridge_network_id string ---@param api_instance PhilipsHueApi ----@param callback fun(driver: HueDriver, svc_info: HueServiceInfo, device_data: table) +---@param callback fun(driver: HueDriver, bridge_network_id: string, primary_services: table, device_data: table) ---@param log_prefix string? ---@param do_delete boolean? function HueDiscovery.search_bridge_for_supported_devices(driver, bridge_network_id, api_instance, callback, log_prefix, do_delete) @@ -282,41 +240,8 @@ function HueDiscovery.search_bridge_for_supported_devices(driver, bridge_network local device_is_joined_to_bridge = {} for _, device_data in ipairs(devices.data or {}) do - local primary_device_services = {} - for _, - svc_info in ipairs(device_data.services or {}) do - if is_device_service_supported(svc_info) then - driver.services_for_device_rid[device_data.id] = driver.services_for_device_rid[device_data.id] or {} - driver.services_for_device_rid[device_data.id][svc_info.rid] = svc_info.rtype - if HueDeviceTypes.can_join_device_for_service(svc_info.rtype) then - local services_for_type = primary_device_services[svc_info.rtype] or {} - table.insert(services_for_type, svc_info) - primary_device_services[svc_info.rtype] = services_for_type - device_is_joined_to_bridge[device_data.id] = true - end - end - end - if next(primary_device_services) then - if type(callback) == "function" then - log.info_with( - { hub_logs = true }, - string.format( - prefix .. - "Processing supported services [%s] for Hue device [v2_id: %s | v1_id: %s], with Hue provided name: %s", - st_utils.stringify_table(primary_device_services), device_data.id, device_data.id_v1, device_data.metadata.name - ) - ) - callback(driver, primary_device_services, device_data) - else - log.warn( - prefix .. "Argument passed in `callback` position for " - .. "`HueDiscovery.search_bridge_for_supported_devices` is not a function" - ) - end - else - log.warn(string.format("No primary services for %s", device_data.metadata.name)) - log.warn(st_utils.stringify_table(device_data.services, "services", true)) - end + device_is_joined_to_bridge[device_data.id] = + HueDiscovery.process_device_service(driver, bridge_network_id, device_data, callback, log_prefix) end if do_delete then @@ -337,12 +262,87 @@ function HueDiscovery.search_bridge_for_supported_devices(driver, bridge_network end ---@param driver HueDriver ----@param primary_service_type HueDeviceTypes ---@param bridge_network_id string ----@param api_instance PhilipsHueApi +---@param device_data HueDeviceInfo +---@param callback fun(driver: HueDriver, bridge_network_id: string, primary_services: table, device_data: table, bridge_device: HueBridgeDevice?)? +---@param log_prefix string? +---@param bridge_device HueBridgeDevice? +---@return boolean device_joined_to_bridge true if device was sent through the join process +function HueDiscovery.process_device_service(driver, bridge_network_id, device_data, callback, log_prefix, bridge_device) + local prefix = "" + if type(log_prefix) == "string" and #log_prefix > 0 then prefix = log_prefix .. " " end + local primary_device_services = {} + + local device_joined_to_bridge = false + for _, + svc_info in ipairs(device_data.services or {}) do + if is_device_service_supported(svc_info) then + driver.services_for_device_rid[device_data.id] = driver.services_for_device_rid[device_data.id] or {} + driver.services_for_device_rid[device_data.id][svc_info.rid] = svc_info.rtype + if HueDeviceTypes.can_join_device_for_service(svc_info.rtype) then + local services_for_type = primary_device_services[svc_info.rtype] or {} + table.insert(services_for_type, svc_info) + primary_device_services[svc_info.rtype] = services_for_type + device_joined_to_bridge = true + end + end + end + if next(primary_device_services) then + if type(callback) == "function" then + log.info_with( + { hub_logs = true }, + string.format( + prefix .. + "Processing supported services [%s] for Hue device [v2_id: %s | v1_id: %s], with Hue provided name: %s", + st_utils.stringify_table(primary_device_services), device_data.id, device_data.id_v1, device_data.metadata.name + ) + ) + callback(driver, bridge_network_id, primary_device_services, device_data, bridge_device) + else + log.warn( + prefix .. "Argument passed in `callback` position for " + .. "`HueDiscovery.search_bridge_for_supported_devices` is not a function" + ) + end + else + log.warn(string.format("No primary services for %s", device_data.metadata.name)) + log.warn(st_utils.stringify_table(device_data.services, "services", true)) + end + + return device_joined_to_bridge +end + +---@param driver HueDriver +---@param bridge_network_id string ---@param primary_services table array of services that *can* map to device records. Multi-buttons wouldn't do so, but compound lights would. ---@param device_info table -function HueDiscovery.handle_discovered_child_device(driver, primary_service_type, bridge_network_id, api_instance, primary_services, device_info) +---@param bridge_device HueBridgeDevice? If the parent bridge is known, it can be passed in here +function HueDiscovery.handle_discovered_child_device(driver, bridge_network_id, primary_services, device_info, bridge_device) + local v1_dni = bridge_network_id .. "/" .. (device_info.id_v1 or "UNKNOWN"):gsub("/lights/", "") + local primary_service_type = HueDeviceTypes.determine_main_service_rtype(device_info, primary_services) + if not primary_service_type then + log.error( + string.format( + "Couldn't determine primary service type for device %s, unable to join", + (device_info.metadata.name) + ) + ) + return + end + + for _, svc_info in ipairs(primary_services[primary_service_type]) do + local v2_resource_id = svc_info.rid or "" + if driver:get_device_by_dni(v1_dni) or driver.hue_identifier_to_device_record[v2_resource_id] then return end + end + + local api_instance = + (bridge_device and bridge_device:get_field(Fields.BRIDGE_API)) or + HueDiscovery.disco_api_instances[bridge_network_id] + if not api_instance then + log.warn("No API instance for bridge_network_id ", bridge_network_id) + return + end + discovered_device_handlers[primary_service_type].handle_discovered_device( driver, bridge_network_id, diff --git a/drivers/SmartThings/philips-hue/src/disco/light.lua b/drivers/SmartThings/philips-hue/src/disco/light.lua index 07dcf9a6d8..8acc6a5669 100644 --- a/drivers/SmartThings/philips-hue/src/disco/light.lua +++ b/drivers/SmartThings/philips-hue/src/disco/light.lua @@ -29,7 +29,13 @@ local function join_light(driver, light, device_service_info, parent_device_id, return end - local device_name = light.metadata.name + local device_name + if light.metadata.name == device_service_info.metadata.name then + device_name = device_service_info.metadata.name + else + device_name = string.format("%s %s", device_service_info.metadata.name, light.metadata.name) + end + local parent_assigned_child_key = string.format("%s:%s", light.type, light.id) local st_metadata = { @@ -86,16 +92,15 @@ local function handle_compound_light( ---@type HueLightInfo[] local all_lights = {} local main_light_resource_id - for idx, svc in ipairs(services) do + for _, svc in ipairs(services) do local light_resource, err, _ = api_instance:get_light_by_id(svc.rid) if not light_resource or (light_resource and #light_resource.errors > 0) or err then log.error(string.format("Couldn't get light resource for rid %s, skipping", svc.rid)) goto continue end table.insert(all_lights, light_resource.data[1]) - if light_resource.data[1].id_v1 and light_resource.data[1].id_v1 == device_service_info.id_v1 then - main_light_resource_id = light_resource.data[1].id_v1 - break + if light_resource.data[1].service_id and light_resource.data[1].service_id == 1 then + main_light_resource_id = light_resource.data[1].id end ::continue:: end @@ -123,7 +128,7 @@ local function handle_compound_light( end else table.insert(grandchild_lights, { - device = light, + waiting_resource_info = light, join_callback = function(driver, waiting_info, parent_device) get_light_state_table_and_update_cache(waiting_info, parent_device.id, device_service_info, cache) join_light( diff --git a/drivers/SmartThings/philips-hue/src/disco/motion.lua b/drivers/SmartThings/philips-hue/src/disco/motion.lua index 4ca14ccda1..e7fb077748 100644 --- a/drivers/SmartThings/philips-hue/src/disco/motion.lua +++ b/drivers/SmartThings/philips-hue/src/disco/motion.lua @@ -1,4 +1,4 @@ -local log = require "log" +local log = require "logjam" local socket = require "cosock".socket local st_utils = require "st.utils" @@ -7,6 +7,7 @@ local HueDeviceTypes = require "hue_device_types" ---@class DiscoveredMotionSensorHandler: DiscoveredChildDeviceHandler local M = {} +-- TODO This should be generalizable to all "sensors", including buttons. ---@param driver HueDriver ---@param api_instance PhilipsHueApi ---@param device_service_info HueDeviceInfo @@ -15,6 +16,7 @@ local M = {} ---@return table? description nil on error ---@return string? err nil on success local function _do_update(driver, api_instance, device_service_info, bridge_network_id, cache) + log.debug("------------ _do_update") local rid_by_rtype = {} for _, svc in ipairs(device_service_info.services) do rid_by_rtype[svc.rtype] = svc.rid @@ -65,6 +67,13 @@ local function _do_update(driver, api_instance, device_service_info, bridge_netw motion_sensor_description.light_level_enabled = illuminance.data[1].enabled end + motion_sensor_description.sensor_list = { + id = HueDeviceTypes.MOTION, + power_id = HueDeviceTypes.DEVICE_POWER, + temperature_id = HueDeviceTypes.TEMPERATURE, + light_level_id = HueDeviceTypes.LIGHT_LEVEL + } + if type(cache) == "table" then cache[resource_id] = motion_sensor_description if device_service_info.id_v1 then @@ -90,7 +99,6 @@ function M.update_state_for_all_device_services(driver, api_instance, device_ser return end - log.debug("------------ _do_update") return _do_update(driver, api_instance, device_service_info.data[1], bridge_network_id, cache) end diff --git a/drivers/SmartThings/philips-hue/src/fields.lua b/drivers/SmartThings/philips-hue/src/fields.lua index fb0acb7ade..0df3c06aec 100644 --- a/drivers/SmartThings/philips-hue/src/fields.lua +++ b/drivers/SmartThings/philips-hue/src/fields.lua @@ -22,12 +22,14 @@ local Fields = { GAMUT = "gamut", HUE_DEVICE_ID = "hue_device_id", IPV4 = "ipv4", + IS_ONLINE = "is_online", + IS_MULTI_SERVICE = "is_multi_service", MIN_DIMMING = "mindim", MIN_KELVIN = "mintemp", MODEL_ID = "modelid", - IS_ONLINE = "is_online", PARENT_DEVICE_ID = "parent_device_id_local", RESOURCE_ID = "rid", + RETRY_MIGRATION = "retry_migration", WRAPPED_HUE = "_wrapped_hue" } diff --git a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua index 5e5736a191..1604889708 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua @@ -1,5 +1,5 @@ local capabilities = require "st.capabilities" -local log = require "log" +local log = require "logjam" 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 @@ -7,7 +7,7 @@ st_utils.stringify_table = st_utils.stringify_table local Consts = require "consts" local Fields = require "fields" -local HueColorUtils = require "hue.cie_utils" +local HueColorUtils = require "utils.cie_utils" local HueDeviceTypes = require "hue_device_types" local utils = require "utils" @@ -114,11 +114,11 @@ end function AttributeEmitters.connectivity_update(child_device, zigbee_status) if zigbee_status.status == "connected" then - child_device.log.info_with({hub_logs=true}, "Light status event, marking device online") + child_device.log.info_with({hub_logs=true}, "Device zigbee status event, marking device online") child_device:online() child_device:set_field(Fields.IS_ONLINE, true) elseif zigbee_status.status == "connectivity_issue" then - child_device.log.info_with({hub_logs=true}, "Light status event, marking device offline") + child_device.log.info_with({hub_logs=true}, "Device zigbee status event, marking device offline") child_device:set_field(Fields.IS_ONLINE, false) child_device:offline() end @@ -148,6 +148,7 @@ function AttributeEmitters.emit_button_attribute_events(button_device, button_in (button_device and button_device.lable) or "unknown button" ) ) + return end local idx = button_idx_map[button_info.id] or 1 @@ -191,18 +192,18 @@ function AttributeEmitters.emit_contact_sensor_attribute_events(sensor_device, s if sensor_info.tamper_reports then log.debug(true, "emit tamper") - local num_reports = #sensor_info.tamper_reports - local not_tampered = 0 + local tampered = false for _, tamper in ipairs(sensor_info.tamper_reports) do - if tamper.state == "not_tampered" then - not_tampered = not_tampered + 1 + if tamper.state == "tampered" then + tampered = true + break end end - if not_tampered == num_reports then - sensor_device:emit_event(capabilities.tamperAlert.tamper.clear()) - else + if tampered then sensor_device:emit_event(capabilities.tamperAlert.tamper.detected()) + else + sensor_device:emit_event(capabilities.tamperAlert.tamper.clear()) end end diff --git a/drivers/SmartThings/philips-hue/src/handlers/commands.lua b/drivers/SmartThings/philips-hue/src/handlers/commands.lua index 608817c5f2..7fdb337325 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/commands.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/commands.lua @@ -1,10 +1,10 @@ local capabilities = require "st.capabilities" -local log = require "log" +local log = require "logjam" local st_utils = require "st.utils" local Consts = require "consts" local Fields = require "fields" -local HueColorUtils = require "hue.cie_utils" +local HueColorUtils = require "utils.cie_utils" local utils = require "utils" @@ -304,8 +304,9 @@ local refresh_handlers = require "handlers.refresh_handlers" ---@param driver HueDriver ---@param device HueDevice ---@param cmd table? +---@return table? refreshed_device_info function CommandHandlers.refresh_handler(driver, device, cmd) - refresh_handlers.handler_for_device_type(device:get_field(Fields.DEVICE_TYPE))(driver, device, cmd) + return refresh_handlers.handler_for_device_type(device:get_field(Fields.DEVICE_TYPE))(driver, device, cmd) end return CommandHandlers diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/bridge.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/bridge.lua index 68973a4cb2..3ed6069a5c 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/bridge.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/bridge.lua @@ -1,5 +1,5 @@ local cosock = require "cosock" -local log = require "log" +local log = require "logjam" local Discovery = require "disco" local Fields = require "fields" 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 f28e07395b..e1f60af2ab 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua @@ -10,6 +10,7 @@ local HueDeviceTypes = require "hue_device_types" local StrayDeviceHelper = require "stray_device_helper" local button_disco = require "disco.button" +local hue_multi_service_device_utils = require "utils.hue_multi_service_device_utils" local utils = require "utils" ---@class ButtonLifecycleHandlers @@ -104,7 +105,7 @@ end function ButtonLifecycleHandlers.init(driver, device) log.info( string.format("Init Button for device %s", (device and device.label or device.id or "unknown button"))) - + device:set_field(Fields.IS_MULTI_SERVICE, true, { persist = true }) local device_button_resource_id = utils.get_hue_rid(device) or device.device_network_id @@ -122,13 +123,9 @@ function ButtonLifecycleHandlers.init(driver, device) local parent_bridge = utils.get_hue_bridge_for_device( driver, device, device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) ) - local api_instance = (parent_bridge and parent_bridge:get_field(Fields.BRIDGE_API)) or - Discovery.api_keys[(parent_bridge and parent_bridge.device_network_id) or ""] + local api_instance = (parent_bridge and parent_bridge:get_field(Fields.BRIDGE_API)) - if not (parent_bridge and api_instance) then - log.warn(string.format("Button %s parent bridge not ready, queuing refresh", device and device.label)) - driver._devices_pending_refresh[device.id] = device - else + if parent_bridge and api_instance then button_info, err = button_disco.update_state_for_all_device_services( driver, api_instance, @@ -150,31 +147,14 @@ function ButtonLifecycleHandlers.init(driver, device) end end end - local svc_rids_for_device = driver.services_for_device_rid[hue_device_id] or {} - if button_info and button_info.num_buttons == nil and not svc_rids_for_device[button_info.id] - then - svc_rids_for_device[button_info.id] = HueDeviceTypes.BUTTON - end - - if button_info and button_info.num_buttons then - for var = 1, (button_info.num_buttons or 1) do - local button_id_key = string.format("button%s_id", var) - local button_id = button_info[button_id_key] - svc_rids_for_device[button_id] = HueDeviceTypes.BUTTON - end - end - - if button_info and not svc_rids_for_device[button_info.power_id] then - svc_rids_for_device[button_info.power_id] = HueDeviceTypes.DEVICE_POWER - end - - driver.services_for_device_rid[hue_device_id] = svc_rids_for_device - log.debug(st_utils.stringify_table(driver.services_for_device_rid[hue_device_id], "svcs for device rid", true)) - for rid, _ in pairs(driver.services_for_device_rid[hue_device_id]) do - log.debug(string.format("Button %s mapping to [%s]", device.label, rid)) - driver.hue_identifier_to_device_record[rid] = device + if not button_info then + log.warn(string.format("Button %s parent bridge not ready, queuing refresh", device and device.label)) + driver._devices_pending_refresh[device.id] = device + else + hue_multi_service_device_utils.update_multi_service_device_maps( + driver, device, hue_device_id, button_info, HueDeviceTypes.BUTTON + ) end - device:set_field(Fields._INIT, true, { persist = false }) if device:get_field(Fields._REFRESH_AFTER_INIT) then refresh_handler(driver, device) 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 0b03f845ed..64be383c56 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/contact.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/contact.lua @@ -1,4 +1,4 @@ -local log = require "log" +local log = require "logjam" local st_utils = require "st.utils" local refresh_handler = require("handlers.commands").refresh_handler @@ -9,6 +9,7 @@ local HueDeviceTypes = require "hue_device_types" local StrayDeviceHelper = require "stray_device_helper" local contact_sensor_disco = require "disco.contact" +local hue_multi_service_device_utils = require "utils.hue_multi_service_device_utils" local utils = require "utils" ---@class ContactLifecycleHandlers @@ -66,7 +67,7 @@ end function ContactLifecycleHandlers.init(driver, device) log.info( string.format("Init Contact Sensor for device %s", (device and device.label or device.id or "unknown sensor"))) - + device:set_field(Fields.IS_MULTI_SERVICE, true, { persist = true }) local device_sensor_resource_id = utils.get_hue_rid(device) or device.device_network_id @@ -84,12 +85,9 @@ function ContactLifecycleHandlers.init(driver, device) local parent_bridge = utils.get_hue_bridge_for_device( driver, device, device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) ) - local api_instance = (parent_bridge and parent_bridge:get_field(Fields.BRIDGE_API)) or Discovery.api_keys[(parent_bridge and parent_bridge.device_network_id) or ""] + local api_instance = (parent_bridge and parent_bridge:get_field(Fields.BRIDGE_API)) - if not (parent_bridge and api_instance) then - log.warn(string.format("Contact Sensor %s parent bridge not ready, queuing refresh", device and device.label)) - driver._devices_pending_refresh[device.id] = device - else + if parent_bridge and api_instance then sensor_info, err = contact_sensor_disco.update_state_for_all_device_services( driver, api_instance, @@ -111,22 +109,14 @@ function ContactLifecycleHandlers.init(driver, device) end end end - sensor_info = sensor_info - local svc_rids_for_device = driver.services_for_device_rid[hue_device_id] or {} - if sensor_info and not svc_rids_for_device[sensor_info.id] then - svc_rids_for_device[sensor_info.id] = HueDeviceTypes.CONTACT - end - if sensor_info and not svc_rids_for_device[sensor_info.power_id] then - svc_rids_for_device[sensor_info.power_id] = HueDeviceTypes.DEVICE_POWER - end - if sensor_info and not svc_rids_for_device[sensor_info.tamper_id] then - svc_rids_for_device[sensor_info.tamper_id] = HueDeviceTypes.TAMPER - end - driver.services_for_device_rid[hue_device_id] = svc_rids_for_device - for rid, _ in pairs(driver.services_for_device_rid[hue_device_id]) do - driver.hue_identifier_to_device_record[rid] = device + if not sensor_info then + log.warn(string.format("Contact Sensor %s parent bridge not ready, queuing refresh", device and device.label)) + driver._devices_pending_refresh[device.id] = device + else + hue_multi_service_device_utils.update_multi_service_device_maps( + driver, device, hue_device_id, sensor_info, HueDeviceTypes.CONTACT + ) end - device:set_field(Fields._INIT, true, { persist = false }) if device:get_field(Fields._REFRESH_AFTER_INIT) then refresh_handler(driver, device) diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua similarity index 81% rename from drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers.lua rename to drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua index e558389a4c..a6d272b7b5 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua @@ -1,4 +1,4 @@ -local log = require "log" +local log = require "logjam" local st_utils = require "st.utils" local Discovery = require "disco" @@ -9,6 +9,31 @@ local StrayDeviceHelper = require "stray_device_helper" local utils = require "utils" +local function check_parent_assigned_child_key(device) + local device_type = utils.determine_device_type(device) + local device_rid = utils.get_hue_rid(device) + + if type(device_type) == "string" and type(device_rid) == "string" then + local expected_parent_assigned_child_key = string.format("%s:%s", device_type, device_rid) + if expected_parent_assigned_child_key ~= device.parent_assigned_child_key then + log.debug( + string.format( + "\n\nDevice [%s] had parent-assigned child key of %s but expected %s, requesting metadata update\n\n", + (device and device.label) or "unknown device", + device.parent_assigned_child_key, + expected_parent_assigned_child_key + ) + ) + -- updating parent_assigned_child_key in metadata isn't supported + -- on Lua Libs API versions before 11. + if require("version").api <= 10 then + return + end + device:try_update_metadata({ parent_assigned_child_key = expected_parent_assigned_child_key }) + end + end +end + -- Lazy-load the lifecycle handlers so we only load the code we need local inner_handlers = utils.lazy_handler_loader("handlers.lifecycle_handlers") @@ -61,13 +86,7 @@ function LifecycleHandlers.device_added(driver, device, ...) local key = parent_bridge and parent_bridge:get_field(HueApi.APPLICATION_KEY_HEADER) local bridge_ip = parent_bridge and parent_bridge:get_field(Fields.IPV4) local bridge_id = parent_bridge and parent_bridge:get_field(Fields.BRIDGE_ID) - log.trace(true, - st_utils.stringify_table( - {parent_bridge and parent_bridge.label, key, bridge_ip, bridge_id}, - "device added bridge deets", - true - ) - ) + if not (bridge_ip and bridge_id and resource_state_known and (Discovery.api_keys[bridge_id or {}] or key)) then log.warn(true, "Found \"stray\" bulb without associated Hue Bridge. Waiting to see if a bridge becomes available.") @@ -132,8 +151,14 @@ function LifecycleHandlers.initialize_device(driver, device, event, _args, ...) driver.datastore.dni_to_device_id[device.device_network_id] = device.id end + if device:get_field(Fields.RETRY_MIGRATION) then + LifecycleHandlers.migrate_device(driver, device, ...) + return + end + log.info( string.format("_initialize handling event %s for device %s", event, (device.label or device.id or "unknown device"))) + if not device:get_field(Fields._ADDED) then log.debug( string.format( @@ -148,6 +173,9 @@ function LifecycleHandlers.initialize_device(driver, device, event, _args, ...) "_INIT for device %s not set while _initialize is handling %s, performing device init lifecycle operations", (device.label or device.id or "unknown device"), event)) LifecycleHandlers.device_init(driver, device, ...) + if not utils.is_bridge(driver, device) then + check_parent_assigned_child_key(device) + end end end 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 42402bf106..f69ef08f36 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua @@ -1,4 +1,4 @@ -local log = require "log" +local log = require "logjam" local refresh_handler = require("handlers.commands").refresh_handler local st_utils = require "st.utils" 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 9f03a3ff30..3c04253da2 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/motion.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/motion.lua @@ -1,4 +1,4 @@ -local log = require "log" +local log = require "logjam" local st_utils = require "st.utils" local refresh_handler = require("handlers.commands").refresh_handler @@ -9,6 +9,7 @@ local HueDeviceTypes = require "hue_device_types" local StrayDeviceHelper = require "stray_device_helper" local motion_sensor_disco = require "disco.motion" +local hue_multi_service_device_utils = require "utils.hue_multi_service_device_utils" local utils = require "utils" ---@class MotionLifecycleHandlers @@ -67,7 +68,7 @@ end function MotionLifecycleHandlers.init(driver, device) log.info( string.format("Init Motion Sensor for device %s", (device and device.label or device.id or "unknown sensor"))) - + device:set_field(Fields.IS_MULTI_SERVICE, true, { persist = true }) local device_sensor_resource_id = utils.get_hue_rid(device) or device.device_network_id @@ -85,12 +86,9 @@ function MotionLifecycleHandlers.init(driver, device) local parent_bridge = utils.get_hue_bridge_for_device( driver, device, device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) ) - local api_instance = (parent_bridge and parent_bridge:get_field(Fields.BRIDGE_API)) or Discovery.api_keys[(parent_bridge and parent_bridge.device_network_id) or ""] + local api_instance = (parent_bridge and parent_bridge:get_field(Fields.BRIDGE_API)) - if not (parent_bridge and api_instance) then - log.warn(string.format("Motion Sensor %s parent bridge not ready, queuing refresh", device and device.label)) - driver._devices_pending_refresh[device.id] = device - else + if parent_bridge and api_instance then log.debug("--------------------- update all start") sensor_info, err = motion_sensor_disco.update_state_for_all_device_services( driver, @@ -114,25 +112,14 @@ function MotionLifecycleHandlers.init(driver, device) end end end - sensor_info = sensor_info - local svc_rids_for_device = driver.services_for_device_rid[hue_device_id] or {} - if sensor_info and not svc_rids_for_device[sensor_info.id] then - svc_rids_for_device[sensor_info.id] = HueDeviceTypes.MOTION - end - if sensor_info and not svc_rids_for_device[sensor_info.power_id] then - svc_rids_for_device[sensor_info.power_id] = HueDeviceTypes.DEVICE_POWER - end - if sensor_info and not svc_rids_for_device[sensor_info.temperature_id] then - svc_rids_for_device[sensor_info.temperature_id] = HueDeviceTypes.TEMPERATURE - end - if sensor_info and not svc_rids_for_device[sensor_info.light_level_id] then - svc_rids_for_device[sensor_info.light_level_id] = HueDeviceTypes.LIGHT_LEVEL - end - driver.services_for_device_rid[hue_device_id] = svc_rids_for_device - for rid, _ in pairs(driver.services_for_device_rid[hue_device_id]) do - driver.hue_identifier_to_device_record[rid] = device + if not sensor_info then + log.warn(string.format("Motion Sensor %s parent bridge not ready, queuing refresh", device and device.label)) + driver._devices_pending_refresh[device.id] = device + else + hue_multi_service_device_utils.update_multi_service_device_maps( + driver, device, hue_device_id, sensor_info, HueDeviceTypes.CONTACT + ) end - log.debug(st_utils.stringify_table(driver.hue_identifier_to_device_record, "hue_ids_map", true)) device:set_field(Fields._INIT, true, { persist = false }) if device:get_field(Fields._REFRESH_AFTER_INIT) then refresh_handler(driver, device) diff --git a/drivers/SmartThings/philips-hue/src/handlers/migration_handlers/bridge.lua b/drivers/SmartThings/philips-hue/src/handlers/migration_handlers/bridge.lua index d1633ec6f1..7b74668bbf 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/migration_handlers/bridge.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/migration_handlers/bridge.lua @@ -1,5 +1,5 @@ local cosock = require "cosock" -local log = require "log" +local log = require "logjam" local Discovery = require "disco" local HueApi = require "hue.api" 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 819085ca8c..9b66522445 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/migration_handlers/light.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/migration_handlers/light.lua @@ -1,5 +1,5 @@ local capabilities = require "st.capabilities" -local log = require "log" +local log = require "logjam" local st_utils = require "st.utils" local Discovery = require "disco" @@ -68,6 +68,7 @@ function LightMigrationHandler.migrate(driver, device, lifecycle_handlers, paren driver.joined_bridges[bridge_id], bridge_dni )) + device:set_field(Fields.RETRY_MIGRATION, true, { persist = false }) driver.stray_device_tx:send({ type = StrayDeviceHelper.MessageTypes.NewStrayDevice, driver = driver, @@ -139,6 +140,7 @@ function LightMigrationHandler.migrate(driver, device, lifecycle_handlers, paren vendor_provided_label = light_resource.hue_device_data.product_data.product_name, } device:try_update_metadata(new_metadata) + device:set_field(Fields.RETRY_MIGRATION, false, { persist = false }) log.info(string.format( "Migration to CLIPV2 for %s complete, going through onboarding flow again", diff --git a/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua b/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua index f733a77469..cfa6a721d6 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua @@ -1,5 +1,5 @@ local cosock = require "cosock" -local log = require "log" +local log = require "logjam" local st_utils = require "st.utils" local Fields = require "fields" @@ -85,7 +85,7 @@ function RefreshHandlers.do_refresh_all_for_bridge(driver, bridge_device) local child_devices = bridge_device:get_child_list() if not bridge_device:get_field(Fields._INIT) then - log.warn("Bridge for lights not yet initialized, can't refresh yet.") + log.warn("Bridge for devices not yet initialized, can't refresh yet.") return end @@ -153,18 +153,23 @@ end -- TODO: [Rule of three](https://en.wikipedia.org/wiki/Rule_of_three_(computer_programming)), this can be generalized. -- At this point I'm pretty confident that we can actually just have a single generic -- "refresh device" function and a "refresh all devices" function. +---@param driver HueDriver +---@param button_device HueChildDevice +---@param _ any +---@param skip_zigbee boolean +---@return table? function RefreshHandlers.do_refresh_button(driver, button_device, _, skip_zigbee) local hue_device_id = button_device:get_field(Fields.HUE_DEVICE_ID) local bridge_id = button_device.parent_device_id or button_device:get_field(Fields.PARENT_DEVICE_ID) local bridge_device = utils.get_hue_bridge_for_device(driver, button_device, bridge_id) if not bridge_device then - log.warn("Couldn't get Hue bridge for light " .. (button_device.label or button_device.id or "unknown device")) + log.warn("Couldn't get Hue bridge for button device " .. (button_device.label or button_device.id or "unknown device")) return end if not bridge_device:get_field(Fields._INIT) then - log.warn("Bridge for light not yet initialized, can't refresh yet.") + log.warn("Bridge for button device not yet initialized, can't refresh yet.") driver._devices_pending_refresh[button_device.id] = button_device return end @@ -180,21 +185,27 @@ function RefreshHandlers.do_refresh_button(driver, button_device, _, skip_zigbee end attribute_emitters.emitter_for_device_type(HueDeviceTypes.BUTTON)(button_device, sensor_info) + return sensor_info end -- TODO: Refresh handlers need to be optimized/generalized for devices with multiple services +---@param driver HueDriver +---@param sensor_device HueChildDevice +---@param _ any +---@param skip_zigbee boolean +---@return table? function RefreshHandlers.do_refresh_motion_sensor(driver, sensor_device, _, skip_zigbee) local hue_device_id = sensor_device:get_field(Fields.HUE_DEVICE_ID) local bridge_id = sensor_device.parent_device_id or sensor_device:get_field(Fields.PARENT_DEVICE_ID) local bridge_device = utils.get_hue_bridge_for_device(driver, sensor_device, bridge_id) if not bridge_device then - log.warn("Couldn't get Hue bridge for light " .. (sensor_device.label or sensor_device.id or "unknown device")) + log.warn("Couldn't get Hue bridge for motion_sensor " .. (sensor_device.label or sensor_device.id or "unknown device")) return end if not bridge_device:get_field(Fields._INIT) then - log.warn("Bridge for light not yet initialized, can't refresh yet.") + log.warn("Bridge for motion_sensor not yet initialized, can't refresh yet.") driver._devices_pending_refresh[sensor_device.id] = sensor_device return end @@ -210,20 +221,26 @@ function RefreshHandlers.do_refresh_motion_sensor(driver, sensor_device, _, skip end attribute_emitters.emitter_for_device_type(HueDeviceTypes.MOTION)(sensor_device, sensor_info) + return sensor_info end +---@param driver HueDriver +---@param sensor_device HueChildDevice +---@param _ any +---@param skip_zigbee boolean +---@return table? function RefreshHandlers.do_refresh_contact_sensor(driver, sensor_device, _, skip_zigbee) local hue_device_id = sensor_device:get_field(Fields.HUE_DEVICE_ID) local bridge_id = sensor_device.parent_device_id or sensor_device:get_field(Fields.PARENT_DEVICE_ID) local bridge_device = utils.get_hue_bridge_for_device(driver, sensor_device, bridge_id) if not bridge_device then - log.warn("Couldn't get Hue bridge for light " .. (sensor_device.label or sensor_device.id or "unknown device")) + log.warn("Couldn't get Hue bridge for contact sensor " .. (sensor_device.label or sensor_device.id or "unknown device")) return end if not bridge_device:get_field(Fields._INIT) then - log.warn("Bridge for light not yet initialized, can't refresh yet.") + log.warn("Bridge for contact sensor not yet initialized, can't refresh yet.") driver._devices_pending_refresh[sensor_device.id] = sensor_device return end @@ -239,12 +256,14 @@ function RefreshHandlers.do_refresh_contact_sensor(driver, sensor_device, _, ski end attribute_emitters.emitter_for_device_type(HueDeviceTypes.CONTACT)(sensor_device, sensor_info) + return sensor_info end ---@param driver HueDriver ---@param light_device HueChildDevice ---@param light_status_cache table|nil ---@param skip_zigbee boolean? +---@return HueLightInfo? light_info function RefreshHandlers.do_refresh_light(driver, light_device, light_status_cache, skip_zigbee) local light_resource_id = utils.get_hue_rid(light_device) local hue_device_id = light_device:get_field(Fields.HUE_DEVICE_ID) @@ -326,7 +345,7 @@ function RefreshHandlers.do_refresh_light(driver, light_device, light_status_cac light_device:set_field(Fields.GAMUT, light_info.color.gamut_type, { persist = true }) end attribute_emitters.emit_light_attribute_events(light_device, light_info) - success = true + return light_info end end end @@ -335,7 +354,7 @@ function RefreshHandlers.do_refresh_light(driver, light_device, light_status_cac if not success then cosock.socket.sleep(backoff_generator()) end - until success or count >= num_attempts + until count >= num_attempts end local function noop_refresh_handler(driver, device, ...) diff --git a/drivers/SmartThings/philips-hue/src/hue/api.lua b/drivers/SmartThings/philips-hue/src/hue/api.lua index 6e80b652da..0afb9fbb47 100644 --- a/drivers/SmartThings/philips-hue/src/hue/api.lua +++ b/drivers/SmartThings/philips-hue/src/hue/api.lua @@ -2,7 +2,7 @@ local cosock = require "cosock" local channel = require "cosock.channel" local json = require "st.json" -local log = require "log" +local log = require "logjam" local RestClient = require "lunchbox.rest" local st_utils = require "st.utils" @@ -317,8 +317,6 @@ function PhilipsHueApi:get_devices() return self:get_all_reprs_for_rtype("device ---@return string? err nil on success function PhilipsHueApi:get_connectivity_status() return self:get_all_reprs_for_rtype("zigbee_connectivity") end -function PhilipsHueApi:get_rooms() return self:get_all_reprs_for_rtype("room") end - ---@param light_resource_id string ---@return HueResourceResponse? ---@return string? err nil on success @@ -344,54 +342,49 @@ end ---@return HueResourceResponse? ---@return string? err nil on success function PhilipsHueApi:get_button_by_id(button_resource_id) - return self:get_rtype_by_rid("button", button_resource_id) + return self:get_rtype_by_rid(HueDeviceTypes.BUTTON, button_resource_id) end ---@param contact_resource_id string ---@return HueResourceResponse? ---@return string? err nil on success function PhilipsHueApi:get_contact_by_id(contact_resource_id) - return self:get_rtype_by_rid("contact", contact_resource_id) + return self:get_rtype_by_rid(HueDeviceTypes.CONTACT, contact_resource_id) end ---@param motion_resource_id string ---@return HueResourceResponse? ---@return string? err nil on success function PhilipsHueApi:get_motion_by_id(motion_resource_id) - return self:get_rtype_by_rid("motion", motion_resource_id) + return self:get_rtype_by_rid(HueDeviceTypes.MOTION, motion_resource_id) end ---@param device_power_resource_id string ---@return HueResourceResponse? ---@return string? err nil on success function PhilipsHueApi:get_device_power_by_id(device_power_resource_id) - return self:get_rtype_by_rid("device_power", device_power_resource_id) + return self:get_rtype_by_rid(HueDeviceTypes.DEVICE_POWER, device_power_resource_id) end ---@param tamper_resource_id string ---@return HueResourceResponse? ---@return string? err nil on success function PhilipsHueApi:get_tamper_by_id(tamper_resource_id) - return self:get_rtype_by_rid("tamper", tamper_resource_id) + return self:get_rtype_by_rid(HueDeviceTypes.TAMPER, tamper_resource_id) end ---@param temperature_resource_id string ---@return HueResourceResponse? ---@return string? err nil on success function PhilipsHueApi:get_temperature_by_id(temperature_resource_id) - return self:get_rtype_by_rid("temperature", temperature_resource_id) + return self:get_rtype_by_rid(HueDeviceTypes.TEMPERATURE, temperature_resource_id) end ---@param light_level_resource_id string ---@return HueResourceResponse? ---@return string? err nil on success function PhilipsHueApi:get_light_level_by_id(light_level_resource_id) - return self:get_rtype_by_rid("light_level", light_level_resource_id) -end - - -function PhilipsHueApi:get_room_by_id(id) - return self:get_rtype_by_rid("room", id) + return self:get_rtype_by_rid(HueDeviceTypes.LIGHT_LEVEL, light_level_resource_id) end ---@param id string @@ -399,7 +392,7 @@ end ---@return { errors: table[], [string]: any }? response json payload in response to the request, nil on error ---@return string? err error, nil on successful HTTP request but the response may indicate a problem with the request itself. function PhilipsHueApi:set_light_on_state(id, on) - local url = string.format("/clip/v2/resource/light/%s", id) + local url = string.format("/clip/v2/resource/%s/%s", HueDeviceTypes.LIGHT, id) if type(on) ~= "boolean" then if on then @@ -420,7 +413,7 @@ end ---@return string? err error, nil on successful HTTP request but the response may indicate a problem with the request itself. function PhilipsHueApi:set_light_level(id, level) if type(level) == "number" then - local url = string.format("/clip/v2/resource/light/%s", id) + local url = string.format("/clip/v2/resource/%s/%s", HueDeviceTypes.LIGHT, id) local payload_table = { dimming = { brightness = level } } return do_put(self, url, json.encode(payload_table)) @@ -439,7 +432,7 @@ function PhilipsHueApi:set_light_color_xy(id, xy_table) local y_valid = (xy_table ~= nil) and ((xy_table.y ~= nil) and (type(xy_table.y) == "number")) if x_valid and y_valid then - local url = string.format("/clip/v2/resource/light/%s", id) + local url = string.format("/clip/v2/resource/%s/%s", HueDeviceTypes.LIGHT, id) local payload = json.encode { color = { xy = xy_table }, on = { on = true } } return do_put(self, url, payload) else @@ -454,7 +447,7 @@ end ---@return string? err error, nil on successful HTTP request but the response may indicate a problem with the request itself. function PhilipsHueApi:set_light_color_temp(id, mirek) if type(mirek) == "number" then - local url = string.format("/clip/v2/resource/light/%s", id) + local url = string.format("/clip/v2/resource/%s/%s", HueDeviceTypes.LIGHT, id) local payload = json.encode { color_temperature = { mirek = mirek }, on = { on = true } } return do_put(self, url, payload) diff --git a/drivers/SmartThings/philips-hue/src/init.lua b/drivers/SmartThings/philips-hue/src/init.lua index 7372c96d2f..a0a0e21b17 100644 --- a/drivers/SmartThings/philips-hue/src/init.lua +++ b/drivers/SmartThings/philips-hue/src/init.lua @@ -29,15 +29,7 @@ local Discovery = require "disco" local HueDriverTemplate = require "hue_driver_template" --- @type HueDriver -local hue = Driver("hue", HueDriverTemplate.new_driver_template( - { - -- enable_debug = true, - -- delay_bridges = true, - -- force_stray_for_device_type = { - -- "light" - -- } - } -)) +local hue = Driver("hue", HueDriverTemplate.new_driver_template()) if hue.datastore["bridge_netinfo"] == nil then hue.datastore["bridge_netinfo"] = {} diff --git a/drivers/SmartThings/philips-hue/src/lunchbox/sse/eventsource.lua b/drivers/SmartThings/philips-hue/src/lunchbox/sse/eventsource.lua index d6d51018e1..44684d6650 100644 --- a/drivers/SmartThings/philips-hue/src/lunchbox/sse/eventsource.lua +++ b/drivers/SmartThings/philips-hue/src/lunchbox/sse/eventsource.lua @@ -5,7 +5,7 @@ local ssl = require "cosock.ssl" ---@type fun(sock: table, config: table?): table?, string? ssl.wrap = ssl.wrap -local log = require "log" +local log = require "logjam" local util = require "lunchbox.util" local Request = require "luncheon.request" local Response = require "luncheon.response" diff --git a/drivers/SmartThings/philips-hue/src/stray_device_helper.lua b/drivers/SmartThings/philips-hue/src/stray_device_helper.lua index 23cceb8c36..721a56b8de 100644 --- a/drivers/SmartThings/philips-hue/src/stray_device_helper.lua +++ b/drivers/SmartThings/philips-hue/src/stray_device_helper.lua @@ -1,5 +1,5 @@ local cosock = require "cosock" -local log = require "log" +local log = require "logjam" local st_utils = require "st.utils" local Discovery = require "disco" @@ -9,8 +9,12 @@ local HueDeviceTypes = require "hue_device_types" local utils = require "utils" +---@type { lifecycle_handlers: LifecycleHandlers } local lazy_handlers = utils.lazy_handler_loader("handlers") +---@type { [string]: DiscoveredChildDeviceHandler } +local lazy_disco_handlers = utils.lazy_handler_loader("disco") + ---@class StrayDeviceHelper local StrayDeviceHelper = {} @@ -21,6 +25,44 @@ local MessageTypes = { } StrayDeviceHelper.MessageTypes = MessageTypes +local function check_strays_for_match(hue_driver, api_instance, stray_devices, bridge_device_uuid, device_data, svc_info) + for _, stray_device in pairs(stray_devices) do + local matching_v1_id = stray_device.data and stray_device.data.bulbId and + stray_device.data.bulbId == device_data.id_v1:gsub("/lights/", "") + local matching_uuid = utils.get_hue_rid(stray_device) == svc_info.rid or + stray_device.device_network_id == svc_info.rid + + if matching_v1_id or matching_uuid then + stray_device:set_field(Fields.RESOURCE_ID, svc_info.rid, { persist = true }) + local api_key_extracted = api_instance.headers["hue-application-key"] + log.info_with({ hub_logs = true }, " ", (stray_device.label or stray_device.id or "unknown device"), + ", re-adding") + log.info_with({ hub_logs = true }, string.format( + 'Found Bridge for stray device %s, retrying onboarding flow.\n' .. + '\tMatching v1 id? %s\n' .. + '\tMatching uuid? %s\n' .. + '\tdevice DNI: %s\n' .. + '\tdevice Parent Assigned Key: %s\n' .. + '\tdevice parent device id: %s\n' .. + '\tProvided bridge_device_id: %s\n' .. + '\tAPI key cached for given bridge_device_id? %s\n' .. + '\tCached bridge device for given API key: %s\n' + , + stray_device.label, + matching_v1_id, + matching_uuid, + stray_device.device_network_id, + stray_device.parent_assigned_child_key, + stray_device.parent_device_id, + bridge_device_uuid, + (Discovery.api_keys[hue_driver:get_device_info(bridge_device_uuid).device_network_id] ~= nil), + hue_driver.api_key_to_bridge_id[api_key_extracted] + )) + break + end + end +end + ---@param driver HueDriver ---@param strays table ---@param bridge_device_uuid string @@ -36,7 +78,9 @@ function StrayDeviceHelper.process_strays(driver, strays, bridge_device_uuid) local cached_device_description = Discovery.device_state_disco_cache[device_rid] if cached_device_description then table.insert(dnis_to_remove, device.device_network_id) - lazy_handlers.lifecycle_handlers.migrate_device(driver, device, bridge_device_uuid, cached_device_description, {force_migrate_type = HueDeviceTypes.LIGHT}) + lazy_handlers.lifecycle_handlers.initialize_device( + driver, device, "added", nil, bridge_device_uuid, cached_device_description + ) end ::continue:: end @@ -46,6 +90,66 @@ function StrayDeviceHelper.process_strays(driver, strays, bridge_device_uuid) end end +---@param hue_driver HueDriver +---@param bridge_network_id string +---@param api_instance PhilipsHueApi +---@param primary_services HueServiceInfo +---@param device_data HueDeviceInfo +---@param msg_device HueDevice +---@param bridge_device_uuid string +---@param stray_devices table +function StrayDeviceHelper.discovery_callback( + hue_driver, bridge_network_id, api_instance, primary_services, + device_data, msg_device, bridge_device_uuid, stray_devices +) + for _, svc_info in pairs(primary_services) do + if not (HueDeviceTypes.can_join_device_for_service(svc_info.rtype)) then return end + local service_resource, rest_err, _ = api_instance:get_rtype_by_rid(svc_info.rtype, svc_info.rid) + if rest_err ~= nil or not service_resource then + log.error_with({ hub_logs = true }, string.format( + "Error getting device info info while processing new bridge %s", + (msg_device.label or msg_device.id or "unknown device"), rest_err + )) + return + end + + if service_resource.errors and #service_resource.errors > 0 then + log.error_with({ hub_logs = true }, "Errors found in API response:") + for idx, resource_err in ipairs(service_resource.errors) do + log.error_with({ hub_logs = true }, string.format( + "Error Number %s in get_rtype_by_rid response while onboarding bridge %s: %s", + idx, + (msg_device.label or msg_device.id or "unknown device"), + st_utils.stringify_table(resource_err) + )) + end + return + end + + if service_resource.data and #service_resource.data > 0 then + lazy_disco_handlers[svc_info.rtype].handle_discovered_device( + hue_driver, + bridge_network_id, + api_instance, + primary_services, + device_data, + Discovery.device_state_disco_cache, + nil + ) + + check_strays_for_match( + hue_driver, + api_instance, + stray_devices, + bridge_device_uuid, + device_data, + svc_info + ) + end + end + +end + --- Spawn the stray device resolution task, returning a handle to the tx side of the --- channel for controlling it. function StrayDeviceHelper.spawn() @@ -103,120 +207,41 @@ function StrayDeviceHelper.spawn() found_bridges[msg_device.id] = msg.device local bridge_device_uuid = msg_device.id - -- TODO: We can optimize around this by keeping track of whether or not this bridge - -- needs to be scanned (maybe skip scanning if there are no stray devices?) - -- - -- @doug.stephen@smartthings.com - log.info( - string.format( - "Stray devices handler notified of new bridge %s, scanning bridge", + if next(stray_devices) ~= nil then + log.info( + string.format( + "Stray devices handler notified of new bridge %s, scanning bridge", + (msg.device.label or msg.device.device_network_id or msg.device.id or "unknown bridge") + ) + ) + Discovery.search_bridge_for_supported_devices(thread_local_driver, + msg_device:get_field(Fields.BRIDGE_ID), + api_instance, + function(driver, bridge_network_id, primary_services, device_data) + StrayDeviceHelper.discovery_callback( + driver, + bridge_network_id, + api_instance, + primary_services, + device_data, + msg_device, + bridge_device_uuid, + stray_devices + ) + end, + "[process_strays]" + ) + log.info(string.format( + "Finished querying bridge %s for devices from stray devices handler", (msg.device.label or msg.device.device_network_id or msg.device.id or "unknown bridge") ) - ) - Discovery.search_bridge_for_supported_devices(thread_local_driver, msg_device:get_field(Fields.BRIDGE_ID), - api_instance, - function(hue_driver, svc_info, device_data) - if not (svc_info.rid and svc_info.rtype and svc_info.rtype == "light") then return end - - local device_light_resource_id = svc_info.rid - local light_resource, rest_err, _ = api_instance:get_light_by_id(device_light_resource_id) - if rest_err ~= nil or not light_resource then - log.error_with({ hub_logs = true }, string.format( - "Error getting light info while processing new bridge %s", - (msg_device.label or msg_device.id or "unknown device"), rest_err - )) - return - end - - if light_resource.errors and #light_resource.errors > 0 then - log.error_with({ hub_logs = true }, "Errors found in API response:") - for idx, resource_err in ipairs(light_resource.errors) do - log.error_with({ hub_logs = true }, string.format( - "Error Number %s in get_light_by_id response while onboarding bridge %s: %s", - idx, - (msg_device.label or msg_device.id or "unknown device"), - st_utils.stringify_table(resource_err) - )) - end - return - end - - if light_resource.data and #light_resource.data > 0 then - for _, light in ipairs(light_resource.data) do - ---@type HueLightInfo - local light_resource_description = { - hue_provided_name = device_data.metadata.name, - id = light.id, - on = light.on, - color = light.color, - dimming = light.dimming, - color_temperature = light.color_temperature, - mode = light.mode, - parent_device_id = bridge_device_uuid, - hue_device_id = light.owner.rid, - hue_device_data = device_data - } - if not Discovery.device_state_disco_cache[light.id] then - log.info(string.format("Caching previously unknown service description for %s", - device_data.metadata.name)) - Discovery.device_state_disco_cache[light.id] = light_resource_description - if device_data.id_v1 then - Discovery.device_state_disco_cache[device_data.id_v1] = light_resource_description - end - end - end - end - - for _, stray_device in pairs(stray_devices) do - local matching_v1_id = stray_device.data and stray_device.data.bulbId and - stray_device.data.bulbId == device_data.id_v1:gsub("/lights/", "") - local matching_uuid = utils.get_hue_rid(stray_device) == svc_info.rid or - stray_device.device_network_id == svc_info.rid - - if matching_v1_id or matching_uuid then - stray_device:set_field(Fields.RESOURCE_ID, svc_info.rid, { persist = true }) - local api_key_extracted = api_instance.headers["hue-application-key"] - log.info_with({ hub_logs = true }, " ", (stray_device.label or stray_device.id or "unknown device"), - ", re-adding") - log.info_with({ hub_logs = true }, string.format( - 'Found Bridge for stray device %s, retrying onboarding flow.\n' .. - '\tMatching v1 id? %s\n' .. - '\tMatching uuid? %s\n' .. - '\tdevice DNI: %s\n' .. - '\tdevice Parent Assigned Key: %s\n' .. - '\tdevice parent device id: %s\n' .. - '\tProvided bridge_device_id: %s\n' .. - '\tAPI key cached for given bridge_device_id? %s\n' .. - '\tCached bridge device for given API key: %s\n' - , - stray_device.label, - matching_v1_id, - matching_uuid, - stray_device.device_network_id, - stray_device.parent_assigned_child_key, - stray_device.parent_device_id, - bridge_device_uuid, - (Discovery.api_keys[hue_driver:get_device_info(bridge_device_uuid).device_network_id] ~= nil), - hue_driver.api_key_to_bridge_id[api_key_extracted] - )) - break - end - end - end, - "[process_strays]" - ) - log.info(string.format( - "Finished querying bridge %s for devices from stray devices handler", - (msg.device.label or msg.device.device_network_id or msg.device.id or "unknown bridge") - ) - ) - StrayDeviceHelper.process_strays(thread_local_driver, stray_devices, msg_device.id) + ) + StrayDeviceHelper.process_strays(thread_local_driver, stray_devices, msg_device.id) + end elseif msg.type == StrayDeviceHelper.MessageTypes.NewStrayDevice then stray_devices[msg_device.device_network_id] = msg_device - local maybe_bridge_id = - msg_device.parent_device_id or msg_device:get_field(Fields.PARENT_DEVICE_ID) - local maybe_bridge = found_bridges[maybe_bridge_id] + local maybe_bridge = utils.get_hue_bridge_for_device(thread_local_driver, msg_device) if maybe_bridge ~= nil then local bridge_ip = maybe_bridge:get_field(Fields.IPV4) diff --git a/drivers/SmartThings/philips-hue/src/hue/types.lua b/drivers/SmartThings/philips-hue/src/types.lua similarity index 99% rename from drivers/SmartThings/philips-hue/src/hue/types.lua rename to drivers/SmartThings/philips-hue/src/types.lua index ed8987ee00..d01868ab5c 100644 --- a/drivers/SmartThings/philips-hue/src/hue/types.lua +++ b/drivers/SmartThings/philips-hue/src/types.lua @@ -13,6 +13,7 @@ ---@field public metadata { name: string, [string]: any} ---@field public id string ---@field public id_v1 string? +---@field public service_id? integer ---@field public type HueDeviceTypes ---@field public owner HueServiceInfo? ---@field public hue_provided_name string diff --git a/drivers/SmartThings/philips-hue/src/hue/cie_utils.lua b/drivers/SmartThings/philips-hue/src/utils/cie_utils.lua similarity index 100% rename from drivers/SmartThings/philips-hue/src/hue/cie_utils.lua rename to drivers/SmartThings/philips-hue/src/utils/cie_utils.lua 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 0c37d15250..040e8c88f0 100644 --- a/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua +++ b/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua @@ -1,5 +1,5 @@ local cosock = require "cosock" -local log = require "log" +local log = require "logjam" local json = require "st.json" local st_utils = require "st.utils" @@ -7,12 +7,14 @@ local Discovery = require "disco" local EventSource = require "lunchbox.sse.eventsource" local Fields = require "fields" local HueApi = require "hue.api" +local HueDeviceTypes = require "hue_device_types" local StrayDeviceHelper = require "stray_device_helper" local attribute_emitters = require "handlers.attribute_emitters" local command_handlers = require "handlers.commands" local lifecycle_handlers = require "handlers.lifecycle_handlers" +local hue_multi_service_device_utils = require "utils.hue_multi_service_device_utils" local lunchbox_util = require "lunchbox.util" local utils = require "utils" @@ -162,8 +164,8 @@ function hue_bridge_utils.do_bridge_network_init(driver, bridge_device, bridge_u end end else - --- for a regular message from a light doing something normal, - --- you get the resource id of the light service for that device in + --- for a regular message from a device doing something normal, + --- you get the resource id of the device service for that device in --- the data field table.insert(resource_ids, update_data.id) end @@ -184,13 +186,13 @@ function hue_bridge_utils.do_bridge_network_init(driver, bridge_device, bridge_u end elseif event.type == "delete" then for _, delete_data in ipairs(event.data) do - if delete_data.type == "light" then + if HueDeviceTypes.can_join_device_for_service(delete_data.type) then local resource_id = delete_data.id local child_device = driver.hue_identifier_to_device_record[resource_id] if child_device ~= nil and child_device.id ~= nil then log.info( string.format( - "Light device \"%s\" was deleted from hue bridge %s", + "Device \"%s\" was deleted from hue bridge %s", (child_device.label or child_device.id or "unknown device"), (bridge_device.label or bridge_device.device_network_id or bridge_device.id or "unknown bridge") ) @@ -202,122 +204,24 @@ function hue_bridge_utils.do_bridge_network_init(driver, bridge_device, bridge_u end elseif event.type == "add" then for _, add_data in ipairs(event.data) do - if add_data.type == "light" and add_data.owner and add_data.owner.rtype == "device" then - ---@cast add_data HueLightInfo + if add_data.type == "device" then log.info( string.format( - "New light added to Hue Bridge \"%s\", light properties: \"%s\"", + "New device added to Hue Bridge \"%s\", device properties: \"%s\"", bridge_device.label, json.encode(add_data) ) ) - + --- Move handling the add off the SSE thread cosock.spawn(function() - local hue_api = bridge_device:get_field(Fields.BRIDGE_API) --[[@as PhilipsHueApi]] - if hue_api == nil then - local _log = bridge_device.log or log - _log.warn("No Hue API instance available for new light event.") - return - end - - local hue_device_rid = add_data.owner.rid - local rest_resp, rest_err = hue_api:get_device_by_id(hue_device_rid) - - if rest_err ~= nil then - log.error( - string.format( - "Error getting device information for new light \"%s\" with device RID %s: %s", - add_data.metadata.name, - hue_device_rid, - st_utils.stringify_table(rest_err) - ) - ) - return - end - - if rest_resp == nil then - log.error("REST Response while handling New Light Event unexpectedly nil without error message") - return - end - - if rest_resp.errors and #rest_resp.errors > 0 then - for _, hue_error in ipairs(rest_resp.errors) do - log.error_with({ hub_logs = true }, "Error in Hue API response: " .. hue_error.description) - end - return - end - - local new_device_info = nil - for _, hue_device in ipairs(rest_resp.data or {}) do - for _, svc_info in ipairs(hue_device.services or {}) do - if svc_info.rtype == "light" and svc_info.rid == add_data.id then - new_device_info = hue_device - break - end - end - if new_device_info ~= nil then break end - end - - if new_device_info == nil then - log.warn( - "Couldn't get all device info for new light, unable to join. Try using Scan Nearby to find new Hue lights.") - return - end - - log.info( - string.format( - "Adding light \"%s\"", - add_data.metadata.name - ) + ---@cast add_data HueDeviceInfo + Discovery.process_device_service( + driver, + bridge_device.device_network_id, + add_data, + Discovery.handle_discovered_child_device, + "New Device Event", + bridge_device ) - - local profile_ref - - if add_data.color then - if add_data.color_temperature then - profile_ref = "white-and-color-ambiance" - else - profile_ref = "legacy-color" - end - elseif add_data.color_temperature then - profile_ref = "white-ambiance" -- all color temp products support `white` (dimming) - elseif add_data.dimming then - profile_ref = "white" -- `white` refers to dimmable and includes filament bulbs - else - log.warn( - string.format( - "Light resource [%s] does not seem to be A White/White-Ambiance/White-Color-Ambiance device, currently unsupported" - , - add_data.id - ) - ) - return - end - - local create_device_msg = { - type = "EDGE_CHILD", - label = add_data.metadata.name, - vendor_provided_label = new_device_info.product_data.product_name, - profile = profile_ref, - manufacturer = new_device_info.product_data.manufacturer_name, - model = new_device_info.product_data.model_id, - parent_device_id = bridge_device.id, - parent_assigned_child_key = string.format("%s:%s", add_data.type, add_data.id) - } - - Discovery.device_state_disco_cache[add_data.id] = { - hue_provided_name = add_data.metadata.name, - id = add_data.id, - on = add_data.on, - color = add_data.color, - dimming = add_data.dimming, - color_temperature = add_data.color_temperature, - mode = add_data.mode, - parent_device_id = bridge_device.id, - hue_device_id = add_data.owner.rid, - hue_device_data = new_device_info - } - - driver:try_create_device(create_device_msg) end, "New Device Event Task") end end @@ -331,10 +235,18 @@ function hue_bridge_utils.do_bridge_network_init(driver, bridge_device, bridge_u bridge_device:set_field(Fields._INIT, true, { persist = false }) local ids_to_remove = {} for id, device in ipairs(driver._devices_pending_refresh) do - local bridge_id = device.parent_device_id or bridge_device:get_field(Fields.PARENT_DEVICE_ID) + 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 table.insert(ids_to_remove, id) - command_handlers.refresh_handler(driver, device) + local refresh_info = command_handlers.refresh_handler(driver, device) + if refresh_info and device:get_field(Fields.IS_MULTI_SERVICE) then + local hue_device_type = utils.determine_device_type(device) + local hue_device_id = device:get_field(Fields.HUE_DEVICE_ID) + hue_multi_service_device_utils.update_multi_service_device_maps( + driver, device, hue_device_id, refresh_info, hue_device_type + ) + end end end for _, id in ipairs(ids_to_remove) do diff --git a/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils.lua b/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils.lua deleted file mode 100644 index ee959a1c93..0000000000 --- a/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils.lua +++ /dev/null @@ -1,18 +0,0 @@ -local utils = require "utils" - -local lazy_disco_handlers = utils.lazy_handler_loader("disco") ----@class MultiServiceDeviceUtils -local multi_service_device_utils = {} - ----@param driver HueDriver ----@param sensor_device_type HueDeviceTypes ----@param api_instance PhilipsHueApi ----@param device_service_id string ----@param bridge_network_id string ----@return table? nil on error ----@return string? err nil on success -function multi_service_device_utils.get_all_service_states(driver, sensor_device_type, api_instance, device_service_id, bridge_network_id) - return lazy_disco_handlers[sensor_device_type].update_state_for_all_device_services(driver, api_instance, device_service_id, bridge_network_id) -end - -return multi_service_device_utils diff --git a/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/init.lua b/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/init.lua new file mode 100644 index 0000000000..ccb0153510 --- /dev/null +++ b/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/init.lua @@ -0,0 +1,45 @@ +local log = require "log" +local utils = require "utils" + +local HueDeviceTypes = require "hue_device_types" + +local lazy_disco_handlers = utils.lazy_handler_loader("disco") +local lazy_map_helpers = utils.lazy_handler_loader("utils.hue_multi_service_device_utils") + +local lookup_transforms = { + [HueDeviceTypes.MOTION] = "sensor", + [HueDeviceTypes.CONTACT] = "sensor", + [HueDeviceTypes.BUTTON] = "sensor" +} + +---@class MultiServiceDeviceUtils +local multi_service_device_utils = {} + +-- TODO refactor this to be generalized for all sensors, similar to the multi service map update. +---@param driver HueDriver +---@param sensor_device_type HueDeviceTypes +---@param api_instance PhilipsHueApi +---@param device_service_id string +---@param bridge_network_id string +---@return table? nil on error +---@return string? err nil on success +function multi_service_device_utils.get_all_service_states(driver, sensor_device_type, api_instance, device_service_id, bridge_network_id) + return lazy_disco_handlers[sensor_device_type].update_state_for_all_device_services(driver, api_instance, device_service_id, bridge_network_id) +end + +function multi_service_device_utils.update_multi_service_device_maps(driver, device, hue_device_id, device_info, device_type) + device_type = device_type or utils.determine_device_type(device) + device_type = lookup_transforms[device_type] or device_type + if not lazy_map_helpers[device_type] then + log.warn( + string.format( + "No multi-service device mapping helper for device %s with type %s", + (device and device.label) or "unknown device", + device_type or "unknown type" + ) + ) + end + return lazy_map_helpers[device_type].update_multi_service_device_maps(driver, device, hue_device_id, device_info) +end + +return multi_service_device_utils diff --git a/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/sensor.lua b/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/sensor.lua new file mode 100644 index 0000000000..a8df7520d6 --- /dev/null +++ b/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/sensor.lua @@ -0,0 +1,23 @@ +local SensorMultiServiceHelper = {} +function SensorMultiServiceHelper.update_multi_service_device_maps(driver, device, hue_device_id, sensor_info) + local svc_rids_for_device = driver.services_for_device_rid[hue_device_id] or {} + + if type(sensor_info.sensor_list) == "table" then + for id_key, sensor_type in pairs(sensor_info.sensor_list) do + if + sensor_info and + sensor_info[id_key] and + not svc_rids_for_device[sensor_info[id_key]] + then + svc_rids_for_device[sensor_info[id_key]] = sensor_type + end + end + end + + driver.services_for_device_rid[hue_device_id] = svc_rids_for_device + for rid, _ in pairs(driver.services_for_device_rid[hue_device_id]) do + driver.hue_identifier_to_device_record[rid] = device + end +end + +return SensorMultiServiceHelper diff --git a/drivers/SmartThings/philips-hue/src/utils.lua b/drivers/SmartThings/philips-hue/src/utils/init.lua similarity index 97% rename from drivers/SmartThings/philips-hue/src/utils.lua rename to drivers/SmartThings/philips-hue/src/utils/init.lua index 34d9a03420..ab269c3e33 100644 --- a/drivers/SmartThings/philips-hue/src/utils.lua +++ b/drivers/SmartThings/philips-hue/src/utils/init.lua @@ -1,4 +1,4 @@ -local log = require "log" +local log = require "logjam" local Fields = require "fields" local HueDeviceTypes = require "hue_device_types" @@ -51,10 +51,6 @@ function utils.safe_wrap_handler(handler) end end -function utils.kelvin_to_mirek(kelvin) return 1000000 / kelvin end - -function utils.mirek_to_kelvin(mirek) return 1000000 / mirek end - -- TODO: The Hue API itself doesn't have events for multipresses, however, it will -- emit batched "short release" eventsource on the SSE stream if they're close together. -- Right now the SSE stream handling is a relatively dumb pass-through that doesn't inspect @@ -72,6 +68,10 @@ function utils.get_supported_button_values(event_values) return values end +function utils.kelvin_to_mirek(kelvin) return 1000000 / kelvin end + +function utils.mirek_to_kelvin(mirek) return 1000000 / mirek end + function utils.str_starts_with(str, start) return str:sub(1, #start) == start end @@ -300,19 +300,6 @@ function utils.is_edge_bridge(device) not (device.data and device.data.username) end ---- Only checked during `added` callback, or as a later ---- fallback check in the chain of booleans used in `is_bridge`. ---- ----@see utils.is_bridge ----@param device HueDevice ----@return boolean -function utils.is_edge_light(device) - return - device.parent_assigned_child_key ~= nil and - not utils.is_valid_mac_addr_string(device.device_network_id) and - not (device.data and device.data.username and device.data.bulbId) -end - --- Only checked during `added` callback ---@param device HueDevice ---@return boolean