Add GUI
This commit is contained in:
parent
00c6da8f0a
commit
273e111477
7 changed files with 741 additions and 35 deletions
242
main.py
242
main.py
|
@ -1,9 +1,241 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from tinge import Tinge
|
||||
from typing import Union
|
||||
|
||||
import wx
|
||||
import wx.lib.scrolledpanel as scrolled
|
||||
|
||||
from tinge import Tinge, HueBridge, HueGroup, HueLight
|
||||
|
||||
|
||||
class Hui(wx.Frame):
|
||||
"""This is the Hue GUI class
|
||||
|
||||
Args:
|
||||
wx (Frame): Parent class
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
"""Constructor
|
||||
"""
|
||||
super().__init__(*args, **kw)
|
||||
self.m_on_icon: str = '☼'
|
||||
self.m_off_icon: str = '☾'
|
||||
self.m_tinge: Tinge = Tinge()
|
||||
self.m_bridges: list[HueBridge] = self.m_tinge.get_bridges()
|
||||
self.cur_bridge: Union[None, HueBridge] = None
|
||||
self.cur_group: Union[None, HueGroup] = None
|
||||
# create a panel in the frame
|
||||
self.pnl: scrolled.ScrolledPanel = scrolled.ScrolledPanel(self, -1, style=wx.VSCROLL)
|
||||
self.pnl.SetupScrolling()
|
||||
# and create a sizer to manage the layout of child widgets
|
||||
self.sizer: wx.BoxSizer = wx.BoxSizer(wx.VERTICAL)
|
||||
self.pnl.SetSizer(self.sizer)
|
||||
self.add_bridges()
|
||||
|
||||
def add_bridges(self):
|
||||
"""Add bridges to sizer, the entry point of the program
|
||||
"""
|
||||
self.sizer.Clear(delete_windows=True)
|
||||
if self.m_bridges:
|
||||
for bridge in self.m_bridges:
|
||||
btn: wx.Button = wx.Button(self.pnl, label=str(bridge))
|
||||
self.sizer.Add(btn, 0, wx.EXPAND)
|
||||
self.Bind(wx.EVT_BUTTON,
|
||||
lambda event, mbridge=bridge: self.goto_bridge(mbridge), btn)
|
||||
else:
|
||||
btn: wx.Button = wx.Button(self.pnl, label="Discover bridges")
|
||||
self.sizer.Add(btn, 0, wx.EXPAND)
|
||||
self.Bind(wx.EVT_BUTTON,
|
||||
lambda event: self.discover_new_bridges(), btn)
|
||||
self.sizer.Layout()
|
||||
|
||||
def add_groups(self, groups: list[HueGroup]):
|
||||
"""This will add the groups to the sizer, when coming down from a bridge, or up from a light
|
||||
|
||||
Args:
|
||||
groups (list[HueGroup]): The groups to display
|
||||
"""
|
||||
self.sizer.Clear(delete_windows=True)
|
||||
bridge_btn: wx.Button = wx.Button(self.pnl, label=str(self.cur_bridge))
|
||||
self.sizer.Add(bridge_btn, 0, wx.EXPAND)
|
||||
self.Bind(wx.EVT_BUTTON,
|
||||
lambda event: self.add_bridges(), bridge_btn)
|
||||
for group in groups:
|
||||
inner_sizer: wx.BoxSizer = wx.BoxSizer(orient=wx.HORIZONTAL)
|
||||
groupid: int = group.get_id()
|
||||
icon: str = self.m_off_icon
|
||||
if group.is_any_on():
|
||||
icon = self.m_on_icon
|
||||
toggle_btn: wx.Button = wx.Button(self.pnl, label=icon)
|
||||
inner_sizer.Add(toggle_btn, 0, wx.EXPAND)
|
||||
self.Bind(wx.EVT_BUTTON,
|
||||
lambda event, mgroupid=groupid: self.toggle_group(mgroupid), toggle_btn)
|
||||
label: str = str(group)
|
||||
group_btn: wx.Button = wx.Button(self.pnl, label=label)
|
||||
inner_sizer.Add(group_btn, 0, wx.EXPAND)
|
||||
self.Bind(wx.EVT_BUTTON,
|
||||
lambda event, mgroupid=groupid: self.goto_group(mgroupid), group_btn)
|
||||
self.sizer.Add(inner_sizer, 0, wx.EXPAND)
|
||||
self.sizer.Layout()
|
||||
|
||||
# noinspection PyDefaultArgument
|
||||
def add_lights(self, lights: list[HueLight]):
|
||||
"""This will add the lights from a group to the sizer
|
||||
|
||||
Args:
|
||||
lights (list[HueLight]): The lights to display
|
||||
"""
|
||||
self.sizer.Clear(delete_windows=True)
|
||||
group_btn: wx.Button = wx.Button(self.pnl, label=str(self.cur_group))
|
||||
self.sizer.Add(group_btn, 0, wx.EXPAND)
|
||||
self.Bind(wx.EVT_BUTTON,
|
||||
lambda event: self.add_groups(self.cur_bridge.get_groups()), group_btn)
|
||||
for light in lights:
|
||||
inner_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)
|
||||
lightid: int = light.get_id()
|
||||
icon: str = self.m_off_icon
|
||||
if light.is_on():
|
||||
icon = self.m_on_icon
|
||||
toggle_btn: wx.Button = wx.Button(self.pnl, label=icon)
|
||||
inner_sizer.Add(toggle_btn, 0, wx.EXPAND)
|
||||
self.Bind(wx.EVT_BUTTON,
|
||||
lambda event, mlightid=lightid, mlights=lights: self.toggle_light_and_goto_group(mlightid,
|
||||
mlights),
|
||||
toggle_btn)
|
||||
label: str = "{}".format(light)
|
||||
light_btn: wx.Button = wx.Button(self.pnl, label=label)
|
||||
inner_sizer.Add(light_btn, 0, wx.EXPAND)
|
||||
self.Bind(wx.EVT_BUTTON,
|
||||
lambda event, mlightid=lightid: self.goto_light(mlightid), light_btn)
|
||||
self.sizer.Add(inner_sizer, 0, wx.EXPAND)
|
||||
self.sizer.Layout()
|
||||
|
||||
def goto_bridge(self, bridge: HueBridge):
|
||||
"""Call back for a bridge button
|
||||
|
||||
Args:
|
||||
bridge (HueBridge): The bridge to display
|
||||
"""
|
||||
self.cur_bridge = bridge
|
||||
groups: list[HueGroup] = bridge.get_groups()
|
||||
if groups:
|
||||
self.add_groups(groups)
|
||||
else:
|
||||
self.add_lights(bridge.get_lights())
|
||||
|
||||
def discover_new_bridges(self):
|
||||
"""Call back for button that is displayed if no bridges were found
|
||||
"""
|
||||
self.m_tinge.discover_new_bridges()
|
||||
self.add_bridges()
|
||||
|
||||
def toggle_group(self, groupid: int):
|
||||
"""Toggle the lights of a group
|
||||
|
||||
Args:
|
||||
groupid (int): The group id of the group to toggle
|
||||
"""
|
||||
self.cur_bridge.get_group_by_id(groupid).toggle()
|
||||
self.add_groups(self.cur_bridge.get_groups())
|
||||
|
||||
def goto_group(self, groupid: int):
|
||||
"""Call back for group button
|
||||
|
||||
Args:
|
||||
groupid (int): The group id of the group to display
|
||||
"""
|
||||
group = self.cur_bridge.get_group_by_id(groupid)
|
||||
self.cur_group = group
|
||||
self.add_lights(group.get_lights())
|
||||
|
||||
def toggle_light_and_goto_group(self, lightid: int, lights: list[HueLight]):
|
||||
"""Combo call back for toggle and goto group
|
||||
|
||||
Args:
|
||||
lightid (int): The light id oof the light to toggle
|
||||
lights (list[HueLight]): The lights to display after toggle
|
||||
"""
|
||||
self.cur_bridge.get_light_by_id(lightid).toggle()
|
||||
self.add_lights(lights)
|
||||
|
||||
def goto_light(self, lightid: int):
|
||||
"""Call back for light button
|
||||
|
||||
Args:
|
||||
lightid (int): The light id of the light to display
|
||||
"""
|
||||
light: HueLight = self.cur_bridge.get_light_by_id(lightid)
|
||||
is_on: bool = light.is_on()
|
||||
self.sizer.Clear(delete_windows=True)
|
||||
group: HueGroup = self.cur_group
|
||||
group_btn: wx.Button = wx.Button(self.pnl, label=str(group))
|
||||
self.sizer.Add(group_btn, 0, wx.EXPAND)
|
||||
self.Bind(wx.EVT_BUTTON,
|
||||
lambda event: self.goto_group(self.cur_group.get_id()), group_btn)
|
||||
# Toggle
|
||||
icon: str = self.m_off_icon
|
||||
if is_on:
|
||||
icon = self.m_on_icon
|
||||
toggle_btn: wx.Button = wx.Button(self.pnl, label=icon)
|
||||
self.sizer.Add(toggle_btn, 0, wx.EXPAND)
|
||||
self.Bind(wx.EVT_BUTTON,
|
||||
lambda event, mlightid=lightid: self.toggle_light_and_goto_light(mlightid),
|
||||
toggle_btn)
|
||||
|
||||
# Slider for brightness
|
||||
if is_on and light.get_brightness() > 0:
|
||||
b_label: wx.StaticText = wx.StaticText(self.pnl, label="Brightness")
|
||||
self.sizer.Add(b_label, 0, wx.EXPAND)
|
||||
b_slider: wx.Slider = wx.Slider(self.pnl, value=light.get_state().get_brightness(), minValue=1,
|
||||
maxValue=254)
|
||||
self.sizer.Add(b_slider, 0, wx.EXPAND)
|
||||
self.Bind(wx.EVT_SCROLL_THUMBRELEASE,
|
||||
lambda event: self.set_brightness(event, light.get_id()), b_slider)
|
||||
# Slider for colortemp
|
||||
if is_on and light.can_set_ct():
|
||||
c_label: wx.StaticText = wx.StaticText(self.pnl, label="Color Temperature")
|
||||
self.sizer.Add(c_label, 0, wx.EXPAND)
|
||||
c_slider: wx.Slider = wx.Slider(self.pnl, value=light.get_ct(), minValue=153, maxValue=500)
|
||||
self.sizer.Add(c_slider, 0, wx.EXPAND)
|
||||
self.Bind(wx.EVT_SCROLL_THUMBRELEASE,
|
||||
lambda event: self.set_colortemp(event, light.get_id()), c_slider)
|
||||
self.sizer.Layout()
|
||||
|
||||
def set_brightness(self, event: wx.ScrollEvent, lightid: int):
|
||||
"""Call back for brightness slider
|
||||
|
||||
Args:
|
||||
event (wx.ScrollEvent): The scroll event to react to
|
||||
lightid (int): The light id of the light to adjust brightness of
|
||||
"""
|
||||
bri: int = event.GetPosition()
|
||||
light: HueLight = self.cur_bridge.get_light_by_id(lightid)
|
||||
light.set_brightness(bri)
|
||||
|
||||
def toggle_light_and_goto_light(self, lightid):
|
||||
"""Combo call back to toggle a light and display that light again
|
||||
|
||||
Args:
|
||||
lightid ([type]): The light id of the light to toggle/display
|
||||
"""
|
||||
self.cur_bridge.get_light_by_id(lightid).toggle()
|
||||
self.goto_light(lightid)
|
||||
|
||||
def set_colortemp(self, event, lightid):
|
||||
"""Call back for colortemp slider
|
||||
|
||||
Args:
|
||||
event (wx.ScrollEvent): The scroll event to react to
|
||||
lightid (int): The light id of the light to adjust colortemp of
|
||||
"""
|
||||
ct: int = event.GetPosition()
|
||||
light: HueLight = self.cur_bridge.get_light_by_id(lightid)
|
||||
light.set_ct(ct)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
m_tinge = Tinge()
|
||||
bridge = m_tinge.get_bridges()[0]
|
||||
print(bridge.discover_new_lights())
|
||||
print(bridge.append_new_lights())
|
||||
app = wx.App()
|
||||
frm = Hui(None, title="Tinge")
|
||||
frm.Show()
|
||||
app.MainLoop()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
from typing import Union
|
||||
|
||||
from ..HueGroup import HueGroup
|
||||
from ..HueLight import HueLight
|
||||
|
@ -8,13 +9,40 @@ from ..HueUtils import make_request
|
|||
|
||||
|
||||
class HueBridge:
|
||||
def __init__(self, ipaddress: str, username: str):
|
||||
"""This class represents a Hue Bridge
|
||||
"""
|
||||
|
||||
def __init__(self, ipaddress: str, username: str, name: str = ""):
|
||||
""" Constructor
|
||||
|
||||
Args:
|
||||
ipaddress (str): The ip address of the bridge
|
||||
username (str): The username for this app for this bridge
|
||||
name (str, optional): A human readable name for this bridge. Is set to ipaddress, if not supplied.
|
||||
"""
|
||||
self.m_ipaddress: str = ipaddress
|
||||
self.m_username: str = username
|
||||
if name:
|
||||
self.m_name: str = name
|
||||
else:
|
||||
self.m_name = self.m_ipaddress
|
||||
self.m_lights: list[HueLight] = self.discover_lights()
|
||||
self.m_groups: list[HueGroup] = self.discover_groups()
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""The string representation of this bridge
|
||||
|
||||
Returns:
|
||||
str: Returns the name
|
||||
"""
|
||||
return self.m_name
|
||||
|
||||
def discover_groups(self) -> list[HueGroup]:
|
||||
"""Get groups from Hue Api
|
||||
|
||||
Returns:
|
||||
list[HueGroup]: discovered groups
|
||||
"""
|
||||
path: str = "{}/groups".format(self.m_username)
|
||||
response = make_request(self.m_ipaddress, path)
|
||||
groups: list[HueGroup] = list()
|
||||
|
@ -27,6 +55,19 @@ class HueBridge:
|
|||
|
||||
def create_group(self, lights: list[HueLight], name: str, group_type: str = "LightGroup",
|
||||
group_class: str = "Other") -> bool:
|
||||
"""Create a group from a list of lights
|
||||
|
||||
Args:
|
||||
lights (list[HueLight]): a list of lights to group
|
||||
name (str): The name of the new group
|
||||
group_type (str, optional): The group type can be “LightGroup”, “Room” or either “Luminaire” or
|
||||
“LightSource” if a Multisource Luminaire is present in the system.
|
||||
Defaults to "LightGroup".
|
||||
group_class (str, optional): Category of Room Types. Defaults to "Other".
|
||||
|
||||
Returns:
|
||||
bool: True if creation was a success, otherwise False
|
||||
"""
|
||||
path = "{}/groups".format(self.get_user())
|
||||
method = "POST"
|
||||
data: dict = {'lights': [], 'name': name, 'type': group_type, 'class': group_class}
|
||||
|
@ -44,6 +85,11 @@ class HueBridge:
|
|||
return False
|
||||
|
||||
def discover_lights(self) -> list[HueLight]:
|
||||
"""Get lights from Hue API
|
||||
|
||||
Returns:
|
||||
list[HueLight]: List of discovered lights
|
||||
"""
|
||||
path: str = "{}/lights".format(self.m_username)
|
||||
response = make_request(self.m_ipaddress, path)
|
||||
lights: list[HueLight] = list()
|
||||
|
@ -51,11 +97,19 @@ class HueBridge:
|
|||
lights.append(HueLight(int(key), value, self.get_ipaddress(), self.get_user()))
|
||||
return lights
|
||||
|
||||
def discover_new_lights(self, device_id_list=None) -> bool:
|
||||
def discover_new_lights(self, light_ids: Union[None, list[int]] = None) -> bool:
|
||||
"""Makes bridge search for new lights
|
||||
|
||||
Args:
|
||||
light_ids (Union[None, list[int]], optional): Either a list of light ids or None. Defaults to None.
|
||||
|
||||
Returns:
|
||||
bool: True if bridge has started looking for new lights, otherwise False
|
||||
"""
|
||||
body: str = ""
|
||||
if device_id_list is not None:
|
||||
if light_ids is not None:
|
||||
body_dict = {'deviceid': []}
|
||||
for device in device_id_list:
|
||||
for device in light_ids:
|
||||
body_dict['deviceid'].append(device)
|
||||
body = json.dumps(body_dict)
|
||||
path: str = "{}/lights".format(self.m_username)
|
||||
|
@ -64,6 +118,12 @@ class HueBridge:
|
|||
return 'success' in response.json()[0].keys()
|
||||
|
||||
def append_new_lights(self) -> bool:
|
||||
"""If any new lights were discovered in discover_new_lights(), they can be appended to this bridges
|
||||
list of lights with this function
|
||||
|
||||
Returns:
|
||||
bool: True if the request was ok, otherwise False
|
||||
"""
|
||||
path: str = "{}/lights/new".format(self.m_username)
|
||||
response = make_request(self.m_ipaddress, path)
|
||||
for key, value in json.loads(response.text).items():
|
||||
|
@ -73,19 +133,60 @@ class HueBridge:
|
|||
self.m_lights.append(HueLight(int(key), response.json(), self.get_ipaddress(), self.get_user()))
|
||||
return response.ok
|
||||
|
||||
def get_groups(self):
|
||||
def get_groups(self) -> list[HueGroup]:
|
||||
"""Return all the groups of this bridge
|
||||
|
||||
Returns:
|
||||
list[HueGroup]: A list of all groups owned by this bridge
|
||||
"""
|
||||
return self.m_groups
|
||||
|
||||
def get_lights(self):
|
||||
def get_lights(self) -> list[HueLight]:
|
||||
"""Get all the lights owned by this bridge
|
||||
|
||||
Returns:
|
||||
list[HueLight]: A flat list of all lights owned by this bridge
|
||||
"""
|
||||
return self.m_lights
|
||||
|
||||
def get_light_by_id(self, id: int) -> HueLight:
|
||||
def get_light_by_id(self, light_id: int) -> HueLight:
|
||||
"""Get a specific light
|
||||
|
||||
Args:
|
||||
light_id (int): The light id of the light to get
|
||||
|
||||
Returns:
|
||||
HueLight: The light
|
||||
"""
|
||||
for light in self.m_lights:
|
||||
if light.get_id() == id:
|
||||
if light.get_id() == light_id:
|
||||
return light
|
||||
|
||||
def get_ipaddress(self):
|
||||
def get_group_by_id(self, group_id: int) -> HueGroup:
|
||||
"""Get a specific group
|
||||
|
||||
Args:
|
||||
group_id (int): The group id of the group to get
|
||||
|
||||
Returns:
|
||||
HueGroup: The group
|
||||
"""
|
||||
for group in self.m_groups:
|
||||
if group.get_id() == group_id:
|
||||
return group
|
||||
|
||||
def get_ipaddress(self) -> str:
|
||||
"""Get the ip address of this bridge
|
||||
|
||||
Returns:
|
||||
str: The ip address
|
||||
"""
|
||||
return self.m_ipaddress
|
||||
|
||||
def get_user(self):
|
||||
def get_user(self) -> str:
|
||||
"""A user, or username, is more like a password and is needed to authenticate with the Hue API
|
||||
|
||||
Returns:
|
||||
str: The username
|
||||
"""
|
||||
return self.m_username
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
|
@ -9,8 +8,19 @@ from ..HueUtils import make_request
|
|||
|
||||
|
||||
class HueGroup:
|
||||
"""A class for groups
|
||||
"""
|
||||
|
||||
class Action:
|
||||
"""The light state of one of the lamps in the group.
|
||||
"""
|
||||
|
||||
def __init__(self, data_slice: dict):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
data_slice (dict): The part of the data structure that concerns this Action
|
||||
"""
|
||||
keys = data_slice.keys()
|
||||
self.m_on: bool = data_slice['on']
|
||||
self.m_bri: int = data_slice['bri']
|
||||
|
@ -41,25 +51,40 @@ class HueGroup:
|
|||
self.m_colormode = str()
|
||||
|
||||
class State:
|
||||
"""A hueGroup.State represents the collective state of the group
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, data_slice: dict):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
data_slice (dict): The part of the data structure that concerns this State
|
||||
"""
|
||||
self.m_all_on: bool = data_slice['all_on']
|
||||
self.m_any_on: bool = data_slice['any_on']
|
||||
|
||||
def is_all_on(self) -> bool:
|
||||
return self.m_all_on
|
||||
def __init__(self, group_id: int, lights: list[HueLight], data: dict, parent_bridge_ip: str,
|
||||
parent_bridge_user: str):
|
||||
"""Constructor
|
||||
|
||||
def is_any_on(self) -> bool:
|
||||
return self.m_any_on
|
||||
|
||||
def __init__(self, id: int, lights: list[HueLight], data: dict, parent_bridge_ip: str, parent_bridge_user: str):
|
||||
self.m_id: int = id
|
||||
Args:
|
||||
group_id (int): The group id for this group
|
||||
lights (list[HueLight]): The lights that is in this group
|
||||
data (dict): The Hue API description of the group
|
||||
parent_bridge_ip (str): ip address of the parent bridge
|
||||
parent_bridge_user (str): username of the parent bridge
|
||||
"""
|
||||
self.m_id: int = group_id
|
||||
self.m_parent_bridge_ip = parent_bridge_ip
|
||||
self.m_parent_bridge_user = parent_bridge_user
|
||||
self.m_name: str = data['name']
|
||||
self.m_lights = lights
|
||||
self.m_sensors: list[str] = data['sensors']
|
||||
self.m_type: str = data['type']
|
||||
self.m_state: HueGroup.State = HueGroup.State(data['state'])
|
||||
# Don't believe the api, it can claim that a light is on even if it is unreachable
|
||||
state: dict = {'all_on': self.is_any_on(), 'any_on': self.is_any_on()}
|
||||
self.m_state: HueGroup.State = HueGroup.State(state)
|
||||
self.m_recycle: bool = data['recycle']
|
||||
if 'class' in data.keys():
|
||||
self.m_class: str = data['class']
|
||||
|
@ -67,32 +92,100 @@ class HueGroup:
|
|||
self.m_class: str = str()
|
||||
self.m_action: HueGroup.Action(data['action'])
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
"""String representation of the group
|
||||
|
||||
Returns:
|
||||
str: The group name
|
||||
"""
|
||||
return self.m_name
|
||||
|
||||
def get_id(self) -> int:
|
||||
"""Get the group id
|
||||
|
||||
Returns:
|
||||
int: The group id
|
||||
"""
|
||||
return self.m_id
|
||||
|
||||
def get_lights(self) -> list[HueLight]:
|
||||
"""Get the lights that belong to this group
|
||||
|
||||
Returns:
|
||||
list[HueLight]: The lights
|
||||
"""
|
||||
return self.m_lights
|
||||
|
||||
def is_all_on(self) -> bool:
|
||||
return self.m_state.is_all_on()
|
||||
"""We really check to make sure
|
||||
|
||||
Returns:
|
||||
bool: True if all lights are on and reachable, otherwise False
|
||||
"""
|
||||
# Dont believe the API, both on and reachable to be on
|
||||
on = True
|
||||
for light in self.m_lights:
|
||||
if not light.is_on():
|
||||
on = False
|
||||
return on
|
||||
|
||||
def is_any_on(self) -> bool:
|
||||
return self.m_state.is_any_on()
|
||||
"""Check if any of the lights are on and reachable
|
||||
|
||||
Returns:
|
||||
bool: True if any light in the group is on and reachable, otherwise False
|
||||
"""
|
||||
# Dont believe the API, both on and reachable to be on
|
||||
on = False
|
||||
for light in self.m_lights:
|
||||
if light.is_on():
|
||||
on = True
|
||||
return on
|
||||
|
||||
def toggle(self):
|
||||
"""Toggle all lights of the group
|
||||
"""
|
||||
for light in self.m_lights:
|
||||
light.toggle()
|
||||
self.update_state()
|
||||
|
||||
def turn_off(self):
|
||||
"""turn off all lights in the group
|
||||
"""
|
||||
state: str = '{"on": false}'
|
||||
self.set_state(state)
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn on all lights in the group
|
||||
"""
|
||||
state: str = '{"on": true}'
|
||||
self.set_state(state)
|
||||
|
||||
def set_state(self, state: str) -> requests.Response:
|
||||
"""Helper method to set the state of the group
|
||||
|
||||
Args:
|
||||
state (str): The state of the group
|
||||
Possible attributes are:
|
||||
* on: True/False
|
||||
* bri: 0-254
|
||||
* hue: 0-65535
|
||||
* sat: 0-254
|
||||
* xy: [0.0-1.0,0.0-1.0]
|
||||
* ct: 153-500
|
||||
* alert: "none"/"select"/"lselect"
|
||||
* effect: "none"/"colorloop"
|
||||
* transitiontime: 0-65535 (multiple of 100ms, default is 4)
|
||||
* bri_inc: -254-254
|
||||
* sat_inc: -254-254
|
||||
* hue_inc: -65534-65534
|
||||
* ct_inc: -65534-65534
|
||||
* xy_inc: -0.5-0.5
|
||||
* scene: "Scene identifier"
|
||||
|
||||
Returns:
|
||||
requests.Response: The API response
|
||||
"""
|
||||
path: str = "{}/groups/{}/action".format(self.m_parent_bridge_user, self.m_id)
|
||||
method: str = "PUT"
|
||||
response = make_request(self.m_parent_bridge_ip, path, method, state)
|
||||
|
@ -100,6 +193,7 @@ class HueGroup:
|
|||
return response
|
||||
|
||||
def update_state(self):
|
||||
path: str = "{}/groups/{}".format(self.m_parent_bridge_user, self.m_id)
|
||||
response = make_request(self.m_parent_bridge_ip, path)
|
||||
self.m_state = HueGroup.State(json.loads(response.text)['state'])
|
||||
"""Update the state after a possible change
|
||||
"""
|
||||
state: dict = {'all_on': self.is_any_on(), 'any_on': self.is_any_on()}
|
||||
self.m_state = HueGroup.State(state)
|
||||
|
|
|
@ -9,8 +9,19 @@ from ..HueUtils import make_request
|
|||
|
||||
|
||||
class HueLight:
|
||||
"""A class representing a light
|
||||
"""
|
||||
|
||||
class State:
|
||||
"""A class representing the state of a light
|
||||
"""
|
||||
|
||||
def __init__(self, data_slice: dict):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
data_slice (dict): The part of the data from the API response that concerns this state
|
||||
"""
|
||||
keys = data_slice.keys()
|
||||
self.m_on: bool = data_slice['on']
|
||||
if 'bri' in keys:
|
||||
|
@ -29,21 +40,69 @@ class HueLight:
|
|||
self.m_mode: str = data_slice['mode']
|
||||
self.m_reachable: bool = data_slice['reachable']
|
||||
|
||||
def get_brightness(self) -> int:
|
||||
"""Get current brightness of the light
|
||||
|
||||
Returns:
|
||||
int: 0-254
|
||||
"""
|
||||
return self.m_bri
|
||||
|
||||
def get_ct(self) -> int:
|
||||
"""Get current color temp of the light
|
||||
|
||||
Returns:
|
||||
int: 0, 153-500, 0 means it cant do color temp
|
||||
"""
|
||||
return self.m_ct
|
||||
|
||||
def is_on(self) -> bool:
|
||||
"""Is this thing on?
|
||||
|
||||
Returns:
|
||||
bool: True if it is on, otherwise False
|
||||
"""
|
||||
return self.m_on
|
||||
|
||||
def is_reachable(self) -> bool:
|
||||
"""Can we reach this light, if not it has most likely been turned off with physical switch
|
||||
|
||||
Returns:
|
||||
bool: True if it is reachable, otherwise False
|
||||
"""
|
||||
return self.m_reachable
|
||||
|
||||
class SwUpdate:
|
||||
"""A class to describe software updates
|
||||
"""
|
||||
|
||||
def __init__(self, data_slice: dict):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
data_slice (dict): The part of the data from the API that concerns this SwUpdate
|
||||
"""
|
||||
self.m_state: str = data_slice['state']
|
||||
self.m_lastinstall: datetime = datetime.strptime(data_slice['lastinstall'], "%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
class Capabilites:
|
||||
"""A class for the light capabilities
|
||||
"""
|
||||
|
||||
class Control:
|
||||
"""A class for the control object
|
||||
"""
|
||||
|
||||
class ColorTemp:
|
||||
"""A class for the color temperature object
|
||||
"""
|
||||
|
||||
def __init__(self, data_slice: dict):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
data_slice (dict): The part of the data from the API that concerns this ColorTemp
|
||||
"""
|
||||
keys = data_slice.keys()
|
||||
if 'min' in keys:
|
||||
self.m_min: int = data_slice['min']
|
||||
|
@ -54,7 +113,20 @@ class HueLight:
|
|||
else:
|
||||
self.m_max: int = 0
|
||||
|
||||
def get_max(self) -> int:
|
||||
"""Get the max colortemp of this light
|
||||
|
||||
Returns:
|
||||
int: Max colortemp of this light, 0 means you can't change colortemp
|
||||
"""
|
||||
return self.m_max
|
||||
|
||||
def __init__(self, data_slice: dict):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
data_slice (dict): The part of the Hue API data that concerns this Control
|
||||
"""
|
||||
keys = data_slice.keys()
|
||||
if 'mindimlevel' in keys:
|
||||
self.m_mindimlevel: int = data_slice['mindimlevel']
|
||||
|
@ -69,30 +141,82 @@ class HueLight:
|
|||
else:
|
||||
self.m_ct = HueLight.Capabilites.Control.ColorTemp({})
|
||||
|
||||
def get_colortemp(self):
|
||||
"""Get the colortemp object
|
||||
|
||||
Returns:
|
||||
HueLight.Capabilities.Control.ColorTemp: the colortemp object of this capability
|
||||
"""
|
||||
return self.m_ct
|
||||
|
||||
class Streaming:
|
||||
"""A class for the streaming object
|
||||
"""
|
||||
|
||||
def __init__(self, data_slice: dict):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
data_slice (dict): The part of the Hue API data that concerns this streaming object
|
||||
"""
|
||||
self.m_renderer: bool = data_slice['renderer']
|
||||
self.m_proxy: bool = data_slice['proxy']
|
||||
|
||||
def __init__(self, data_slice: dict):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
data_slice (dict): The part of the Hue API data that concerns this capabilities object
|
||||
"""
|
||||
self.m_certified: bool = data_slice['certified']
|
||||
self.m_control = HueLight.Capabilites.Control(data_slice['control'])
|
||||
self.m_streaming = HueLight.Capabilites.Streaming(data_slice['streaming'])
|
||||
|
||||
def get_control(self):
|
||||
"""Get the control object
|
||||
|
||||
Returns:
|
||||
HueLight.Capabilities.Control: the control object of this capability
|
||||
"""
|
||||
|
||||
class Config:
|
||||
"""A class for the config object
|
||||
"""
|
||||
|
||||
class Startup:
|
||||
"""A class for the startup object
|
||||
"""
|
||||
|
||||
def __init__(self, data_slice: dict):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
data_slice (dict): The part of the Hue API data that concerns this startup
|
||||
"""
|
||||
self.m_mode: str = data_slice['mode']
|
||||
self.m_configured: bool = data_slice['configured']
|
||||
|
||||
def __init__(self, data_slice: dict):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
data_slice (dict): The part of the Hue API response that concerns this config object
|
||||
"""
|
||||
self.m_archetype: str = data_slice['archetype']
|
||||
self.m_function: str = data_slice['function']
|
||||
self.m_direction: str = data_slice['direction']
|
||||
self.m_startup = HueLight.Config.Startup(data_slice['startup'])
|
||||
|
||||
def __init__(self, id: int, data: dict, parent_bridge_ip: str, parent_bridge_user: str):
|
||||
self.m_id: int = id
|
||||
def __init__(self, light_id: int, data: dict, parent_bridge_ip: str, parent_bridge_user: str):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
light_id (int): The id of this light
|
||||
data (dict): The response data from the Hue API
|
||||
parent_bridge_ip (str): ip address of the parent bridge
|
||||
parent_bridge_user (str): username of the parent bridge
|
||||
"""
|
||||
self.m_id: int = light_id
|
||||
self.m_parent_bridge_ip = parent_bridge_ip
|
||||
self.m_parent_bridge_user = parent_bridge_user
|
||||
self.m_state = HueLight.State(data['state'])
|
||||
|
@ -109,28 +233,91 @@ class HueLight:
|
|||
self.m_productid: str = data['productid']
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of this light
|
||||
|
||||
Returns:
|
||||
str: The name of the light
|
||||
"""
|
||||
return self.m_name
|
||||
|
||||
def can_set_ct(self) -> bool:
|
||||
"""Check if we can set a color temp for this light
|
||||
|
||||
Returns:
|
||||
bool: True if we can, otherwise False
|
||||
"""
|
||||
return self.get_ct() > 0
|
||||
|
||||
def get_state(self):
|
||||
"""Get the state object for this light
|
||||
|
||||
Returns:
|
||||
HueLight.State: The current state of affairs
|
||||
"""
|
||||
return self.m_state
|
||||
|
||||
def get_id(self):
|
||||
def get_id(self) -> int:
|
||||
"""Get the id of this light
|
||||
|
||||
Returns:
|
||||
int: The light id
|
||||
"""
|
||||
return self.m_id
|
||||
|
||||
def is_on(self) -> bool:
|
||||
return self.get_state().is_on()
|
||||
"""Is it on though?
|
||||
|
||||
Returns:
|
||||
bool: True if it is both on and reachable, otherwise False
|
||||
"""
|
||||
# A light has to be both on and reachable
|
||||
return self.get_state().is_on() and self.is_reachable()
|
||||
|
||||
def is_reachable(self) -> bool:
|
||||
"""Is it reachable
|
||||
|
||||
Returns:
|
||||
bool: True if it is, False otherwise
|
||||
"""
|
||||
return self.get_state().is_reachable()
|
||||
|
||||
def set_state(self, state: str) -> requests.Response:
|
||||
"""A helper method to set the state of the light
|
||||
|
||||
Args:
|
||||
state (str): The state of the light
|
||||
|
||||
Returns:
|
||||
requests.Response: The response from the Hue API
|
||||
"""
|
||||
path: str = "{}/lights/{}/state".format(self.m_parent_bridge_user, self.m_id)
|
||||
method: str = "PUT"
|
||||
response = make_request(self.m_parent_bridge_ip, path, method, state)
|
||||
self.update_state()
|
||||
return response
|
||||
|
||||
def set_brightness(self, bri: int):
|
||||
"""Set the brightness of the light
|
||||
|
||||
Args:
|
||||
bri (int): 0-254
|
||||
"""
|
||||
state = '{{"bri":{0}}}'.format(bri)
|
||||
self.set_state(state)
|
||||
|
||||
def set_ct(self, colortemp: int):
|
||||
"""Set the colortemp of the light, if possible
|
||||
|
||||
Args:
|
||||
colortemp (int): 153-500
|
||||
"""
|
||||
if self.can_set_ct():
|
||||
state = '{{"ct":{0}}}'.format(colortemp)
|
||||
self.set_state(state)
|
||||
|
||||
def toggle(self):
|
||||
"""Toggle light
|
||||
"""
|
||||
if self.is_reachable():
|
||||
state: str = '{"on":true}'
|
||||
if self.is_on():
|
||||
|
@ -138,6 +325,24 @@ class HueLight:
|
|||
self.set_state(state)
|
||||
|
||||
def update_state(self):
|
||||
"""See if anything has changed
|
||||
"""
|
||||
path: str = "{}/lights/{}".format(self.m_parent_bridge_user, self.m_id)
|
||||
response = make_request(self.m_parent_bridge_ip, path)
|
||||
self.m_state = HueLight.State(json.loads(response.text)['state'])
|
||||
|
||||
def get_brightness(self) -> int:
|
||||
"""Get currrent brightness
|
||||
|
||||
Returns:
|
||||
int: 0-254
|
||||
"""
|
||||
return self.get_state().get_brightness()
|
||||
|
||||
def get_ct(self) -> int:
|
||||
"""Get current colortemp
|
||||
|
||||
Returns:
|
||||
int: 0,153-500
|
||||
"""
|
||||
return self.get_state().get_ct()
|
||||
|
|
|
@ -8,6 +8,14 @@ from ..UserOrError import UserOrError
|
|||
|
||||
|
||||
def connect(ipaddress: str) -> UserOrError:
|
||||
"""Connect this bridge to tinge and get the username
|
||||
|
||||
Args:
|
||||
ipaddress (str): ip address of the bridge
|
||||
|
||||
Returns:
|
||||
UserOrError: Error if it is not connected otherwise the username
|
||||
"""
|
||||
user_or_error: UserOrError = UserOrError()
|
||||
body: str = '{{"devicetype":"{0}"}}'.format("tinge")
|
||||
path: str = ""
|
||||
|
@ -25,11 +33,30 @@ def connect(ipaddress: str) -> UserOrError:
|
|||
|
||||
|
||||
def is_valid_config(filename: str) -> bool:
|
||||
"""Currently doesn't do much, but we ckan get mor elaborate checks later if we want
|
||||
|
||||
Args:
|
||||
filename (str): The filename to check
|
||||
|
||||
Returns:
|
||||
bool: True if we think it is ok, otherwise False
|
||||
"""
|
||||
return os.path.exists(filename) and os.path.getsize(filename) > 0
|
||||
|
||||
|
||||
def make_request(ipaddress: str, path: str, method: str = "GET",
|
||||
body: str = '') -> requests.Response:
|
||||
"""Helper function to make an API call to the Hue API on the bridge
|
||||
|
||||
Args:
|
||||
ipaddress (str): ip address of the bridge
|
||||
path (str): the API endpoint
|
||||
method (str, optional): HTTP method. Defaults to "GET".
|
||||
body (str, optional): The body or parameters to the API call. Defaults to ''.
|
||||
|
||||
Returns:
|
||||
requests.Response: The response from the Hue API
|
||||
"""
|
||||
rfunct = requests.get
|
||||
url = "http://{}/api/{}".format(ipaddress, path)
|
||||
if method == "PUT":
|
||||
|
|
|
@ -3,26 +3,55 @@
|
|||
|
||||
|
||||
class UserOrError:
|
||||
"""Class that can be either a username or an error message from the bridge
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructor
|
||||
"""
|
||||
self.UNKNOWNERROR = 9999
|
||||
self.muser: str = str()
|
||||
self.merror: bool = True
|
||||
self.mcode: int = self.UNKNOWNERROR
|
||||
|
||||
def get_error_code(self) -> int:
|
||||
"""Thhis is an error code if this an error
|
||||
|
||||
Returns:
|
||||
int: error code from Hue API
|
||||
"""
|
||||
return self.mcode
|
||||
|
||||
def get_user(self) -> str:
|
||||
"""Get the username
|
||||
|
||||
Returns:
|
||||
str: the username we stored
|
||||
"""
|
||||
return self.muser
|
||||
|
||||
def is_error(self) -> bool:
|
||||
"""Check if this is an error
|
||||
|
||||
Returns:
|
||||
bool: True if ther is an error or False otherwise
|
||||
"""
|
||||
return self.merror
|
||||
|
||||
def set_error(self, code: int):
|
||||
"""Set an error code
|
||||
|
||||
Args:
|
||||
code (int): An error code to set
|
||||
"""
|
||||
self.merror = True
|
||||
self.mcode = code
|
||||
|
||||
def set_user(self, username: str):
|
||||
"""Set the username
|
||||
|
||||
Args:
|
||||
username (str): Username to set
|
||||
"""
|
||||
self.merror = False
|
||||
self.muser = username
|
||||
|
|
|
@ -12,7 +12,12 @@ from .UserOrError import UserOrError
|
|||
|
||||
|
||||
class Tinge:
|
||||
"""The class that keeps track of bridges and config
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructor
|
||||
"""
|
||||
self.m_bridges: list[HueBridge] = list()
|
||||
self.m_discovered: list[str] = list()
|
||||
self.m_config = os.path.join(os.environ['HOME'], ".config/tinge/config")
|
||||
|
@ -22,10 +27,14 @@ class Tinge:
|
|||
self.write_all_bridges_to_conf()
|
||||
|
||||
def create_confdir(self):
|
||||
"""Create the config dir if it does not allready exist
|
||||
"""
|
||||
if not os.path.exists(os.path.dirname(self.m_config)):
|
||||
os.makedirs(os.path.dirname(self.m_config))
|
||||
|
||||
def discover_new_bridges(self):
|
||||
"""Use UPnP to discover bridges on the current network
|
||||
"""
|
||||
upnp: UPnP = UPnP()
|
||||
discovered_devices = upnp.discover()
|
||||
if not discovered_devices:
|
||||
|
@ -47,10 +56,17 @@ class Tinge:
|
|||
self.m_discovered.append(device.host)
|
||||
return
|
||||
|
||||
def get_bridges(self):
|
||||
def get_bridges(self) -> list[HueBridge]:
|
||||
"""Get the bridges
|
||||
|
||||
Returns:
|
||||
list[HueBridge]: The bridges we keep track off
|
||||
"""
|
||||
return self.m_bridges
|
||||
|
||||
def read_bridges_from_file(self):
|
||||
"""Read config file and add back previously discovered bridges
|
||||
"""
|
||||
if is_valid_config(self.m_config):
|
||||
with open(self.m_config, 'r') as configfile:
|
||||
mbridges = toml.loads(configfile.read())
|
||||
|
@ -61,6 +77,8 @@ class Tinge:
|
|||
self.m_discovered.append(key)
|
||||
|
||||
def write_all_bridges_to_conf(self):
|
||||
"""Save to file
|
||||
"""
|
||||
with open(self.m_config, 'w') as configfile:
|
||||
for bridge in self.m_bridges:
|
||||
configfile.write('["{}"]\nuser = "{}"\n'.format(bridge.get_ipaddress(), bridge.get_user()))
|
||||
|
|
Loading…
Add table
Reference in a new issue