Skip to content

Commit c06fcbc

Browse files
authored
Merge pull request #2075 from SmartThingsCommunity/hue_jiffy_pop
Philips-hue batched command handling support
2 parents f3f5d44 + 90da94a commit c06fcbc

File tree

11 files changed

+1053
-10
lines changed

11 files changed

+1053
-10
lines changed

drivers/SmartThings/philips-hue/src/fields.lua

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ local Fields = {
1919
DEVICE_TYPE = "devicetype",
2020
EVENT_SOURCE = "eventsource",
2121
GAMUT = "gamut",
22+
GROUPS = "groups",
23+
GROUPS_SCAN_QUEUE = "groups_scan_queue",
2224
HUE_DEVICE_ID = "hue_device_id",
2325
IPV4 = "ipv4",
2426
IS_ONLINE = "is_online",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
local log = require "log"
2+
local st_utils = require "st.utils"
3+
local Consts = require "consts"
4+
local Fields = require "fields"
5+
local HueColorUtils = require "utils.cie_utils"
6+
local grouped_utils = require "utils.grouped_utils"
7+
local utils = require "utils"
8+
9+
10+
---@class GroupedLightCommandHandlers
11+
local GroupedLightCommandHandlers = {}
12+
13+
---@param driver HueDriver
14+
---@param bridge_device HueBridgeDevice
15+
---@param group table
16+
---@param args table
17+
local function do_switch_action(driver, bridge_device, group, args)
18+
local on = args.command == "on"
19+
20+
local grouped_light_id = group.grouped_light_rid
21+
if not grouped_light_id then
22+
log.error(string.format("Couldn't find grouped light id for group %s",
23+
group.id or "unknown group id"))
24+
return
25+
end
26+
27+
local hue_api = bridge_device:get_field(Fields.BRIDGE_API) --[[@as PhilipsHueApi]]
28+
if not hue_api then
29+
log.error(string.format("Couldn't find api instance for bridge %s",
30+
bridge_device.label or bridge_device.id or "unknown bridge"))
31+
return
32+
end
33+
34+
local resp, err = hue_api:set_grouped_light_on_state(grouped_light_id, on)
35+
if not resp or (resp.errors and #resp.errors == 0) then
36+
if err ~= nil then
37+
log.error_with({ hub_logs = true }, "Error performing on/off action: " .. err)
38+
elseif resp and #resp.errors > 0 then
39+
for _, error in ipairs(resp.errors) do
40+
log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description)
41+
end
42+
end
43+
end
44+
end
45+
46+
---@param driver HueDriver
47+
---@param bridge_device HueBridgeDevice
48+
---@param group table
49+
---@param args table
50+
local function do_switch_level_action(driver, bridge_device, group, args)
51+
local level = st_utils.clamp_value(args.args.level, 1, 100)
52+
53+
local grouped_light_id = group.grouped_light_rid
54+
if not grouped_light_id then
55+
log.error(string.format("Couldn't find grouped light id for group %s",
56+
group.id or "unknown group id"))
57+
return
58+
end
59+
60+
local hue_api = bridge_device:get_field(Fields.BRIDGE_API) --[[@as PhilipsHueApi]]
61+
if not hue_api then
62+
log.error(string.format("Couldn't find api instance for bridge %s",
63+
bridge_device.label or bridge_device.id or "unknown bridge"))
64+
return
65+
end
66+
67+
-- An individual command checks the state of the device before doing this.
68+
-- It is probably not worth iterating through all the devices to check their state.
69+
local resp, err = hue_api:set_grouped_light_on_state(grouped_light_id, true)
70+
if not resp or (resp.errors and #resp.errors == 0) then
71+
if err ~= nil then
72+
log.error_with({ hub_logs = true }, "Error performing on/off action: " .. err)
73+
elseif resp and #resp.errors > 0 then
74+
for _, error in ipairs(resp.errors) do
75+
log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description)
76+
end
77+
end
78+
end
79+
80+
local resp, err = hue_api:set_grouped_light_level(grouped_light_id, level)
81+
if not resp or (resp.errors and #resp.errors == 0) then
82+
if err ~= nil then
83+
log.error_with({ hub_logs = true }, "Error performing switch level action: " .. err)
84+
elseif resp and #resp.errors > 0 then
85+
for _, error in ipairs(resp.errors) do
86+
log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description)
87+
end
88+
end
89+
end
90+
end
91+
92+
93+
---@param driver HueDriver
94+
---@param bridge_device HueBridgeDevice
95+
---@param group table
96+
---@param args table
97+
---@param aux table auxilary data needed for the command that the devices all had in common
98+
local function do_color_action(driver, bridge_device, group, args, aux)
99+
local hue, sat = (args.args.color.hue / 100), (args.args.color.saturation / 100)
100+
if hue == 1 then -- 0 and 360 degrees are equivalent in HSV, but not in our conversion function
101+
hue = 0
102+
grouped_utils.set_field_on_group_devices(group, Fields.WRAPPED_HUE, true)
103+
end
104+
105+
local grouped_light_id = group.grouped_light_rid
106+
if not grouped_light_id then
107+
log.error(string.format("Couldn't find grouped light id for group %s",
108+
group.id or "unknown group id"))
109+
return
110+
end
111+
112+
local hue_api = bridge_device:get_field(Fields.BRIDGE_API) --[[@as PhilipsHueApi]]
113+
if not hue_api then
114+
log.error(string.format("Couldn't find api instance for bridge %s",
115+
bridge_device.label or bridge_device.id or "unknown bridge"))
116+
return
117+
end
118+
119+
local red, green, blue = st_utils.hsv_to_rgb(hue, sat)
120+
local xy = HueColorUtils.safe_rgb_to_xy(red, green, blue, aux[Fields.GAMUT])
121+
122+
local resp, err = hue_api:set_grouped_light_color_xy(grouped_light_id, xy)
123+
if not resp or (resp.errors and #resp.errors == 0) then
124+
if err ~= nil then
125+
log.error_with({ hub_logs = true }, "Error performing color action: " .. err)
126+
elseif resp and #resp.errors > 0 then
127+
for _, error in ipairs(resp.errors) do
128+
log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description)
129+
end
130+
end
131+
end
132+
end
133+
134+
---@param driver HueDriver
135+
---@param bridge_device HueBridgeDevice
136+
---@param group table
137+
---@param args table
138+
---@param aux table auxilary data needed for the command that the devices all had in common
139+
local function do_setHue_action(driver, bridge_device, group, args, aux)
140+
local currentSaturation = aux[Fields.COLOR_SATURATION] or 0
141+
args.args.color = {
142+
hue = args.args.hue,
143+
saturation = currentSaturation
144+
}
145+
do_color_action(driver, bridge_device, group, args, aux)
146+
end
147+
148+
---@param driver HueDriver
149+
---@param bridge_device HueBridgeDevice
150+
---@param group table
151+
---@param args table
152+
---@param aux table auxilary data needed for the command that the devices all had in common
153+
local function do_setSaturation_action(driver, bridge_device, group, args, aux)
154+
local currentHue = aux[Fields.COLOR_HUE] or 0
155+
args.args.color = {
156+
hue = currentHue,
157+
saturation = args.args.saturation
158+
}
159+
do_color_action(driver, bridge_device, group, args, aux)
160+
end
161+
162+
---@param driver HueDriver
163+
---@param bridge_device HueBridgeDevice
164+
---@param group table
165+
---@param args table
166+
---@param aux table auxilary data needed for the command that the devices all had in common
167+
local function do_color_temp_action(driver, bridge_device, group, args, aux)
168+
local kelvin = args.args.temperature
169+
170+
local grouped_light_id = group.grouped_light_rid
171+
if not grouped_light_id then
172+
log.error(string.format("Couldn't find grouped light id for group %s",
173+
group.id or "unknown group id"))
174+
return
175+
end
176+
177+
local hue_api = bridge_device:get_field(Fields.BRIDGE_API) --[[@as PhilipsHueApi]]
178+
if not hue_api then
179+
log.error(string.format("Couldn't find api instance for bridge %s",
180+
bridge_device.label or bridge_device.id or "unknown bridge"))
181+
return
182+
end
183+
184+
local min = aux[Fields.MIN_KELVIN] or Consts.MIN_TEMP_KELVIN_WHITE_AMBIANCE
185+
local clamped_kelvin = st_utils.clamp_value(kelvin, min, Consts.MAX_TEMP_KELVIN)
186+
local mirek = math.floor(utils.kelvin_to_mirek(clamped_kelvin))
187+
188+
local resp, err = hue_api:set_grouped_light_color_temp(grouped_light_id, mirek)
189+
190+
if not resp or (resp.errors and #resp.errors == 0) then
191+
if err ~= nil then
192+
log.error_with({ hub_logs = true }, "Error performing color temp action: " .. err)
193+
elseif resp and #resp.errors > 0 then
194+
for _, error in ipairs(resp.errors) do
195+
log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description)
196+
end
197+
end
198+
end
199+
grouped_utils.set_field_on_group_devices(group, Fields.COLOR_TEMP_SETPOINT, clamped_kelvin);
200+
end
201+
202+
---@param driver HueDriver
203+
---@param bridge_device HueBridgeDevice
204+
---@param group table
205+
---@param args table
206+
---@param aux table auxilary data needed for the command that the devices all had in common
207+
function GroupedLightCommandHandlers.switch_on_handler(driver, bridge_device, group, args, aux)
208+
do_switch_action(driver, bridge_device, group, args)
209+
end
210+
211+
---@param driver HueDriver
212+
---@param bridge_device HueBridgeDevice
213+
---@param group table
214+
---@param args table
215+
---@param aux table auxilary data needed for the command that the devices all had in common
216+
function GroupedLightCommandHandlers.switch_off_handler(driver, bridge_device, group, args, aux)
217+
do_switch_action(driver, bridge_device, group, args)
218+
end
219+
220+
---@param driver HueDriver
221+
---@param bridge_device HueBridgeDevice
222+
---@param group table
223+
---@param args table
224+
---@param aux table auxilary data needed for the command that the devices all had in common
225+
function GroupedLightCommandHandlers.switch_level_handler(driver, bridge_device, group, args, aux)
226+
do_switch_level_action(driver, bridge_device, group, args)
227+
end
228+
229+
---@param driver HueDriver
230+
---@param bridge_device HueBridgeDevice
231+
---@param group table
232+
---@param args table
233+
---@param aux table auxilary data needed for the command that the devices all had in common
234+
function GroupedLightCommandHandlers.set_color_handler(driver, bridge_device, group, args, aux)
235+
do_color_action(driver, bridge_device, group, args, aux)
236+
end
237+
238+
---@param driver HueDriver
239+
---@param bridge_device HueBridgeDevice
240+
---@param group table
241+
---@param args table
242+
---@param aux table auxilary data needed for the command that the devices all had in common
243+
function GroupedLightCommandHandlers.set_hue_handler(driver, bridge_device, group, args, aux)
244+
do_setHue_action(driver, bridge_device, group, args, aux)
245+
end
246+
247+
---@param driver HueDriver
248+
---@param bridge_device HueBridgeDevice
249+
---@param group table
250+
---@param args table
251+
---@param aux table auxilary data needed for the command that the devices all had in common
252+
function GroupedLightCommandHandlers.set_saturation_handler(driver, bridge_device, group, args, aux)
253+
do_setSaturation_action(driver, bridge_device, group, args, aux)
254+
end
255+
256+
---@param driver HueDriver
257+
---@param bridge_device HueBridgeDevice
258+
---@param group table
259+
---@param args table
260+
---@param aux table auxilary data needed for the command that the devices all had in common
261+
function GroupedLightCommandHandlers.set_color_temp_handler(driver, bridge_device, group, args, aux)
262+
do_color_temp_action(driver, bridge_device, group, args, aux)
263+
end
264+
265+
266+
return GroupedLightCommandHandlers

drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua

+8
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ local HueDeviceTypes = require "hue_device_types"
1313
local StrayDeviceHelper = require "stray_device_helper"
1414

1515
local utils = require "utils"
16+
local grouped_utils = require "utils.grouped_utils"
1617

1718
---@class LightLifecycleHandlers
1819
local LightLifecycleHandlers = {}
@@ -197,6 +198,13 @@ function LightLifecycleHandlers.added(driver, device, parent_device_id, resource
197198
command = capabilities.refresh.commands.refresh.NAME,
198199
args = {}
199200
})
201+
202+
local bridge_device = utils.get_hue_bridge_for_device(driver, device, parent_device_id)
203+
if bridge_device then
204+
grouped_utils.queue_group_scan(driver, bridge_device)
205+
else
206+
log.warn("Unable to queue group scan on device added, missing bridge device")
207+
end
200208
end
201209

202210
---@param driver HueDriver

drivers/SmartThings/philips-hue/src/hue/api.lua

+46-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ st_utils.stringify_table = st_utils.stringify_table
1111

1212
local HueDeviceTypes = require "hue_device_types"
1313

14+
local GROUPED_LIGHT = "grouped_light"
15+
1416
-- trick to fix the VS Code Lua Language Server typechecking
1517
---@type fun(val: any?, name: string?, multi_line: boolean?): string
1618
st_utils.stringify_table = st_utils.stringify_table
@@ -325,6 +327,14 @@ function PhilipsHueApi:get_devices() return self:get_all_reprs_for_rtype("device
325327
---@return string? err nil on success
326328
function PhilipsHueApi:get_connectivity_status() return self:get_all_reprs_for_rtype("zigbee_connectivity") end
327329

330+
---@return HueResourceResponse<HueZoneInfo>?
331+
---@return string? err nil on success
332+
function PhilipsHueApi:get_zones() return self:get_all_reprs_for_rtype("zone") end
333+
334+
---@return HueResourceResponse<HueRoomInfo>?
335+
---@return string? err nil on success
336+
function PhilipsHueApi:get_rooms() return self:get_all_reprs_for_rtype("room") end
337+
328338
---@param light_resource_id string
329339
---@return HueResourceResponse<HueLightInfo>?
330340
---@return string? err nil on success
@@ -400,7 +410,15 @@ end
400410
---@return { errors: table[], [string]: any }? response json payload in response to the request, nil on error
401411
---@return string? err error, nil on successful HTTP request but the response may indicate a problem with the request itself.
402412
function PhilipsHueApi:set_light_on_state(id, on)
403-
local url = string.format("/clip/v2/resource/%s/%s", HueDeviceTypes.LIGHT, id)
413+
return self:set_light_on_state_by_device_type(id, on, HueDeviceTypes.LIGHT)
414+
end
415+
416+
function PhilipsHueApi:set_grouped_light_on_state(id, on)
417+
return self:set_light_on_state_by_device_type(id, on, GROUPED_LIGHT)
418+
end
419+
420+
function PhilipsHueApi:set_light_on_state_by_device_type(id, on, device_type)
421+
local url = string.format("/clip/v2/resource/%s/%s", device_type, id)
404422

405423
if type(on) ~= "boolean" then
406424
if on then
@@ -420,8 +438,16 @@ end
420438
---@return { errors: table[], [string]: any }? response json payload in response to the request, nil on error
421439
---@return string? err error, nil on successful HTTP request but the response may indicate a problem with the request itself.
422440
function PhilipsHueApi:set_light_level(id, level)
441+
return self:set_light_level_by_device_type(id, level, HueDeviceTypes.LIGHT)
442+
end
443+
444+
function PhilipsHueApi:set_grouped_light_level(id, level)
445+
return self:set_light_level_by_device_type(id, level, GROUPED_LIGHT)
446+
end
447+
448+
function PhilipsHueApi:set_light_level_by_device_type(id, level, device_type)
423449
if type(level) == "number" then
424-
local url = string.format("/clip/v2/resource/%s/%s", HueDeviceTypes.LIGHT, id)
450+
local url = string.format("/clip/v2/resource/%s/%s", device_type, id)
425451
local payload_table = { dimming = { brightness = level } }
426452

427453
return do_put(self, url, json.encode(payload_table))
@@ -436,11 +462,19 @@ end
436462
---@return { errors: table[], [string]: any }? response json payload in response to the request, nil on error
437463
---@return string? err error, nil on successful HTTP request but the response may indicate a problem with the request itself.
438464
function PhilipsHueApi:set_light_color_xy(id, xy_table)
465+
return self:set_light_color_xy_by_device_type(id, xy_table, HueDeviceTypes.LIGHT)
466+
end
467+
468+
function PhilipsHueApi:set_grouped_light_color_xy(id, xy_table)
469+
return self:set_light_color_xy_by_device_type(id, xy_table, GROUPED_LIGHT)
470+
end
471+
472+
function PhilipsHueApi:set_light_color_xy_by_device_type(id, xy_table, device_type)
439473
local x_valid = (xy_table ~= nil) and ((xy_table.x ~= nil) and (type(xy_table.x) == "number"))
440474
local y_valid = (xy_table ~= nil) and ((xy_table.y ~= nil) and (type(xy_table.y) == "number"))
441475

442476
if x_valid and y_valid then
443-
local url = string.format("/clip/v2/resource/%s/%s", HueDeviceTypes.LIGHT, id)
477+
local url = string.format("/clip/v2/resource/%s/%s", device_type, id)
444478
local payload = json.encode { color = { xy = xy_table }, on = { on = true } }
445479
return do_put(self, url, payload)
446480
else
@@ -454,8 +488,16 @@ end
454488
---@return { errors: table[], [string]: any }? response json payload in response to the request, nil on error
455489
---@return string? err error, nil on successful HTTP request but the response may indicate a problem with the request itself.
456490
function PhilipsHueApi:set_light_color_temp(id, mirek)
491+
return self:set_light_color_temp_by_device_type(id, mirek, HueDeviceTypes.LIGHT)
492+
end
493+
494+
function PhilipsHueApi:set_grouped_light_color_temp(id, mirek)
495+
return self:set_light_color_temp_by_device_type(id, mirek, GROUPED_LIGHT)
496+
end
497+
498+
function PhilipsHueApi:set_light_color_temp_by_device_type(id, mirek, device_type)
457499
if type(mirek) == "number" then
458-
local url = string.format("/clip/v2/resource/%s/%s", HueDeviceTypes.LIGHT, id)
500+
local url = string.format("/clip/v2/resource/%s/%s", device_type, id)
459501
local payload = json.encode { color_temperature = { mirek = mirek }, on = { on = true } }
460502

461503
return do_put(self, url, payload)

0 commit comments

Comments
 (0)