Compare commits
7 commits
Author | SHA1 | Date | |
---|---|---|---|
|
7943fd9846 | ||
|
84bdbc2476 | ||
|
85c997430b | ||
|
cf593320bb | ||
|
26bbaf3aa4 | ||
|
9d3856f91b | ||
|
25a2d75ae3 |
10 changed files with 240 additions and 583 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -4,6 +4,10 @@ __pycache__/
|
|||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
.buildozer/
|
||||
bin/
|
||||
deb_dist/
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
|
|
12
hue.kv
Normal file
12
hue.kv
Normal file
|
@ -0,0 +1,12 @@
|
|||
<BridgeScreen>:
|
||||
name: 'bridges'
|
||||
layout: layout
|
||||
ScrollView:
|
||||
do_scroll_x: False
|
||||
do_scroll_y: True
|
||||
BoxLayout:
|
||||
id: layout
|
||||
orientation: 'vertical'
|
||||
|
||||
|
||||
|
169
main.py
Executable file
169
main.py
Executable file
|
@ -0,0 +1,169 @@
|
|||
from logging import disable
|
||||
|
||||
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.label import Label
|
||||
from kivy.uix.popup import Popup
|
||||
from kivy.uix.screenmanager import Screen, ScreenManager
|
||||
from kivy.uix.stacklayout import StackLayout
|
||||
|
||||
from tinge import HueBridge, HueGroup, HueUtils, Tinge
|
||||
|
||||
|
||||
class HueApp(App):
|
||||
|
||||
def build(self):
|
||||
self.tinge = Tinge()
|
||||
sm = ScreenManager()
|
||||
sm.add_widget(BridgeScreen(sm, self.tinge, name='bridge_screen'))
|
||||
|
||||
return sm
|
||||
|
||||
|
||||
class GroupScreen(Screen):
|
||||
|
||||
def __init__(self, sm, tinge, id: int, bridge: HueBridge, **kwargs):
|
||||
super(GroupScreen, self).__init__(**kwargs)
|
||||
self.sm = sm
|
||||
self.tinge = tinge
|
||||
self.id = id
|
||||
self.group = bridge.get_group_by_id(id)
|
||||
self.bridge = bridge
|
||||
self.layout = StackLayout()
|
||||
self.add_widget(self.layout)
|
||||
for button in self.get_light_buttons():
|
||||
self.layout.add_widget(button)
|
||||
|
||||
def get_light_buttons(self):
|
||||
buttons = []
|
||||
for light in self.group.get_lights():
|
||||
button = Button(text=str(light),
|
||||
size_hint=(1, 0.1),
|
||||
on_press=lambda _: self.press_button(light))
|
||||
buttons.append(button)
|
||||
backbutton = Button(text="Back",
|
||||
size_hint=(1, 0.1),
|
||||
on_press=lambda _: self.press_back_button())
|
||||
buttons.append(backbutton)
|
||||
return buttons
|
||||
|
||||
def press_back_button(self):
|
||||
self.sm.current = "bridge_" + str(self.bridge.id)
|
||||
|
||||
def press_button(self, light):
|
||||
print(button, light)
|
||||
|
||||
|
||||
class SingleBridgeScreen(Screen):
|
||||
|
||||
def __init__(self, sm, tinge, bridge: HueBridge, **kwargs):
|
||||
super(SingleBridgeScreen, self).__init__(**kwargs)
|
||||
self.sm = sm
|
||||
self.tinge = tinge
|
||||
self.bridge = bridge
|
||||
self.layout = StackLayout()
|
||||
self.add_widget(self.layout)
|
||||
self.groups = []
|
||||
for group in self.bridge.get_groups():
|
||||
self.layout.add_widget(self.get_group_button(group))
|
||||
self.layout.add_widget(
|
||||
Button(text="Back",
|
||||
size_hint=(1, 0.1),
|
||||
on_press=lambda _: self.press_back_button()))
|
||||
|
||||
def get_group_button(self, group):
|
||||
button = Button(text=str(group), size_hint=(1, 0.1))
|
||||
button.bind(on_press=lambda _: self.press_button(group.get_id()))
|
||||
return button
|
||||
|
||||
def press_back_button(self):
|
||||
self.sm.current = "bridge_screen"
|
||||
|
||||
def press_button(self, group: int):
|
||||
print(group)
|
||||
group_name: str = 'group_' + str(group)
|
||||
group_screen = GroupScreen(self.sm,
|
||||
self.tinge,
|
||||
group,
|
||||
self.bridge,
|
||||
name=group_name)
|
||||
if not self.sm.has_screen(group_name):
|
||||
self.sm.add_widget(group_screen)
|
||||
self.sm.current = group_name
|
||||
|
||||
|
||||
class BridgeScreen(Screen):
|
||||
bridges = []
|
||||
layout = BoxLayout()
|
||||
|
||||
def __init__(self, sm, tinge, **kwargs):
|
||||
super(BridgeScreen, self).__init__(**kwargs)
|
||||
self.sm = sm
|
||||
self.tinge = tinge
|
||||
for bridge_button in self.get_bridge_buttons():
|
||||
self.layout.add_widget(bridge_button)
|
||||
|
||||
def press_button(self, bridge):
|
||||
bridge_name = "bridge_" + str(bridge.id)
|
||||
if not self.sm.has_screen(bridge_name):
|
||||
self.sm.add_widget(
|
||||
SingleBridgeScreen(self.sm,
|
||||
self.tinge,
|
||||
bridge,
|
||||
name=bridge_name))
|
||||
self.sm.current = bridge_name
|
||||
|
||||
def discover_bridge(self):
|
||||
bridges = self.tinge.discover_new_bridges()
|
||||
if bridges:
|
||||
for bridge in bridges:
|
||||
print(bridge)
|
||||
if not self.tinge.bridge_discovered(bridge['ipaddress']):
|
||||
# Display popup lable to add bridge and get username
|
||||
popup = Popup(title='Connect Bridge',
|
||||
size_hint=(None, None),
|
||||
size=(400, 400))
|
||||
new_bridge_button = Button(
|
||||
text='Press button on Bridge',
|
||||
size_hint=(1, 0.1),
|
||||
on_press=lambda _: self.wait_for_button_press(
|
||||
popup, bridge['ipaddress']))
|
||||
popup.add_widget(new_bridge_button)
|
||||
popup.open()
|
||||
|
||||
def wait_for_button_press(self, popup, ipaddress):
|
||||
bridge_id = len(self.tinge.get_bridges())
|
||||
user_or_error = HueUtils.connect(ipaddress)
|
||||
while user_or_error.is_error():
|
||||
user_or_error = HueUtils.connect(ipaddress)
|
||||
self.tinge.append_bridge(
|
||||
HueBridge(bridge_id, ipaddress, user_or_error.get_user(),
|
||||
ipaddress))
|
||||
self.tinge.write_all_bridges_to_conf()
|
||||
popup.dismiss()
|
||||
|
||||
def get_bridge_buttons(self):
|
||||
buttons = []
|
||||
bridges = self.tinge.get_bridges()
|
||||
for bridge in bridges:
|
||||
bridge_button = Button(
|
||||
text=str(bridge.get_ipaddress()),
|
||||
size_hint=(1, 0.1),
|
||||
on_press=lambda _: self.press_button(bridge))
|
||||
buttons.append(bridge_button)
|
||||
bridge_button = Button(text='Discover Bridges',
|
||||
size_hint=(1, 0.1),
|
||||
on_press=lambda _: self.discover_bridge())
|
||||
buttons.append(bridge_button)
|
||||
sizer = Label(disabled=True)
|
||||
buttons.append(sizer)
|
||||
return buttons
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
HueApp().run()
|
555
scripts/tinge
555
scripts/tinge
|
@ -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()
|
52
setup.py
52
setup.py
|
@ -3,26 +3,32 @@ import setuptools
|
|||
with open("README.md", "r", encoding="utf-8") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setuptools.setup(
|
||||
name="tinge",
|
||||
version="0.0.3",
|
||||
author="Micke Nordin",
|
||||
author_email="hej@mic.ke",
|
||||
data_files = [('share/applications', ['data/org.smolnet.tinge.desktop']),('share/icons/hicolor/scalable/apps',['data/org.smolnet.tinge.svg']),],
|
||||
description="A GUI for Philips Hue lights.",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://code.smolnet.org/micke/tinge",
|
||||
project_urls={
|
||||
"Bug Tracker": "https://code.smolnet.org/micke/tinge/issues",
|
||||
},
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: GPL-3.0",
|
||||
"Operating System :: OS Independent",
|
||||
],
|
||||
package_dir={"": "src"},
|
||||
packages=setuptools.find_packages(where="src"),
|
||||
python_requires=">=3.9",
|
||||
scripts=["scripts/tinge"],
|
||||
)
|
||||
setuptools.setup(name="tinge",
|
||||
version="0.0.3",
|
||||
author="Micke Nordin",
|
||||
author_email="hej@mic.ke",
|
||||
data_files=[
|
||||
('share/applications', ['data/org.smolnet.tinge.desktop'
|
||||
]),
|
||||
('share/icons/hicolor/scalable/apps',
|
||||
['data/org.smolnet.tinge.svg']),
|
||||
],
|
||||
description="A GUI for Philips Hue lights.",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://code.smolnet.org/micke/tinge",
|
||||
project_urls={
|
||||
"Bug Tracker":
|
||||
"https://code.smolnet.org/micke/tinge/issues",
|
||||
},
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: GPL-3.0",
|
||||
"Operating System :: OS Independent",
|
||||
],
|
||||
package_dir={"": "src"},
|
||||
packages=setuptools.find_packages(where="src"),
|
||||
python_requires=">=3.9",
|
||||
entry_points={'gui_scripts': [
|
||||
'tinge = main:main',
|
||||
]})
|
||||
|
|
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
|
@ -12,16 +12,18 @@ class HueBridge:
|
|||
"""This class represents a Hue Bridge
|
||||
"""
|
||||
|
||||
def __init__(self, ipaddress: str, username: str,
|
||||
def __init__(self, id: int, ipaddress: str, username: str,
|
||||
name: str = "", is_reachable: bool = True):
|
||||
""" Constructor
|
||||
|
||||
Args:
|
||||
id (int): The id of the bridge
|
||||
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.id: int = id
|
||||
self.m_ipaddress: str = ipaddress
|
||||
self.m_username: str = username
|
||||
self.m_is_reachable = is_reachable
|
||||
|
|
|
@ -72,6 +72,14 @@ class HueLight:
|
|||
"""
|
||||
return self.m_hue
|
||||
|
||||
def get_name(self) -> str:
|
||||
"""Get the name of the light
|
||||
|
||||
Returns:
|
||||
str: Name of the light
|
||||
"""
|
||||
return self.m_name
|
||||
|
||||
def get_sat(self):
|
||||
"""Get current saturation of the light
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ class Tinge:
|
|||
"""
|
||||
upnp: UPnP = UPnP()
|
||||
discovered_devices = list()
|
||||
print("Discovering new bridges")
|
||||
try:
|
||||
discovered_devices = upnp.discover()
|
||||
except http.client.BadStatusLine:
|
||||
|
@ -84,6 +85,15 @@ class Tinge:
|
|||
"""
|
||||
return self.m_bridges
|
||||
|
||||
def bridge_discovered(self, ipaddress: str) -> bool:
|
||||
"""
|
||||
Check if a bridge has been discovered
|
||||
"""
|
||||
for bridge in self.m_bridges:
|
||||
if bridge.ipaddress == ipaddress:
|
||||
return True
|
||||
return False
|
||||
|
||||
def read_bridges_from_file(self) -> None:
|
||||
"""Read config file and add back previously discovered bridges
|
||||
"""
|
||||
|
@ -92,14 +102,15 @@ class Tinge:
|
|||
mbridges = toml.loads(configfile.read())
|
||||
for key, value in mbridges.items():
|
||||
if key not in self.m_discovered:
|
||||
id = len(self.m_discovered)
|
||||
response = make_request(key, "{}/".format(value['user']))
|
||||
if response:
|
||||
name = "{} ({})".format(response.json()['config']['name'], key)
|
||||
bridge: HueBridge = HueBridge(key, value['user'], name)
|
||||
bridge: HueBridge = HueBridge(id, key, value['user'], name)
|
||||
self.m_bridges.append(bridge)
|
||||
self.m_discovered.append(key)
|
||||
else:
|
||||
bridge: HueBridge = HueBridge(key, value['user'], is_reachable=False)
|
||||
bridge: HueBridge = HueBridge(id, key, value['user'], is_reachable=False)
|
||||
self.m_bridges.append(bridge)
|
||||
self.m_discovered.append(key)
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
[DEFAULT]
|
||||
Depends3: python3-upnpy, python3-wxgtk4.0, python3-toml, python3-requests
|
||||
Debian-Version: 2
|
||||
Depends3: python3-upnpy, python3-kivy, python3-toml, python3-requests
|
||||
Debian-Version: 1
|
||||
|
|
Loading…
Add table
Reference in a new issue