#!/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 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') if self.m_tinge.get_bridges(): for bridge in self.m_tinge.get_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="Press Hue Bridge button, and then press here") 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") 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, 1, 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, 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) @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): """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) self.SetTitle("Tinge - {}".format(light)) is_on: bool = light.is_on() 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), 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() 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): """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) 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()