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