diff --git a/api.py b/api.py index 5b32bc9..1d2048e 100644 --- a/api.py +++ b/api.py @@ -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}) diff --git a/app.py b/app.py index 8fb6d78..9d09373 100644 --- a/app.py +++ b/app.py @@ -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 - - 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 + print(f"Screen change: {c}") + return c - 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, ())