You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tinge/main.py

451 lines
19 KiB

#!/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()