diff --git a/.gitignore b/.gitignore index 8f0d67a..fe130d3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ __pycache__/ *.py[cod] *$py.class +.buildozer/ +bin/ + # C extensions *.so diff --git a/hue.kv b/hue.kv new file mode 100644 index 0000000..27b9f4a --- /dev/null +++ b/hue.kv @@ -0,0 +1,12 @@ +: + name: 'bridges' + layout: layout + ScrollView: + do_scroll_x: False + do_scroll_y: True + BoxLayout: + id: layout + orientation: 'vertical' + + + diff --git a/main.py b/main.py new file mode 100755 index 0000000..a614dd2 --- /dev/null +++ b/main.py @@ -0,0 +1,57 @@ +import kivy + +kivy.require('2.2.1') + +from kivy.app import App +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.button import Button +from kivy.uix.screenmanager import Screen, ScreenManager + +from tinge import HueBridge, Tinge + + +class HueApp(App): + + def build(self): + self.tinge = Tinge() + sm = ScreenManager() + sm.add_widget(BridgeScreen(self.tinge)) + + return sm + + +class BridgeScreen(Screen): + bridges = [] + layout = BoxLayout() + + def __init__(self, tinge, **kwargs): + super(BridgeScreen, self).__init__(**kwargs) + self.tinge = tinge + for bridge_button in self.get_bridge_buttons(): + self.layout.add_widget(bridge_button) + + def press_button(self, bridge, button): + print(bridge.get_ipaddress()) + + def manually_add(self, button): + print('hello') + + def get_bridge_buttons(self): + bridges = self.tinge.discover_new_bridges() + buttons = [] + if bridges: + for bridge in bridges: + bridge_button = Button( + text=str(bridge.get_ipaddress()), + on_press=lambda button: self.press_button(bridge, button)) + buttons.append(bridge_button) + else: + bridge_button = Button( + text='Manually add bridge', + on_press=lambda button: self.manually_add(button)) + buttons.append(bridge_button) + return buttons + + +if __name__ == '__main__': + HueApp().run() diff --git a/scripts/tinge b/scripts/tinge deleted file mode 100755 index 4b1e921..0000000 --- a/scripts/tinge +++ /dev/null @@ -1,555 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from typing import Union - -import wx -import wx.lib.scrolledpanel as scrolled - -from tinge import HueBridge, HueGroup, HueLight, HueUtils, Tinge, 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_manage_bridge(self): - """Add bridges to sizer, the entry point of the program - """ - self.SetTitle('Tinge - Manage Bridge') - label = "Delete 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.delete_bridge(), - btn) - back_btn: wx.Button = wx.Button(self.pnl, label="Go Back") - self.sizer.Add(back_btn, 0, wx.EXPAND) - self.Bind(wx.EVT_BUTTON, lambda event: self.add_groups(self.cur_bridge.get_groups()), - back_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) - bridge_mgm_btn: wx.Button = wx.Button(self.pnl, label="Manage Bridge") - self.sizer.Add(bridge_mgm_btn, 0, wx.EXPAND) - self.Bind(wx.EVT_BUTTON, lambda event: self.manage_bridge(), bridge_mgm_btn) - - 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_bridge(self): - dlg: wx.MessageDialog = wx.MessageDialog(self.pnl, - "Delete " + self.cur_bridge.m_name + "?", - "Are you sure?", - style=wx.CANCEL | wx.CANCEL_DEFAULT | wx.OK) - dlg.SetOKCancelLabels("&Yes", "&Don't delete") - reply: int = dlg.ShowModal() - if reply == wx.ID_CANCEL: - self.add_groups(self.cur_bridge.get_groups()) - else: - self.m_tinge.delete_bridge(self.cur_bridge) - self.add_bridges() - - 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 manage_bridge(self): - """Call back for manage bridge button - - """ - self.add_manage_bridge() - - 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()