From 4cd2d21906d5967b09f10d185cdde8feeea27ecc Mon Sep 17 00:00:00 2001 From: Kiera Robinson Date: Sat, 27 Apr 2024 14:00:56 -0500 Subject: [PATCH 1/5] Added support for the Philips Hue Smart Plug Philips Hue Smart Plug is exposed as a 'light' service so existing light code was modified to include the case for Philips Hue Smart Plug new device adoption and use --- drivers/SmartThings/philips-hue/profiles/plug.yml | 12 ++++++++++++ drivers/SmartThings/philips-hue/src/disco/light.lua | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/philips-hue/profiles/plug.yml diff --git a/drivers/SmartThings/philips-hue/profiles/plug.yml b/drivers/SmartThings/philips-hue/profiles/plug.yml new file mode 100644 index 0000000000..e5901cff1b --- /dev/null +++ b/drivers/SmartThings/philips-hue/profiles/plug.yml @@ -0,0 +1,12 @@ +name: plug +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: samsungim.hueSyncMode + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartPlug diff --git a/drivers/SmartThings/philips-hue/src/disco/light.lua b/drivers/SmartThings/philips-hue/src/disco/light.lua index 5bade096c4..243a2417d9 100644 --- a/drivers/SmartThings/philips-hue/src/disco/light.lua +++ b/drivers/SmartThings/philips-hue/src/disco/light.lua @@ -64,10 +64,12 @@ function M.handle_discovered_device( profile_ref = "white-ambiance" -- all color temp products support `white` (dimming) elseif light.dimming then profile_ref = "white" -- `white` refers to dimmable and includes filament bulbs + elseif light.on then -- Case for plug which uses same category as 'light' + profile_ref = "plug" else log.warn( string.format( - "Light resource [%s] does not seem to be A White/White-Ambiance/White-Color-Ambiance device, currently unsupported" + "Light resource [%s] does not seem to be A White/White-Ambiance/White-Color-Ambiance/Plug device, currently unsupported" , resource_id ) From e297d71d7e4fa14918c937e14c006ce313a6170e Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Sat, 27 Apr 2024 14:00:56 -0500 Subject: [PATCH 2/5] Adds support for 4-button Hue remote accessories This is the Hue Dimmer Remote and the Hue Tap Dial. Note that we do *not* yet have support for the rotary portion of the tap dial. --- .../philips-hue/profiles/4-button-remote.yml | 30 +++ .../philips-hue/src/disco/button.lua | 148 ++++++++++++++ .../philips-hue/src/disco/contact.lua | 4 +- .../philips-hue/src/disco/init.lua | 2 +- .../philips-hue/src/disco/motion.lua | 4 +- .../SmartThings/philips-hue/src/fields.lua | 1 + .../src/handlers/attribute_emitters.lua | 57 +++++- .../handlers/lifecycle_handlers/button.lua | 185 ++++++++++++++++++ .../handlers/lifecycle_handlers/contact.lua | 4 +- .../handlers/lifecycle_handlers/motion.lua | 4 +- .../src/handlers/refresh_handlers.lua | 40 +++- .../SmartThings/philips-hue/src/hue/api.lua | 7 + .../SmartThings/philips-hue/src/hue/types.lua | 4 + .../philips-hue/src/hue_device_types.lua | 10 + .../philips-hue/src/stray_device_helper.lua | 2 +- drivers/SmartThings/philips-hue/src/utils.lua | 17 ++ .../src/utils/hue_bridge_utils.lua | 3 + .../src/utils/hue_motion_sensor_utils.lua | 17 -- .../utils/hue_multi_service_device_utils.lua | 17 ++ 19 files changed, 524 insertions(+), 32 deletions(-) create mode 100644 drivers/SmartThings/philips-hue/profiles/4-button-remote.yml create mode 100644 drivers/SmartThings/philips-hue/src/disco/button.lua create mode 100644 drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua delete mode 100644 drivers/SmartThings/philips-hue/src/utils/hue_motion_sensor_utils.lua create mode 100644 drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils.lua diff --git a/drivers/SmartThings/philips-hue/profiles/4-button-remote.yml b/drivers/SmartThings/philips-hue/profiles/4-button-remote.yml new file mode 100644 index 0000000000..e5beb3872e --- /dev/null +++ b/drivers/SmartThings/philips-hue/profiles/4-button-remote.yml @@ -0,0 +1,30 @@ +name: 4-button-remote +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/philips-hue/src/disco/button.lua b/drivers/SmartThings/philips-hue/src/disco/button.lua new file mode 100644 index 0000000000..ceacf2e8ff --- /dev/null +++ b/drivers/SmartThings/philips-hue/src/disco/button.lua @@ -0,0 +1,148 @@ +local log = require "logjam" +local socket = require "cosock".socket +local st_utils = require "st.utils" + +local HueDeviceTypes = require "hue_device_types" + +---@class DiscoveredButtonHandler: DiscoveredChildDeviceHandler +local M = {} + +---@param api_instance PhilipsHueApi +---@param device_service_info HueDeviceInfo +---@param bridge_id string +---@param resource_id string +---@param cache table? +---@return table? description nil on error +---@return string? err nil on success +local function _do_update(api_instance, device_service_info, bridge_id, resource_id, cache) + local rid_by_rtype = {} + local button_services = {} + local num_buttons = 0 + + for _, svc in ipairs(device_service_info.services) do + if svc.rtype == HueDeviceTypes.BUTTON then + num_buttons = num_buttons + 1 + table.insert(button_services, svc.rid) + else + rid_by_rtype[svc.rtype] = svc.rid + end + end + + local button_remote_description = { + hue_provided_name = device_service_info.metadata.name, + parent_device_id = bridge_id, + hue_device_id = device_service_info.id, + hue_device_data = device_service_info, + id = resource_id, + num_buttons = num_buttons + } + + for _, button_rid in ipairs(button_services) do + local button_repr, err = api_instance:get_button_by_id(button_rid) + if err or not button_repr then + log.error("Error getting button representation: " .. tostring(err or "unknown error")) + else + local control_id = button_repr.data[1].metadata.control_id + local button_key = string.format("button%s", control_id) + local button_id_key = string.format("%s_id", button_key) + button_remote_description[button_key] = button_repr.data[1].button + button_remote_description[button_id_key] = button_repr.data[1].id + + if control_id == 1 and button_remote_description.id == nil then + button_remote_description.id = button_repr.data[1].id + end + end + end + + local battery, battery_err = api_instance:get_device_power_by_id(rid_by_rtype[HueDeviceTypes.DEVICE_POWER]) + if battery_err then return nil, battery_err end + + if battery and battery.data and battery.data[1] then + button_remote_description.power_id = battery.data[1].id + button_remote_description.power_state = battery.data[1].power_state + end + + if type(cache) == "table" then + cache[resource_id] = button_remote_description + if device_service_info.id_v1 then + cache[device_service_info.id_v1] = button_remote_description + end + end + + return button_remote_description +end + +---@param api_instance PhilipsHueApi +---@param device_service_id string +---@param bridge_id string +---@param primary_button_resource_id string +---@param cache table? +---@return table? description nil on error +---@return string? err nil on success +function M.update_state_for_all_device_services(api_instance, device_service_id, bridge_id, primary_button_resource_id, cache) + log.debug("----------- Calling REST API") + local device_service_info, err = api_instance:get_device_by_id(device_service_id) + if err or not (device_service_info and device_service_info.data) then + log.error("Couldn't get device info for button, error: " .. st_utils.stringify_table(err)) + return + end + + log.debug("------------ _do_update") + return _do_update(api_instance, device_service_info.data[1], bridge_id, primary_button_resource_id, cache) +end + +---@param driver HueDriver +---@param bridge_id string +---@param api_instance PhilipsHueApi +---@param resource_id string +---@param device_service_info HueDeviceInfo +---@param device_state_disco_cache table +---@param st_metadata_callback fun(driver: HueDriver, metadata: table)? +function M.handle_discovered_device( + driver, bridge_id, api_instance, + resource_id, device_service_info, + device_state_disco_cache, st_metadata_callback +) + local button_description, err = _do_update( + api_instance, device_service_info, bridge_id, resource_id, device_state_disco_cache + ) + if err then + log.error("Error updating contact button initial state: " .. st_utils.stringify_table(err)) + return + end + + if type(st_metadata_callback) == "function" then + if not (button_description and HueDeviceTypes.supports_button_configuration(button_description)) then + button_description = button_description or {num_buttons = "unknown"} + log.error( + string.format( + "Driver does not currently support remotes with %s buttons, cannot create device", button_description.num_buttons + ) + ) + return + end + local button_profile_ref = "" + if button_description.num_buttons == 4 then + button_profile_ref = "4-button-remote" + end + local bridge_device = driver:get_device_by_dni(bridge_id) or {} + local st_metadata = { + type = "EDGE_CHILD", + label = device_service_info.metadata.name, + vendor_provided_label = device_service_info.product_data.product_name, + profile = button_profile_ref, + manufacturer = device_service_info.product_data.manufacturer_name, + model = device_service_info.product_data.model_id, + parent_device_id = bridge_device.id, + parent_assigned_child_key = string.format("%s:%s", HueDeviceTypes.BUTTON, resource_id) + } + + log.debug(st_utils.stringify_table(st_metadata, "button create", true)) + + st_metadata_callback(driver, st_metadata) + -- rate limit ourself. + socket.sleep(0.1) + end +end + +return M diff --git a/drivers/SmartThings/philips-hue/src/disco/contact.lua b/drivers/SmartThings/philips-hue/src/disco/contact.lua index 14376dbcde..b69cd5e2db 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" @@ -69,7 +69,7 @@ end ---@param cache table? ---@return table? description nil on error ---@return string? err nil on success -function M.update_all_services_for_sensor(api_instance, device_service_id, bridge_id, cache) +function M.update_state_for_all_device_services(api_instance, device_service_id, bridge_id, cache) log.debug("----------- Calling REST API") local device_service_info, err = api_instance:get_device_by_id(device_service_id) if err or not (device_service_info and device_service_info.data) then diff --git a/drivers/SmartThings/philips-hue/src/disco/init.lua b/drivers/SmartThings/philips-hue/src/disco/init.lua index 2c048da2ce..7e3087cc36 100644 --- a/drivers/SmartThings/philips-hue/src/disco/init.lua +++ b/drivers/SmartThings/philips-hue/src/disco/init.lua @@ -270,7 +270,7 @@ function HueDiscovery.search_bridge_for_supported_devices(driver, bridge_id, api 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 + if HueDeviceTypes.can_join_device_for_service(svc_info.rtype) and primary_device_service == nil then primary_device_service = svc_info device_is_joined_to_bridge[device_data.id] = true end diff --git a/drivers/SmartThings/philips-hue/src/disco/motion.lua b/drivers/SmartThings/philips-hue/src/disco/motion.lua index cfdbd0895c..f325e91240 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" @@ -79,7 +79,7 @@ end ---@param cache table? ---@return table? description nil on error ---@return string? err nil on success -function M.update_all_services_for_sensor(api_instance, device_service_id, bridge_id, cache) +function M.update_state_for_all_device_services(api_instance, device_service_id, bridge_id, cache) log.debug("----------- Calling REST API") local device_service_info, err = api_instance:get_device_by_id(device_service_id) if err or not (device_service_info and device_service_info.data) then diff --git a/drivers/SmartThings/philips-hue/src/fields.lua b/drivers/SmartThings/philips-hue/src/fields.lua index 5eca9656f9..fb0acb7ade 100644 --- a/drivers/SmartThings/philips-hue/src/fields.lua +++ b/drivers/SmartThings/philips-hue/src/fields.lua @@ -16,6 +16,7 @@ local Fields = { BRIDGE_API = "bridge_api", BRIDGE_ID = "bridgeid", BRIDGE_SW_VERSION = "swversion", + BUTTON_INDEX_MAP = "button_rid_to_index", DEVICE_TYPE = "devicetype", EVENT_SOURCE = "eventsource", GAMUT = "gamut", diff --git a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua index 46b2f1492d..a4bd3d99e2 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua @@ -124,6 +124,60 @@ function AttributeEmitters.connectivity_update(child_device, zigbee_status) end end +function AttributeEmitters.emit_button_attribute_events(button_device, button_info) + if button_device == nil or (button_device and button_device.id == nil) then + log.warn("Tried to emit attribute events for a device that has been deleted") + return + end + + if button_info.power_state then + log.debug(true, "emit power") + button_device:emit_event( + capabilities.battery.battery( + st_utils.clamp_value(button_info.power_state.battery_level, 0, 100) + ) + ) + end + + local button_idx_map = button_device:get_field(Fields.BUTTON_INDEX_MAP) + if not button_idx_map then + log.error( + string.format( + "Button ID to Button Index map lost, " .. + "cannot find componenet to emit attribute event on for button [%s]", + (button_device and button_device.lable) or "unknown button" + ) + ) + end + + local idx = button_idx_map[button_info.id] or 1 + local component_idx + if idx == 1 then + component_idx = "main" + else + component_idx = string.format("button%s", idx) + end + + local button_report = button_info.button.button_report or { event = "" } + + if button_report.event == "long_press" and not button_device:get_field("button_held") then + button_device:set_field("button_held", true) + button_device.profile.components[component_idx]:emit_event( + capabilities.button.button.held({state_change = true}) + ) + end + + if button_report.event == "long_release" and button_device:get_field("button_held") then + button_device:set_field("button_held", false) + end + + if button_report.event == "short_release" and not button_device:get_field("button_held") then + button_device.profile.components[component_idx]:emit_event( + capabilities.button.button.pushed({state_change = true}) + ) + end +end + function AttributeEmitters.emit_contact_sensor_attribute_events(sensor_device, sensor_info) if sensor_device == nil or (sensor_device and sensor_device.id == nil) then log.warn("Tried to emit attribute events for a device that has been deleted") @@ -224,8 +278,9 @@ function AttributeEmitters.emitter_for_device_type(device_type) end -- TODO: Generalize this like the other handlers, and maybe even separate out non-primary services +device_type_emitter_map[HueDeviceTypes.BUTTON] = AttributeEmitters.emit_button_attribute_events +device_type_emitter_map[HueDeviceTypes.CONTACT] = AttributeEmitters.emit_contact_sensor_attribute_events device_type_emitter_map[HueDeviceTypes.LIGHT] = AttributeEmitters.emit_light_attribute_events device_type_emitter_map[HueDeviceTypes.MOTION] = AttributeEmitters.emit_motion_sensor_attribute_events -device_type_emitter_map[HueDeviceTypes.CONTACT] = AttributeEmitters.emit_contact_sensor_attribute_events return AttributeEmitters diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua new file mode 100644 index 0000000000..78f3c7e732 --- /dev/null +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua @@ -0,0 +1,185 @@ +local capabilities = require "st.capabilities" +local log = require "logjam" +local st_utils = require "st.utils" + +local refresh_handler = require("handlers.commands").refresh_handler + +local Discovery = require "disco" +local Fields = require "fields" +local HueDeviceTypes = require "hue_device_types" +local StrayDeviceHelper = require "stray_device_helper" + +local button_disco = require "disco.button" +local utils = require "utils" + +---@class ButtonLifecycleHandlers +local ButtonLifecycleHandlers = {} + +---@param driver HueDriver +---@param device HueChildDevice +---@param parent_device_id string? +---@param resource_id string? +function ButtonLifecycleHandlers.added(driver, device, parent_device_id, resource_id) + log.info( + string.format("Button Added for device %s", (device.label or device.id or "unknown device"))) + local device_button_resource_id = resource_id or utils.get_hue_rid(device) + if not device_button_resource_id then + log.error( + string.format( + "Could not determine the Hue Resource ID for added button %s", + (device and device.label) or "unknown button" + ) + ) + return + end + + local button_info = Discovery.device_state_disco_cache[device_button_resource_id] + if not button_info then + log.error( + string.format( + "Expected button info to be cached, sending button %s to stray resolver", + (device and device.label) or "unknown button" + ) + ) + driver.stray_device_tx:send({ + type = StrayDeviceHelper.MessageTypes.NewStrayDevice, + driver = driver, + device = device + }) + return + end + + local button_rid_to_index_map = {} + if button_info.button then + driver.hue_identifier_to_device_record[button_info.id] = device + button_rid_to_index_map[button_info.id] = 1 + end + + if button_info.num_buttons and button_info.num_buttons > 1 then + for var = 1, button_info.num_buttons do + local button_key = string.format("button%s", var) + local button_id_key = string.format("%s_id", button_key) + local button = button_info[button_key] + local button_id = button_info[button_id_key] + + if button and button_id then + driver.hue_identifier_to_device_record[button_id] = device + button_rid_to_index_map[button_id] = var + + local supported_button_values = utils.get_supported_button_values(button.event_values) + local component + if var == 1 then + component = "main" + else + component = button_key + end + device.profile.components[component]:emit_event( + capabilities.button.supportedButtonValues( + supported_button_values, + { visibility = { displayed = false } } + ) + ) + end + end + end + + if button_info.power_id then + driver.hue_identifier_to_device_record[button_info.power_id] = device + end + + log.debug(st_utils.stringify_table(button_rid_to_index_map, "button index map", true)) + device:set_field(Fields.BUTTON_INDEX_MAP, button_rid_to_index_map, { persist = true }) + device:set_field(Fields.DEVICE_TYPE, HueDeviceTypes.BUTTON, { persist = true }) + device:set_field(Fields.HUE_DEVICE_ID, button_info.hue_device_id, { persist = true }) + device:set_field(Fields.PARENT_DEVICE_ID, button_info.parent_device_id, { persist = true }) + device:set_field(Fields.RESOURCE_ID, device_button_resource_id, { persist = true }) + device:set_field(Fields._ADDED, true, { persist = true }) + device:set_field(Fields._REFRESH_AFTER_INIT, true, { persist = true }) + + driver.hue_identifier_to_device_record[device_button_resource_id] = device +end + +---@param driver HueDriver +---@param device HueChildDevice +function ButtonLifecycleHandlers.init(driver, device) + log.info( + string.format("Init Button for device %s", (device and device.label or device.id or "unknown button"))) + + local device_button_resource_id = + utils.get_hue_rid(device) or + device.device_network_id + + log.debug("resource id " .. tostring(device_button_resource_id)) + + local hue_device_id = device:get_field(Fields.HUE_DEVICE_ID) + if not driver.hue_identifier_to_device_record[device_button_resource_id] then + driver.hue_identifier_to_device_record[device_button_resource_id] = device + end + local button_info, err + button_info = Discovery.device_state_disco_cache[device_button_resource_id] + if not button_info then + log.debug("no button info") + local parent_bridge = driver:get_device_info(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 ""] + + 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 + button_info, err = button_disco.update_state_for_all_device_services( + api_instance, + hue_device_id, + parent_bridge.device_network_id, + device_button_resource_id, + Discovery.device_state_disco_cache + ) + if err then + log.error( + st_utils.stringify_table( + err, + string.format( + "Error populating initial state for button %s", + (device and device.label) or "unknown button" + ), + true + ) + ) + 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 or button_info.num_buttons == 1) 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 and button_info.num_buttons > 1 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 + end + + device:set_field(Fields._INIT, true, { persist = false }) + if device:get_field(Fields._REFRESH_AFTER_INIT) then + refresh_handler(driver, device) + device:set_field(Fields._REFRESH_AFTER_INIT, false, { persist = true }) + end +end + +return ButtonLifecycleHandlers 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 2f4ad1a805..2ef9f73f64 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 @@ -88,7 +88,7 @@ function ContactLifecycleHandlers.init(driver, device) 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 - sensor_info, err = contact_sensor_disco.update_all_services_for_sensor( + sensor_info, err = contact_sensor_disco.update_state_for_all_device_services( api_instance, hue_device_id, parent_bridge.device_network_id, 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 d278ae6bb7..5a87f866bc 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 @@ -90,7 +90,7 @@ function MotionLifecycleHandlers.init(driver, device) driver._devices_pending_refresh[device.id] = device else log.debug("--------------------- update all start") - sensor_info, err = motion_sensor_disco.update_all_services_for_sensor( + sensor_info, err = motion_sensor_disco.update_state_for_all_device_services( api_instance, hue_device_id, parent_bridge.device_network_id, diff --git a/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua b/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua index 93ed60585c..2bc201196a 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua @@ -4,7 +4,7 @@ local st_utils = require "st.utils" local Fields = require "fields" local HueDeviceTypes = require "hue_device_types" -local SensorUtils = require "utils.hue_motion_sensor_utils" +local MultiServiceDeviceUtils = require "utils.hue_multi_service_device_utils" local attribute_emitters = require "handlers.attribute_emitters" local utils = require "utils" @@ -150,6 +150,37 @@ function RefreshHandlers.do_refresh_all_for_bridge(driver, bridge_device) (bridge_device and bridge_device.label) or "Unknown Bridge") ) 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. +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 = driver:get_device_info(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")) + return + end + + if not bridge_device:get_field(Fields._INIT) then + log.warn("Bridge for light not yet initialized, can't refresh yet.") + driver._devices_pending_refresh[button_device.id] = button_device + return + end + + local hue_api = bridge_device:get_field(Fields.BRIDGE_API) --[[@as PhilipsHueApi]] + if skip_zigbee ~= true then + _refresh_zigbee(button_device, hue_api) + end + + local sensor_info, err = MultiServiceDeviceUtils.get_all_service_states(HueDeviceTypes.BUTTON, hue_api, hue_device_id, bridge_id) + if err then + log.error(string.format("Error refreshing motion sensor %s: %s", (button_device and button_device.label), err)) + end + + attribute_emitters.emitter_for_device_type(HueDeviceTypes.BUTTON)(button_device, sensor_info) +end -- TODO: Refresh handlers need to be optimized/generalized for devices with multiple services function RefreshHandlers.do_refresh_motion_sensor(driver, sensor_device, _, skip_zigbee) @@ -173,7 +204,7 @@ function RefreshHandlers.do_refresh_motion_sensor(driver, sensor_device, _, skip _refresh_zigbee(sensor_device, hue_api) end - local sensor_info, err = SensorUtils.get_all_service_states(HueDeviceTypes.MOTION, hue_api, hue_device_id, bridge_id) + local sensor_info, err = MultiServiceDeviceUtils.get_all_service_states(HueDeviceTypes.MOTION, hue_api, hue_device_id, bridge_id) if err then log.error(string.format("Error refreshing motion sensor %s: %s", (sensor_device and sensor_device.label), err)) end @@ -202,7 +233,7 @@ function RefreshHandlers.do_refresh_contact_sensor(driver, sensor_device, _, ski _refresh_zigbee(sensor_device, hue_api) end - local sensor_info, err = SensorUtils.get_all_service_states(HueDeviceTypes.CONTACT, hue_api, hue_device_id, bridge_id) + local sensor_info, err = MultiServiceDeviceUtils.get_all_service_states(HueDeviceTypes.CONTACT, hue_api, hue_device_id, bridge_id) if err then log.error(string.format("Error refreshing contact sensor %s: %s", (sensor_device and sensor_device.label), err)) end @@ -319,7 +350,8 @@ end -- TODO: Generalize this like the other handlers, and maybe even separate out non-primary services device_type_refresh_handlers_map[HueDeviceTypes.BRIDGE] = RefreshHandlers.do_refresh_all_for_bridge +device_type_refresh_handlers_map[HueDeviceTypes.BUTTON] = RefreshHandlers.do_refresh_button +device_type_refresh_handlers_map[HueDeviceTypes.CONTACT] = RefreshHandlers.do_refresh_contact_sensor device_type_refresh_handlers_map[HueDeviceTypes.LIGHT] = RefreshHandlers.do_refresh_light device_type_refresh_handlers_map[HueDeviceTypes.MOTION] = RefreshHandlers.do_refresh_motion_sensor -device_type_refresh_handlers_map[HueDeviceTypes.CONTACT] = RefreshHandlers.do_refresh_contact_sensor return RefreshHandlers diff --git a/drivers/SmartThings/philips-hue/src/hue/api.lua b/drivers/SmartThings/philips-hue/src/hue/api.lua index de63c874f7..033f3aa57c 100644 --- a/drivers/SmartThings/philips-hue/src/hue/api.lua +++ b/drivers/SmartThings/philips-hue/src/hue/api.lua @@ -340,6 +340,13 @@ function PhilipsHueApi:get_zigbee_connectivity_by_id(zigbee_resource_id) return self:get_rtype_by_rid("zigbee_connectivity", zigbee_resource_id) end +---@param button_resource_id string +---@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) +end + ---@param contact_resource_id string ---@return HueResourceResponse? ---@return string? err nil on success diff --git a/drivers/SmartThings/philips-hue/src/hue/types.lua b/drivers/SmartThings/philips-hue/src/hue/types.lua index 1adfd4349c..ed8987ee00 100644 --- a/drivers/SmartThings/philips-hue/src/hue/types.lua +++ b/drivers/SmartThings/philips-hue/src/hue/types.lua @@ -58,6 +58,10 @@ ---@field public enabled boolean ---@field public light { light_level: number, light_level_valid: boolean, light_level_report: { light_level: number, changed: string } } +---@class HueButtonInfo: HueResourceInfo +---@field public metadata { control_id: integer } +---@field public button { repeat_interval: integer, last_event: string?, button_report: { updated: string, event: string }?, event_values: string[] } + ---@class HueContactInfo: HueResourceInfo ---@field public enabled boolean ---@field public contact_report { changed: string, state: string } diff --git a/drivers/SmartThings/philips-hue/src/hue_device_types.lua b/drivers/SmartThings/philips-hue/src/hue_device_types.lua index 0fdbd57c26..2fc1598124 100644 --- a/drivers/SmartThings/philips-hue/src/hue_device_types.lua +++ b/drivers/SmartThings/philips-hue/src/hue_device_types.lua @@ -1,6 +1,7 @@ ---@enum HueDeviceTypes local HueDeviceTypes = { BRIDGE = "bridge", + BUTTON = "button", CONTACT = "contact", DEVICE_POWER = "device_power", LIGHT = "light", @@ -11,7 +12,12 @@ local HueDeviceTypes = { ZIGBEE_CONNECTIVITY = "zigbee_connectivity" } +local SupportedNumberOfButtons = { + [4] = true +} + local PrimaryDeviceTypes = { + [HueDeviceTypes.BUTTON] = true, [HueDeviceTypes.CONTACT] = true, [HueDeviceTypes.LIGHT] = true, [HueDeviceTypes.MOTION] = true @@ -32,4 +38,8 @@ function HueDeviceTypes.is_valid_device_type(device_type_str) return bimap[device_type_str] ~= nil end +function HueDeviceTypes.supports_button_configuration(button_description) + return SupportedNumberOfButtons[button_description.num_buttons] +end + return HueDeviceTypes diff --git a/drivers/SmartThings/philips-hue/src/stray_device_helper.lua b/drivers/SmartThings/philips-hue/src/stray_device_helper.lua index 23cceb8c36..c5a614f627 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" diff --git a/drivers/SmartThings/philips-hue/src/utils.lua b/drivers/SmartThings/philips-hue/src/utils.lua index fa82c77afd..dd1c1019d4 100644 --- a/drivers/SmartThings/philips-hue/src/utils.lua +++ b/drivers/SmartThings/philips-hue/src/utils.lua @@ -50,6 +50,23 @@ function utils.safe_wrap_handler(handler) end 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 +-- the data as it unpacks it; it just dispatches each event in the batch as it encounters it. +-- We could implement 2x-6x press if we add some smarts to the SSE stream handling. +function utils.get_supported_button_values(event_values) + local values = {"pushed"} + for _, event_value in ipairs(event_values) do + if event_value == "long_press" then + table.insert(values, "held") + break + end + end + + return values +end + function utils.kelvin_to_mirek(kelvin) return 1000000 / kelvin end function utils.mirek_to_kelvin(mirek) return 1000000 / mirek end 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 930ad2ca38..302de61662 100644 --- a/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua +++ b/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua @@ -147,6 +147,9 @@ function hue_bridge_utils.do_bridge_network_init(driver, bridge_device, bridge_u end for _, event in ipairs(events) do + -- TODO as mentioned in a paired TODO in utils.lua, we could try to add special-case handling + -- here to detect batched button releases. This would let us support multi-press capability + -- events. if event.type == "update" then for _, update_data in ipairs(event.data) do log.debug(true, "Received update event with type " .. update_data.type) diff --git a/drivers/SmartThings/philips-hue/src/utils/hue_motion_sensor_utils.lua b/drivers/SmartThings/philips-hue/src/utils/hue_motion_sensor_utils.lua deleted file mode 100644 index 16d7d0a7af..0000000000 --- a/drivers/SmartThings/philips-hue/src/utils/hue_motion_sensor_utils.lua +++ /dev/null @@ -1,17 +0,0 @@ -local utils = require "utils" - -local lazy_disco_handlers = utils.lazy_handler_loader("disco") ----@class SensorUtils -local motion_sensor_utils = {} - ----@param sensor_device_type HueDeviceTypes ----@param api_instance PhilipsHueApi ----@param device_service_id string ----@param bridge_id string ----@return table? nil on error ----@return string? err nil on success -function motion_sensor_utils.get_all_service_states(sensor_device_type, api_instance, device_service_id, bridge_id) - return lazy_disco_handlers[sensor_device_type].update_all_services_for_sensor(api_instance, device_service_id, bridge_id) -end - -return motion_sensor_utils 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 new file mode 100644 index 0000000000..af3a75384f --- /dev/null +++ b/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils.lua @@ -0,0 +1,17 @@ +local utils = require "utils" + +local lazy_disco_handlers = utils.lazy_handler_loader("disco") +---@class MultiServiceDeviceUtils +local multi_service_device_utils = {} + +---@param sensor_device_type HueDeviceTypes +---@param api_instance PhilipsHueApi +---@param device_service_id string +---@param bridge_id string +---@return table? nil on error +---@return string? err nil on success +function multi_service_device_utils.get_all_service_states(sensor_device_type, api_instance, device_service_id, bridge_id) + return lazy_disco_handlers[sensor_device_type].update_state_for_all_device_services(api_instance, device_service_id, bridge_id) +end + +return multi_service_device_utils From 482506f579501e5cbf0e925e189bbf3648a0fe4e Mon Sep 17 00:00:00 2001 From: Kiera Robinson Date: Sat, 27 Apr 2024 14:00:56 -0500 Subject: [PATCH 3/5] Added support for the Philips Hue Smart Button Philips Hue Smart Button is exposed as a 'button' service in addition to including the 'device_power' service. Development and inclusion of this device driver was modelled after the 4-button-remote device driver --- .../philips-hue/profiles/hue-smart-button.yml | 12 ++++++++++++ drivers/SmartThings/philips-hue/src/disco/button.lua | 8 +++++++- drivers/SmartThings/philips-hue/src/disco/init.lua | 6 +++++- .../src/handlers/lifecycle_handlers/button.lua | 8 +++----- .../philips-hue/src/handlers/refresh_handlers.lua | 1 + .../SmartThings/philips-hue/src/hue_device_types.lua | 3 ++- drivers/SmartThings/philips-hue/src/utils.lua | 8 ++++---- 7 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 drivers/SmartThings/philips-hue/profiles/hue-smart-button.yml diff --git a/drivers/SmartThings/philips-hue/profiles/hue-smart-button.yml b/drivers/SmartThings/philips-hue/profiles/hue-smart-button.yml new file mode 100644 index 0000000000..ff4687187f --- /dev/null +++ b/drivers/SmartThings/philips-hue/profiles/hue-smart-button.yml @@ -0,0 +1,12 @@ +name: HueSmartButton +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/philips-hue/src/disco/button.lua b/drivers/SmartThings/philips-hue/src/disco/button.lua index ceacf2e8ff..bd9c67e95f 100644 --- a/drivers/SmartThings/philips-hue/src/disco/button.lua +++ b/drivers/SmartThings/philips-hue/src/disco/button.lua @@ -121,10 +121,16 @@ function M.handle_discovered_device( ) return end + local button_profile_ref = "" - if button_description.num_buttons == 4 then + -- For Philips Hue Smart Button device which contains only 1 button + if button_description.num_buttons == 1 then + button_profile_ref = "HueSmartButton" + -- For Philips Hue Dimmer Remote which contains 4 buttons + elseif button_description.num_buttons == 4 then button_profile_ref = "4-button-remote" end + local bridge_device = driver:get_device_by_dni(bridge_id) or {} local st_metadata = { type = "EDGE_CHILD", diff --git a/drivers/SmartThings/philips-hue/src/disco/init.lua b/drivers/SmartThings/philips-hue/src/disco/init.lua index 7e3087cc36..89f6c49fb4 100644 --- a/drivers/SmartThings/philips-hue/src/disco/init.lua +++ b/drivers/SmartThings/philips-hue/src/disco/init.lua @@ -24,6 +24,9 @@ local DOMAIN = "local" ---@field public api_keys table ---@field public disco_api_instances table ---@field public device_state_disco_cache table> +---@field public ServiceType string +---@field public Domain string +---@field public discovery_active boolean local HueDiscovery = { api_keys = {}, disco_api_instances = {}, @@ -266,7 +269,8 @@ function HueDiscovery.search_bridge_for_supported_devices(driver, bridge_id, api local device_is_joined_to_bridge = {} for _, device_data in ipairs(devices.data or {}) do local primary_device_service - for _, svc_info in ipairs(device_data.services or {}) do + 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 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 78f3c7e732..94e5369ce5 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua @@ -55,7 +55,7 @@ function ButtonLifecycleHandlers.added(driver, device, parent_device_id, resourc button_rid_to_index_map[button_info.id] = 1 end - if button_info.num_buttons and button_info.num_buttons > 1 then + if button_info.num_buttons then for var = 1, button_info.num_buttons do local button_key = string.format("button%s", var) local button_id_key = string.format("%s_id", button_key) @@ -149,14 +149,12 @@ function ButtonLifecycleHandlers.init(driver, device) 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 or button_info.num_buttons == 1) and - not svc_rids_for_device[button_info.id] + 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 and button_info.num_buttons > 1 then + 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] diff --git a/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua b/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua index 2bc201196a..60c946f8c6 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua @@ -354,4 +354,5 @@ device_type_refresh_handlers_map[HueDeviceTypes.BUTTON] = RefreshHandlers.do_ref device_type_refresh_handlers_map[HueDeviceTypes.CONTACT] = RefreshHandlers.do_refresh_contact_sensor device_type_refresh_handlers_map[HueDeviceTypes.LIGHT] = RefreshHandlers.do_refresh_light device_type_refresh_handlers_map[HueDeviceTypes.MOTION] = RefreshHandlers.do_refresh_motion_sensor + return RefreshHandlers diff --git a/drivers/SmartThings/philips-hue/src/hue_device_types.lua b/drivers/SmartThings/philips-hue/src/hue_device_types.lua index 2fc1598124..d267fbe3b5 100644 --- a/drivers/SmartThings/philips-hue/src/hue_device_types.lua +++ b/drivers/SmartThings/philips-hue/src/hue_device_types.lua @@ -13,7 +13,8 @@ local HueDeviceTypes = { } local SupportedNumberOfButtons = { - [4] = true + [1] = true, -- For Philips Hue Smart Button device which contains only 1 button + [4] = true, -- For Philips Hue Dimmer Remote which contains 4 buttons } local PrimaryDeviceTypes = { diff --git a/drivers/SmartThings/philips-hue/src/utils.lua b/drivers/SmartThings/philips-hue/src/utils.lua index dd1c1019d4..bfc98e3630 100644 --- a/drivers/SmartThings/philips-hue/src/utils.lua +++ b/drivers/SmartThings/philips-hue/src/utils.lua @@ -50,6 +50,10 @@ 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 @@ -67,10 +71,6 @@ 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 From 27226020df27925f05ba2386b8c9125b8524a5da Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Sat, 27 Apr 2024 14:00:56 -0500 Subject: [PATCH 4/5] Support for compound devices and "grandchildren" This refactor furthers the generalizations done previously for devices that provide multiple services to allow for not only mapping of services to capabilities or components, but also multiple device records. This allows for us to support devices such as the Dymera up/down light. Other changes: - I found myself frequently introducing bugs related to using the Hue Bridge ID (MAC Address) as a Device ID, and the Bridge Device ID as a Hue Bridge ID. We renamed all instances of `bridge_id` referencing the MAC to `bridge_network_id` to try and alleviate the confusion. - Button support is further generalized to support two-button models, which covers the In-Wall Switch Module in the 2-gang configuration. --- ...hue-smart-button.yml => single-button.yml} | 2 +- .../philips-hue/profiles/two-button.yml | 18 ++ .../philips-hue/src/disco/button.lua | 47 ++-- .../philips-hue/src/disco/contact.lua | 30 ++- .../philips-hue/src/disco/init.lua | 191 +++++++------ .../philips-hue/src/disco/light.lua | 251 +++++++++++++----- .../philips-hue/src/disco/motion.lua | 30 ++- .../src/handlers/attribute_emitters.lua | 10 +- .../philips-hue/src/handlers/commands.lua | 10 +- .../src/handlers/lifecycle_handlers.lua | 14 +- .../handlers/lifecycle_handlers/bridge.lua | 2 +- .../handlers/lifecycle_handlers/button.lua | 8 +- .../handlers/lifecycle_handlers/contact.lua | 7 +- .../src/handlers/lifecycle_handlers/light.lua | 8 +- .../handlers/lifecycle_handlers/motion.lua | 7 +- .../handlers/migration_handlers/bridge.lua | 2 +- .../src/handlers/migration_handlers/light.lua | 4 +- .../src/handlers/refresh_handlers.lua | 16 +- .../SmartThings/philips-hue/src/hue/api.lua | 2 +- .../philips-hue/src/hue_debug/init.lua | 2 +- .../philips-hue/src/hue_device_types.lua | 31 ++- .../philips-hue/src/hue_driver_template.lua | 44 ++- drivers/SmartThings/philips-hue/src/init.lua | 2 +- .../src/lunchbox/sse/eventsource.lua | 2 +- .../philips-hue/src/stray_device_helper.lua | 2 +- drivers/SmartThings/philips-hue/src/utils.lua | 77 ++++-- .../src/utils/hue_bridge_utils.lua | 2 +- .../utils/hue_multi_service_device_utils.lua | 7 +- 28 files changed, 546 insertions(+), 282 deletions(-) rename drivers/SmartThings/philips-hue/profiles/{hue-smart-button.yml => single-button.yml} (90%) create mode 100644 drivers/SmartThings/philips-hue/profiles/two-button.yml diff --git a/drivers/SmartThings/philips-hue/profiles/hue-smart-button.yml b/drivers/SmartThings/philips-hue/profiles/single-button.yml similarity index 90% rename from drivers/SmartThings/philips-hue/profiles/hue-smart-button.yml rename to drivers/SmartThings/philips-hue/profiles/single-button.yml index ff4687187f..ad0c0ddb02 100644 --- a/drivers/SmartThings/philips-hue/profiles/hue-smart-button.yml +++ b/drivers/SmartThings/philips-hue/profiles/single-button.yml @@ -1,4 +1,4 @@ -name: HueSmartButton +name: single-button components: - id: main capabilities: diff --git a/drivers/SmartThings/philips-hue/profiles/two-button.yml b/drivers/SmartThings/philips-hue/profiles/two-button.yml new file mode 100644 index 0000000000..f5fb34ff54 --- /dev/null +++ b/drivers/SmartThings/philips-hue/profiles/two-button.yml @@ -0,0 +1,18 @@ +name: two-button +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/philips-hue/src/disco/button.lua b/drivers/SmartThings/philips-hue/src/disco/button.lua index bd9c67e95f..88af38f89d 100644 --- a/drivers/SmartThings/philips-hue/src/disco/button.lua +++ b/drivers/SmartThings/philips-hue/src/disco/button.lua @@ -1,4 +1,4 @@ -local log = require "logjam" +local log = require "log" local socket = require "cosock".socket local st_utils = require "st.utils" @@ -7,14 +7,14 @@ local HueDeviceTypes = require "hue_device_types" ---@class DiscoveredButtonHandler: DiscoveredChildDeviceHandler local M = {} +---@param driver HueDriver ---@param api_instance PhilipsHueApi ---@param device_service_info HueDeviceInfo ----@param bridge_id string ----@param resource_id string +---@param bridge_network_id string ---@param cache table? ---@return table? description nil on error ---@return string? err nil on success -local function _do_update(api_instance, device_service_info, bridge_id, resource_id, cache) +local function _do_update(driver, api_instance, device_service_info, bridge_network_id, cache) local rid_by_rtype = {} local button_services = {} local num_buttons = 0 @@ -28,12 +28,12 @@ local function _do_update(api_instance, device_service_info, bridge_id, resource end end + local bridge_device = driver:get_device_by_dni(bridge_network_id) --[[@as HueBridgeDevice]] local button_remote_description = { hue_provided_name = device_service_info.metadata.name, - parent_device_id = bridge_id, + parent_device_id = bridge_device.id, hue_device_id = device_service_info.id, hue_device_data = device_service_info, - id = resource_id, num_buttons = num_buttons } @@ -48,7 +48,7 @@ local function _do_update(api_instance, device_service_info, bridge_id, resource button_remote_description[button_key] = button_repr.data[1].button button_remote_description[button_id_key] = button_repr.data[1].id - if control_id == 1 and button_remote_description.id == nil then + if control_id == 1 then button_remote_description.id = button_repr.data[1].id end end @@ -63,7 +63,7 @@ local function _do_update(api_instance, device_service_info, bridge_id, resource end if type(cache) == "table" then - cache[resource_id] = button_remote_description + cache[button_remote_description.id] = button_remote_description if device_service_info.id_v1 then cache[device_service_info.id_v1] = button_remote_description end @@ -72,14 +72,14 @@ local function _do_update(api_instance, device_service_info, bridge_id, resource return button_remote_description end +---@param driver HueDriver ---@param api_instance PhilipsHueApi ---@param device_service_id string ----@param bridge_id string ----@param primary_button_resource_id string +---@param bridge_network_id string ---@param cache table? ---@return table? description nil on error ---@return string? err nil on success -function M.update_state_for_all_device_services(api_instance, device_service_id, bridge_id, primary_button_resource_id, cache) +function M.update_state_for_all_device_services(driver, api_instance, device_service_id, bridge_network_id, cache) log.debug("----------- Calling REST API") local device_service_info, err = api_instance:get_device_by_id(device_service_id) if err or not (device_service_info and device_service_info.data) then @@ -88,23 +88,23 @@ function M.update_state_for_all_device_services(api_instance, device_service_id, end log.debug("------------ _do_update") - return _do_update(api_instance, device_service_info.data[1], bridge_id, primary_button_resource_id, cache) + return _do_update(driver, api_instance, device_service_info.data[1], bridge_network_id, cache) end ---@param driver HueDriver ----@param bridge_id string +---@param bridge_network_id string ---@param api_instance PhilipsHueApi ----@param resource_id string +---@param primary_services table ---@param device_service_info HueDeviceInfo ---@param device_state_disco_cache table ---@param st_metadata_callback fun(driver: HueDriver, metadata: table)? function M.handle_discovered_device( - driver, bridge_id, api_instance, - resource_id, device_service_info, + driver, bridge_network_id, api_instance, + primary_services, device_service_info, device_state_disco_cache, st_metadata_callback ) local button_description, err = _do_update( - api_instance, device_service_info, bridge_id, resource_id, device_state_disco_cache + driver, api_instance, device_service_info, bridge_network_id, device_state_disco_cache ) if err then log.error("Error updating contact button initial state: " .. st_utils.stringify_table(err)) @@ -123,15 +123,18 @@ function M.handle_discovered_device( end local button_profile_ref = "" - -- For Philips Hue Smart Button device which contains only 1 button + -- 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 = "HueSmartButton" - -- For Philips Hue Dimmer Remote which contains 4 buttons + button_profile_ref = "single-button" + -- For double switch In-Wall Switch module + elseif button_description.num_buttons == 2 then + button_profile_ref = "two-button" + -- 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" end - local bridge_device = driver:get_device_by_dni(bridge_id) or {} + local bridge_device = driver:get_device_by_dni(bridge_network_id) or {} local st_metadata = { type = "EDGE_CHILD", label = device_service_info.metadata.name, @@ -140,7 +143,7 @@ function M.handle_discovered_device( manufacturer = device_service_info.product_data.manufacturer_name, model = device_service_info.product_data.model_id, parent_device_id = bridge_device.id, - parent_assigned_child_key = string.format("%s:%s", HueDeviceTypes.BUTTON, resource_id) + parent_assigned_child_key = string.format("%s:%s", HueDeviceTypes.BUTTON, button_description.id) } log.debug(st_utils.stringify_table(st_metadata, "button create", true)) diff --git a/drivers/SmartThings/philips-hue/src/disco/contact.lua b/drivers/SmartThings/philips-hue/src/disco/contact.lua index b69cd5e2db..b714838bd5 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 "logjam" +local log = require "log" local socket = require "cosock".socket local st_utils = require "st.utils" @@ -7,13 +7,14 @@ local HueDeviceTypes = require "hue_device_types" ---@class DiscoveredContactSensorHandler: DiscoveredChildDeviceHandler local M = {} +---@param driver HueDriver ---@param api_instance PhilipsHueApi ---@param device_service_info HueDeviceInfo ----@param bridge_id string +---@param bridge_network_id string ---@param cache table? ---@return table? description nil on error ---@return string? err nil on success -local function _do_update(api_instance, device_service_info, bridge_id, cache) +local function _do_update(driver, api_instance, device_service_info, bridge_network_id, cache) local rid_by_rtype = {} for _, svc in ipairs(device_service_info.services) do rid_by_rtype[svc.rtype] = svc.rid @@ -29,10 +30,11 @@ local function _do_update(api_instance, device_service_info, bridge_id, cache) if battery_err then return nil, battery_err end local resource_id = rid_by_rtype[HueDeviceTypes.CONTACT] + local bridge_device = driver:get_device_by_dni(bridge_network_id) --[[@as HueBridgeDevice]] local contact_sensor_description = { hue_provided_name = device_service_info.metadata.name, id = resource_id, - parent_device_id = bridge_id, + parent_device_id = bridge_device.id, hue_device_id = device_service_info.id, hue_device_data = device_service_info, } @@ -63,13 +65,14 @@ local function _do_update(api_instance, device_service_info, bridge_id, cache) return contact_sensor_description end +---@param driver HueDriver ---@param api_instance PhilipsHueApi ---@param device_service_id string ----@param bridge_id string +---@param bridge_network_id string ---@param cache table? ---@return table? description nil on error ---@return string? err nil on success -function M.update_state_for_all_device_services(api_instance, device_service_id, bridge_id, cache) +function M.update_state_for_all_device_services(driver, api_instance, device_service_id, bridge_network_id, cache) log.debug("----------- Calling REST API") local device_service_info, err = api_instance:get_device_by_id(device_service_id) if err or not (device_service_info and device_service_info.data) then @@ -78,24 +81,24 @@ function M.update_state_for_all_device_services(api_instance, device_service_id, end log.debug("------------ _do_update") - return _do_update(api_instance, device_service_info.data[1], bridge_id, cache) + return _do_update(driver, api_instance, device_service_info.data[1], bridge_network_id, cache) end ---@param driver HueDriver ----@param bridge_id string +---@param bridge_network_id string ---@param api_instance PhilipsHueApi ----@param resource_id string +---@param primary_services table ---@param device_service_info HueDeviceInfo ---@param device_state_disco_cache table ---@param st_metadata_callback fun(driver: HueDriver, metadata: table)? function M.handle_discovered_device( - driver, bridge_id, api_instance, - resource_id, device_service_info, + driver, bridge_network_id, api_instance, + primary_services, device_service_info, device_state_disco_cache, st_metadata_callback ) local err = select(2, _do_update( - api_instance, device_service_info, bridge_id, device_state_disco_cache + driver, api_instance, device_service_info, bridge_network_id, device_state_disco_cache ) ) if err then @@ -104,7 +107,8 @@ function M.handle_discovered_device( end if type(st_metadata_callback) == "function" then - local bridge_device = driver:get_device_by_dni(bridge_id) or {} + local resource_id = primary_services[HueDeviceTypes.CONTACT][1].rid + local bridge_device = driver:get_device_by_dni(bridge_network_id) or {} local st_metadata = { type = "EDGE_CHILD", label = device_service_info.metadata.name, diff --git a/drivers/SmartThings/philips-hue/src/disco/init.lua b/drivers/SmartThings/philips-hue/src/disco/init.lua index 89f6c49fb4..b798c04d1d 100644 --- a/drivers/SmartThings/philips-hue/src/disco/init.lua +++ b/drivers/SmartThings/philips-hue/src/disco/init.lua @@ -1,4 +1,4 @@ -local log = require "logjam" +local log = require "log" local socket = require "cosock.socket" local mdns = require "st.mdns" @@ -15,7 +15,7 @@ local SERVICE_TYPE = "_hue._tcp" local DOMAIN = "local" ---@class DiscoveredChildDeviceHandler ----@field public handle_discovered_device fun(driver: HueDriver, bridge_id: string, api_instance: PhilipsHueApi, resource_id: string, device_service_info: table, device_state_disco_cache: table, st_metadata_callback: fun(driver: HueDriver, metadata: table)?) +---@field public handle_discovered_device fun(driver: HueDriver, bridge_network_id: string, api_instance: PhilipsHueApi, primary_services: { [HueDeviceTypes]: HueServiceInfo[] }, device_service_info: table, device_state_disco_cache: table, st_metadata_callback: fun(driver: HueDriver, metadata: table)?) -- This `api_keys` table is an in-memory fall-back table. It gets overwritten -- with a reference to a driver datastore table before the Driver's `run` loop @@ -48,25 +48,39 @@ local function is_device_service_supported(svc_info) end ---@param driver HueDriver ----@param bridge_id string ----@param svc_info HueServiceInfo +---@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_id, svc_info, device_info) - local v1_dni = bridge_id .. "/" .. (device_info.id_v1 or "UNKNOWN"):gsub("/lights/", "") - 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 +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_id] + local api_instance = HueDiscovery.disco_api_instances[bridge_network_id] if not api_instance then - log.warn("No API instance for bridge_id ", bridge_id) + log.warn("No API instance for bridge_network_id ", bridge_network_id) return end HueDiscovery.handle_discovered_child_device( driver, - bridge_id, + primary_service_type, + bridge_network_id, api_instance, - svc_info, + primary_services, device_info ) end @@ -74,50 +88,50 @@ end -- "forward declarations" ---@param driver HueDriver ---@param bridge_ip string ----@param bridge_id string -local function discovered_bridge_callback(driver, bridge_ip, bridge_id) - if driver.ignored_bridges[bridge_id] then return end +---@param bridge_network_id string +local function discovered_bridge_callback(driver, bridge_ip, bridge_network_id) + if driver.ignored_bridges[bridge_network_id] then return end - local known_bridge_device = driver:get_device_by_dni(bridge_id) + local known_bridge_device = driver:get_device_by_dni(bridge_network_id) if known_bridge_device and known_bridge_device:get_field(HueApi.APPLICATION_KEY_HEADER) then - HueDiscovery.api_keys[bridge_id] = known_bridge_device:get_field(HueApi.APPLICATION_KEY_HEADER) + HueDiscovery.api_keys[bridge_network_id] = known_bridge_device:get_field(HueApi.APPLICATION_KEY_HEADER) end if known_bridge_device ~= nil - and driver.joined_bridges[bridge_id] - and HueDiscovery.api_keys[bridge_id] + and driver.joined_bridges[bridge_network_id] + and HueDiscovery.api_keys[bridge_network_id] then - log.info_with({ hub_logs = true }, string.format("Scanning bridge %s for devices...", bridge_id)) + log.info_with({ hub_logs = true }, string.format("Scanning bridge %s for devices...", bridge_network_id)) - HueDiscovery.disco_api_instances[bridge_id] = HueDiscovery.disco_api_instances[bridge_id] + HueDiscovery.disco_api_instances[bridge_network_id] = HueDiscovery.disco_api_instances[bridge_network_id] or HueApi.new_bridge_manager( "https://" .. bridge_ip, - HueDiscovery.api_keys[bridge_id], - utils.labeled_socket_builder((known_bridge_device.label or bridge_id or known_bridge_device.id or "unknown bridge")) + HueDiscovery.api_keys[bridge_network_id], + utils.labeled_socket_builder((known_bridge_device.label or bridge_network_id or known_bridge_device.id or "unknown bridge")) ) HueDiscovery.search_bridge_for_supported_devices( driver, - bridge_id, - HueDiscovery.disco_api_instances[bridge_id], + bridge_network_id, + HueDiscovery.disco_api_instances[bridge_network_id], function(hue_driver, svc_info, device_info) - discovered_device_callback(hue_driver, bridge_id, svc_info, device_info) + discovered_device_callback(hue_driver, bridge_network_id, svc_info, device_info) end, "[Discovery: " .. - (known_bridge_device.label or bridge_id or known_bridge_device.id or "unknown bridge") .. + (known_bridge_device.label or bridge_network_id or known_bridge_device.id or "unknown bridge") .. " bridge scan]" ) return end - if not HueDiscovery.api_keys[bridge_id] then - local socket_builder = utils.labeled_socket_builder(bridge_id) + if not HueDiscovery.api_keys[bridge_network_id] then + local socket_builder = utils.labeled_socket_builder(bridge_network_id) local api_key_response, err, _ = HueApi.request_api_key(bridge_ip, socket_builder) if err ~= nil or not api_key_response then log.warn(string.format( "Error while trying to request Bridge API Key for %s: %s", - bridge_id, + bridge_network_id, err ) ) @@ -126,40 +140,40 @@ local function discovered_bridge_callback(driver, bridge_ip, bridge_id) for _, item in ipairs(api_key_response) do if item.error ~= nil then - log.warn(string.format("Error payload in bridge %s API key response: %s", bridge_id, item.error.description)) + log.warn(string.format("Error payload in bridge %s API key response: %s", bridge_network_id, item.error.description)) elseif item.success and item.success.username then - log.info(string.format("API key received for Hue Bridge %s", bridge_id)) + log.info(string.format("API key received for Hue Bridge %s", bridge_network_id)) local api_key = item.success.username local bridge_base_url = "https://" .. bridge_ip local api_instance = HueApi.new_bridge_manager(bridge_base_url, api_key, socket_builder) - HueDiscovery.api_keys[bridge_id] = api_key - HueDiscovery.disco_api_instances[bridge_id] = api_instance + HueDiscovery.api_keys[bridge_network_id] = api_key + HueDiscovery.disco_api_instances[bridge_network_id] = api_instance end end end - if HueDiscovery.api_keys[bridge_id] and not driver.joined_bridges[bridge_id] then - local bridge_info = driver.datastore.bridge_netinfo[bridge_id] + if HueDiscovery.api_keys[bridge_network_id] and not driver.joined_bridges[bridge_network_id] then + local bridge_info = driver.datastore.bridge_netinfo[bridge_network_id] if not bridge_info then - log.debug(string.format("Bridge info for %s not yet available", bridge_id)) + log.debug(string.format("Bridge info for %s not yet available", bridge_network_id)) return end if tonumber(bridge_info.swversion or "0", 10) < HueApi.MIN_CLIP_V2_SWVERSION then log.warn(string.format("Found bridge %s that does not support CLIP v2 API, ignoring", bridge_info.name)) - driver.ignored_bridges[bridge_id] = true + driver.ignored_bridges[bridge_network_id] = true return end - driver.joined_bridges[bridge_id] = true + driver.joined_bridges[bridge_network_id] = true if not known_bridge_device then local create_device_msg = { type = "LAN", - device_network_id = bridge_id, + device_network_id = bridge_network_id, label = (bridge_info.name or "Philips Hue Bridge"), profile = "hue-bridge", manufacturer = "Signify Netherlands B.V.", @@ -189,8 +203,8 @@ function HueDiscovery.discover(driver, _, should_continue) HueDiscovery.do_mdns_scan(driver) HueDiscovery.search_for_bridges( driver, - function(hue_driver, bridge_ip, bridge_id) - discovered_bridge_callback(hue_driver, bridge_ip, bridge_id) + function(hue_driver, bridge_ip, bridge_network_id) + discovered_bridge_callback(hue_driver, bridge_ip, bridge_network_id) end ) socket.sleep(1.0) @@ -203,9 +217,9 @@ end ---@param callback fun(driver: HueDriver, ip: string, id: string) function HueDiscovery.search_for_bridges(driver, callback) local scanned_bridges = driver.datastore.bridge_netinfo or {} - for bridge_id, bridge_info in pairs(scanned_bridges) do + for bridge_network_id, bridge_info in pairs(scanned_bridges) do if type(callback) == "function" and bridge_info ~= nil then - callback(driver, bridge_info.ip, bridge_id) + callback(driver, bridge_info.ip, bridge_network_id) else log.warn( "Argument passed in `callback` position for " @@ -216,25 +230,25 @@ function HueDiscovery.search_for_bridges(driver, callback) end ---@param driver HueDriver ----@param bridge_id string -function HueDiscovery.scan_bridge_and_update_devices(driver, bridge_id) - if driver.ignored_bridges[bridge_id] then return end +---@param bridge_network_id string +function HueDiscovery.scan_bridge_and_update_devices(driver, bridge_network_id) + if driver.ignored_bridges[bridge_network_id] then return end - local known_bridge_device = driver:get_device_by_dni(bridge_id) + local known_bridge_device = driver:get_device_by_dni(bridge_network_id) if known_bridge_device then if known_bridge_device:get_field(HueApi.APPLICATION_KEY_HEADER) then - HueDiscovery.api_keys[bridge_id] = known_bridge_device:get_field(HueApi.APPLICATION_KEY_HEADER) + HueDiscovery.api_keys[bridge_network_id] = known_bridge_device:get_field(HueApi.APPLICATION_KEY_HEADER) end HueDiscovery.search_bridge_for_supported_devices( driver, - bridge_id, - HueDiscovery.disco_api_instances[bridge_id], + bridge_network_id, + HueDiscovery.disco_api_instances[bridge_network_id], function(hue_driver, svc_info, device_info) - discovered_device_callback(hue_driver, bridge_id, svc_info, device_info) + discovered_device_callback(hue_driver, bridge_network_id, svc_info, device_info) end, "[Discovery: " .. - (known_bridge_device.label or bridge_id or known_bridge_device.id or "unknown bridge") .. + (known_bridge_device.label or bridge_network_id or known_bridge_device.id or "unknown bridge") .. " bridge re-scan]", true ) @@ -242,12 +256,12 @@ function HueDiscovery.scan_bridge_and_update_devices(driver, bridge_id) end ---@param driver HueDriver ----@param bridge_id string +---@param bridge_network_id string ---@param api_instance PhilipsHueApi ---@param callback fun(driver: HueDriver, svc_info: HueServiceInfo, device_data: table) ---@param log_prefix string? ---@param do_delete boolean? -function HueDiscovery.search_bridge_for_supported_devices(driver, bridge_id, api_instance, callback, log_prefix, do_delete) +function HueDiscovery.search_bridge_for_supported_devices(driver, bridge_network_id, api_instance, callback, log_prefix, do_delete) local prefix = "" if type(log_prefix) == "string" and #log_prefix > 0 then prefix = log_prefix .. " " end @@ -268,32 +282,40 @@ function HueDiscovery.search_bridge_for_supported_devices(driver, bridge_id, api local device_is_joined_to_bridge = {} for _, device_data in ipairs(devices.data or {}) do - local primary_device_service + 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) and primary_device_service == nil then - primary_device_service = svc_info + 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 primary_device_service and type(callback) == "function" then - log.info_with( - { hub_logs = true }, string.format( - prefix .. - "Processing supported svc [rid: %s | type: %s] for Hue device [v2_id: %s | v1_id: %s], with Hue provided name: %s", - primary_device_service.rid, primary_device_service.rtype, device_data.id, device_data.id_v1, device_data.metadata.name + 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_service, device_data) + 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( - prefix .. "Argument passed in `callback` position for " - .. "`HueDiscovery.search_bridge_for_supported_devices` is not a function" - ) + 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 end @@ -303,8 +325,8 @@ function HueDiscovery.search_bridge_for_supported_devices(driver, bridge_id, api if utils.is_bridge(driver, device) then goto continue end local not_known_to_bridge = device_is_joined_to_bridge[device:get_field(Fields.HUE_DEVICE_ID) or ""] local parent_device_id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) or "" - local parent_bridge_device = driver:get_device_info(parent_device_id) - local is_child_of_bridge = parent_bridge_device and (parent_bridge_device:get_field(Fields.BRIDGE_ID) == bridge_id) + local parent_bridge_device = utils.get_hue_bridge_for_device(driver, device, parent_device_id) + local is_child_of_bridge = parent_bridge_device and (parent_bridge_device:get_field(Fields.BRIDGE_ID) == bridge_network_id) if parent_bridge_device and is_child_of_bridge and not not_known_to_bridge and device.id then device.log.info(string.format("Device is no longer joined to Hue Bridge %q, deleting", parent_bridge_device.label)) driver:do_hue_child_delete(device) @@ -315,16 +337,17 @@ function HueDiscovery.search_bridge_for_supported_devices(driver, bridge_id, api end ---@param driver HueDriver ----@param bridge_id string +---@param primary_service_type HueDeviceTypes +---@param bridge_network_id string ---@param api_instance PhilipsHueApi ----@param svc_info table +---@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, bridge_id, api_instance, svc_info, device_info) - discovered_device_handlers[svc_info.rtype].handle_discovered_device( +function HueDiscovery.handle_discovered_child_device(driver, primary_service_type, bridge_network_id, api_instance, primary_services, device_info) + discovered_device_handlers[primary_service_type].handle_discovered_device( driver, - bridge_id, + bridge_network_id, api_instance, - svc_info.rid, + primary_services, device_info, HueDiscovery.device_state_disco_cache, driver.try_create_device @@ -394,7 +417,7 @@ function HueDiscovery.do_mdns_scan(driver) end bridge_info.ip = ip_addr - local bridge_id = bridge_info.mac:gsub("-", ""):gsub(":", ""):upper() + local bridge_network_id = bridge_info.mac:gsub("-", ""):gsub(":", ""):upper() -- sanitize userdata nulls from JSON decode for k, v in pairs(bridge_info) do @@ -405,16 +428,16 @@ function HueDiscovery.do_mdns_scan(driver) ---@type boolean? local update_needed = false - if not utils.deep_table_eq((bridge_netinfo[bridge_id] or {}), bridge_info) then - bridge_netinfo[bridge_id] = bridge_info + if not utils.deep_table_eq((bridge_netinfo[bridge_network_id] or {}), bridge_info) then + bridge_netinfo[bridge_network_id] = bridge_info update_needed = true end - if driver.joined_bridges[bridge_id] and not driver.ignored_bridges[bridge_id] then - local bridge_device = driver:get_device_by_dni(bridge_id, true) + if driver.joined_bridges[bridge_network_id] and not driver.ignored_bridges[bridge_network_id] then + local bridge_device = driver:get_device_by_dni(bridge_network_id, true) update_needed = update_needed or (bridge_device and (bridge_device:get_field(Fields.IPV4) ~= bridge_info.ip)) if update_needed then - driver:update_bridge_netinfo(bridge_id, bridge_info) + driver:update_bridge_netinfo(bridge_network_id, bridge_info) end end ::continue:: diff --git a/drivers/SmartThings/philips-hue/src/disco/light.lua b/drivers/SmartThings/philips-hue/src/disco/light.lua index 243a2417d9..07dcf9a6d8 100644 --- a/drivers/SmartThings/philips-hue/src/disco/light.lua +++ b/drivers/SmartThings/philips-hue/src/disco/light.lua @@ -1,21 +1,152 @@ -local log = require "logjam" +local log = require "log" local socket = require "cosock".socket local st_utils = require "st.utils" ----@class DiscoveredLightHandler: DiscoveredChildDeviceHandler -local M = {} +local HueDeviceTypes = require "hue_device_types" + +local function join_light(driver, light, device_service_info, parent_device_id, st_metadata_callback) + local profile_ref + if light.color then + if light.color_temperature then + profile_ref = "white-and-color-ambiance" + else + profile_ref = "legacy-color" + end + elseif light.color_temperature then + profile_ref = "white-ambiance" -- all color temp products support `white` (dimming) + elseif light.dimming then + profile_ref = "white" -- `white` refers to dimmable and includes filament bulbs + elseif light.on then -- Case for plug which uses same category as 'light' + profile_ref = "plug" + else + log.warn( + string.format( + "Light resource [%s] does not seem to be A White/White-Ambiance/White-Color-Ambiance/Plug device, currently unsupported" + , + light.id + ) + ) + return + end + + local device_name = light.metadata.name + local parent_assigned_child_key = string.format("%s:%s", light.type, light.id) + + local st_metadata = { + type = "EDGE_CHILD", + label = device_name, + vendor_provided_label = device_service_info.product_data.product_name, + profile = profile_ref, + manufacturer = device_service_info.product_data.manufacturer_name, + model = device_service_info.product_data.model_id, + parent_device_id = parent_device_id, + parent_assigned_child_key = parent_assigned_child_key + } + + log.debug(true, st_utils.stringify_table(st_metadata, "light create", true)) + st_metadata_callback(driver, st_metadata) + -- rate limit ourself. + socket.sleep(0.1) +end + +local function get_light_state_table_and_update_cache(light, parent_device_id, device_service_info, cache) + local light_resource_description = { + hue_provided_name = light.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 = parent_device_id, + hue_device_id = light.owner.rid, + hue_device_data = device_service_info + } + + if type(cache) == "table" then + cache[light.id] = light_resource_description + if device_service_info.id_v1 then + cache[device_service_info.id_v1] = light_resource_description + end + end + return light_resource_description +end + +---@param driver HueDriver +---@param api_instance PhilipsHueApi +---@param services HueServiceInfo[] +---@param device_service_info HueDeviceInfo +---@param bridge_network_id string +---@param cache table +---@param st_metadata_callback fun(driver: HueDriver, metadata: table)? +local function handle_compound_light( + driver, api_instance, services, + device_service_info, bridge_network_id, cache, st_metadata_callback +) + ---@type HueLightInfo[] + local all_lights = {} + local main_light_resource_id + for idx, 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 + end + ::continue:: + end + + if type(main_light_resource_id) ~= "string" then + log.warn( + string.format( + "Couldn't determine the primary light for compound light [%s] from V1 ID, picking the first light service", + device_service_info.metadata.name + ) + ) + main_light_resource_id = services[1].rid + end + + ---@type HueLightInfo[] + local grandchild_lights = {} + for _, light in pairs(all_lights) do + if light.id == main_light_resource_id then + local bridge_device = driver:get_device_by_dni(bridge_network_id) --[[@as HueBridgeDevice]] + get_light_state_table_and_update_cache(light, bridge_device.id, device_service_info, cache) + if type(st_metadata_callback) == "function" then + join_light( + driver, light, device_service_info, bridge_device.id, st_metadata_callback + ) + end + else + table.insert(grandchild_lights, { + device = 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( + driver, waiting_info, device_service_info, parent_device.id, st_metadata_callback + ) + end + }) + end + end + + driver:queue_grandchild_device_for_join(grandchild_lights, main_light_resource_id) +end ---@param driver HueDriver ----@param bridge_id string ---@param api_instance PhilipsHueApi ---@param resource_id string ---@param device_service_info HueDeviceInfo ----@param device_state_disco_cache table +---@param bridge_network_id string +---@param cache table ---@param st_metadata_callback fun(driver: HueDriver, metadata: table)? -function M.handle_discovered_device( - driver, bridge_id, api_instance, - resource_id, device_service_info, - device_state_disco_cache, st_metadata_callback +local function handle_simple_light( + driver, api_instance, resource_id, + device_service_info, bridge_network_id, cache, st_metadata_callback ) local light_resource, err, _ = api_instance:get_light_by_id(resource_id) if err ~= nil or not light_resource then @@ -27,76 +158,50 @@ function M.handle_discovered_device( if light_resource.errors and #light_resource.errors > 0 then log.error_with({ hub_logs = true }, "Errors found in API response:") - for idx, err in ipairs(light_resource.errors) do - log.error_with({ hub_logs = true }, st_utils.stringify_table(err, "Error " .. idx, true)) + for idx, rest_err in ipairs(light_resource.errors) do + log.error_with({ hub_logs = true }, st_utils.stringify_table(rest_err, "Error " .. idx, true)) end return end - for _, light in ipairs(light_resource.data or {}) do - local bridge_device = driver:get_device_by_dni(bridge_id) --[[@as HueBridgeDevice]] - local light_resource_description = { - hue_provided_name = light.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.id, - hue_device_id = light.owner.rid, - hue_device_data = device_service_info - } - device_state_disco_cache[light.id] = light_resource_description - if device_service_info.id_v1 then - device_state_disco_cache[device_service_info.id_v1] = light_resource_description - end + local light = light_resource.data[1] + local bridge_device = driver:get_device_by_dni(bridge_network_id) --[[@as HueBridgeDevice]] + get_light_state_table_and_update_cache(light, bridge_device.id, device_service_info, cache) - if type(st_metadata_callback) == "function" then - local profile_ref - if light.color then - if light.color_temperature then - profile_ref = "white-and-color-ambiance" - else - profile_ref = "legacy-color" - end - elseif light.color_temperature then - profile_ref = "white-ambiance" -- all color temp products support `white` (dimming) - elseif light.dimming then - profile_ref = "white" -- `white` refers to dimmable and includes filament bulbs - elseif light.on then -- Case for plug which uses same category as 'light' - profile_ref = "plug" - else - log.warn( - string.format( - "Light resource [%s] does not seem to be A White/White-Ambiance/White-Color-Ambiance/Plug device, currently unsupported" - , - resource_id - ) - ) - goto continue - end + if type(st_metadata_callback) == "function" then + join_light( + driver, light, device_service_info, bridge_device.id, st_metadata_callback + ) + end +end - local device_name = light.metadata.name - local parent_assigned_child_key = string.format("%s:%s", light.type, light.id) - - local st_metadata = { - type = "EDGE_CHILD", - label = device_name, - vendor_provided_label = device_service_info.product_data.product_name, - profile = profile_ref, - manufacturer = device_service_info.product_data.manufacturer_name, - model = device_service_info.product_data.model_id, - parent_device_id = bridge_device.id, - parent_assigned_child_key = parent_assigned_child_key - } - - log.debug(true, st_utils.stringify_table(st_metadata, "light create", true)) - st_metadata_callback(driver, st_metadata) - -- rate limit ourself. - socket.sleep(0.1) - end - ::continue:: +---@class DiscoveredLightHandler: DiscoveredChildDeviceHandler +local M = {} + +---@param driver HueDriver +---@param bridge_network_id string +---@param api_instance PhilipsHueApi +---@param primary_services table +---@param device_service_info HueDeviceInfo +---@param device_state_disco_cache table +---@param st_metadata_callback fun(driver: HueDriver, metadata: table)? +function M.handle_discovered_device( + driver, bridge_network_id, api_instance, + primary_services, device_service_info, + device_state_disco_cache, st_metadata_callback +) + local light_services = primary_services[HueDeviceTypes.LIGHT] + local is_compound_light = #light_services > 1 + if is_compound_light then + handle_compound_light( + driver, api_instance, light_services, device_service_info, + bridge_network_id, device_state_disco_cache, st_metadata_callback + ) + else + handle_simple_light( + driver, api_instance, light_services[1].rid, device_service_info, + bridge_network_id, device_state_disco_cache, st_metadata_callback + ) end end diff --git a/drivers/SmartThings/philips-hue/src/disco/motion.lua b/drivers/SmartThings/philips-hue/src/disco/motion.lua index f325e91240..4ca14ccda1 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 "logjam" +local log = require "log" local socket = require "cosock".socket local st_utils = require "st.utils" @@ -7,13 +7,14 @@ local HueDeviceTypes = require "hue_device_types" ---@class DiscoveredMotionSensorHandler: DiscoveredChildDeviceHandler local M = {} +---@param driver HueDriver ---@param api_instance PhilipsHueApi ---@param device_service_info HueDeviceInfo ----@param bridge_id string +---@param bridge_network_id string ---@param cache table? ---@return table? description nil on error ---@return string? err nil on success -local function _do_update(api_instance, device_service_info, bridge_id, cache) +local function _do_update(driver, api_instance, device_service_info, bridge_network_id, cache) local rid_by_rtype = {} for _, svc in ipairs(device_service_info.services) do rid_by_rtype[svc.rtype] = svc.rid @@ -32,10 +33,11 @@ local function _do_update(api_instance, device_service_info, bridge_id, cache) if battery_err then return nil, battery_err end local resource_id = rid_by_rtype[HueDeviceTypes.MOTION] + local bridge_device = driver:get_device_by_dni(bridge_network_id) --[[@as HueBridgeDevice]] local motion_sensor_description = { hue_provided_name = device_service_info.metadata.name, id = resource_id, - parent_device_id = bridge_id, + parent_device_id = bridge_device.id, hue_device_id = device_service_info.id, hue_device_data = device_service_info, } @@ -73,13 +75,14 @@ local function _do_update(api_instance, device_service_info, bridge_id, cache) return motion_sensor_description end +---@param driver HueDriver ---@param api_instance PhilipsHueApi ---@param device_service_id string ----@param bridge_id string +---@param bridge_network_id string ---@param cache table? ---@return table? description nil on error ---@return string? err nil on success -function M.update_state_for_all_device_services(api_instance, device_service_id, bridge_id, cache) +function M.update_state_for_all_device_services(driver, api_instance, device_service_id, bridge_network_id, cache) log.debug("----------- Calling REST API") local device_service_info, err = api_instance:get_device_by_id(device_service_id) if err or not (device_service_info and device_service_info.data) then @@ -88,24 +91,24 @@ function M.update_state_for_all_device_services(api_instance, device_service_id, end log.debug("------------ _do_update") - return _do_update(api_instance, device_service_info.data[1], bridge_id, cache) + return _do_update(driver, api_instance, device_service_info.data[1], bridge_network_id, cache) end ---@param driver HueDriver ----@param bridge_id string +---@param bridge_network_id string ---@param api_instance PhilipsHueApi ----@param resource_id string +---@param primary_services table ---@param device_service_info HueDeviceInfo ---@param device_state_disco_cache table ---@param st_metadata_callback fun(driver: HueDriver, metadata: table)? function M.handle_discovered_device( - driver, bridge_id, api_instance, - resource_id, device_service_info, + driver, bridge_network_id, api_instance, + primary_services, device_service_info, device_state_disco_cache, st_metadata_callback ) local err = select(2, _do_update( - api_instance, device_service_info, bridge_id, device_state_disco_cache + driver, api_instance, device_service_info, bridge_network_id, device_state_disco_cache ) ) if err then @@ -114,7 +117,8 @@ function M.handle_discovered_device( end if type(st_metadata_callback) == "function" then - local bridge_device = driver:get_device_by_dni(bridge_id) or {} + local resource_id = primary_services[HueDeviceTypes.MOTION][1].rid + local bridge_device = driver:get_device_by_dni(bridge_network_id) or {} local st_metadata = { type = "EDGE_CHILD", label = device_service_info.metadata.name, diff --git a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua index a4bd3d99e2..5e5736a191 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 "logjam" +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 @@ -130,7 +130,7 @@ function AttributeEmitters.emit_button_attribute_events(button_device, button_in return end - if button_info.power_state then + if button_info.power_state and type(button_info.power_state.battery_level) == "number" then log.debug(true, "emit power") button_device:emit_event( capabilities.battery.battery( @@ -158,7 +158,7 @@ function AttributeEmitters.emit_button_attribute_events(button_device, button_in component_idx = string.format("button%s", idx) end - local button_report = button_info.button.button_report or { event = "" } + local button_report = (button_info.button and button_info.button.button_report) or { event = "" } if button_report.event == "long_press" and not button_device:get_field("button_held") then button_device:set_field("button_held", true) @@ -184,7 +184,7 @@ function AttributeEmitters.emit_contact_sensor_attribute_events(sensor_device, s return end - if sensor_info.power_state then + if sensor_info.power_state and type(sensor_info.power_state.battery_level) == "number" then log.debug(true, "emit power") sensor_device:emit_event(capabilities.battery.battery(st_utils.clamp_value(sensor_info.power_state.battery_level, 0, 100))) end @@ -222,7 +222,7 @@ function AttributeEmitters.emit_motion_sensor_attribute_events(sensor_device, se return end - if sensor_info.power_state then + if sensor_info.power_state and type(sensor_info.power_state.battery_level) == "number" then log.debug(true, "emit power") sensor_device:emit_event(capabilities.battery.battery(st_utils.clamp_value(sensor_info.power_state.battery_level, 0, 100))) end diff --git a/drivers/SmartThings/philips-hue/src/handlers/commands.lua b/drivers/SmartThings/philips-hue/src/handlers/commands.lua index b118659382..608817c5f2 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/commands.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/commands.lua @@ -1,5 +1,5 @@ local capabilities = require "st.capabilities" -local log = require "logjam" +local log = require "log" local st_utils = require "st.utils" local Consts = require "consts" @@ -21,7 +21,7 @@ local CommandHandlers = {} local function do_switch_action(driver, device, args) local on = args.command == "on" local id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) - local bridge_device = driver:get_device_info(id) + local bridge_device = utils.get_hue_bridge_for_device(driver, device, id) if not bridge_device then log.warn( @@ -66,7 +66,7 @@ end local function do_switch_level_action(driver, device, args) local level = st_utils.clamp_value(args.args.level, 1, 100) local id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) - local bridge_device = driver:get_device_info(id) + local bridge_device = utils.get_hue_bridge_for_device(driver, device, id) if not bridge_device then log.warn( @@ -130,7 +130,7 @@ local function do_color_action(driver, device, args) device:set_field(Fields.WRAPPED_HUE, true) end local id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) - local bridge_device = driver:get_device_info(id) + local bridge_device = utils.get_hue_bridge_for_device(driver, device, id) if not bridge_device then log.warn( @@ -207,7 +207,7 @@ end local function do_color_temp_action(driver, device, args) local kelvin = args.args.temperature local id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) - local bridge_device = driver:get_device_info(id) + local bridge_device = utils.get_hue_bridge_for_device(driver, device, id) if not bridge_device then log.warn( diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers.lua index 1110c6469f..e558389a4c 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers.lua @@ -1,4 +1,4 @@ -local log = require "logjam" +local log = require "log" local st_utils = require "st.utils" local Discovery = require "disco" @@ -53,11 +53,21 @@ function LifecycleHandlers.device_added(driver, device, ...) local resource_state_known = (Discovery.device_state_disco_cache[resource_id] ~= nil) log.info( string.format("Querying device info for parent of %s", (device.label or device.id or "unknown device"))) - local parent_bridge = driver:get_device_info(device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID)) + + local parent_bridge = utils.get_hue_bridge_for_device( + driver, device, device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) + ) 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.") 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 3ed6069a5c..68973a4cb2 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 "logjam" +local log = require "log" 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 94e5369ce5..f28e07395b 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua @@ -1,5 +1,5 @@ local capabilities = require "st.capabilities" -local log = require "logjam" +local log = require "log" local st_utils = require "st.utils" local refresh_handler = require("handlers.commands").refresh_handler @@ -119,7 +119,9 @@ function ButtonLifecycleHandlers.init(driver, device) button_info = Discovery.device_state_disco_cache[device_button_resource_id] if not button_info then log.debug("no button info") - local parent_bridge = driver:get_device_info(device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID)) + 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 ""] @@ -128,10 +130,10 @@ function ButtonLifecycleHandlers.init(driver, device) driver._devices_pending_refresh[device.id] = device else button_info, err = button_disco.update_state_for_all_device_services( + driver, api_instance, hue_device_id, parent_bridge.device_network_id, - device_button_resource_id, Discovery.device_state_disco_cache ) if err then diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/contact.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/contact.lua index 2ef9f73f64..0b03f845ed 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 "logjam" +local log = require "log" local st_utils = require "st.utils" local refresh_handler = require("handlers.commands").refresh_handler @@ -81,7 +81,9 @@ function ContactLifecycleHandlers.init(driver, device) sensor_info = Discovery.device_state_disco_cache[device_sensor_resource_id] if not sensor_info then log.debug("no sensor info") - local parent_bridge = driver:get_device_info(device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID)) + 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 ""] if not (parent_bridge and api_instance) then @@ -89,6 +91,7 @@ function ContactLifecycleHandlers.init(driver, device) driver._devices_pending_refresh[device.id] = device else sensor_info, err = contact_sensor_disco.update_state_for_all_device_services( + driver, api_instance, hue_device_id, parent_bridge.device_network_id, 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 7c660dda29..42402bf106 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 "logjam" +local log = require "log" local refresh_handler = require("handlers.commands").refresh_handler local st_utils = require "st.utils" @@ -36,8 +36,7 @@ function LightLifecycleHandlers.added(driver, device, parent_device_id, resource if not light_info_known then log.info( string.format("Querying device info for parent of %s", (device.label or device.id or "unknown device"))) - local parent_bridge = driver:get_device_info(parent_device_id or device.parent_device_id or - device:get_field(Fields.PARENT_DEVICE_ID)) + local parent_bridge = utils.get_hue_bridge_for_device(driver, device, parent_device_id) if not parent_bridge then log.error_with({ hub_logs = true }, string.format( "Device %s added with parent UUID of %s but could not find a device with that UUID in the driver", @@ -107,7 +106,7 @@ function LightLifecycleHandlers.added(driver, device, parent_device_id, resource dimming = light.dimming, color_temperature = light.color_temperature, mode = light.mode, - parent_device_id = parent_bridge.id, + parent_device_id = parent_device_id or device.parent_device_id, hue_device_id = light.owner.rid, hue_device_data = { product_data = { @@ -190,6 +189,7 @@ function LightLifecycleHandlers.init(driver, device) refresh_handler(driver, device) device:set_field(Fields._REFRESH_AFTER_INIT, false, { persist = true }) end + driver:check_waiting_grandchildren_for_device(device) end return LightLifecycleHandlers 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 5a87f866bc..9f03a3ff30 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 "logjam" +local log = require "log" local st_utils = require "st.utils" local refresh_handler = require("handlers.commands").refresh_handler @@ -82,7 +82,9 @@ function MotionLifecycleHandlers.init(driver, device) sensor_info = Discovery.device_state_disco_cache[device_sensor_resource_id] if not sensor_info then log.debug("no sensor info") - local parent_bridge = driver:get_device_info(device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID)) + 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 ""] if not (parent_bridge and api_instance) then @@ -91,6 +93,7 @@ function MotionLifecycleHandlers.init(driver, device) else log.debug("--------------------- update all start") sensor_info, err = motion_sensor_disco.update_state_for_all_device_services( + driver, api_instance, hue_device_id, parent_bridge.device_network_id, 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 7b74668bbf..d1633ec6f1 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 "logjam" +local log = require "log" 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 4581ff4203..819085ca8c 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 "logjam" +local log = require "log" local st_utils = require "st.utils" local Discovery = require "disco" @@ -29,7 +29,7 @@ function LightMigrationHandler.migrate(driver, device, lifecycle_handlers, paren local bridge_device = nil if parent_device_id ~= nil then - bridge_device = driver:get_device_info(parent_device_id, false) + bridge_device = utils.get_hue_bridge_for_device(driver, device, parent_device_id) end if not bridge_device then diff --git a/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua b/drivers/SmartThings/philips-hue/src/handlers/refresh_handlers.lua index 60c946f8c6..f733a77469 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 "logjam" +local log = require "log" local st_utils = require "st.utils" local Fields = require "fields" @@ -156,7 +156,7 @@ end 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 = driver:get_device_info(bridge_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")) @@ -174,7 +174,7 @@ function RefreshHandlers.do_refresh_button(driver, button_device, _, skip_zigbee _refresh_zigbee(button_device, hue_api) end - local sensor_info, err = MultiServiceDeviceUtils.get_all_service_states(HueDeviceTypes.BUTTON, hue_api, hue_device_id, bridge_id) + local sensor_info, err = MultiServiceDeviceUtils.get_all_service_states(driver, HueDeviceTypes.BUTTON, hue_api, hue_device_id, bridge_device.device_network_id) if err then log.error(string.format("Error refreshing motion sensor %s: %s", (button_device and button_device.label), err)) end @@ -186,7 +186,7 @@ end 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 = driver:get_device_info(bridge_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")) @@ -204,7 +204,7 @@ function RefreshHandlers.do_refresh_motion_sensor(driver, sensor_device, _, skip _refresh_zigbee(sensor_device, hue_api) end - local sensor_info, err = MultiServiceDeviceUtils.get_all_service_states(HueDeviceTypes.MOTION, hue_api, hue_device_id, bridge_id) + local sensor_info, err = MultiServiceDeviceUtils.get_all_service_states(driver, HueDeviceTypes.MOTION, hue_api, hue_device_id, bridge_device.device_network_id) if err then log.error(string.format("Error refreshing motion sensor %s: %s", (sensor_device and sensor_device.label), err)) end @@ -215,7 +215,7 @@ end 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 = driver:get_device_info(bridge_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")) @@ -233,7 +233,7 @@ function RefreshHandlers.do_refresh_contact_sensor(driver, sensor_device, _, ski _refresh_zigbee(sensor_device, hue_api) end - local sensor_info, err = MultiServiceDeviceUtils.get_all_service_states(HueDeviceTypes.CONTACT, hue_api, hue_device_id, bridge_id) + local sensor_info, err = MultiServiceDeviceUtils.get_all_service_states(driver, HueDeviceTypes.CONTACT, hue_api, hue_device_id, bridge_device.device_network_id) if err then log.error(string.format("Error refreshing contact sensor %s: %s", (sensor_device and sensor_device.label), err)) end @@ -274,7 +274,7 @@ function RefreshHandlers.do_refresh_light(driver, light_device, light_status_cac end local bridge_id = light_device.parent_device_id or light_device:get_field(Fields.PARENT_DEVICE_ID) - local bridge_device = driver:get_device_info(bridge_id) + local bridge_device = utils.get_hue_bridge_for_device(driver, light_device, bridge_id) if not bridge_device then log.warn("Couldn't get Hue bridge for light " .. (light_device.label or light_device.id or "unknown device")) diff --git a/drivers/SmartThings/philips-hue/src/hue/api.lua b/drivers/SmartThings/philips-hue/src/hue/api.lua index 033f3aa57c..6e80b652da 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 "logjam" +local log = require "log" local RestClient = require "lunchbox.rest" local st_utils = require "st.utils" diff --git a/drivers/SmartThings/philips-hue/src/hue_debug/init.lua b/drivers/SmartThings/philips-hue/src/hue_debug/init.lua index 3d4075213e..875c94076c 100644 --- a/drivers/SmartThings/philips-hue/src/hue_debug/init.lua +++ b/drivers/SmartThings/philips-hue/src/hue_debug/init.lua @@ -1,6 +1,6 @@ local st_utils = require "st.utils" -local log = require "logjam" +local log = require "log" local utils = require "utils" local Discovery = require "disco" diff --git a/drivers/SmartThings/philips-hue/src/hue_device_types.lua b/drivers/SmartThings/philips-hue/src/hue_device_types.lua index d267fbe3b5..f0c4dec0c7 100644 --- a/drivers/SmartThings/philips-hue/src/hue_device_types.lua +++ b/drivers/SmartThings/philips-hue/src/hue_device_types.lua @@ -13,8 +13,9 @@ local HueDeviceTypes = { } local SupportedNumberOfButtons = { - [1] = true, -- For Philips Hue Smart Button device which contains only 1 button - [4] = true, -- For Philips Hue Dimmer Remote which contains 4 buttons + [1] = true, -- For Philips Hue Smart Button or single switch In-Wall Switch module which contains only 1 button + [2] = true, -- For double switch In-Wall Switch module + [4] = true, -- For Philips Hue Dimmer Remote and Tap Dial, which contains 4 buttons } local PrimaryDeviceTypes = { @@ -43,4 +44,30 @@ function HueDeviceTypes.supports_button_configuration(button_description) return SupportedNumberOfButtons[button_description.num_buttons] end +---@param device_info HueDeviceInfo +---@param primary_services table +---@return HueDeviceTypes? svc_rtype the main service rtype for the device +function HueDeviceTypes.determine_main_service_rtype(device_info, primary_services) + -- If the id_v1 is present, it'll be of the form '//' + local service_from_v1_id = string.match((device_info.id_v1 or ""), "/([%a]+)/[%d]+") or "" + -- Lights show up as `light` here, but buttons and sensors both show up as `sensors` + if PrimaryDeviceTypes[service_from_v1_id] then + return service_from_v1_id + end + + local has_service = {} + for rtype, _ in pairs(primary_services) do + has_service[rtype] = PrimaryDeviceTypes[rtype] + end + + -- At this point we'll make our best guess by establishing an order of precedence as a heuristic; + -- lights first, then the actual sensors, then buttons. + if has_service[HueDeviceTypes.LIGHT] then return HueDeviceTypes.LIGHT end + if has_service[HueDeviceTypes.CONTACT] then return HueDeviceTypes.CONTACT end + if has_service[HueDeviceTypes.MOTION] then return HueDeviceTypes.MOTION end + if has_service[HueDeviceTypes.BUTTON] then return HueDeviceTypes.BUTTON end + + return nil +end + return HueDeviceTypes diff --git a/drivers/SmartThings/philips-hue/src/hue_driver_template.lua b/drivers/SmartThings/philips-hue/src/hue_driver_template.lua index a93f300fd5..3f7f7d03c9 100644 --- a/drivers/SmartThings/philips-hue/src/hue_driver_template.lua +++ b/drivers/SmartThings/philips-hue/src/hue_driver_template.lua @@ -1,7 +1,8 @@ local Driver = require "st.driver" local capabilities = require "st.capabilities" -local log = require "logjam" +local cosock = require "cosock" +local log = require "log" local Discovery = require "disco" local Fields = require "fields" @@ -65,6 +66,7 @@ local set_color_temp_handler = utils.safe_wrap_handler(command_handlers.set_colo --- @field public joined_bridges table --- @field public hue_identifier_to_device_record table --- @field public services_for_device_rid table> Map the device resource ID to another map that goes from service rid to service rtype +--- @field public waiting_grandchildren table? --- @field public stray_device_tx table cosock channel --- @field public datastore HueDriverDatastore persistent store --- @field public api_key_to_bridge_id table @@ -133,16 +135,44 @@ function HueDriver:run() Driver.run(self) end ----@param bridge_id string +---@param grandchild_devices { waiting_resource_info: HueResourceInfo, join_callback: fun(driver: HueDriver, waiting_resource_info: HueResourceInfo, parent_device: HueChildDevice)}[] +---@param waiting_for string +function HueDriver:queue_grandchild_device_for_join(grandchild_devices, waiting_for) + self.waiting_grandchildren = self.waiting_grandchildren or {} + + for _, waiting_info in ipairs(grandchild_devices) do + self.waiting_grandchildren[waiting_for] = self.waiting_grandchildren[waiting_for] or {} + table.insert(self.waiting_grandchildren[waiting_for], waiting_info) + end +end + +---@param new_device HueChildDevice +function HueDriver:check_waiting_grandchildren_for_device(new_device) + if not self.waiting_grandchildren then + return + end + local rid = utils.get_hue_rid(new_device) + for _, waiting in pairs(self.waiting_grandchildren[rid or ""] or {}) do + local waiting_info = waiting.waiting_resource_info + local join_callback = waiting.join_callback + if type(join_callback) == "function" then + cosock.spawn(function() + join_callback(self, waiting_info, new_device) + end) + end + end +end + +---@param bridge_network_id string ---@param bridge_info HueBridgeInfo -function HueDriver:update_bridge_netinfo(bridge_id, bridge_info) - if self.joined_bridges[bridge_id] then - local bridge_device = self:get_device_by_dni(bridge_id) --[[@as HueBridgeDevice]] +function HueDriver:update_bridge_netinfo(bridge_network_id, bridge_info) + if self.joined_bridges[bridge_network_id] then + local bridge_device = self:get_device_by_dni(bridge_network_id) --[[@as HueBridgeDevice]] if not bridge_device then log.warn_with({ hub_logs = true }, string.format( "Couldn't locate bridge device for joined bridge with DNI %s", - bridge_id + bridge_network_id ) ) return @@ -151,7 +181,7 @@ function HueDriver:update_bridge_netinfo(bridge_id, bridge_info) if bridge_info.ip ~= bridge_device:get_field(Fields.IPV4) then bridge_utils.update_bridge_fields_from_info(self, bridge_info, bridge_device) local maybe_api_client = bridge_device:get_field(Fields.BRIDGE_API) --[[@as PhilipsHueApi]] - local maybe_api_key = bridge_device:get_field(HueApi.APPLICATION_KEY_HEADER) or Discovery.api_keys[bridge_id] + local maybe_api_key = bridge_device:get_field(HueApi.APPLICATION_KEY_HEADER) or Discovery.api_keys[bridge_network_id] local maybe_event_source = bridge_device:get_field(Fields.EVENT_SOURCE) local bridge_url = "https://" .. bridge_info.ip diff --git a/drivers/SmartThings/philips-hue/src/init.lua b/drivers/SmartThings/philips-hue/src/init.lua index 8f20a40706..7372c96d2f 100644 --- a/drivers/SmartThings/philips-hue/src/init.lua +++ b/drivers/SmartThings/philips-hue/src/init.lua @@ -19,7 +19,7 @@ -- =============================================================================================== local Driver = require "st.driver" -local log = require "logjam" +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 diff --git a/drivers/SmartThings/philips-hue/src/lunchbox/sse/eventsource.lua b/drivers/SmartThings/philips-hue/src/lunchbox/sse/eventsource.lua index 44684d6650..d6d51018e1 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 "logjam" +local log = require "log" 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 c5a614f627..23cceb8c36 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 "logjam" +local log = require "log" local st_utils = require "st.utils" local Discovery = require "disco" diff --git a/drivers/SmartThings/philips-hue/src/utils.lua b/drivers/SmartThings/philips-hue/src/utils.lua index bfc98e3630..34d9a03420 100644 --- a/drivers/SmartThings/philips-hue/src/utils.lua +++ b/drivers/SmartThings/philips-hue/src/utils.lua @@ -1,4 +1,4 @@ -local log = require "logjam" +local log = require "log" local Fields = require "fields" local HueDeviceTypes = require "hue_device_types" @@ -44,7 +44,8 @@ function utils.safe_wrap_handler(handler) end local success, result = pcall(handler, driver, device, ...) if not success then - log.error_with({ hub_logs = true }, string.format("Failed to invoke capability command handler. Reason: %s", result)) + log.error_with({ hub_logs = true }, + string.format("Failed to invoke capability command handler. Reason: %s", result)) end return result end @@ -60,7 +61,7 @@ function utils.mirek_to_kelvin(mirek) return 1000000 / mirek end -- the data as it unpacks it; it just dispatches each event in the batch as it encounters it. -- We could implement 2x-6x press if we add some smarts to the SSE stream handling. function utils.get_supported_button_values(event_values) - local values = {"pushed"} + local values = { "pushed" } for _, event_value in ipairs(event_values) do if event_value == "long_press" then table.insert(values, "held") @@ -212,7 +213,8 @@ function utils.get_hue_rid(device) end if utils.is_dth_light(device) then - return nil, string.format("Can't get the Hue RID of migrated DTH light %s that hasn't completed migration", device.label) + return nil, + string.format("Can't get the Hue RID of migrated DTH light %s that hasn't completed migration", device.label) end local success, rid, _ = utils.parse_parent_assigned_key(device) @@ -221,11 +223,11 @@ function utils.get_hue_rid(device) end return - nil, - string.format( - "error establishing Hue RID from parent assigned key [%s]", - (device and device.parent_assigned_child_key) or "Parent Assigned Key Not Available" - ) + nil, + string.format( + "error establishing Hue RID from parent assigned key [%s]", + (device and device.parent_assigned_child_key) or "Parent Assigned Key Not Available" + ) end --- Get the HueDeviceType value for a device. If available on the device field, then it @@ -266,11 +268,11 @@ function utils.determine_device_type(device) end return - nil, - string.format( - "Couldn't determine device type for device %s", - (device and device.label) or "Unknown Device" - ) + nil, + string.format( + "Couldn't determine device type for device %s", + (device and device.label) or "Unknown Device" + ) end --- Attempts an exhaustive check of all the ways a device @@ -280,9 +282,9 @@ end ---@return boolean is_bridge true if the device record represents a Hue Bridge function utils.is_bridge(driver, device) return (device:get_field(Fields.DEVICE_TYPE) == "bridge") - or (driver.datastore.bridge_netinfo[device.device_network_id] ~= nil) - or utils.is_edge_bridge(device) or utils.is_dth_light(device) - or (device.parent_assigned_child_key == nil) + or (driver.datastore.bridge_netinfo[device.device_network_id] ~= nil) + or utils.is_edge_bridge(device) or utils.is_dth_light(device) + or (device.parent_assigned_child_key == nil) end --- Only checked during `added` callback, or as a later @@ -293,9 +295,9 @@ end ---@return boolean function utils.is_edge_bridge(device) return - device.device_network_id and - utils.is_valid_mac_addr_string(device.device_network_id) and - not (device.data and device.data.username) + device.device_network_id and + utils.is_valid_mac_addr_string(device.device_network_id) and + not (device.data and device.data.username) end --- Only checked during `added` callback, or as a later @@ -306,9 +308,9 @@ end ---@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) + 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 @@ -333,6 +335,35 @@ function utils.is_dth_light(device) and device.data.username ~= nil end +--- Get the Hue Bridge for a Device; useful when you don't know if you have a child +--- or a grandchild device and you need to walk up the family tree. Will return the +--- passed argument if the argument is a bridge. +---@param driver HueDriver +---@param device HueDevice +---@param parent_device_id string? +---@return HueBridgeDevice? bridge_device +function utils.get_hue_bridge_for_device(driver, device, parent_device_id) + log.trace(string.format("------------------------ Looking for bridge for %s with parent_device_id %s", device.label, device.parent_device_id)) + if utils.is_bridge(driver, device) then + log.trace(string.format("------------------------- %s is a bridge", device.label)) + return device --[[ @as HueBridgeDevice ]] + end + + local parent_device_id = parent_device_id or device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) + local parent_device = driver:get_device_info(parent_device_id) + if not parent_device then + log.trace(string.format("------------------------- get_device_info for %s was nil", parent_device_id)) + return nil + end + + log.trace(string.format("------------------------- parent_device label is %s, checking if bridge", parent_device.label)) + if parent_device and utils.is_bridge(driver, parent_device) then + return parent_device --[[ @as HueBridgeDevice ]] + end + + return utils.get_hue_bridge_for_device(driver, parent_device) +end + --- build a exponential backoff time value generator --- ---@param max number the maximum wait interval (not including `rand factor`) 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 302de61662..0c37d15250 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 "logjam" +local log = require "log" local json = require "st.json" local st_utils = require "st.utils" 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 index af3a75384f..ee959a1c93 100644 --- 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 @@ -4,14 +4,15 @@ 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_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(sensor_device_type, api_instance, device_service_id, bridge_id) - return lazy_disco_handlers[sensor_device_type].update_state_for_all_device_services(api_instance, device_service_id, bridge_id) +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 From 56f4a0a4174bccbfd1dd4bb3eee6e208f157929c Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Sat, 27 Apr 2024 14:00:56 -0500 Subject: [PATCH 5/5] Addresses a handful of bugs with the new device types - Fixes an issue where compound devices wouldn't be labeled properly - Fixes an issue where the stray device task was scanning a bridge every time it onboarded, even if there were no strays - Fixes an issue where refresh on devices could fail early in driver start by accidentally passing in an API Key string instead of an API Rest Client instance to a function - Fixes an issue where multi-service devices (Buttons, Motion Sensors, Contact Sensors) could sometimes fail to emit attributes if their init handler ran before their parent bridge's network connection was open. Move `battery` above refresh in profile for motion Remove debug logic from tamper emit Remove unused Rooms API calls Replace strings with consts in API methods Add missing early return in button attribute handler Early return in device discovery for unsupported buttons Move debug logs to correct function Update category for single button profile Fix duped line from bad rerere rebase Finish SSE event stream generalizations Tidy up docs/logs that still assume lights-only Generalize stray device handling Normalize module structures --- drivers/SmartThings/philips-hue/PROGRESS.md | 32 +-- .../philips-hue/profiles/motion-sensor.yml | 4 +- .../philips-hue/profiles/single-button.yml | 2 +- .../philips-hue/src/disco/button.lua | 19 +- .../philips-hue/src/disco/contact.lua | 11 +- .../philips-hue/src/disco/init.lua | 166 ++++++------ .../philips-hue/src/disco/light.lua | 17 +- .../philips-hue/src/disco/motion.lua | 12 +- .../SmartThings/philips-hue/src/fields.lua | 4 +- .../src/handlers/attribute_emitters.lua | 23 +- .../philips-hue/src/handlers/commands.lua | 7 +- .../handlers/lifecycle_handlers/bridge.lua | 2 +- .../handlers/lifecycle_handlers/button.lua | 42 +-- .../handlers/lifecycle_handlers/contact.lua | 34 +-- .../init.lua} | 44 +++- .../src/handlers/lifecycle_handlers/light.lua | 2 +- .../handlers/lifecycle_handlers/motion.lua | 37 +-- .../handlers/migration_handlers/bridge.lua | 2 +- .../src/handlers/migration_handlers/light.lua | 4 +- .../src/handlers/refresh_handlers.lua | 39 ++- .../SmartThings/philips-hue/src/hue/api.lua | 31 +-- drivers/SmartThings/philips-hue/src/init.lua | 10 +- .../src/lunchbox/sse/eventsource.lua | 2 +- .../philips-hue/src/stray_device_helper.lua | 247 ++++++++++-------- .../philips-hue/src/{hue => }/types.lua | 1 + .../src/{hue => utils}/cie_utils.lua | 0 .../src/utils/hue_bridge_utils.lua | 144 ++-------- .../utils/hue_multi_service_device_utils.lua | 18 -- .../hue_multi_service_device_utils/init.lua | 45 ++++ .../hue_multi_service_device_utils/sensor.lua | 23 ++ .../src/{utils.lua => utils/init.lua} | 23 +- 31 files changed, 525 insertions(+), 522 deletions(-) rename drivers/SmartThings/philips-hue/src/handlers/{lifecycle_handlers.lua => lifecycle_handlers/init.lua} (81%) rename drivers/SmartThings/philips-hue/src/{hue => }/types.lua (99%) rename drivers/SmartThings/philips-hue/src/{hue => utils}/cie_utils.lua (100%) delete mode 100644 drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils.lua create mode 100644 drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/init.lua create mode 100644 drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/sensor.lua rename drivers/SmartThings/philips-hue/src/{utils.lua => utils/init.lua} (97%) 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