Skip to content

Commit

Permalink
fixup! fix: Update refresh to poll connectivity status
Browse files Browse the repository at this point in the history
  • Loading branch information
dljsjr committed Oct 17, 2023
1 parent 7e4e068 commit fcf3ae8
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 66 deletions.
215 changes: 149 additions & 66 deletions drivers/SmartThings/philips-hue/src/handlers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions drivers/SmartThings/philips-hue/src/hue/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions drivers/SmartThings/philips-hue/src/hue/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit fcf3ae8

Please sign in to comment.