Compare commits

...

22 Commits

@ -3,24 +3,22 @@
Tinge is a mobile first application for controlling Philips Hue lights on Linux Tinge is a mobile first application for controlling Philips Hue lights on Linux
## Installation ## Installation
Install wxPython (don't use pip to install it) eg. for PostmarketOS: On Debian/Mobian
``` ```
sudo apk add py3-wxpython wget -O - https://repo.mic.ke/PUBLIC.KEY | sudo gpg --output /usr/share/keyrings/micke-archive-unstable.gpg --dearmor
``` echo "deb [signed-by=/usr/share/keyrings/micke-archive-unstable.gpg] https://repo.mic.ke/debian/ unstable main
or for Debian/Mobian: deb-src [signed-by=/usr/share/keyrings/micke-archive-unstable.gpg] https://repo.mic.ke/debian/ unstable main" | \
``` sudo tee /etc/apt/sources.list.d/debian-micke-unstable.list
sudo apt install python3-wxgtk4.0 sudo apt update && sudo apt install python3-tinge
``` ```
clone this repository: If you want to help with packaging in other format, please get in touch by commenting on the appropriate issue:
``` - [Alpine](https://code.smolnet.org/micke/tinge/issues/10)
git clone https://code.smolnet.org/micke/tinge - [Appimage](https://code.smolnet.org/micke/tinge/issues/22)
``` - [Arch](https://code.smolnet.org/micke/tinge/issues/20)
Run the crude installer: - [Flatpak](https://code.smolnet.org/micke/tinge/issues/21)
``` - [RPM](https://code.smolnet.org/micke/tinge/issues/9)
cd tinge
./install.sh
```
## Requirements ## Requirements
Requires a recent Python3 and pip3, most likely python >= 3.7. It is only tested with 3.9 on PostmarketOS on the PinePhone and on Debian Bullseye though. Requires a recent Python3 and pip3, most likely python >= 3.7. It is only tested with 3.9 on PostmarketOS on the PinePhone and on Debian Bullseye though.
@ -32,6 +30,11 @@ If you find any bugs (that is broken, existing features) please open an issue he
You can also join #tinge on irc.libera.chat or use the [kiwiirc webchat](https://kiwiirc.com/nextclient/irc.libera.chat/#tinge) to ask any questions. You can also join #tinge on irc.libera.chat or use the [kiwiirc webchat](https://kiwiirc.com/nextclient/irc.libera.chat/#tinge) to ask any questions.
## Donations
[![Donate using Liberapay](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/micke/donate)
If you want, you can chip in for server costs on [Liberapay](https://liberapay.com/micke/donate).
## Screenshots ## Screenshots
![Discover Bridge View](https://code.smolnet.org/micke/tinge/raw/branch/master/screenshots/scrot1.png) ![Discover Bridge View](https://code.smolnet.org/micke/tinge/raw/branch/master/screenshots/scrot1.png)
![All Bridge View](https://code.smolnet.org/micke/tinge/raw/branch/master/screenshots/scrot2.png) ![All Bridge View](https://code.smolnet.org/micke/tinge/raw/branch/master/screenshots/scrot2.png)

@ -2,22 +2,24 @@
- [x] [Show unattached lights](https://code.smolnet.org/micke/tinge/issues/1) - [x] [Show unattached lights](https://code.smolnet.org/micke/tinge/issues/1)
- [x] [Discover new lights](https://code.smolnet.org/micke/tinge/issues/2) - [x] [Discover new lights](https://code.smolnet.org/micke/tinge/issues/2)
# 0.1.0
- [ ] [Create group](https://code.smolnet.org/micke/tinge/issues/3)
- [ ] [Move light between groups](https://code.smolnet.org/micke/tinge/issues/4)
# 0.0.2 # 0.0.2
- [x] [Manually add bridge](https://code.smolnet.org/micke/tinge/issues/5) - [x] [Manually add bridge](https://code.smolnet.org/micke/tinge/issues/5)
- [ ] [Manually delete bridge](https://code.smolnet.org/micke/tinge/issues/6) - [x] [Manually delete bridge](https://code.smolnet.org/micke/tinge/issues/6)
# 0.0.3 # 0.0.3
- [ ] [pip package](https://code.smolnet.org/micke/tinge/issues/7) - [x] [pip package](https://code.smolnet.org/micke/tinge/issues/7)
# 0.1.0
- [ ] [Create group](https://code.smolnet.org/micke/tinge/issues/3)
- [ ] [Move light between groups](https://code.smolnet.org/micke/tinge/issues/4)
# 1.0.0 # 1.0.0
- [ ] [Debian package](https://code.smolnet.org/micke/tinge/issues/8)
- [ ] [RPM](https://code.smolnet.org/micke/tinge/issues/9)
- [ ] [Alpine package](https://code.smolnet.org/micke/tinge/issues/10) - [ ] [Alpine package](https://code.smolnet.org/micke/tinge/issues/10)
- [ ] [Custom icon/logo for project](https://code.smolnet.org/micke/tinge/issues/11) - [ ] [Arch package](https://code.smolnet.org/micke/tinge/issues/20)
- [ ] [RPM](https://code.smolnet.org/micke/tinge/issues/9)
- [x] [Custom icon/logo for project](https://code.smolnet.org/micke/tinge/issues/11)
- [x] [Debian package](https://code.smolnet.org/micke/tinge/issues/8)
# 2.0.0 # 2.0.0
- [ ] [Schedules](https://code.smolnet.org/micke/tinge/issues/12) - [ ] [Schedules](https://code.smolnet.org/micke/tinge/issues/12)

@ -0,0 +1,5 @@
[Desktop Entry]
Type=Application
Name=Tinge
Exec=tinge
Icon=org.smolnet.tinge

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

@ -0,0 +1,166 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="170.17047mm"
height="166.50186mm"
viewBox="0 0 170.17047 166.50186"
version="1.1"
id="svg5"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="org.smolnet.tinge.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:snap-others="false"
inkscape:snap-to-guides="false"
inkscape:snap-grids="false"
inkscape:object-nodes="false"
inkscape:zoom="0.48222205"
inkscape:cx="1046.1985"
inkscape:cy="1128.111"
inkscape:window-width="3440"
inkscape:window-height="1440"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
inkscape:lockguides="false" />
<defs
id="defs2">
<marker
style="overflow:visible"
id="Arrow1Lstart"
refX="0"
refY="0"
orient="auto"
inkscape:stockid="Arrow1Lstart"
inkscape:isstock="true">
<path
transform="matrix(0.8,0,0,0.8,10,0)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
id="path11947" />
</marker>
<linearGradient
id="linearGradient5905"
inkscape:swatch="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop5903" />
</linearGradient>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-13.673282,-10.01244)">
<ellipse
style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:8, 2, 1, 2;stroke-dashoffset:0;stroke-opacity:1"
id="path18033"
cx="98.75856"
cy="93.263443"
rx="84.585197"
ry="82.750923" />
<g
id="g17717"
inkscape:export-filename="/home/micke/sources/tinge/data/icon.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
transform="translate(2.327225e-5,-2.0701957e-4)">
<path
style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 26.092287,73.074775 c 70.193335,-50.00393 70.193335,-50.00393 70.193335,-50.00393 l 0.581165,-0.312814"
id="path42" />
<path
style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 166.47895,73.07477 C 96.285622,23.070845 96.285622,23.070845 96.285622,23.070845 l -0.58117,-0.312814"
id="path42-3" />
</g>
<g
id="g17745"
transform="translate(0.20055697,-2.0701957e-4)"
inkscape:export-filename="/home/micke/sources/tinge/data/icon.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<path
id="path6410"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:2.74442"
d="M 121.50265,103.04602 A 25.417566,23.020323 0 0 1 96.085083,126.06634 25.417566,23.020323 0 0 1 70.667517,103.04602 25.417566,23.020323 0 0 1 96.085083,80.025698 25.417566,23.020323 0 0 1 121.50265,103.04602 Z" />
<path
id="rect7636"
style="fill:none;stroke:#000000;stroke-width:2.04433;stroke-linejoin:round"
d="m 83.574081,67.314621 h 25.425929 v 14.94211 H 83.574081 Z" />
<path
id="path10454"
style="fill-rule:evenodd;stroke-width:0.180298"
transform="matrix(0.99965484,-0.02627152,0.02730158,0.99962724,0,0)"
d="m 98.603306,69.474854 a 4.2533302,4.1503177 0 0 1 -4.25333,4.150317 4.2533302,4.1503177 0 0 1 -4.253331,-4.150317 4.2533302,4.1503177 0 0 1 4.253331,-4.150318 4.2533302,4.1503177 0 0 1 4.25333,4.150318 z" />
<path
id="rect10973"
style="fill:#ffffff;stroke-width:2.07357;stroke-linejoin:round"
d="M 84.609299,68.315971 H 107.96863 V 81.20656 H 84.609299 Z" />
<path
style="fill:none;stroke:#000000;stroke-width:2.04433;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 106.58519,73.865222 C 86.369848,75.251831 85.97181,75.608161 85.97181,75.608161"
id="path14714-0" />
<path
style="fill:none;stroke:#000000;stroke-width:2.04433;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 106.50582,77.709435 C 86.290472,79.096046 85.892434,79.452382 85.892434,79.452382"
id="path14714-3" />
<path
id="path13562"
style="fill:#ffffff;fill-rule:evenodd;stroke-width:0.218573"
d="m 98.125623,66.730598 a 2.0876291,1.9277372 0 0 1 -2.087629,1.927738 2.0876291,1.9277372 0 0 1 -2.087629,-1.927738 2.0876291,1.9277372 0 0 1 2.087629,-1.927737 2.0876291,1.9277372 0 0 1 2.087629,1.927737 z" />
<path
id="rect13810"
style="fill:#ffffff;stroke-width:1.36289;stroke-linecap:round"
d="m 93.913414,66.6595 h 4.235808 v 2.746707 h -4.235808 z" />
<path
style="fill:none;stroke:#000000;stroke-width:2.04433;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 106.56281,69.908446 C 86.347459,71.295059 85.949423,71.65139 85.949423,71.65139"
id="path14714" />
</g>
<ellipse
style="display:inline;opacity:0;fill:none;fill-opacity:1;stroke:#000000;stroke-width:4.30889;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path6182"
cx="122.49568"
cy="109.44332"
rx="44.78046"
ry="34.012333"
inkscape:export-filename="/home/micke/sources/tinge/data/icon.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<g
id="g17734"
inkscape:export-filename="/home/micke/sources/tinge/data/icon.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
transform="translate(2.327225e-5,-2.0701957e-4)">
<path
style="fill:none;stroke:#000000;stroke-width:2.04433;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:2.04433, 2.04433;stroke-dashoffset:0;stroke-opacity:1"
d="m 95.174916,129.92892 c -0.124802,18.27339 -0.124802,18.27339 -0.124802,18.27339"
id="path16430" />
<path
style="fill:none;stroke:#000000;stroke-width:2.04433;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:2.04433, 2.04433;stroke-dashoffset:0;stroke-opacity:1"
d="M 73.852586,123.2925 C 61.36663,137.12556 61.36663,137.12556 61.36663,137.12556"
id="path16792" />
<path
style="fill:none;stroke:#000000;stroke-width:2.04433;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:2.04433, 2.04433;stroke-dashoffset:0;stroke-opacity:1"
d="m 118.71865,123.50097 c 12.48596,13.83307 12.48596,13.83307 12.48596,13.83307"
id="path16792-9" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

@ -1,7 +1,26 @@
#!/usr/bin/env bash #!/usr/bin/env bash
sudo pip3 install -r ./requirements.txt pip=pip3
python_version=3.9
if [[ -f /usr/bin/apk ]] || [[ -f /sbin/apk ]]; then # PostmarketOS/Alpine
sudo apk add py3-wxpython py3-pip
python_version=3.10
elif [[ -f /usr/bin/apt ]]; then # Mobian/Debian/Ubuntu
sudo apt install python3-wxgtk4 python3-pip
elif [[ -f /usr/bin/pacman ]]; then # Arch/Manjaro
sudo pacman -S python-wxpython python-pip
pip=pip
else
echo " This distribution is not supported by this installer.
manually install: wxpython python3-pip
and then copy tinge to /usr/local/bin and org.smolnet.tinge.desktop to /usr/share/applications/"
exit 1
fi
sudo ${pip} install -r ./requirements.txt
mkdir -p ~/.local/bin/ ~/.local/share/applications/ mkdir -p ~/.local/bin/ ~/.local/share/applications/
chmod +x ./main.py chmod +x ./scripts/tinge
cp ./main.py ~/.local/bin/tinge cp ./scripts/tinge ~/.local/bin/
sed "s/##USER##/${USER}/" tinge.desktop > ~/.local/share/applications/tinge.desktop cp ./data/org.smolnet.tinge.desktop ~/.local/share/applications/
sudo cp -r ./tinge /usr/lib/python3.9/site-packages/ sudo cp -r ./src/tinge /usr/lib/python${python_version}/site-packages/
exit 0

@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Where I keep the sources
deb_version=${1}
if [[ "x${deb_version}" == "x" ]]; then
deb_version=1
fi
BASEDIR=~/sources
SRCDIR="${BASEDIR}/tinge"
# Version of Tinge
VERSION=$(grep version= ${SRCDIR}/setup.py | awk -F '"' '{print $2}' )
# Change to source dir
olddir=$(pwd)
cd ${SRCDIR}
# Build deb
python3 setup.py --command-packages=stdeb.command bdist_deb #--debian-version ${deb_version}
cp deb_dist/* ${olddir}
rm -r ${SRCDIR}/deb_dist/ ${SRCDIR}/tinge-${VERSION}.tar.gz
cd ${olddir}

@ -2,5 +2,4 @@ toml==0.10.1
UPnPy==1.1.8 UPnPy==1.1.8
requests==2.25.1 requests==2.25.1
wxPython~=4.0.7
simplejson~=3.17.2 simplejson~=3.17.2

@ -5,7 +5,7 @@ from typing import Union
import wx import wx
import wx.lib.scrolledpanel as scrolled import wx.lib.scrolledpanel as scrolled
from tinge import Tinge, HueBridge, HueGroup, HueLight, HueUtils, is_bridge from tinge import HueBridge, HueGroup, HueLight, HueUtils, Tinge, is_bridge
class Hui(wx.Frame): class Hui(wx.Frame):
@ -14,7 +14,6 @@ class Hui(wx.Frame):
Args: Args:
wx (Frame): Parent class wx (Frame): Parent class
""" """
def redraw(*args): def redraw(*args):
"""Decorator used for redrawing the widgets in the sizer """Decorator used for redrawing the widgets in the sizer
@ -43,7 +42,8 @@ class Hui(wx.Frame):
self.cur_bridge: Union[None, HueBridge] = None self.cur_bridge: Union[None, HueBridge] = None
self.cur_group: Union[None, HueGroup] = None self.cur_group: Union[None, HueGroup] = None
# create a panel in the frame # create a panel in the frame
self.pnl: scrolled.ScrolledPanel = scrolled.ScrolledPanel(self, -1, style=wx.VSCROLL) self.pnl: scrolled.ScrolledPanel = scrolled.ScrolledPanel(
self, -1, style=wx.VSCROLL)
self.pnl.SetupScrolling() self.pnl.SetupScrolling()
# and create a sizer to manage the layout of child widgets # and create a sizer to manage the layout of child widgets
self.sizer: wx.BoxSizer = wx.BoxSizer(wx.VERTICAL) self.sizer: wx.BoxSizer = wx.BoxSizer(wx.VERTICAL)
@ -66,9 +66,12 @@ class Hui(wx.Frame):
btn: wx.Button = wx.Button(self.pnl, label=str(bridge)) btn: wx.Button = wx.Button(self.pnl, label=str(bridge))
self.sizer.Add(btn, 0, wx.EXPAND) self.sizer.Add(btn, 0, wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.Bind(wx.EVT_BUTTON,
lambda event, mbridge=bridge: self.goto_bridge(mbridge), btn) lambda event, mbridge=bridge: self.goto_bridge(
mbridge),
btn)
else: else:
label = "{} {} ({})".format(self.m_unreachable_icon, str(bridge), "unreachable") label = "{} {} ({})".format(self.m_unreachable_icon,
str(bridge), "unreachable")
btn: wx.Button = wx.Button(self.pnl, label=label) btn: wx.Button = wx.Button(self.pnl, label=label)
self.sizer.Add(btn, 0, wx.EXPAND) self.sizer.Add(btn, 0, wx.EXPAND)
@ -76,8 +79,22 @@ class Hui(wx.Frame):
label = "Discover bridge" label = "Discover bridge"
btn: wx.Button = wx.Button(self.pnl, label=label) btn: wx.Button = wx.Button(self.pnl, label=label)
self.sizer.Add(btn, 0, wx.EXPAND) self.sizer.Add(btn, 0, wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.Bind(wx.EVT_BUTTON, lambda event: self.discover_new_bridges(),
lambda event: self.discover_new_bridges(), btn) 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 @redraw
def add_groups(self, groups: list[HueGroup]): def add_groups(self, groups: list[HueGroup]):
@ -91,10 +108,11 @@ class Hui(wx.Frame):
has_unattached: bool = len(self.cur_bridge.unattached_lights) > 0 has_unattached: bool = len(self.cur_bridge.unattached_lights) > 0
self.sizer.Add(bridge_btn, 0, wx.EXPAND) self.sizer.Add(bridge_btn, 0, wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.Bind(wx.EVT_BUTTON, lambda event: self.add_bridges(), bridge_btn)
lambda event: self.add_bridges(), bridge_btn)
if has_unattached: if has_unattached:
group_label: wx.StaticText = wx.StaticText(self.pnl, label=" ⚯ Groups ⚯ ", style=wx.ALIGN_CENTER) group_label: wx.StaticText = wx.StaticText(self.pnl,
label=" ⚯ Groups ⚯ ",
style=wx.ALIGN_CENTER)
self.sizer.Add(group_label, 0, wx.EXPAND) self.sizer.Add(group_label, 0, wx.EXPAND)
for group in groups: for group in groups:
inner_sizer: wx.BoxSizer = wx.BoxSizer(orient=wx.HORIZONTAL) inner_sizer: wx.BoxSizer = wx.BoxSizer(orient=wx.HORIZONTAL)
@ -104,17 +122,25 @@ class Hui(wx.Frame):
icon = self.m_on_icon icon = self.m_on_icon
toggle_btn: wx.Button = wx.Button(self.pnl, label=icon) toggle_btn: wx.Button = wx.Button(self.pnl, label=icon)
inner_sizer.Add(toggle_btn, 1, wx.EXPAND) inner_sizer.Add(toggle_btn, 1, wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.Bind(
lambda event, mgroupid=groupid: self.toggle_group(mgroupid), toggle_btn) wx.EVT_BUTTON,
lambda event, mgroupid=groupid: self.toggle_group(mgroupid),
toggle_btn)
label: str = "{}".format(str(group)) label: str = "{}".format(str(group))
group_btn: wx.Button = wx.Button(self.pnl, label=label, style=wx.BU_LEFT) group_btn: wx.Button = wx.Button(self.pnl,
label=label,
style=wx.BU_LEFT)
inner_sizer.Add(group_btn, 4, wx.EXPAND) inner_sizer.Add(group_btn, 4, wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.Bind(
lambda event, mgroupid=groupid: self.goto_group(mgroupid), group_btn) wx.EVT_BUTTON,
lambda event, mgroupid=groupid: self.goto_group(mgroupid),
group_btn)
self.sizer.Add(inner_sizer, 0, wx.EXPAND) self.sizer.Add(inner_sizer, 0, wx.EXPAND)
if has_unattached: if has_unattached:
unattached_label: wx.StaticText = wx.StaticText(self.pnl, label=" ⚬ Unattached lights ⚬ ", unattached_label: wx.StaticText = wx.StaticText(
style=wx.ALIGN_CENTER) self.pnl,
label=" ⚬ Unattached lights ⚬ ",
style=wx.ALIGN_CENTER)
self.sizer.Add(unattached_label, 0, wx.EXPAND) self.sizer.Add(unattached_label, 0, wx.EXPAND)
for light in self.cur_bridge.unattached_lights: for light in self.cur_bridge.unattached_lights:
inner_sizer = wx.BoxSizer(orient=wx.HORIZONTAL) inner_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)
@ -127,21 +153,32 @@ class Hui(wx.Frame):
toggle_btn: wx.Button = wx.Button(self.pnl, label=icon) toggle_btn: wx.Button = wx.Button(self.pnl, label=icon)
inner_sizer.Add(toggle_btn, 1, wx.EXPAND) inner_sizer.Add(toggle_btn, 1, wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.Bind(wx.EVT_BUTTON,
lambda event, mlightid=lightid: self.toggle_light_and_goto_group(mlightid, lights), lambda event, mlightid=lightid: self.
toggle_light_and_goto_group(mlightid, lights),
toggle_btn) toggle_btn)
label: str = "{}".format(light) label: str = "{}".format(light)
light_btn: wx.Button = wx.Button(self.pnl, label=label, style=wx.BU_LEFT) light_btn: wx.Button = wx.Button(self.pnl,
label=label,
style=wx.BU_LEFT)
inner_sizer.Add(light_btn, 4, wx.EXPAND) inner_sizer.Add(light_btn, 4, wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.Bind(wx.EVT_BUTTON,
lambda event, mlightid=lightid: self.add_single_light(mlightid, True), light_btn) lambda event, mlightid=lightid: self.
add_single_light(mlightid, True),
light_btn)
self.sizer.Add(inner_sizer, 0, wx.EXPAND) 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: def add_manual_discovery_dialog(self) -> bool:
self.sizer.Clear(delete_windows=True) self.sizer.Clear(delete_windows=True)
found_any: bool = False found_any: bool = False
text_entry: wx.TextEntryDialog = wx.TextEntryDialog(self.pnl, "Manually enter IP address of bridge:", text_entry: wx.TextEntryDialog = wx.TextEntryDialog(
caption="Auto discovery failure") self.pnl,
warn_label: wx.StaticText = wx.StaticText(self.pnl, label="Waiting for Button Press on Bridge") "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: if text_entry.ShowModal() == wx.ID_OK:
ipaddress: str = text_entry.GetValue() ipaddress: str = text_entry.GetValue()
if is_bridge(ipaddress): if is_bridge(ipaddress):
@ -150,12 +187,14 @@ class Hui(wx.Frame):
user_or_error = HueUtils.connect(ipaddress) user_or_error = HueUtils.connect(ipaddress)
while user_or_error.is_error(): while user_or_error.is_error():
user_or_error = HueUtils.connect(ipaddress) user_or_error = HueUtils.connect(ipaddress)
self.m_tinge.append_bridge(HueBridge(ipaddress, user_or_error.get_user(), ipaddress)) self.m_tinge.append_bridge(
HueBridge(ipaddress, user_or_error.get_user(), ipaddress))
found_any = True found_any = True
self.m_tinge.write_all_bridges_to_conf() self.m_tinge.write_all_bridges_to_conf()
else: else:
label = "Supplied IP Address did not match a Bridge.", label = "Supplied IP Address did not match a Bridge.",
failure_msg: wx.GenericMessageDialog = wx.GenericMessageDialog(self.pnl, label, caption="Try again!") failure_msg: wx.GenericMessageDialog = wx.GenericMessageDialog(
self.pnl, label, caption="Try again!")
failure_msg.ShowModal() failure_msg.ShowModal()
return found_any return found_any
@ -170,7 +209,8 @@ class Hui(wx.Frame):
group_btn: wx.Button = wx.Button(self.pnl, label=str(self.cur_bridge)) group_btn: wx.Button = wx.Button(self.pnl, label=str(self.cur_bridge))
self.sizer.Add(group_btn, 0, wx.EXPAND) self.sizer.Add(group_btn, 0, wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.Bind(wx.EVT_BUTTON,
lambda event: self.add_groups(self.cur_bridge.get_groups()), group_btn) lambda event: self.add_groups(self.cur_bridge.get_groups()),
group_btn)
for light in lights: for light in lights:
inner_sizer = wx.BoxSizer(orient=wx.HORIZONTAL) inner_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)
lightid: int = light.get_id() lightid: int = light.get_id()
@ -182,13 +222,18 @@ class Hui(wx.Frame):
toggle_btn: wx.Button = wx.Button(self.pnl, label=icon) toggle_btn: wx.Button = wx.Button(self.pnl, label=icon)
inner_sizer.Add(toggle_btn, 1, wx.EXPAND) inner_sizer.Add(toggle_btn, 1, wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.Bind(wx.EVT_BUTTON,
lambda event, mlightid=lightid: self.toggle_light_and_goto_group(mlightid, lights), lambda event, mlightid=lightid: self.
toggle_light_and_goto_group(mlightid, lights),
toggle_btn) toggle_btn)
label: str = "{}".format(light) label: str = "{}".format(light)
light_btn: wx.Button = wx.Button(self.pnl, label=label, style=wx.BU_LEFT) light_btn: wx.Button = wx.Button(self.pnl,
label=label,
style=wx.BU_LEFT)
inner_sizer.Add(light_btn, 4, wx.EXPAND) inner_sizer.Add(light_btn, 4, wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.Bind(wx.EVT_BUTTON,
lambda event, mlightid=lightid: self.add_single_light(mlightid), light_btn) lambda event, mlightid=lightid: self.add_single_light(
mlightid),
light_btn)
self.sizer.Add(inner_sizer, 0, wx.EXPAND) self.sizer.Add(inner_sizer, 0, wx.EXPAND)
@redraw @redraw
@ -203,16 +248,20 @@ class Hui(wx.Frame):
self.SetTitle("Tinge - {}".format(light)) self.SetTitle("Tinge - {}".format(light))
is_on: bool = light.is_on() is_on: bool = light.is_on()
if unattached: if unattached:
group_btn: wx.Button = wx.Button(self.pnl, label=str(self.cur_bridge)) group_btn: wx.Button = wx.Button(self.pnl,
label=str(self.cur_bridge))
self.sizer.Add(group_btn, 0, wx.EXPAND) self.sizer.Add(group_btn, 0, wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.Bind(
lambda event: self.add_groups(self.cur_bridge.get_groups()), group_btn) wx.EVT_BUTTON,
lambda event: self.add_groups(self.cur_bridge.get_groups()),
group_btn)
else: else:
group: HueGroup = self.cur_group group: HueGroup = self.cur_group
group_btn: wx.Button = wx.Button(self.pnl, label=str(group)) group_btn: wx.Button = wx.Button(self.pnl, label=str(group))
self.sizer.Add(group_btn, 0, wx.EXPAND) self.sizer.Add(group_btn, 0, wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.Bind(wx.EVT_BUTTON,
lambda event: self.goto_group(self.cur_group.get_id()), group_btn) lambda event: self.goto_group(self.cur_group.get_id()),
group_btn)
# Toggle # Toggle
icon: str = self.m_off_icon icon: str = self.m_off_icon
if is_on: if is_on:
@ -222,63 +271,102 @@ class Hui(wx.Frame):
toggle_btn: wx.Button = wx.Button(self.pnl, label=icon) toggle_btn: wx.Button = wx.Button(self.pnl, label=icon)
self.sizer.Add(toggle_btn, 0, wx.EXPAND) self.sizer.Add(toggle_btn, 0, wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.Bind(wx.EVT_BUTTON,
lambda event, mlightid=lightid: self.toggle_light_and_goto_light(mlightid), lambda event, mlightid=lightid: self.
toggle_light_and_goto_light(mlightid),
toggle_btn) toggle_btn)
# Slider for brightness # Slider for brightness
if is_on: if is_on:
if light.can_set_brightness(): if light.can_set_brightness():
b_label: wx.StaticText = wx.StaticText(self.pnl, label="Brightness") b_label: wx.StaticText = wx.StaticText(self.pnl,
label="Brightness")
self.sizer.Add(b_label, 0, wx.EXPAND) self.sizer.Add(b_label, 0, wx.EXPAND)
b_slider: wx.Slider = wx.Slider(self.pnl, value=light.get_state().get_brightness(), minValue=1, b_slider: wx.Slider = wx.Slider(
maxValue=254) self.pnl,
value=light.get_state().get_brightness(),
minValue=1,
maxValue=254)
self.sizer.Add(b_slider, 0, wx.EXPAND) self.sizer.Add(b_slider, 0, wx.EXPAND)
self.Bind(wx.EVT_SCROLL, self.Bind(
lambda event: self.set_brightness(event, light.get_id()), b_slider) wx.EVT_SCROLL,
lambda event: self.set_brightness(event, light.get_id()),
b_slider)
# Slider for colortemp # Slider for colortemp
if light.can_set_ct(): if light.can_set_ct():
c_label: wx.StaticText = wx.StaticText(self.pnl, label="Color Temperature") c_label: wx.StaticText = wx.StaticText(
self.pnl, label="Color Temperature")
self.sizer.Add(c_label, 0, wx.EXPAND) self.sizer.Add(c_label, 0, wx.EXPAND)
c_slider: wx.Slider = wx.Slider(self.pnl, value=light.get_ct(), minValue=153, maxValue=500) 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.sizer.Add(c_slider, 0, wx.EXPAND)
self.Bind(wx.EVT_SCROLL, self.Bind(
lambda event: self.set_colortemp(event, light.get_id()), c_slider) wx.EVT_SCROLL,
lambda event: self.set_colortemp(event, light.get_id()),
c_slider)
# Slider for hue # Slider for hue
if light.can_set_hue(): if light.can_set_hue():
d_label: wx.StaticText = wx.StaticText(self.pnl, label="Hue") d_label: wx.StaticText = wx.StaticText(self.pnl, label="Hue")
self.sizer.Add(d_label, 0, wx.EXPAND) self.sizer.Add(d_label, 0, wx.EXPAND)
d_slider: wx.Slider = wx.Slider(self.pnl, value=light.get_hue(), minValue=0, maxValue=65535) 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.sizer.Add(d_slider, 0, wx.EXPAND)
self.Bind(wx.EVT_SCROLL, self.Bind(wx.EVT_SCROLL,
lambda event: self.set_hue(event, light.get_id()), d_slider) lambda event: self.set_hue(event, light.get_id()),
d_slider)
# Slider for saturation # Slider for saturation
if light.can_set_sat(): if light.can_set_sat():
e_label: wx.StaticText = wx.StaticText(self.pnl, label="Saturation") e_label: wx.StaticText = wx.StaticText(self.pnl,
label="Saturation")
self.sizer.Add(e_label, 0, wx.EXPAND) self.sizer.Add(e_label, 0, wx.EXPAND)
e_slider: wx.Slider = wx.Slider(self.pnl, value=light.get_sat(), minValue=0, maxValue=254) 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.sizer.Add(e_slider, 0, wx.EXPAND)
self.Bind(wx.EVT_SCROLL, self.Bind(
lambda event: self.set_saturation(event, light.get_id()), e_slider) wx.EVT_SCROLL,
lambda event: self.set_saturation(event, light.get_id()),
e_slider)
rename_btn: wx.Button = wx.Button(self.pnl, label="Rename") rename_btn: wx.Button = wx.Button(self.pnl, label="Rename")
self.sizer.Add(rename_btn, 0, wx.EXPAND) self.sizer.Add(rename_btn, 0, wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.Bind(wx.EVT_BUTTON,
lambda event, mlightid=lightid: self.rename_light_and_goto_light(mlightid, unattached), lambda event, mlightid=lightid: self.
rename_light_and_goto_light(mlightid, unattached),
rename_btn) rename_btn)
delete_btn: wx.Button = wx.Button(self.pnl, label="Delete") delete_btn: wx.Button = wx.Button(self.pnl, label="Delete")
self.sizer.Add(delete_btn, 0, wx.EXPAND) self.sizer.Add(delete_btn, 0, wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.Bind(wx.EVT_BUTTON,
lambda event, mlightid=lightid: self.delete_light_and_goto_group(mlightid), lambda event, mlightid=lightid: self.
delete_light_and_goto_group(mlightid),
delete_btn) 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): def delete_light_and_goto_group(self, lightid):
"""Combo call back for delete and goto group """Combo call back for delete and goto group
Args: Args:
lightid (int): The light id of the light to delete 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?"): 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: HueLight = self.cur_bridge.get_light_by_id(lightid)
light.delete() light.delete()
self.cur_bridge.remove_light(light) self.cur_bridge.remove_light(light)
@ -299,7 +387,9 @@ class Hui(wx.Frame):
user_or_error = HueUtils.connect(bridge['ipaddress']) user_or_error = HueUtils.connect(bridge['ipaddress'])
while user_or_error.is_error(): while user_or_error.is_error():
user_or_error = HueUtils.connect(bridge['ipaddress']) user_or_error = HueUtils.connect(bridge['ipaddress'])
self.m_tinge.append_bridge(HueBridge(bridge['ipaddress'], user_or_error.get_user(), bridge['name'])) self.m_tinge.append_bridge(
HueBridge(bridge['ipaddress'], user_or_error.get_user(),
bridge['name']))
found_any = True found_any = True
self.m_tinge.write_all_bridges_to_conf() self.m_tinge.write_all_bridges_to_conf()
self.add_bridges() self.add_bridges()
@ -317,10 +407,16 @@ class Hui(wx.Frame):
Returns: Returns:
bool: The response from the user bool: The response from the user
""" """
with wx.MessageDialog(self.pnl, message, style=wx.OK | wx.CANCEL | wx.CANCEL_DEFAULT) as dlg: with wx.MessageDialog(self.pnl,
message,
style=wx.OK | wx.CANCEL
| wx.CANCEL_DEFAULT) as dlg:
return dlg.ShowModal() == wx.ID_OK return dlg.ShowModal() == wx.ID_OK
def get_text_answer_from_modal(self, message: str, cap: str, val: str = "") -> str: def get_text_answer_from_modal(self,
message: str,
cap: str,
val: str = "") -> str:
"""Display a text entry and return the content """Display a text entry and return the content
Args: Args:
@ -331,7 +427,8 @@ class Hui(wx.Frame):
Returns: Returns:
str: The response from the user str: The response from the user
""" """
with wx.TextEntryDialog(self.pnl, message, caption=cap, value=val) as dlg: with wx.TextEntryDialog(self.pnl, message, caption=cap,
value=val) as dlg:
dlg.ShowModal() dlg.ShowModal()
answer: str = dlg.GetValue() answer: str = dlg.GetValue()
return answer return answer
@ -359,13 +456,20 @@ class Hui(wx.Frame):
self.cur_group = group self.cur_group = group
self.add_lights(group.get_lights()) 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): def rename_light_and_goto_light(self, lightid, unattached: bool = False):
"""Combo call back to rename a light and display that light again """Combo call back to rename a light and display that light again
Args: Args:
lightid ([type]): The light id of the light to rename/display lightid ([type]): The light id of the light to rename/display
""" """
newname: str = self.get_text_answer_from_modal("Set new name", "New name:") newname: str = self.get_text_answer_from_modal("Set new name",
"New name:")
if newname: if newname:
self.cur_bridge.get_light_by_id(lightid).rename(newname) self.cur_bridge.get_light_by_id(lightid).rename(newname)
self.add_single_light(lightid, unattached) self.add_single_light(lightid, unattached)
@ -432,7 +536,8 @@ class Hui(wx.Frame):
self.cur_bridge.get_light_by_id(lightid).toggle() self.cur_bridge.get_light_by_id(lightid).toggle()
self.add_single_light(lightid) self.add_single_light(lightid)
def toggle_light_and_goto_group(self, lightid: int, lights: list[HueLight]): def toggle_light_and_goto_group(self, lightid: int,
lights: list[HueLight]):
"""Combo call back for toggle and goto group """Combo call back for toggle and goto group
Args: Args:

@ -0,0 +1,28 @@
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"],
)

@ -12,13 +12,15 @@ class HueBridge:
"""This class represents a Hue Bridge """This class represents a Hue Bridge
""" """
def __init__(self, ipaddress: str, username: str, name: str = "", is_reachable: bool = True): def __init__(self, ipaddress: str, username: str,
name: str = "", is_reachable: bool = True):
""" Constructor """ Constructor
Args: Args:
ipaddress (str): The ip address of the bridge ipaddress (str): The ip address of the bridge
username (str): The username for this app for this 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. name (str, optional): A human readable name for this bridge.
Is set to ipaddress, if not supplied.
""" """
self.m_ipaddress: str = ipaddress self.m_ipaddress: str = ipaddress
self.m_username: str = username self.m_username: str = username
@ -58,8 +60,8 @@ class HueBridge:
return self.m_name return self.m_name
def append_new_lights(self) -> bool: def append_new_lights(self) -> bool:
"""If any new lights were discovered in discover_new_lights(), they can be appended to this bridges """If any new lights were discovered in discover_new_lights(),
list of lights with this function they can be appended to this bridges list of lights with this function
Returns: Returns:
bool: True if the request was ok, otherwise False bool: True if the request was ok, otherwise False
@ -70,36 +72,44 @@ class HueBridge:
if key != 'lastscan': if key != 'lastscan':
path: str = "{}/lights/{}".format(self.m_username, key) path: str = "{}/lights/{}".format(self.m_username, key)
response = make_request(self.m_ipaddress, path) response = make_request(self.m_ipaddress, path)
self.m_lights.append(HueLight(int(key), response.json(), self.get_ipaddress(), self.get_user())) self.m_lights.append(HueLight(int(key), response.json(),
self.get_ipaddress(), self.get_user()))
return response.ok return response.ok
def create_group(self, lights: list[HueLight], name: str, group_type: str = "LightGroup", def create_group(self, lights: list[HueLight], name: str,
group_type: str = "LightGroup",
group_class: str = "Other") -> bool: group_class: str = "Other") -> bool:
"""Create a group from a list of lights """Create a group from a list of lights
Args: Args:
lights (list[HueLight]): a list of lights to group lights (list[HueLight]): a list of lights to group
name (str): The name of the new group name (str): The name of the new group
group_type (str, optional): The group type can be LightGroup, Room or either Luminaire or group_type (str, optional): The group type can be LightGroup,
LightSource if a Multisource Luminaire is present in the system. Room or either Luminaire or
Defaults to "LightGroup". LightSource if a Multisource
group_class (str, optional): Category of Room Types. Defaults to "Other". Luminaire is present in the system.
Defaults to "LightGroup".
group_class (str, optional): Category of Room Types.
Defaults to "Other".
Returns: Returns:
bool: True if creation was a success, otherwise False bool: True if creation was a success, otherwise False
""" """
path = "{}/groups".format(self.get_user()) path = "{}/groups".format(self.get_user())
method = "POST" method = "POST"
data: dict = {'lights': [], 'name': name, 'type': group_type, 'class': group_class} data: dict = {'lights': [], 'name': name,
'type': group_type, 'class': group_class}
for light in lights: for light in lights:
data['lights'].append(str(light.get_id())) data['lights'].append(str(light.get_id()))
response = make_request(self.get_ipaddress(), path, method, json.dumps(data)) response = make_request(self.get_ipaddress(),
path, method, json.dumps(data))
r_json = response.json() r_json = response.json()
if 'success' in r_json.keys(): if 'success' in r_json.keys():
new_id = r_json['success']['id'] new_id = r_json['success']['id']
new_path = "{}/groups/{}".format(self.get_user(), new_id) new_path = "{}/groups/{}".format(self.get_user(), new_id)
new_group = make_request(self.get_ipaddress(), new_path).json() new_group = make_request(self.get_ipaddress(), new_path).json()
self.m_groups.append(HueGroup(int(new_id), lights, new_group, self.get_ipaddress(), self.get_user())) self.m_groups.append(HueGroup(int(new_id), lights, new_group,
self.get_ipaddress(), self.get_user()))
return True return True
else: else:
return False return False
@ -117,7 +127,8 @@ class HueBridge:
lights: list[HueLight] = list() lights: list[HueLight] = list()
for light in value['lights']: for light in value['lights']:
lights.append(self.get_light_by_id(int(light))) lights.append(self.get_light_by_id(int(light)))
groups.append(HueGroup(int(key), lights, value, self.get_ipaddress(), self.get_user())) groups.append(HueGroup(int(key), lights, value,
self.get_ipaddress(), self.get_user()))
return groups return groups
def discover_lights(self) -> list[HueLight]: def discover_lights(self) -> list[HueLight]:
@ -130,7 +141,8 @@ class HueBridge:
response = make_request(self.m_ipaddress, path) response = make_request(self.m_ipaddress, path)
lights: list[HueLight] = list() lights: list[HueLight] = list()
for key, value in json.loads(response.text).items(): for key, value in json.loads(response.text).items():
lights.append(HueLight(int(key), value, self.get_ipaddress(), self.get_user())) lights.append(HueLight(int(key),
value, self.get_ipaddress(), self.get_user()))
return lights return lights
def discover_new_lights(self, light_ids: Union[None, list[int]] = None) -> bool: def discover_new_lights(self, light_ids: Union[None, list[int]] = None) -> bool:
@ -211,7 +223,8 @@ class HueBridge:
if lightid != "lastscan": if lightid != "lastscan":
print(lightid) print(lightid)
if not self.get_light_by_id(int(lightid)): if not self.get_light_by_id(int(lightid)):
lightpath: str = "{}/lights/{}".format(self.m_username, int(lightid)) lightpath: str = "{}/lights/{}".format(
self.m_username, int(lightid))
lightresponse = make_request(self.m_ipaddress, lightpath) lightresponse = make_request(self.m_ipaddress, lightpath)
newlights.append( newlights.append(
HueLight(int(lightid), lightresponse.json(), self.get_ipaddress(), self.get_user())) HueLight(int(lightid), lightresponse.json(), self.get_ipaddress(), self.get_user()))

@ -19,7 +19,8 @@ class HueGroup:
"""Constructor """Constructor
Args: Args:
data_slice (dict): The part of the data structure that concerns this Action data_slice (dict): The part of the data structure that
concerns this Action
""" """
keys = data_slice.keys() keys = data_slice.keys()
self.m_on: bool = data_slice['on'] self.m_on: bool = data_slice['on']
@ -52,14 +53,15 @@ class HueGroup:
class State: class State:
"""A hueGroup.State represents the collective state of the group """A hueGroup.State represents the collective state of the group
""" """
def __init__(self, data_slice: dict): def __init__(self, data_slice: dict):
"""Constructor """Constructor
Args: Args:
data_slice (dict): The part of the data structure that concerns this State data_slice (dict): The part of the data structure
that concerns this State
""" """
self.m_all_on: bool = data_slice['all_on'] self.m_all_on: bool = data_slice['all_on']
self.m_any_on: bool = data_slice['any_on'] self.m_any_on: bool = data_slice['any_on']
@ -199,7 +201,8 @@ class HueGroup:
Returns: Returns:
requests.Response: The API response requests.Response: The API response
""" """
path: str = "{}/groups/{}/action".format(self.m_parent_bridge_user, self.m_id) path: str = "{}/groups/{}/action".format(
self.m_parent_bridge_user, self.m_id)
method: str = "PUT" method: str = "PUT"
response = make_request(self.m_parent_bridge_ip, path, method, state) response = make_request(self.m_parent_bridge_ip, path, method, state)
self.update_state() self.update_state()

@ -4,13 +4,11 @@ import http
import os import os
from typing import Union from typing import Union
import simplejson
import toml import toml
from upnpy import UPnP from upnpy import UPnP
from .HueBridge import HueBridge from .HueBridge import HueBridge
from .HueUtils import connect, is_valid_config, make_request, is_bridge from .HueUtils import is_valid_config, make_request, is_bridge
from .UserOrError import UserOrError
class Tinge: class Tinge:
@ -26,16 +24,25 @@ class Tinge:
self.create_confdir() self.create_confdir()
self.read_bridges_from_file() self.read_bridges_from_file()
def append_bridge(self, bridge: HueBridge): def append_bridge(self, bridge: HueBridge) -> None:
"""Append a bridge to the list
"""
self.m_bridges.append(bridge) self.m_bridges.append(bridge)
self.m_discovered.append(bridge.get_ipaddress()) self.m_discovered.append(bridge.get_ipaddress())
def create_confdir(self): def create_confdir(self) -> None:
"""Create the config dir if it does not allready exist """Create the config dir if it does not allready exist
""" """
if not os.path.exists(os.path.dirname(self.m_config)): if not os.path.exists(os.path.dirname(self.m_config)):
os.makedirs(os.path.dirname(self.m_config)) os.makedirs(os.path.dirname(self.m_config))
def delete_bridge(self, bridge: HueBridge) -> None:
"""Delete a bridge from the list
"""
self.m_bridges.remove(bridge)
self.m_discovered.remove(bridge.get_ipaddress())
self.write_all_bridges_to_conf()
def discover_new_bridges(self) -> Union[None, list[dict]]: def discover_new_bridges(self) -> Union[None, list[dict]]:
"""Use UPnP to discover bridges on the current network """Use UPnP to discover bridges on the current network
""" """
@ -56,8 +63,10 @@ class Tinge:
if (device.host not in self.m_discovered) and (device.host not in seen_ips): if (device.host not in self.m_discovered) and (device.host not in seen_ips):
seen_ips.append(device.host) seen_ips.append(device.host)
# Let's check if the device has the default name, if so we assume it's a hue bridge # Let's check if the device has the default name, if so we assume it's a hue bridge
if device.get_friendly_name().startswith("Philips hue"): f_name = device.get_friendly_name()
discovered = True if f_name:
if f_name.startswith("Philips hue"):
discovered = True
# If not we try to do a request against the api and see if we get an answer we can understand # If not we try to do a request against the api and see if we get an answer we can understand
else: else:
discovered = is_bridge(device.host) discovered = is_bridge(device.host)
@ -75,7 +84,7 @@ class Tinge:
""" """
return self.m_bridges return self.m_bridges
def read_bridges_from_file(self): def read_bridges_from_file(self) -> None:
"""Read config file and add back previously discovered bridges """Read config file and add back previously discovered bridges
""" """
if is_valid_config(self.m_config): if is_valid_config(self.m_config):
@ -94,9 +103,11 @@ class Tinge:
self.m_bridges.append(bridge) self.m_bridges.append(bridge)
self.m_discovered.append(key) self.m_discovered.append(key)
def write_all_bridges_to_conf(self): def write_all_bridges_to_conf(self) -> None:
"""Save to file """Save to file
""" """
with open(self.m_config, 'w') as configfile: with open(self.m_config, 'w') as configfile:
for bridge in self.m_bridges: for bridge in self.m_bridges:
configfile.write('["{}"]\nuser = "{}"\n'.format(bridge.get_ipaddress(), bridge.get_user())) configfile.write('["{}"]\nuser = "{}"\n'.format(bridge.get_ipaddress(), bridge.get_user()))
if len(self.m_bridges) == 0:
configfile.truncate()

@ -0,0 +1,3 @@
[DEFAULT]
Depends3: python3-upnpy, python3-wxgtk4.0, python3-toml, python3-requests
Debian-Version: 2

@ -1,5 +0,0 @@
[Desktop Entry]
Type=Application
Name=Tinge
Exec=/home/##USER##/.local/bin/tinge
Icon=face-cool
Loading…
Cancel
Save