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/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/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/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/profiles/single-button.yml b/drivers/SmartThings/philips-hue/profiles/single-button.yml
new file mode 100644
index 0000000000..49003127c4
--- /dev/null
+++ b/drivers/SmartThings/philips-hue/profiles/single-button.yml
@@ -0,0 +1,12 @@
+name: single-button
+components:
+ - id: main
+ capabilities:
+ - id: button
+ version: 1
+ - id: battery
+ version: 1
+ - id: refresh
+ version: 1
+ categories:
+ - name: Button
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
new file mode 100644
index 0000000000..fce804f0e7
--- /dev/null
+++ b/drivers/SmartThings/philips-hue/src/disco/button.lua
@@ -0,0 +1,170 @@
+local log = require "log"
+local socket = require "cosock".socket
+local st_utils = require "st.utils"
+
+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
+---@param bridge_network_id string
+---@param cache table?
+---@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
+
+ 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 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_device.id,
+ hue_device_id = device_service_info.id,
+ hue_device_data = device_service_info,
+ num_buttons = num_buttons,
+ sensor_list = { power_id = HueDeviceTypes.DEVICE_POWER }
+ }
+
+ 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 then
+ button_remote_description.id = button_repr.data[1].id
+ end
+
+ button_remote_description.sensor_list[button_id_key] = HueDeviceTypes.BUTTON
+ 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[button_remote_description.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 driver HueDriver
+---@param api_instance PhilipsHueApi
+---@param device_service_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(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
+ log.error("Couldn't get device info for button, error: " .. st_utils.stringify_table(err))
+ return
+ end
+
+ return _do_update(driver, api_instance, device_service_info.data[1], bridge_network_id, cache)
+end
+
+---@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 button_description, err = _do_update(
+ 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))
+ 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
+ -- 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"
+ -- 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"
+ 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 {}
+ 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, button_description.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..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,13 +7,16 @@ 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
----@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)
+ log.debug("------------ _do_update")
local rid_by_rtype = {}
for _, svc in ipairs(device_service_info.services) do
rid_by_rtype[svc.rtype] = svc.rid
@@ -29,10 +32,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,
}
@@ -53,6 +57,12 @@ local function _do_update(api_instance, device_service_info, bridge_id, cache)
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
@@ -63,13 +73,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_all_services_for_sensor(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
@@ -77,25 +88,24 @@ function M.update_all_services_for_sensor(api_instance, device_service_id, bridg
return
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 +114,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 2c048da2ce..e26578ff5d 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
@@ -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 = {},
@@ -44,77 +47,51 @@ local function is_device_service_supported(svc_info)
return discovered_device_handlers[svc_info.rtype or ""] ~= nil
end
----@param driver HueDriver
----@param bridge_id string
----@param svc_info HueServiceInfo
----@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 api_instance = HueDiscovery.disco_api_instances[bridge_id]
- if not api_instance then
- log.warn("No API instance for bridge_id ", bridge_id)
- return
- end
-
- HueDiscovery.handle_discovered_child_device(
- driver,
- bridge_id,
- api_instance,
- svc_info,
- device_info
- )
-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],
- function(hue_driver, svc_info, device_info)
- discovered_device_callback(hue_driver, bridge_id, svc_info, device_info)
- end,
+ bridge_network_id,
+ HueDiscovery.disco_api_instances[bridge_network_id],
+ HueDiscovery.handle_discovered_child_device,
"[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
)
)
@@ -123,40 +100,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.",
@@ -186,8 +163,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)
@@ -200,9 +177,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 "
@@ -213,25 +190,23 @@ 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],
- function(hue_driver, svc_info, device_info)
- discovered_device_callback(hue_driver, bridge_id, svc_info, device_info)
- end,
+ bridge_network_id,
+ HueDiscovery.disco_api_instances[bridge_network_id],
+ HueDiscovery.handle_discovered_child_device,
"[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
)
@@ -239,12 +214,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 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_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
@@ -265,32 +240,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
- 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
- primary_device_service = svc_info
- 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
- )
- )
- callback(driver, primary_device_service, device_data)
- else
- log.warn(
- prefix .. "Argument passed in `callback` position for "
- .. "`HueDiscovery.search_bridge_for_supported_devices` is not a function"
- )
- 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
@@ -299,8 +250,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)
@@ -311,16 +262,92 @@ function HueDiscovery.search_bridge_for_supported_devices(driver, bridge_id, api
end
---@param driver HueDriver
----@param bridge_id string
----@param api_instance PhilipsHueApi
----@param svc_info table
+---@param bridge_network_id string
+---@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, bridge_id, api_instance, svc_info, device_info)
- discovered_device_handlers[svc_info.rtype].handle_discovered_device(
+---@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_id,
+ bridge_network_id,
api_instance,
- svc_info.rid,
+ primary_services,
device_info,
HueDiscovery.device_state_disco_cache,
driver.try_create_device
@@ -390,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
@@ -401,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 5bade096c4..8acc6a5669 100644
--- a/drivers/SmartThings/philips-hue/src/disco/light.lua
+++ b/drivers/SmartThings/philips-hue/src/disco/light.lua
@@ -1,21 +1,157 @@
-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
+ 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 = {
+ 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 _, 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].service_id and light_resource.data[1].service_id == 1 then
+ main_light_resource_id = light_resource.data[1].id
+ 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, {
+ 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(
+ 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,74 +163,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
- else
- log.warn(
- string.format(
- "Light resource [%s] does not seem to be A White/White-Ambiance/White-Color-Ambiance 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 cfdbd0895c..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,13 +7,16 @@ 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
----@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)
+ log.debug("------------ _do_update")
local rid_by_rtype = {}
for _, svc in ipairs(device_service_info.services) do
rid_by_rtype[svc.rtype] = svc.rid
@@ -32,10 +35,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,
}
@@ -63,6 +67,13 @@ local function _do_update(api_instance, device_service_info, bridge_id, cache)
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
@@ -73,13 +84,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_all_services_for_sensor(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
@@ -87,25 +99,24 @@ function M.update_all_services_for_sensor(api_instance, device_service_id, bridg
return
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 +125,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/fields.lua b/drivers/SmartThings/philips-hue/src/fields.lua
index 5eca9656f9..0df3c06aec 100644
--- a/drivers/SmartThings/philips-hue/src/fields.lua
+++ b/drivers/SmartThings/philips-hue/src/fields.lua
@@ -16,17 +16,20 @@ 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",
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 46b2f1492d..1604889708 100644
--- a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua
+++ b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua
@@ -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,41 +114,96 @@ 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
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 and type(button_info.power_state.battery_level) == "number" 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"
+ )
+ )
+ return
+ 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 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)
+ 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")
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
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
@@ -168,7 +223,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
@@ -224,8 +279,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/commands.lua b/drivers/SmartThings/philips-hue/src/handlers/commands.lua
index b118659382..7fdb337325 100644
--- a/drivers/SmartThings/philips-hue/src/handlers/commands.lua
+++ b/drivers/SmartThings/philips-hue/src/handlers/commands.lua
@@ -4,7 +4,7 @@ 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"
@@ -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(
@@ -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/button.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua
new file mode 100644
index 0000000000..e1f60af2ab
--- /dev/null
+++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua
@@ -0,0 +1,165 @@
+local capabilities = require "st.capabilities"
+local log = require "log"
+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 hue_multi_service_device_utils = require "utils.hue_multi_service_device_utils"
+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 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")))
+ 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
+
+ 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 = 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))
+
+ if parent_bridge and api_instance then
+ button_info, err = button_disco.update_state_for_all_device_services(
+ driver,
+ api_instance,
+ hue_device_id,
+ parent_bridge.device_network_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
+ 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)
+ 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..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
@@ -81,14 +82,14 @@ 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 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 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))
- 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
- sensor_info, err = contact_sensor_disco.update_all_services_for_sensor(
+ if parent_bridge and api_instance then
+ sensor_info, err = contact_sensor_disco.update_state_for_all_device_services(
+ driver,
api_instance,
hue_device_id,
parent_bridge.device_network_id,
@@ -108,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 79%
rename from drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers.lua
rename to drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua
index 1110c6469f..a6d272b7b5 100644
--- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers.lua
+++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua
@@ -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")
@@ -53,11 +78,15 @@ 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)
+
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.")
@@ -122,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(
@@ -138,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 7c660dda29..f69ef08f36 100644
--- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua
+++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua
@@ -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 d278ae6bb7..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
@@ -82,15 +83,15 @@ 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 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 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))
- 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_all_services_for_sensor(
+ sensor_info, err = motion_sensor_disco.update_state_for_all_device_services(
+ driver,
api_instance,
hue_device_id,
parent_bridge.device_network_id,
@@ -111,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/light.lua b/drivers/SmartThings/philips-hue/src/handlers/migration_handlers/light.lua
index 4581ff4203..9b66522445 100644
--- a/drivers/SmartThings/philips-hue/src/handlers/migration_handlers/light.lua
+++ b/drivers/SmartThings/philips-hue/src/handlers/migration_handlers/light.lua
@@ -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
@@ -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 93ed60585c..cfa6a721d6 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"
@@ -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
@@ -150,20 +150,62 @@ 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.
+---@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 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 button device 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(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
+
+ 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 = 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"))
+ 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
@@ -173,26 +215,32 @@ 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(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
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 = 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"))
+ 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
@@ -202,18 +250,20 @@ 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(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
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)
@@ -243,7 +293,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"))
@@ -295,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
@@ -304,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, ...)
@@ -319,7 +369,9 @@ 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..0afb9fbb47 100644
--- a/drivers/SmartThings/philips-hue/src/hue/api.lua
+++ b/drivers/SmartThings/philips-hue/src/hue/api.lua
@@ -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
@@ -340,51 +338,53 @@ 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(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
@@ -392,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
@@ -413,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))
@@ -432,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
@@ -447,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/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 0fdbd57c26..f0c4dec0c7 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,14 @@ local HueDeviceTypes = {
ZIGBEE_CONNECTIVITY = "zigbee_connectivity"
}
+local SupportedNumberOfButtons = {
+ [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 = {
+ [HueDeviceTypes.BUTTON] = true,
[HueDeviceTypes.CONTACT] = true,
[HueDeviceTypes.LIGHT] = true,
[HueDeviceTypes.MOTION] = true
@@ -32,4 +40,34 @@ 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
+
+---@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..a0a0e21b17 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
@@ -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/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 93%
rename from drivers/SmartThings/philips-hue/src/hue/types.lua
rename to drivers/SmartThings/philips-hue/src/types.lua
index 1adfd4349c..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
@@ -58,6 +59,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/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 930ad2ca38..040e8c88f0 100644
--- a/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua
+++ b/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua
@@ -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"
@@ -147,6 +149,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)
@@ -159,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
@@ -181,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")
)
@@ -199,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
@@ -328,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_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/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 82%
rename from drivers/SmartThings/philips-hue/src/utils.lua
rename to drivers/SmartThings/philips-hue/src/utils/init.lua
index fa82c77afd..ab269c3e33 100644
--- a/drivers/SmartThings/philips-hue/src/utils.lua
+++ b/drivers/SmartThings/philips-hue/src/utils/init.lua
@@ -44,12 +44,30 @@ 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
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
@@ -195,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)
@@ -204,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
@@ -249,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
@@ -263,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
@@ -276,22 +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)
-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)
+ 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
@@ -316,6 +322,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`)