From fcf3ae8f281a4327fa47c8ed0eae755991b38e40 Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Wed, 13 Sep 2023 14:27:27 -0500 Subject: [PATCH] fixup! fix: Update refresh to poll connectivity status --- .../SmartThings/philips-hue/src/handlers.lua | 215 ++++++++++++------ .../SmartThings/philips-hue/src/hue/api.lua | 4 + .../SmartThings/philips-hue/src/hue/types.lua | 1 + 3 files changed, 154 insertions(+), 66 deletions(-) diff --git a/drivers/SmartThings/philips-hue/src/handlers.lua b/drivers/SmartThings/philips-hue/src/handlers.lua index 02a9cf6a2a..6d790b428c 100644 --- a/drivers/SmartThings/philips-hue/src/handlers.lua +++ b/drivers/SmartThings/philips-hue/src/handlers.lua @@ -2,6 +2,7 @@ local Fields = require "hue.fields" local HueApi = require "hue.api" local HueColorUtils = require "hue.cie_utils" local log = require "log" +local utils = require "utils" local cosock = require "cosock" local capabilities = require "st.capabilities" @@ -246,9 +247,42 @@ end ---@param driver HueDriver ---@param light_device HueChildDevice -local function do_refresh_light(driver, light_device) +---@param conn_status_cache table|nil +---@param light_status_cache table|nil +local function do_refresh_light(driver, light_device, conn_status_cache, light_status_cache) local light_resource_id = light_device:get_field(Fields.RESOURCE_ID) local hue_device_id = light_device:get_field(Fields.HUE_DEVICE_ID) + + local do_zigbee_request = true + local do_light_request = true + + if type(conn_status_cache) == "table" then + local zigbee_status = conn_status_cache[hue_device_id] + if zigbee_status ~= nil then + if zigbee_status.status and zigbee_status.status == "connected" then + light_device.log.debug(string.format("Zigbee Status for %s is connected", light_device.label)) + light_device:online() + do_zigbee_request = false + else + light_device:offline() + do_zigbee_request = false + end + end + end + + if type(light_status_cache) == "table" then + local light_info = light_status_cache[hue_device_id] + if light_info ~= nil then + if light_info.id == light_resource_id then + if light_info.color ~= nil and light_info.color.gamut then + light_device:set_field(Fields.GAMUT, light_info.color.gamut_type, { persist = true }) + end + driver.emit_light_status_events(light_device, light_info) + do_light_request = false + end + end + 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) @@ -264,11 +298,12 @@ local function do_refresh_light(driver, light_device) end local hue_api = bridge_device:get_field(Fields.BRIDGE_API) - local success = false + local success = not (do_light_request or do_zigbee_request) local count = 0 local num_attempts = 3 local zigbee_resource_id local rest_resp, rest_err + local backoff_generator = utils.backoff_builder(10, 0.1, 0.1) --- this loop is a rate-limit dodge. --- --- One of the various symptoms of hitting the Hue Bridge's rate limit is that you'll get a silent @@ -277,31 +312,8 @@ local function do_refresh_light(driver, light_device) --- the information for the light that we expect to getting the info for. repeat count = count + 1 - rest_resp, rest_err = hue_api:get_device_by_id(hue_device_id) - if rest_err ~= nil then - log.error_with({ hub_logs = true }, rest_err) - goto continue - end - - if rest_resp ~= nil then - if #rest_resp.errors > 0 then - for _, err in ipairs(rest_resp.errors) do - log.error_with({ hub_logs = true }, "Error in Hue API response: " .. err.description) - end - goto continue - end - - for _, hue_device in ipairs(rest_resp.data) do - for _, svc_info in ipairs(hue_device.services or {}) do - if svc_info.rtype == "zigbee_connectivity" then - zigbee_resource_id = svc_info.rid - end - end - end - end - - if zigbee_resource_id ~= nil then - rest_resp, rest_err = hue_api:get_zigbee_connectivity_by_id(zigbee_resource_id) + if do_zigbee_request then + rest_resp, rest_err = hue_api:get_device_by_id(hue_device_id) if rest_err ~= nil then log.error_with({ hub_logs = true }, rest_err) goto continue @@ -315,44 +327,74 @@ local function do_refresh_light(driver, light_device) goto continue end - for _, zigbee_svc in ipairs(rest_resp.data) do - if zigbee_svc.owner and zigbee_svc.owner.rid == hue_device_id then - if zigbee_svc.status and zigbee_svc.status == "connected" then - light_device.log.debug(string.format("Zigbee Status for %s is connected", light_device.label)) - light_device:online() - else - light_device:offline() + for _, hue_device in ipairs(rest_resp.data) do + for _, svc_info in ipairs(hue_device.services or {}) do + if svc_info.rtype == "zigbee_connectivity" then + zigbee_resource_id = svc_info.rid end end end end - end - rest_resp, rest_err = hue_api:get_light_by_id(light_resource_id) - if rest_err ~= nil then - log.error_with({ hub_logs = true }, rest_err) - goto continue - end + if zigbee_resource_id ~= nil then + rest_resp, rest_err = hue_api:get_zigbee_connectivity_by_id(zigbee_resource_id) + if rest_err ~= nil then + log.error_with({ hub_logs = true }, rest_err) + goto continue + end - if rest_resp ~= nil then - if #rest_resp.errors > 0 then - for _, err in ipairs(rest_resp.errors) do - log.error_with({ hub_logs = true }, "Error in Hue API response: " .. err.description) + if rest_resp ~= nil then + if #rest_resp.errors > 0 then + for _, err in ipairs(rest_resp.errors) do + log.error_with({ hub_logs = true }, "Error in Hue API response: " .. err.description) + end + goto continue + end + + for _, zigbee_svc in ipairs(rest_resp.data) do + if zigbee_svc.owner and zigbee_svc.owner.rid == hue_device_id then + if zigbee_svc.status and zigbee_svc.status == "connected" then + light_device.log.debug(string.format("Zigbee Status for %s is connected", light_device.label)) + light_device:online() + else + light_device:offline() + end + end + end end + end + end + + if do_light_request then + rest_resp, rest_err = hue_api:get_light_by_id(light_resource_id) + if rest_err ~= nil then + log.error_with({ hub_logs = true }, rest_err) goto continue end - for _, light_info in ipairs(rest_resp.data) do - if light_info.id == light_resource_id then - if light_info.color ~= nil and light_info.color.gamut then - light_device:set_field(Fields.GAMUT, light_info.color.gamut_type, { persist = true }) + if rest_resp ~= nil then + if #rest_resp.errors > 0 then + for _, err in ipairs(rest_resp.errors) do + log.error_with({ hub_logs = true }, "Error in Hue API response: " .. err.description) + end + goto continue + end + + for _, light_info in ipairs(rest_resp.data) do + if light_info.id == light_resource_id then + if light_info.color ~= nil and light_info.color.gamut then + light_device:set_field(Fields.GAMUT, light_info.color.gamut_type, { persist = true }) + end + driver.emit_light_status_events(light_device, light_info) + success = true end - driver.emit_light_status_events(light_device, light_info) - success = true end end end ::continue:: + if not success then + cosock.socket.sleep(backoff_generator()) + end until success or count >= num_attempts end @@ -361,26 +403,67 @@ end local function do_refresh_all_for_bridge(driver, bridge_device) local child_devices = bridge_device:get_child_list() --[=[@as HueChildDevice[]]=] - for _, device in ipairs(child_devices) do - if device and device.datastore and device.datastore.__devices_store then - log.trace( - st_utils.stringify_table( - (device.datastore.__devices_store[device.id] or { unknown = "no datastore entry" }), - string.format( - "%s device datastore", (device.label or string.format("unlabeled device with id %s", device.id)) - ), - false - ) + if not bridge_device:get_field(Fields._INIT) then + log.warn("Bridge for lights not yet initialized, can't refresh yet.") + return + end + + local hue_api = bridge_device:get_field(Fields.BRIDGE_API) --[[@as PhilipsHueApi]] + + local conn_status, conn_rest_err = hue_api:get_connectivity_status() + local light_status, light_rest_err = hue_api:get_lights() + + if conn_rest_err ~= nil or light_rest_err ~= nil then + bridge_device.log.error( + string.format( + "Couldn't refresh devices connected to bridge.\n" .. + "get_connectivity_status error? %s\n" .. + "get_lights error? %s\n", + conn_rest_err, + light_rest_err ) - else - log.warn( - string.format("Device %s does not have a proper datastore association", - (device.label or device.id or "unknown device")) + ) + return + end + + if (not conn_status) or (not light_status) then + bridge_device.log.warn( + string.format( + "Received empty status payloads with no errors while refreshing, aborting refresh handler.\n" .. + "Connectivity status nil? %s\n" .. + "Light status nil? %s\n", + (conn_status == nil), + (light_status == nil) ) - end + ) + return + end + + if conn_status.errors and #conn_status.errors > 0 then + bridge_device.log.error("Errors in connectivity status payload: " .. st_utils.stringify_table(conn_status.errors)) + return + end + + if light_status.errors and #light_status.errors > 0 then + bridge_device.log.error("Errors in light status payload: " .. st_utils.stringify_table(light_status.errors)) + return + end + + local conn_status_cache = {} + local light_status_cache = {} + + for _, zigbee_status in ipairs(conn_status.data) do + conn_status_cache[zigbee_status.owner.rid] = zigbee_status + end + + for _, light_status in ipairs(light_status.data) do + light_status_cache[light_status.owner.rid] = light_status + end + + for _, device in ipairs(child_devices) do local device_type = device:get_field(Fields.DEVICE_TYPE) if device_type == "light" then - do_refresh_light(driver, device) + do_refresh_light(driver, device, conn_status_cache, light_status_cache) end end end diff --git a/drivers/SmartThings/philips-hue/src/hue/api.lua b/drivers/SmartThings/philips-hue/src/hue/api.lua index 543850107f..04a8bb70d4 100644 --- a/drivers/SmartThings/philips-hue/src/hue/api.lua +++ b/drivers/SmartThings/philips-hue/src/hue/api.lua @@ -175,6 +175,8 @@ function PhilipsHueApi:update_connection(hub_base_url, api_key) self._ctrl_tx:send(msg) end +---@return table|nil response REST response, nil if error +---@return nil|string error nil on success local function do_get(instance, path) local reply_tx, reply_rx = channel.new() reply_rx:settimeout(10) @@ -188,6 +190,8 @@ local function do_get(instance, path) return table.unpack(recv, 1, recv.n) end +---@return table|nil response REST response, nil if error +---@return nil|string error nil on success local function do_put(instance, path, payload) local reply_tx, reply_rx = channel.new() reply_rx:settimeout(10) diff --git a/drivers/SmartThings/philips-hue/src/hue/types.lua b/drivers/SmartThings/philips-hue/src/hue/types.lua index 5437777529..f39870cf67 100644 --- a/drivers/SmartThings/philips-hue/src/hue/types.lua +++ b/drivers/SmartThings/philips-hue/src/hue/types.lua @@ -30,6 +30,7 @@ --- @field public id string --- @field public device_network_id string --- @field public data table|nil migration data for a migrated device +--- @field public log table device-scoped logging module --- @field public get_field fun(self: HueDevice, key: string):any --- @field public set_field fun(self: HueDevice, key: string, value: any, args?: table) --- @field public emit_event fun(self: HueDevice, event: any)