Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for the Philips Hue Smart Plug #1350

Merged
merged 5 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 16 additions & 16 deletions drivers/SmartThings/philips-hue/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<dir>/init.lua` instead of `<mod>.lua` as a sibling to `<dir>`.
- [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 `<dir>/init.lua` instead of `<mod>.lua` as a sibling to `<dir>`.
30 changes: 30 additions & 0 deletions drivers/SmartThings/philips-hue/profiles/4-button-remote.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions drivers/SmartThings/philips-hue/profiles/motion-sensor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions drivers/SmartThings/philips-hue/profiles/plug.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: plug
components:
- id: main
capabilities:
- id: switch
version: 1
- id: samsungim.hueSyncMode
tpmanley marked this conversation as resolved.
Show resolved Hide resolved
version: 1
- id: refresh
version: 1
categories:
- name: SmartPlug
12 changes: 12 additions & 0 deletions drivers/SmartThings/philips-hue/profiles/single-button.yml
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions drivers/SmartThings/philips-hue/profiles/two-button.yml
Original file line number Diff line number Diff line change
@@ -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
170 changes: 170 additions & 0 deletions drivers/SmartThings/philips-hue/src/disco/button.lua
Original file line number Diff line number Diff line change
@@ -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<string,any>? 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<string,any>? 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<HueDeviceTypes,HueServiceInfo[]>
---@param device_service_info HueDeviceInfo
---@param device_state_disco_cache table<string, 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
Loading
Loading