Refactor screens into new classes and allow changing light brightness

This commit is contained in:
2024-05-16 21:04:54 +01:00
parent d98ec27a1b
commit 0c4cbfcbb1
2 changed files with 80 additions and 198 deletions

40
api.py
View File

@@ -15,8 +15,11 @@ class Api:
"content-type": "application/json"
}
print("Starting request for " + endpoint)
response = get(url, headers=headers)
return response.json()
try:
response = get(url, headers=headers)
return response.json()
except:
return {}
def __post(self, endpoint, d) -> bool:
gc.collect()
@@ -26,23 +29,26 @@ class Api:
"content-type": "application/json"
}
print("Starting post request to " + endpoint)
response = post(url, headers=headers, json=d)
return response.status_code == 200
try:
response = post(url, headers=headers, json=d)
return response.status_code == 200
except:
return False
def getLightData(self, entity_id: str) -> dict:
# TODO: error handling if can't access hass (e.g. no connection, invalid token)
response = self.__request("/api/states/" + entity_id)
on = response["state"] == "on"
on = "state" in response and response["state"] == "on"
return {
"on": on,
"rgb_color": response["attributes"]["rgb_color"] if on else None,
"rgbw_color": response["attributes"]["rgbw_color"] if on else None,
"brightness": response["attributes"]["brightness"] if on else 0,
"friendly_name": response["attributes"]["friendly_name"]
"brightness": response["attributes"]["brightness"] if on else 0.0
}
def getMediaPlayerData(self, entity_id: str) -> dict:
response = self.__request("/api/states/" + entity_id)
e = "state" in response and "attributes" in response
if ("media_position" in (dict)(response["attributes"])):
p = response["attributes"]["media_position"]
else:
@@ -53,16 +59,15 @@ class Api:
p += time.mktime(time.localtime()) - t
return {
"playing": response["state"] in ["on", "playing", "buffering"],
"shuffle": response["attributes"]["shuffle"],
"repeat": response["attributes"]["repeat"] == "on",
"volume_level": response["attributes"]["volume_level"],
"friendly_name": response["attributes"]["friendly_name"],
"entity_picture": response["attributes"]["entity_picture"] if "entity_picture" in (dict)(response["attributes"]) else None,
"media_duration": response["attributes"]["media_duration"] if "media_duration" in (dict)(response["attributes"]) else 0,
"shuffle": response["attributes"]["shuffle"] if e and "shuffle" in response["attributes"] else False,
"repeat": response["attributes"]["repeat"] == "on" if e and "repeat" in response["attributes"] else False,
"volume_level": response["attributes"]["volume_level"] if e and "volume_level" in response["attributes"] else 0,
"entity_picture": response["attributes"]["entity_picture"] if e and "entity_picture" in (dict)(response["attributes"]) else None,
"media_duration": response["attributes"]["media_duration"] if e and "media_duration" in (dict)(response["attributes"]) else 0,
"media_position": p,
"media_title": response["attributes"]["media_title"] if "media_title" in (dict)(response["attributes"]) else "Nothing playing",
"media_artist": response["attributes"]["media_artist"] if "media_artist" in (dict)(response["attributes"]) else "",
"media_album_name": response["attributes"]["media_album_name"] if "media_album_name" in (dict)(response["attributes"]) else ""
"media_title": response["attributes"]["media_title"] if e and "media_title" in (dict)(response["attributes"]) else "Nothing playing",
"media_artist": response["attributes"]["media_artist"] if e and "media_artist" in (dict)(response["attributes"]) else "",
"media_album_name": response["attributes"]["media_album_name"] if e and "media_album_name" in (dict)(response["attributes"]) else ""
}
def changeVolume(self, entity_id: str, up: bool = True) -> None:
@@ -82,3 +87,6 @@ class Api:
def toggleLight(self, entity_id: str) -> None:
self.__post("/api/services/light/toggle", {"entity_id": entity_id})
def setBrightness(self, entity_id: str, v: int) -> None:
self.__post("/api/services/light/turn_on", {"entity_id": entity_id, "brightness": v})

238
app.py
View File

@@ -6,21 +6,19 @@ from lcd import LCD
from font import Font
import gc
from env import *
from utils import Utils
import _thread
import bmp_file_reader as bmpr
import ntptime
import time
from screens import *
class App:
def __init__(self) -> None:
if (len(SCREENS) == 0):
print("No screens configured. Check env.py.")
self.screen = -1
else: self.screen = 0
self.scr_n = -1
else: self.scr_n = 0
self.lcd = LCD()
self.api = Api(HASS_URL, TOKEN)
self.scr_vals = {} # stored values from the current screen
def __connect(self) -> int:
wlan = network.WLAN(network.STA_IF)
@@ -35,14 +33,18 @@ class App:
return self.ip
def __boot(self) -> None:
gc.collect()
print("Booting")
self.lcd.fill(self.lcd.black)
Font.cntr_st(self.lcd, self.lcd.width, "Booting...", 120, 2, 150, 150, 150)
self.lcd.show()
self.__connect()
print("Local time before synchronization%s" %str(time.localtime()))
ntptime.host = "1.europe.pool.ntp.org"
ntptime.settime()
print("Local time after synchronization%s" %str(time.localtime()))
try:
ntptime.settime()
print("Local time after synchronization: %s" %str(time.localtime()))
except:
pass
def __resetButtonStatuses(self) -> None:
self.lcd.keyA["v"] = False
@@ -89,206 +91,78 @@ class App:
sleep(0.2)
def __changeScreen(self) -> bool:
orig = self.screen
c = False
if (self.lcd.right["v"]):
if (self.screen == len(SCREENS) - 1):
self.screen = 0
c = True
if (self.scr_n == len(SCREENS) - 1):
self.scr_n = 0
else:
self.screen += 1
self.scr_n += 1
elif (self.lcd.left["v"]):
if (self.screen == 0):
self.screen = len(SCREENS) - 1
c = True
if (self.scr_n == 0):
self.scr_n = len(SCREENS) - 1
else:
self.screen -= 1
change = self.screen != orig
if (change):
self.scr_vals = {}
self.scr_n -= 1
self.lcd.left["v"] = False
self.lcd.right["v"] = False
print(f"Screen change: {change}")
return change
print(f"Screen change: {c}")
return c
def __displayLightEntity(self, i: int, w: int, h: int, xo: int, yo: int, n: str, d) -> None:
self.scr_vals[i] = d
# if the light is turned on, display the filled-in lightbulb icon in the colour of the light, centrally in the light's grid square
if (d["on"]):
color = Utils.colour(d["rgb_color"][0], d["rgb_color"][1], d["rgb_color"][2])
with open("images/lightbulb-on.bmp", "rb") as file_handle:
reader = bmpr.BMPFileReader(file_handle)
img_height = reader.get_height()
x_offset = w//2 + xo - reader.get_width()//2
y_offset = h//2 + yo - reader.get_height()//2 - 4
for row_i in range(0, reader.get_height()):
row = reader.get_row(row_i)
for col_i, color in enumerate(row):
r = d["rgb_color"][0] if (color.red) != 0 else 0
g = d["rgb_color"][1] if (color.green) != 0 else 0
b = d["rgb_color"][2] if (color.blue) != 0 else 0
if (color.red != 0 or color.green != 0 or color.blue != 0):
self.lcd.pixel(col_i + x_offset, row_i + y_offset, Utils.colour(r,g,b))
# otherwise display the outline lightbulb icon in grey, centrally in the light's grid square
else:
color = Utils.colour(80, 80, 80)
with open("images/lightbulb-off.bmp", "rb") as file_handle:
reader = bmpr.BMPFileReader(file_handle)
img_height = reader.get_height()
x_offset = w//2 + xo - reader.get_width()//2
y_offset = h//2 + yo - reader.get_height()//2 - 4
for row_i in range(0, reader.get_height()):
row = reader.get_row(row_i)
for col_i, color in enumerate(row):
if (color.red != 0 or color.green != 0 or color.blue != 0):
self.lcd.pixel(col_i + x_offset, row_i + y_offset, Utils.colour(color.red, color.green, color.blue))
else:
self.lcd.pixel(col_i + x_offset, row_i + y_offset, Utils.colour(0,0,0))
# display the name of the light 8px below the lightbulb icon
Font.cntr_st(self.lcd, w, n, y_offset + img_height + 8, 2, 220, 220, 220, xo)
def __displayLightsScreen(self, d=None) -> None:
# display up to four lights as defined in env.py
self.lcd.fill(self.lcd.black)
Font.cntr_st(self.lcd, self.lcd.width, SCREENS[self.screen]["name"], 10, 2, 255, 255, 255)
entities = len(SCREENS[self.screen]["entities"])
# for each defined entity (up to a max total of 4), display its data in a 2x2 grid
if (d == None): d = []
for i in range(0, min(entities, 4)):
if (len(d) == i): d.append(self.api.getLightData(SCREENS[self.screen]["entities"][i]["id"]))
self.__displayLightEntity(i, self.lcd.width//2, self.lcd.height//2, self.lcd.width//2 * (i % 2), self.lcd.height//2 * (i//2), SCREENS[self.screen]["entities"][i]["name"], d[i])
self.lcd.show()
# TODO: handle buttons
def __updateMediaPositionBar(self, d):
if (d["media_position"] != None and d["media_duration"] != None and d["media_duration"] != 0):
for x in range (0, (self.lcd.width * d["media_position"])//d["media_duration"]):
self.lcd.pixel(x, self.lcd.height - 5, self.lcd.white)
self.lcd.pixel(x, self.lcd.height - 4, self.lcd.white)
self.lcd.pixel(x, self.lcd.height - 3, self.lcd.white)
self.lcd.pixel(x, self.lcd.height - 2, self.lcd.white)
self.lcd.pixel(x, self.lcd.height - 1, self.lcd.white)
self.lcd.pixel(x, self.lcd.height, self.lcd.white)
def __displayMediaScreen(self, d=None) -> None:
self.lcd.fill(self.lcd.black)
e = SCREENS[self.screen]["entity"]
if (e == None and d == None):
Font.cntr_st(self.lcd, self.lcd.width, "No media player selected", 110, 3, 255, 255, 255)
self.lcd.show()
return
if (d == None): d = self.api.getMediaPlayerData(e)
self.scr_vals[0] = d
self.lcd.fill(self.lcd.black)
Font.cntr_st(self.lcd, self.lcd.width, SCREENS[self.screen]["name"], 20, 2, 255, 255, 255)
self.__updateMediaPositionBar(d)
if (d["media_duration"] != None):
mins = d["media_duration"] // 60
secs = d["media_duration"] % 60
Font.rght_st(self.lcd, f"{mins}:{secs}", self.lcd.width, self.lcd.height - 16, 1, 180, 180, 180)
Font.cntr_st(self.lcd, self.lcd.width, d["media_title"], self.lcd.height - 72, 3, 255, 255, 255)
Font.cntr_st(self.lcd, self.lcd.width, d["media_artist"], self.lcd.height - 96, 2, 255, 255, 255)
self.lcd.show()
# TODO: display album art
# TODO: handle button presses for volume and changing tracks
# TODO: add icons next to buttons
def __displayUnknownScreen(self) -> None:
self.lcd.fill(self.lcd.black)
Font.cntr_st(self.lcd, self.lcd.width, "Invalid config", 110, 3, 255, 255, 255)
Font.cntr_st(self.lcd, self.lcd.width, "Screen " + str(self.screen), 130, 2, 255, 255, 255)
def __updateLightsScreen(self) -> None:
e = min(len(SCREENS[self.screen]["entities"]), 4)
# for each light to be displayed
for i in range(0, e):
# if its settings have changed, re-draw them without clearing the display
d = self.api.getLightData(SCREENS[self.screen]["entities"][i]["id"])
if (d != self.scr_vals[i]):
self.__displayLightEntity(i, self.lcd.width//2, self.lcd.height//2, self.lcd.width//2 * (i % 2), self.lcd.height//2 * (i//2), SCREENS[self.screen]["entities"][i]["name"], d)
# TODO: double button press to select a light, then up/down to change brightness? and single button click to turn on/off?
self.lcd.show()
def __handleLightsScreenButtons(self) -> bool:
# TODO: reset buttons used
b = [self.lcd.keyA["v"], self.lcd.keyB["v"], self.lcd.keyX["v"], self.lcd.keyY["v"]]
self.lcd.keyA["v"] = False
self.lcd.keyB["v"] = False
self.lcd.keyX["v"] = False
self.lcd.keyY["v"] = False
e = min(len(SCREENS[self.screen]["entities"]), 4)
a = False
for i in range(0, e):
# if button for light clicked, toggle the light
if (b[i]):
self.api.toggleLight(SCREENS[self.screen]["entities"][i]["id"])
a = True
return a
def __updateMediaScreen(self) -> None:
d = self.api.getMediaPlayerData(SCREENS[self.screen]["entity"])
# if same media is playing (same title and duration), just update the position bar
if (self.scr_vals[0]["media_title"] == d["media_title"] and self.scr_vals[0]["media_duration"] == d["media_duration"]):
self.__updateMediaPositionBar(d)
self.lcd.show()
# otherwise redraw the whole screen
else:
self.__displayMediaScreen(d)
def __handleMediaScreenButtons(self) -> bool:
def handleButtons(self):
up = self.lcd.up["v"]
down = self.lcd.down["v"]
left = self.lcd.left["v"]
right = self.lcd.right["v"]
ctrl = self.lcd.ctrl["v"]
keyA = self.lcd.keyA["v"]
keyB = self.lcd.keyB["v"]
keyX = self.lcd.keyX["v"]
keyY = self.lcd.keyY["v"]
self.lcd.up["v"] = False
self.lcd.down["v"] = False
self.lcd.keyA["v"] = False
self.lcd.keyX["v"] = False
self.lcd.keyY["v"] = False
a = False
e = SCREENS[self.screen]["entity"]
if (up):
self.api.changeVolume(e)
a = True
elif (down):
self.api.changeVolume(e, False)
a = True
if (keyX):
self.api.nextTrack(e)
elif (keyY):
self.api.prevTrack(e)
if (keyA):
self.api.playPause(e)
return a
# if (a): self.__handleMediaScreenButtons()
# self.lcd.up["v"] = False
# self.lcd.down["v"] = False
# self.lcd.left["v"] = False
# self.lcd.right["v"] = False
# self.lcd.ctrl["v"] = False
# self.lcd.keyA["v"] = False
# self.lcd.keyB["v"] = False
# self.lcd.keyX["v"] = False
# self.lcd.keyY["v"] = False
self.__resetButtonStatuses()
if (ctrl):
machine.reset()
self.s.handleButtons(up, down, left, right, keyA, keyB, keyX, keyY, ctrl)
def __manageScreen(self) -> None:
started = False
while (True):
gc.collect()
if (time.localtime()[3] == 0): ntptime.settime()
if (time.localtime()[3] == 0):
try:
ntptime.settime()
except:
pass
changed = not started or self.__changeScreen()
if (not started): started = True
# if the screen has changed, redraw the whole screen
if (changed):
if (SCREENS[self.screen]["type"] == 0):
self.__resetButtonStatuses()
self.__displayLightsScreen()
elif (SCREENS[self.screen]["type"] == 1):
self.__resetButtonStatuses()
self.__displayMediaScreen()
self.__resetButtonStatuses()
gc.collect()
if (SCREENS[self.scr_n]["type"] == 0):
self.s = LightsScreen(self.api, SCREENS[self.scr_n]["name"], SCREENS[self.scr_n]["entities"])
elif (SCREENS[self.scr_n]["type"] == 1):
self.s = MediaScreen(self.api, SCREENS[self.scr_n]["name"], SCREENS[self.scr_n]["entity"])
else:
self.__resetButtonStatuses()
self.__displayUnknownScreen()
self.s = UnknownScreen(self.api, -1, "Unknown")
self.s.display(self.lcd)
# otherwise minimise the number of pixels being changed
else:
if (SCREENS[self.screen]["type"] == 0):
if (self.__handleLightsScreenButtons()): continue
self.__updateLightsScreen()
elif (SCREENS[self.screen]["type"] == 1):
if (self.__handleMediaScreenButtons()): continue
self.__updateMediaScreen()
b = self.handleButtons()
if (b): continue
self.s.update(self.lcd)
def run(self) -> None:
if (self.screen == None): return
if (self.scr_n == None): return
self.__boot()
try:
_thread.start_new_thread(self.__manageButtons, ())