298 lines
13 KiB
Python
298 lines
13 KiB
Python
from api import Api
|
||
import machine
|
||
import network
|
||
from time import sleep
|
||
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
|
||
|
||
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.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)
|
||
wlan.active(True)
|
||
wlan.config(hostname=HOSTNAME)
|
||
wlan.connect(SSID, WIFI_PASSWORD)
|
||
while wlan.isconnected() == False:
|
||
print('Waiting for connection...')
|
||
sleep(1)
|
||
self.ip = wlan.ifconfig()[0]
|
||
print(f'Connected on {self.ip}')
|
||
return self.ip
|
||
|
||
def __boot(self) -> None:
|
||
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()))
|
||
|
||
def __resetButtonStatuses(self) -> None:
|
||
self.lcd.keyA["v"] = False
|
||
self.lcd.keyB["v"] = False
|
||
self.lcd.keyX["v"] = False
|
||
self.lcd.keyY["v"] = False
|
||
self.lcd.left["v"] = False
|
||
self.lcd.right["v"] = False
|
||
self.lcd.up["v"] = False
|
||
self.lcd.down["v"] = False
|
||
self.lcd.ctrl["v"] = False
|
||
|
||
def __manageButtons(self) -> None:
|
||
while (True):
|
||
a = False
|
||
if (self.lcd.keyA["p"].value() == 0):
|
||
self.lcd.keyA["v"] = True
|
||
a = True
|
||
if (self.lcd.keyB["p"].value() == 0):
|
||
self.lcd.keyB["v"] = True
|
||
a = True
|
||
if (self.lcd.keyX["p"].value() == 0):
|
||
self.lcd.keyX["v"] = True
|
||
a = True
|
||
if (self.lcd.keyY["p"].value() == 0):
|
||
self.lcd.keyY["v"] = True
|
||
a = True
|
||
if (self.lcd.right["p"].value() == 0):
|
||
self.lcd.right["v"] = True
|
||
a = True
|
||
if (self.lcd.left["p"].value() == 0):
|
||
self.lcd.left["v"] = True
|
||
a = True
|
||
if (self.lcd.up["p"].value() == 0):
|
||
self.lcd.up["v"] = True
|
||
a = True
|
||
if (self.lcd.down["p"].value() == 0):
|
||
self.lcd.down["v"] = True
|
||
a = True
|
||
if (self.lcd.ctrl["p"].value() == 0):
|
||
self.lcd.ctrl["v"] = True
|
||
a = True
|
||
if (a):
|
||
sleep(0.2)
|
||
|
||
def __changeScreen(self) -> bool:
|
||
orig = self.screen
|
||
if (self.lcd.right["v"]):
|
||
if (self.screen == len(SCREENS) - 1):
|
||
self.screen = 0
|
||
else:
|
||
self.screen += 1
|
||
elif (self.lcd.left["v"]):
|
||
if (self.screen == 0):
|
||
self.screen = len(SCREENS) - 1
|
||
else:
|
||
self.screen -= 1
|
||
change = self.screen != orig
|
||
if (change):
|
||
self.scr_vals = {}
|
||
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
|
||
|
||
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:
|
||
up = self.lcd.up["v"]
|
||
down = self.lcd.down["v"]
|
||
keyA = self.lcd.keyA["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()
|
||
|
||
def __manageScreen(self) -> None:
|
||
started = False
|
||
while (True):
|
||
gc.collect()
|
||
if (time.localtime()[3] == 0): ntptime.settime()
|
||
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()
|
||
else:
|
||
self.__resetButtonStatuses()
|
||
self.__displayUnknownScreen()
|
||
# 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()
|
||
|
||
def run(self) -> None:
|
||
if (self.screen == None): return
|
||
self.__boot()
|
||
try:
|
||
_thread.start_new_thread(self.__manageButtons, ())
|
||
self.__manageScreen()
|
||
except KeyboardInterrupt:
|
||
machine.reset()
|