All long labels have been removed as the bridge list interface and discovery interface was reworked. Discovery of new lights happens automatically and bridges are refereshed when changes happen.
450 lines
19 KiB
Python
Executable file
450 lines
19 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
from typing import Union
|
|
|
|
import wx
|
|
import wx.lib.scrolledpanel as scrolled
|
|
|
|
from tinge import Tinge, HueBridge, HueGroup, HueLight, HueUtils, is_bridge
|
|
|
|
|
|
class Hui(wx.Frame):
|
|
"""This is the Hue GUI class
|
|
|
|
Args:
|
|
wx (Frame): Parent class
|
|
"""
|
|
|
|
def redraw(*args):
|
|
"""Decorator used for redrawing the widgets in the sizer
|
|
|
|
Returns:
|
|
function: The decorated function
|
|
"""
|
|
func = args[0]
|
|
|
|
def wrapper(self, *wrapper_args):
|
|
"""The wrapper function for the decorator
|
|
"""
|
|
self.sizer.Clear(delete_windows=True)
|
|
func(self, *wrapper_args)
|
|
self.sizer.Layout()
|
|
|
|
return wrapper
|
|
|
|
def __init__(self, *args, **kw):
|
|
"""Constructor
|
|
"""
|
|
super().__init__(*args, **kw)
|
|
self.m_on_icon: str = '☼'
|
|
self.m_off_icon: str = '☾'
|
|
self.m_unreachable_icon: str = '⚠'
|
|
self.m_tinge: Tinge = Tinge()
|
|
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()
|
|
|
|
@redraw
|
|
def add_bridges(self):
|
|
"""Add bridges to sizer, the entry point of the program
|
|
"""
|
|
self.SetTitle('Tinge - All Bridges')
|
|
all_unreachable: bool = True
|
|
no_bridges: bool = True
|
|
if self.m_tinge.get_bridges():
|
|
no_bridges = False
|
|
for bridge in self.m_tinge.get_bridges():
|
|
if bridge.is_reachable():
|
|
bridge.refresh_bridge()
|
|
all_unreachable = False
|
|
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:
|
|
label = "{} {} ({})".format(self.m_unreachable_icon, str(bridge), "unreachable")
|
|
btn: wx.Button = wx.Button(self.pnl, label=label)
|
|
self.sizer.Add(btn, 0, wx.EXPAND)
|
|
|
|
if no_bridges or all_unreachable:
|
|
label = "Discover bridge"
|
|
btn: wx.Button = wx.Button(self.pnl, label=label)
|
|
self.sizer.Add(btn, 0, wx.EXPAND)
|
|
self.Bind(wx.EVT_BUTTON,
|
|
lambda event: self.discover_new_bridges(), btn)
|
|
|
|
@redraw
|
|
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.SetTitle("Tinge - {}".format(self.cur_bridge.m_name))
|
|
bridge_btn: wx.Button = wx.Button(self.pnl, label="All Bridges")
|
|
|
|
has_unattached: bool = len(self.cur_bridge.unattached_lights) > 0
|
|
self.sizer.Add(bridge_btn, 0, wx.EXPAND)
|
|
self.Bind(wx.EVT_BUTTON,
|
|
lambda event: self.add_bridges(), bridge_btn)
|
|
if has_unattached:
|
|
group_label: wx.StaticText = wx.StaticText(self.pnl, label=" ⚯ Groups ⚯ ", style=wx.ALIGN_CENTER)
|
|
self.sizer.Add(group_label, 0, wx.EXPAND)
|
|
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, 1, wx.EXPAND)
|
|
self.Bind(wx.EVT_BUTTON,
|
|
lambda event, mgroupid=groupid: self.toggle_group(mgroupid), toggle_btn)
|
|
label: str = "{}".format(str(group))
|
|
group_btn: wx.Button = wx.Button(self.pnl, label=label, style=wx.BU_LEFT)
|
|
inner_sizer.Add(group_btn, 4, 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)
|
|
if has_unattached:
|
|
unattached_label: wx.StaticText = wx.StaticText(self.pnl, label=" ⚬ Unattached lights ⚬ ",
|
|
style=wx.ALIGN_CENTER)
|
|
self.sizer.Add(unattached_label, 0, wx.EXPAND)
|
|
for light in self.cur_bridge.unattached_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
|
|
elif not light.is_reachable():
|
|
icon = self.m_unreachable_icon
|
|
toggle_btn: wx.Button = wx.Button(self.pnl, label=icon)
|
|
inner_sizer.Add(toggle_btn, 1, wx.EXPAND)
|
|
self.Bind(wx.EVT_BUTTON,
|
|
lambda event, mlightid=lightid: self.toggle_light_and_goto_group(mlightid, lights),
|
|
toggle_btn)
|
|
label: str = "{}".format(light)
|
|
light_btn: wx.Button = wx.Button(self.pnl, label=label, style=wx.BU_LEFT)
|
|
inner_sizer.Add(light_btn, 4, wx.EXPAND)
|
|
self.Bind(wx.EVT_BUTTON,
|
|
lambda event, mlightid=lightid: self.add_single_light(mlightid, True), light_btn)
|
|
self.sizer.Add(inner_sizer, 0, wx.EXPAND)
|
|
|
|
def add_manual_discovery_dialog(self) -> bool:
|
|
self.sizer.Clear(delete_windows=True)
|
|
found_any: bool = False
|
|
text_entry: wx.TextEntryDialog = wx.TextEntryDialog(self.pnl, "Manually enter IP address of bridge:",
|
|
caption="Auto discovery failure")
|
|
warn_label: wx.StaticText = wx.StaticText(self.pnl, label="Waiting for Button Press on Bridge")
|
|
if text_entry.ShowModal() == wx.ID_OK:
|
|
ipaddress: str = text_entry.GetValue()
|
|
if is_bridge(ipaddress):
|
|
self.sizer.Add(warn_label, 0, wx.ALIGN_CENTER)
|
|
self.sizer.Layout()
|
|
user_or_error = HueUtils.connect(ipaddress)
|
|
while user_or_error.is_error():
|
|
user_or_error = HueUtils.connect(ipaddress)
|
|
self.m_tinge.append_bridge(HueBridge(ipaddress, user_or_error.get_user(), ipaddress))
|
|
found_any = True
|
|
self.m_tinge.write_all_bridges_to_conf()
|
|
else:
|
|
label = "Supplied IP Address did not match a Bridge.",
|
|
failure_msg: wx.GenericMessageDialog = wx.GenericMessageDialog(self.pnl, label, caption="Try again!")
|
|
failure_msg.ShowModal()
|
|
return found_any
|
|
|
|
@redraw
|
|
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.SetTitle("Tinge - {}".format(self.cur_group))
|
|
group_btn: wx.Button = wx.Button(self.pnl, label=str(self.cur_bridge))
|
|
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
|
|
elif not light.is_reachable():
|
|
icon = self.m_unreachable_icon
|
|
toggle_btn: wx.Button = wx.Button(self.pnl, label=icon)
|
|
inner_sizer.Add(toggle_btn, 1, wx.EXPAND)
|
|
self.Bind(wx.EVT_BUTTON,
|
|
lambda event, mlightid=lightid: self.toggle_light_and_goto_group(mlightid, lights),
|
|
toggle_btn)
|
|
label: str = "{}".format(light)
|
|
light_btn: wx.Button = wx.Button(self.pnl, label=label, style=wx.BU_LEFT)
|
|
inner_sizer.Add(light_btn, 4, wx.EXPAND)
|
|
self.Bind(wx.EVT_BUTTON,
|
|
lambda event, mlightid=lightid: self.add_single_light(mlightid), light_btn)
|
|
self.sizer.Add(inner_sizer, 0, wx.EXPAND)
|
|
|
|
@redraw
|
|
def add_single_light(self, lightid: int, unattached: bool = False):
|
|
"""Call back for light button
|
|
|
|
Args:
|
|
lightid (int): The light id of the light to display
|
|
unattached (bool, optional): Is the light unattached to any group?
|
|
"""
|
|
light: HueLight = self.cur_bridge.get_light_by_id(lightid)
|
|
self.SetTitle("Tinge - {}".format(light))
|
|
is_on: bool = light.is_on()
|
|
if unattached:
|
|
group_btn: wx.Button = wx.Button(self.pnl, label=str(self.cur_bridge))
|
|
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)
|
|
else:
|
|
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
|
|
elif not light.is_reachable():
|
|
icon = self.m_unreachable_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:
|
|
if light.can_set_brightness():
|
|
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,
|
|
lambda event: self.set_brightness(event, light.get_id()), b_slider)
|
|
# Slider for colortemp
|
|
if 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,
|
|
lambda event: self.set_colortemp(event, light.get_id()), c_slider)
|
|
|
|
# Slider for hue
|
|
if light.can_set_hue():
|
|
d_label: wx.StaticText = wx.StaticText(self.pnl, label="Hue")
|
|
self.sizer.Add(d_label, 0, wx.EXPAND)
|
|
d_slider: wx.Slider = wx.Slider(self.pnl, value=light.get_hue(), minValue=0, maxValue=65535)
|
|
self.sizer.Add(d_slider, 0, wx.EXPAND)
|
|
self.Bind(wx.EVT_SCROLL,
|
|
lambda event: self.set_hue(event, light.get_id()), d_slider)
|
|
|
|
# Slider for saturation
|
|
if light.can_set_sat():
|
|
e_label: wx.StaticText = wx.StaticText(self.pnl, label="Saturation")
|
|
self.sizer.Add(e_label, 0, wx.EXPAND)
|
|
e_slider: wx.Slider = wx.Slider(self.pnl, value=light.get_sat(), minValue=0, maxValue=254)
|
|
self.sizer.Add(e_slider, 0, wx.EXPAND)
|
|
self.Bind(wx.EVT_SCROLL,
|
|
lambda event: self.set_saturation(event, light.get_id()), e_slider)
|
|
rename_btn: wx.Button = wx.Button(self.pnl, label="Rename")
|
|
self.sizer.Add(rename_btn, 0, wx.EXPAND)
|
|
self.Bind(wx.EVT_BUTTON,
|
|
lambda event, mlightid=lightid: self.rename_light_and_goto_light(mlightid, unattached),
|
|
rename_btn)
|
|
delete_btn: wx.Button = wx.Button(self.pnl, label="Delete")
|
|
self.sizer.Add(delete_btn, 0, wx.EXPAND)
|
|
self.Bind(wx.EVT_BUTTON,
|
|
lambda event, mlightid=lightid: self.delete_light_and_goto_group(mlightid),
|
|
delete_btn)
|
|
|
|
def delete_light_and_goto_group(self, lightid):
|
|
"""Combo call back for delete and goto group
|
|
|
|
Args:
|
|
lightid (int): The light id of the light to delete
|
|
"""
|
|
if self.get_ok_cancel_answer_from_modal("Are you sure you want to delete this light?"):
|
|
light: HueLight = self.cur_bridge.get_light_by_id(lightid)
|
|
light.delete()
|
|
self.cur_bridge.remove_light(light)
|
|
self.add_lights(self.cur_group.get_lights())
|
|
else:
|
|
self.add_single_light(lightid)
|
|
|
|
def discover_new_bridges(self) -> bool:
|
|
"""Call back for button that is displayed if no bridges were found
|
|
|
|
Returns:
|
|
bool: True if we found any bridge, False otherwise
|
|
"""
|
|
found_any: bool = False
|
|
found_bridges: list[dict] = self.m_tinge.discover_new_bridges()
|
|
if found_bridges:
|
|
for bridge in found_bridges:
|
|
user_or_error = HueUtils.connect(bridge['ipaddress'])
|
|
while user_or_error.is_error():
|
|
user_or_error = HueUtils.connect(bridge['ipaddress'])
|
|
self.m_tinge.append_bridge(HueBridge(bridge['ipaddress'], user_or_error.get_user(), bridge['name']))
|
|
found_any = True
|
|
self.m_tinge.write_all_bridges_to_conf()
|
|
self.add_bridges()
|
|
else:
|
|
found_any = self.add_manual_discovery_dialog()
|
|
self.add_bridges()
|
|
return found_any
|
|
|
|
def get_ok_cancel_answer_from_modal(self, message: str) -> bool:
|
|
"""Display a message dialog and return ok or cancel
|
|
|
|
Args:
|
|
message (str): The message to display
|
|
|
|
Returns:
|
|
bool: The response from the user
|
|
"""
|
|
with wx.MessageDialog(self.pnl, message, style=wx.OK | wx.CANCEL | wx.CANCEL_DEFAULT) as dlg:
|
|
return dlg.ShowModal() == wx.ID_OK
|
|
|
|
def get_text_answer_from_modal(self, message: str, cap: str, val: str = "") -> str:
|
|
"""Display a text entry and return the content
|
|
|
|
Args:
|
|
message (str): The message to display
|
|
cap (str): The caption to display
|
|
val (str, optional): The default value to display, defaults to the empty string
|
|
|
|
Returns:
|
|
str: The response from the user
|
|
"""
|
|
with wx.TextEntryDialog(self.pnl, message, caption=cap, value=val) as dlg:
|
|
dlg.ShowModal()
|
|
answer: str = dlg.GetValue()
|
|
return answer
|
|
|
|
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 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 rename_light_and_goto_light(self, lightid, unattached: bool = False):
|
|
"""Combo call back to rename a light and display that light again
|
|
|
|
Args:
|
|
lightid ([type]): The light id of the light to rename/display
|
|
"""
|
|
newname: str = self.get_text_answer_from_modal("Set new name", "New name:")
|
|
if newname:
|
|
self.cur_bridge.get_light_by_id(lightid).rename(newname)
|
|
self.add_single_light(lightid, unattached)
|
|
|
|
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 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)
|
|
|
|
def set_hue(self, event, lightid):
|
|
"""Call back for hue slider
|
|
|
|
Args:
|
|
event (wx.ScrollEvent): The scroll event to react to
|
|
lightid (int): The light id of the light to adjust hue of
|
|
"""
|
|
hue: int = event.GetPosition()
|
|
light: HueLight = self.cur_bridge.get_light_by_id(lightid)
|
|
light.set_hue(hue)
|
|
|
|
def set_saturation(self, event, lightid):
|
|
"""Call back for saturation slider
|
|
|
|
Args:
|
|
event (wx.ScrollEvent): The scroll event to react to
|
|
lightid (int): The light id of the light to adjust saturation of
|
|
"""
|
|
sat: int = event.GetPosition()
|
|
light: HueLight = self.cur_bridge.get_light_by_id(lightid)
|
|
light.set_sat(sat)
|
|
|
|
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 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.add_single_light(lightid)
|
|
|
|
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 of 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)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = wx.App()
|
|
frm = Hui(None, title="Tinge")
|
|
frm.Show()
|
|
app.MainLoop()
|