Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a8b3846
philips-hue: Fix a few inconsistencies with json decodingn
varzac Aug 12, 2026
e8c0547
Add initial set of LAN integration tests for Hue
varzac Aug 12, 2026
c3893ff
Cleanup HueDeviceBuilder test util
varzac Aug 20, 2026
6b2d7b9
philips-hue: Add LAN integration tests for batched command routing
varzac Oct 13, 2026
727f8f1
philips-hue: Add recorded bridge payload fixtures for tests
varzac Oct 15, 2026
359bd6a
philips-hue: Add tests replaying behavior recorded on real hardware
varzac Oct 15, 2026
d5b54e9
philips-hue: Keep a light's gamut and skip colour temperature in colo…
varzac Oct 15, 2026
eb14898
philips-hue: Only turn a group on before setLevel if one of its light…
varzac Oct 15, 2026
7e9c7e3
philips-hue: Register a light's services when it initializes
varzac Oct 15, 2026
5d08569
philips-hue: Mark devices offline once per event stream outage
varzac Oct 15, 2026
501a2a4
philips-hue: Don't emit levelRange for on/off-only lights
varzac Oct 15, 2026
dd570d0
philips-hue: Test a real event stream outage with failed reconnects
varzac Oct 15, 2026
35bf716
philips-hue: Test resending a command after the bridge closes a keep-…
varzac Oct 15, 2026
ed747e3
philips-hue: Test event stream reconnects rejected with 503
varzac Oct 15, 2026
0c0bf3f
philips-hue: Replay recorded light resources with their sync mode
varzac Oct 15, 2026
720dac4
philips-hue: Check request bodies as JSON in the newer tests
varzac Oct 15, 2026
6cf36a2
philips-hue: Count online/offline calls with the framework's status h…
varzac Oct 15, 2026
b75665d
philips-hue: Use the framework's wait_until in tests
varzac Oct 15, 2026
1530566
philips-hue: Share one REST test connection for reads and commands
varzac Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 33 additions & 21 deletions drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua
Original file line number Diff line number Diff line change
Expand Up @@ -93,34 +93,46 @@ local function _emit_light_events_inner(light_device, light_repr)
light_device.log.debug(st_utils.stringify_table(color_temp_range, "color temp range unchanged"));
end

-- local min = or Consts.MIN_TEMP_KELVIN_WHITE_AMBIANCE
local kelvin = math.floor(
st_utils.clamp_value(utils.mirek_to_kelvin(mirek), min_kelvin, max_kelvin)
)
if utils.is_nan(kelvin) then
light_device.log.warn(
string.format(
"Non numeric value %s computed for colorTemperature Attribute Event, ignoring.",
kelvin
)
)
-- In xy colour mode the bridge reports `mirek_valid = false` (and `mirek = null`); there is
-- no colour temperature to report then.
if light_repr.color_temperature.mirek_valid == false then
light_device.log.debug("Light is in colour mode, not emitting a colour temperature")
else
local last_kelvin_setpoint = light_device:get_field(Fields.COLOR_TEMP_SETPOINT)
if
last_kelvin_setpoint ~= nil and
last_kelvin_setpoint >= utils.mirek_to_kelvin(mirek + 1) and
last_kelvin_setpoint <= utils.mirek_to_kelvin(mirek - 1)
then
kelvin = last_kelvin_setpoint;
-- local min = or Consts.MIN_TEMP_KELVIN_WHITE_AMBIANCE
local kelvin = math.floor(
st_utils.clamp_value(utils.mirek_to_kelvin(mirek), min_kelvin, max_kelvin)
)
if utils.is_nan(kelvin) then
light_device.log.warn(
string.format(
"Non numeric value %s computed for colorTemperature Attribute Event, ignoring.",
kelvin
)
)
else
local last_kelvin_setpoint = light_device:get_field(Fields.COLOR_TEMP_SETPOINT)
if
last_kelvin_setpoint ~= nil and
last_kelvin_setpoint >= utils.mirek_to_kelvin(mirek + 1) and
last_kelvin_setpoint <= utils.mirek_to_kelvin(mirek - 1)
then
kelvin = last_kelvin_setpoint;
end
light_device:emit_event(capabilities.colorTemperature.colorTemperature(kelvin))
end
light_device:emit_event(capabilities.colorTemperature.colorTemperature(kelvin))
end
light_device:set_field(Fields.COLOR_TEMP_SETPOINT, nil);
end

if light_repr.color then
light_device:set_field(Fields.GAMUT, light_repr.color.gamut, { persist = true })
local r, g, b = HueColorUtils.safe_xy_to_rgb(light_repr.color.xy, light_repr.color.gamut)
-- Updates (e.g. over SSE) usually carry only `xy`; keep the gamut learned from the full resource.
local gamut = light_repr.color.gamut
if gamut ~= nil then
light_device:set_field(Fields.GAMUT, gamut, { persist = true })
else
gamut = light_device:get_field(Fields.GAMUT)
end
local r, g, b = HueColorUtils.safe_xy_to_rgb(light_repr.color.xy, gamut)
local hue, sat, _ = st_utils.rgb_to_hsv(r, g, b)
-- We sent a command where hue == 100 and wrapped the value to 0, reverse that here
if light_device:get_field(Fields.WRAPPED_HUE) == true and (hue + .05 >= 1 or hue - .05 <= 0) then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,23 @@ local function do_switch_level_action(driver, bridge_device, group, args)
return
end

-- An individual command checks the state of the device before doing this.
-- It is probably not worth iterating through all the devices to check their state.
local resp, err = hue_api:set_grouped_light_on_state(grouped_light_id, true)
if not resp or (resp.errors and #resp.errors == 0) then
if err ~= nil then
log.error_with({ hub_logs = true }, "Error performing on/off action: " .. err)
elseif resp and #resp.errors > 0 then
for _, error in ipairs(resp.errors) do
log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description)
-- As with an individual command, only turn the group on first if one of its lights is off.
local any_off = false
for _, device in ipairs(group.devices or {}) do
if device:get_field(Fields.SWITCH_STATE) == "off" then
any_off = true
break
end
end
if any_off then
local resp, err = hue_api:set_grouped_light_on_state(grouped_light_id, true)
if not resp or (resp.errors and #resp.errors == 0) then
if err ~= nil then
log.error_with({ hub_logs = true }, "Error performing on/off action: " .. err)
elseif resp and #resp.errors > 0 then
for _, error in ipairs(resp.errors) do
log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description)
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,14 @@ function LightLifecycleHandlers.init(driver, device)
if not svc_rids_for_device[device_light_resource_id] then
svc_rids_for_device[device_light_resource_id] = HueDeviceTypes.LIGHT
end
if hue_device_id ~= nil then
driver.services_for_device_rid[hue_device_id] = svc_rids_for_device
end
device:set_field(Fields._INIT, true, { persist = false })
device:emit_event(capabilities.switchLevel.levelRange({ minimum = 1, maximum = 100 }))
-- On/off-only lights (e.g. smart plugs) have no switchLevel capability.
if device:supports_capability_by_id(capabilities.switchLevel.ID) then
device:emit_event(capabilities.switchLevel.levelRange({ minimum = 1, maximum = 100 }))
end

if device:get_field(Fields._REFRESH_AFTER_INIT) then
driver:inject_capability_command(device, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ function RefreshHandlers.do_refresh_light(driver, light_device, light_status_cac
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 })
light_device:set_field(Fields.GAMUT, light_info.color.gamut, { persist = true })
end
attribute_emitters.emit_light_attribute_events(light_device, light_info)
do_light_request = false
Expand Down Expand Up @@ -348,7 +348,7 @@ function RefreshHandlers.do_refresh_light(driver, light_device, light_status_cac
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 })
light_device:set_field(Fields.GAMUT, light_info.color.gamut, { persist = true })
end
attribute_emitters.emit_light_attribute_events(light_device, light_info)
return light_info
Expand Down
6 changes: 5 additions & 1 deletion drivers/SmartThings/philips-hue/src/hue/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,11 @@ local function process_rest_response(response, err, partial, err_callback)
)
end

return table.unpack(json_result, 1, json_result.n)
-- json.decode (dkjson) returns (value, next_position, error_message) -- only the first of
-- those is the decoded value this function documents returning; propagating all of them
-- here means the *parse position* gets misinterpreted as this function's `err` return by
-- every caller, on every successful decode.
return json_result[1]
else
return nil, "no response or error received"
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"id": "samsungim.hueSyncMode",
"version": 1,
"status": "proposed",
"name": "Hue Sync Mode",
"ephemeral": false,
"attributes": {
"mode": {
"schema": {
"type": "object",
"properties": {
"value": {
"type": "string",
"title": "Hue Sync mode",
"enum": [
"normal",
"streaming"
]
}
},
"additionalProperties": false,
"required": [
"value"
]
},
"enumCommands": []
}
},
"commands": {}
}
Loading
Loading