From 429de391ecf48e70b6b81fa7b239c5720c7da371 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sun, 7 Jan 2024 11:12:55 -0800 Subject: [PATCH] Remove myq --- constants.py | 2 +- kiosk.py | 2 +- kiosk_constants.py | 2 +- kiosk_secrets.py | 6 +- main.py | 552 ---------------------------- myq_renderer.py | 127 ------- ratago_renderer.py | 161 ++++++++ myq_trigger.py => ratago_trigger.py | 6 +- renderer_catalog.py | 6 +- trigger_catalog.py | 4 +- 10 files changed, 174 insertions(+), 694 deletions(-) delete mode 100755 main.py delete mode 100644 myq_renderer.py create mode 100644 ratago_renderer.py rename myq_trigger.py => ratago_trigger.py (63%) diff --git a/constants.py b/constants.py index ce04f7e..8ebb553 100644 --- a/constants.py +++ b/constants.py @@ -14,6 +14,6 @@ seconds_per_minute = 60 seconds_per_hour = seconds_per_minute * 60 seconds_per_day = seconds_per_hour * 24 -myq_pagename = "myq_4_300.html" +ratago_pagename = "ratago_4_300.html" render_stats_pagename = 'internal/render-stats_1_1000.html' gcal_imminent_pagename = "hidden/gcal-imminent_0_none.html" diff --git a/kiosk.py b/kiosk.py index 3a593b3..14dae57 100755 --- a/kiosk.py +++ b/kiosk.py @@ -101,7 +101,7 @@ def guess_page(command: str, page_chooser: chooser.chooser) -> str: page = page.replace("gohouse", "house list honey do") page = page.replace("gcal", "google calendar events") page = page.replace("mynorthwest", "northwest news") - page = page.replace("myq", "myq garage door status") + page = page.replace("ratago", "ratago garage door status") page = page.replace("gomenu", "dinner menu") page = page.replace("gmaps-seattle-unwrapped", "traffic") page = page.replace("gomenu", "dinner menu") diff --git a/kiosk_constants.py b/kiosk_constants.py index 39ee43f..d5897d1 100644 --- a/kiosk_constants.py +++ b/kiosk_constants.py @@ -14,7 +14,7 @@ seconds_per_minute = 60 seconds_per_hour = seconds_per_minute * 60 seconds_per_day = seconds_per_hour * 24 -myq_pagename = "myq_4_300.html" +ratago_pagename = "ratago_4_300.html" render_stats_pagename = 'internal/render-stats_1_1000.html' gcal_imminent_pagename = "hidden/gcal-imminent_0_none.html" diff --git a/kiosk_secrets.py b/kiosk_secrets.py index b25f1fa..8fbb4c7 100644 --- a/kiosk_secrets.py +++ b/kiosk_secrets.py @@ -10,10 +10,8 @@ google_key = "" google_client_id = '.apps.googleusercontent.com' google_client_secret = '' -# These are from your myq mobile app login. -myq_username = "@gmail.com" -myq_password = "" -myq_appid = "" +# Your home assistant key +home_assistant_key = '' # These can be generated on the developer console at Twitter. twitter_consumer_key = "" diff --git a/main.py b/main.py deleted file mode 100755 index 162a9a9..0000000 --- a/main.py +++ /dev/null @@ -1,552 +0,0 @@ -#!/usr/bin/env python3 - -from datetime import datetime -import gc -import linecache -import os -import re -import sys -from threading import Thread -import time -import traceback -import tracemalloc -from typing import Optional, List -from queue import Queue - -import astral # type: ignore -from astral.sun import sun # type: ignore -import pytz - -import datetime_utils -import file_utils - -import kiosk_constants as constants -import renderer_catalog -import chooser -import listen -import logging -import pvporcupine -import trigger_catalog - - -def thread_janitor() -> None: - tracemalloc.start() - tracemalloc_target = 0.0 - gc_target = 0.0 - gc.enable() - - while True: - now = time.time() - if now > tracemalloc_target: - tracemalloc_target = now + 30.0 - snapshot = tracemalloc.take_snapshot() - snapshot = snapshot.filter_traces(( - tracemalloc.Filter(False, ""), - tracemalloc.Filter(False, ""), - )) - key_type = 'lineno' - limit = 10 - top_stats = snapshot.statistics(key_type) - print("janitor: Top %s lines" % limit) - for index, stat in enumerate(top_stats[:limit], 1): - frame = stat.traceback[0] - # replace "/path/to/module/file.py" with "module/file.py" - filename = os.sep.join(frame.filename.split(os.sep)[-2:]) - print("janitor: #%s: %s:%s: %.1f KiB" - % (index, filename, frame.lineno, stat.size / 1024)) - line = linecache.getline(frame.filename, frame.lineno).strip() - if line: - print('janitor: %s' % line) - - other = top_stats[limit:] - if other: - size = sum(stat.size for stat in other) - print("janitor: %s other: %.1f KiB" % (len(other), size / 1024)) - total = sum(stat.size for stat in top_stats) - print("janitor: Total allocated size: %.1f KiB" % (total / 1024)) - if now > gc_target: - print("janitor: Running gc operation") - gc_target = now + 60.0 - gc.collect() - time.sleep(10.0) - - -def guess_page(command: str, page_chooser: chooser.chooser) -> str: - best_page = None - best_score = None - for page in page_chooser.get_page_list(): - page = page.replace('(', ' ') - page = page.replace('_', ' ') - page = page.replace(')', ' ') - page = page.replace('.html', '') - page = page.replace('CNNNews', 'news') - page = page.replace('CNNTechnology', 'technology') - page = page.replace('gocostco', 'costco list') - page = page.replace('gohardware', 'hardware list') - page = page.replace('gohouse', 'house list honey do') - page = page.replace('gcal', 'google calendar events') - page = page.replace('mynorthwest', 'northwest news') - page = page.replace('myq', 'myq garage door status') - page = page.replace('gomenu', 'dinner menu') - page = page.replace('wsdot', 'traffic') - page = page.replace('gomenu', 'dinner menu') - page = page.replace('WSJNews', 'news') - page = page.replace('telma', 'telma cabin') - page = page.replace('WSJBusiness', 'business news') - page = re.sub(r'[0-9]+', '', page) - score = SequenceMatcher(None, command, page).ratio() - if best_score is None or score > best_score: - best_page = page - assert best_page is not None - return best_page - - -def process_command(command: str, page_history: List[str]) -> str: - page = None - if 'hold' in command: - page = page_history[0] - elif 'back' in command: - page = page_history[1] - elif 'skip' in command: - while True: - (page, _) = page_chooser.choose_next_page() - if page != page_history[0]: - break - elif 'weather' in command: - if 'telma' in command or 'cabin' in command: - page = 'weather-telma_3_10800.html' - elif 'stevens' in command: - page = 'weather-stevens_3_10800.html' - else: - page = 'weather-home_3_10800.html' - elif 'cabin' in command: - if 'list' in command: - page = 'Cabin-(gocabin)_2_3600.html' - else: - page = 'hidden/cabin_driveway.html' - elif 'news' in command or 'headlines' in command: - page = 'cnn-CNNNews_4_25900.html' - elif 'clock' in command or 'time' in command: - page = 'clock_10_none.html' - elif 'countdown' in command or 'countdowns' in command: - page = 'countdown_3_7200.html' - elif 'costco' in command: - page = 'Costco-(gocostco)_2_3600.html' - elif 'calendar' in command or 'events' in command: - page = 'gcal_3_86400.html' - elif 'countdown' in command or 'countdowns' in command: - page = 'countdown_3_7200.html' - elif 'grocery' in command or 'groceries' in command: - page = 'Grocery-(gogrocery)_2_3600.html' - elif 'hardware' in command: - page = 'Hardware-(gohardware)_2_3600.html' - elif 'garage' in command: - page = 'myq_4_300.html' - elif 'menu' in command: - page = 'Menu-(gomenu)_2_3600.html' - elif 'cron' in command or 'health' in command: - page = 'periodic-health_6_300.html' - elif 'photo' in command or 'picture' in command: - page = 'photo_23_3600.html' - elif 'quote' in command or 'quotation' in command or 'quotes' in command: - page = 'quotes_4_10800.html' - elif 'stevens' in command: - page = 'stevens-conditions_1_86400.html' - elif 'stock' in command or 'stocks' in command: - page = 'stock_3_86400.html' - elif 'twitter' in command: - page = 'twitter_10_3600.html' - elif 'traffic' in command: - page = 'wsdot-bridges_3_none.html' - elif 'front' in command and 'door' in command: - page = 'hidden/frontdoor.html' - elif 'driveway' in command: - page = 'hidden/driveway.html' - elif 'backyard' in command: - page = 'hidden/backyard.html' - else: - page = guess_page(command, page_chooser) - assert page is not None - return page - - -def thread_change_current(command_queue: Queue) -> None: - page_history = [ "", "" ] - swap_page_target = 0.0 - - def filter_news_during_dinnertime(page: str) -> bool: - now = datetime.now() - is_dinnertime = now.hour >= 17 and now.hour <= 20 - print(f"is dinnertime = {is_dinnertime}") - print(f'page = {page}') - return not is_dinnertime or not ( - "cnn" in page - or "news" in page - or "mynorthwest" in page - or "seattle" in page - or "stranger" in page - or "twitter" in page - or "wsj" in page - ) - page_chooser = chooser.weighted_random_chooser_with_triggers( - trigger_catalog.get_triggers(), [filter_news_during_dinnertime] - ) - - while True: - now = time.time() - - # Check for a verbal command. - command = None - try: - command = command_queue.get(block=False) - except Exception: - command = None - pass - if command is not None: - triggered = True - page = process_command(command, page_history) - - # Else pick a page randomly. - else: - while True: - (page, triggered) = page_chooser.choose_next_page() - if triggered or page != page_history[0]: - break - - if triggered: - print("chooser[%s] - WE ARE TRIGGERED." % datetime_utils.timestamp()) - if page != page_history[0] or (swap_page_target - now < 10.0): - print( - "chooser[%s] - EMERGENCY PAGE %s LOAD NEEDED" - % (datetime_utils.timestamp(), page) - ) - try: - with open(os.path.join(constants.pages_dir, "current.shtml"), "w") as f: - emit_wrapped(f, page, override_refresh_sec = 40, command = command) - page_history.insert(0, page) - page_history = page_history[0:10] - swap_page_target = now + 40 - except: - print("chooser[%s] - page does not exist?!" % (datetime_utils.timestamp())) - continue - - # Also notify XMLHTTP clients that they need to refresh now. - path = os.path.join(constants.pages_dir, "reload_immediately.html") - with open(path, "w") as f: - f.write("Reload, suckers!") - - # Fix this hack... maybe read the webserver logs and see if it - # actually was picked up? - time.sleep(0.75) - os.remove(path) - - elif now >= swap_page_target: - assert page != page_history[0] - print("chooser[%s] - nominal choice of %s" % (datetime_utils.timestamp(), page)) - try: - with open(os.path.join(constants.pages_dir, "current.shtml"), "w") as f: - emit_wrapped(f, page) - page_history.insert(0, page) - page_history = page_history[0:10] - swap_page_target = now + constants.refresh_period_sec - except: - print("chooser[%s] - page does not exist?!" % (datetime_utils.timestamp())) - continue - time.sleep(1) - - -def emit_wrapped(f, - filename: str, - *, - override_refresh_sec: int = None, - command: str = None) -> None: - def pick_background_color() -> str: - now = datetime.now(tz=pytz.timezone("US/Pacific")) - city = astral.LocationInfo( - "Bellevue", "USA", "US/Pacific", 47.610, -122.201 - ) - s = sun(city.observer, date=now, tzinfo=pytz.timezone("US/Pacific")) - sunrise_mod = datetime_utils.minute_number(s["sunrise"].hour, s["sunrise"].minute) - sunset_mod = datetime_utils.minute_number(s["sunset"].hour, s["sunset"].minute) - now_mod = datetime_utils.minute_number(now.hour, now.minute) - if now_mod < sunrise_mod or now_mod > (sunset_mod + 45): - return "E6B8B8" - elif now_mod < (sunrise_mod + 45) or now_mod > (sunset_mod + 120): - return "EECDCD" - else: - return "FFFFFF" - - def get_refresh_period() -> float: - if override_refresh_sec is not None: - return float(override_refresh_sec * 1000.0) - now = datetime.now() - if now.hour < 7: - return float(constants.refresh_period_night_sec * 1000.0) - else: - return float(constants.refresh_period_sec * 1000.0) - - age = file_utils.describe_file_ctime(f"pages/{filename}") - bgcolor = pick_background_color() - if command is None: - pageid = filename - else: - pageid = f'"{command}" -> {filename}' - - f.write( - """ - - Kitchen Kiosk - - - - - - - - - - - - - -
-
 
-
-
-
-
 
-
- -
-
-

- %s @ %s ago. -

-
-
-
-""" - % ( - bgcolor, - get_refresh_period(), - constants.hostname, - bgcolor, - filename, - pageid, - age, - ) - ) - - -def thread_invoke_renderers() -> None: - while True: - print(f"renderer[{datetime_utils.timestamp()}]: invoking all renderers in catalog...") - for r in renderer_catalog.get_renderers(): - now = time.time() - try: - r.render() - except Exception as e: - traceback.print_exc(file=sys.stdout) - print( - f"renderer[{datetime_utils.timestamp()}] unknown exception ({e}) in {r.get_name()}, swallowing it." - ) - delta = time.time() - now - if delta > 1.0: - print( - f"renderer[{datetime_utils.timestamp()}]: Warning: {r.get_name()}'s rendering took {delta:5.2f}s." - ) - print( - f"renderer[{datetime_utils.timestamp()}]: thread having a little break for {constants.render_period_sec}s..." - ) - time.sleep(constants.render_period_sec) - - -if __name__ == "__main__": - logging.basicConfig() - command_queue: Queue = Queue() - changer_thread: Optional[Thread] = None - renderer_thread: Optional[Thread] = None - janitor_thread: Optional[Thread] = None - hotword_thread: Optional[Thread] = None - while True: - if hotword_thread is None or not hotword_thread.is_alive(): - keyword_paths = [pvporcupine.KEYWORD_PATHS[x] for x in ["bumblebee"]] - sensitivities = [0.7] * len(keyword_paths) - listener = listen.HotwordListener( - command_queue, - keyword_paths, - sensitivities, - ) - hotword_thread = Thread(target=listener.listen_forever, args=()) - hotword_thread.start() - if changer_thread is None or not changer_thread.is_alive(): - print( - f"MAIN[{datetime_utils.timestamp()}] - (Re?)initializing chooser thread... (wtf?!)" - ) - changer_thread = Thread(target=thread_change_current, args=(command_queue,)) - changer_thread.start() - if renderer_thread is None or not renderer_thread.is_alive(): - print( - f"MAIN[{datetime_utils.timestamp()}] - (Re?)initializing render thread... (wtf?!)" - ) - renderer_thread = Thread(target=thread_invoke_renderers, args=()) - renderer_thread.start() - if janitor_thread is None or not janitor_thread.is_alive(): - print( - f"MAIN[{datetime_utils.timestamp()}] - (Re?)initializing janitor thread... (wtf?!)" - ) - janitor_thread = Thread(target=thread_janitor, args=()) - janitor_thread.start() - time.sleep(60) diff --git a/myq_renderer.py b/myq_renderer.py deleted file mode 100644 index ebfac23..0000000 --- a/myq_renderer.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python3 - -from aiohttp import ClientSession -import asyncio -import datetime -from dateutil.parser import parse -import pymyq # type: ignore -from typing import Dict, Optional - -from pyutils.datetimes import datetime_utils - -import kiosk_constants -import file_writer -import renderer -import kiosk_secrets as secrets - - -class garage_door_renderer(renderer.abstaining_renderer): - def __init__(self, name_to_timeout_dict: Dict[str, int]) -> None: - super().__init__(name_to_timeout_dict) - self.doors: Optional[Dict] = None - self.last_update: Optional[datetime.datetime] = None - - def debug_prefix(self) -> str: - return "myq" - - def periodic_render(self, key: str) -> bool: - if key == "Poll MyQ": - self.last_update = datetime.datetime.now() - return asyncio.run(self.poll_myq()) - elif key == "Update Page": - return self.update_page() - else: - raise Exception("Unknown operaiton") - - async def poll_myq(self) -> bool: - async with ClientSession() as websession: - myq = await pymyq.login( - secrets.myq_username, secrets.myq_password, websession - ) - self.doors = myq.devices - assert(self.doors is not None) - return len(self.doors) > 0 - - def update_page(self) -> bool: - with file_writer.file_writer(kiosk_constants.myq_pagename) as f: - f.write( - f""" -

Garage Door Status

- -
- - -""" - ) - html = self.do_door("Near House") - if html is None: - return False - f.write(html) - - html = self.do_door("Middle Door") - if html is None: - return False - f.write(html) - f.write( - """ - -
""" - ) - return True - - def get_state_icon(self, state: str) -> str: - if state == "open": - return "/kiosk/images/garage_open.png" - elif state == "closed": - return "/kiosk/images/garage_closed.png" - elif state == "opening": - return "/kiosk/images/garage_opening.png" - elif state == "closing": - return "/kiosk/images/garage_closing.png" - else: - return str(state) + ", an unknown state for the door." - - def do_door(self, name: str) -> Optional[str]: - if self.doors is None: - return None - for key in self.doors: - door = self.doors[key] - if door.name == name: - j = self.doors[key].device_json - state = j["state"]["door_state"] - - # "last_update": "2020-07-04T18:11:34.2981419Z" - raw = j["state"]["last_update"] - ts = parse(raw) - tz_info = ts.tzinfo - now = datetime.datetime.now(tz_info) - delta = (now - ts).total_seconds() - now = datetime.datetime.now() - is_night = now.hour <= 7 or now.hour >= 21 - duration = datetime_utils.describe_duration_briefly(int(delta)) - width = 0 - if is_night and door.state == "open": - color = "border-color: #ff0000;" - width = 15 - else: - color = "" - width = 0 - return f""" - -
- {name}
- -
- {state}

- for {duration} -
-""" - return None - - -# Test -#x = garage_door_renderer({"Test": 1}) -#x.periodic_render("Poll MyQ") -#x.periodic_render("Update Page") diff --git a/ratago_renderer.py b/ratago_renderer.py new file mode 100644 index 0000000..4e33ed6 --- /dev/null +++ b/ratago_renderer.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 + +import datetime +import logging +import json +from dateutil.parser import parse +from typing import Dict, Optional + +import requests +from pyutils.datetimes import datetime_utils + +import file_writer +import globals +import kiosk_constants +import kiosk_secrets as secrets +import renderer + + +logger = logging.getLogger(__name__) + + +class garage_door_renderer(renderer.abstaining_renderer): + def __init__(self, name_to_timeout_dict: Dict[str, int]) -> None: + super().__init__(name_to_timeout_dict) + self.last_update: Optional[datetime.datetime] = None + self.doors = { + "cover.ratgdo_middle_door_door": {"state": "unknown"}, + "cover.ratgdo_near_house_door": {"state": "unknown"}, + "cover.ratgdo_shop_door": {"state": "unknown"}, + } + + def debug_prefix(self) -> str: + return "ratago" + + def periodic_render(self, key: str) -> bool: + if key == "Poll Home Assistant": + return self.poll_home_assistant() + elif key == "Update Page": + return self.update_page() + else: + raise Exception("Unknown operaiton") + + def poll_home_assistant(self) -> bool: + key = secrets.HOMEASSISTANT_API_KEY + headers = { + "Authorization": f"Bearer {key}", + "Content-Type": "application/json", + } + for door in self.doors.keys(): + try: + r = requests.get( + f"https://home.acknak.org/api/states/{door}", + headers=headers, + timeout=3.0, + ) + if r.ok: + j = json.loads(r.content.decode()) + logger.debug(j) + self.doors[door] = j + else: + logger.warning("Unable to get state of garage door {door}, using 'unknown'") + except Exception: + logger.exception("Unable to get state of garage door {door}, using 'unknown'") + self.last_update = datetime_utils.now_pacific() + return True + + def update_page(self) -> bool: + with file_writer.file_writer(kiosk_constants.ratago_pagename) as f: + f.write( + f""" +

Garage Door Status

+ +
+ + +""" + ) + + html = self.do_door("cover.ratago_near_house_door") + if html is None: + return False + f.write(html) + + html = self.do_door("cover.ratago_middle_door_door") + if html is None: + return False + f.write(html) + + html = self.do_door("cover.ratago_shop_door") + if html is None: + return False + f.write(html) + f.write( + """ + +
""" + ) + return True + + def get_state_icon(self, state: str) -> str: + if state == "open": + return "/kiosk/images/garage_open.png" + elif state == "closed": + return "/kiosk/images/garage_closed.png" + elif state == "opening": + return "/kiosk/images/garage_opening.png" + elif state == "closing": + return "/kiosk/images/garage_closing.png" + else: + return str(state) + ", an unknown state for the door." + + def do_door(self, name: str) -> Optional[str]: + friendly_door_names = { + "cover.ratgdo_middle_door_door": "Middle Door", + "cover.ratgdo_near_house_door": "Near House Door", + "cover.ratgdo_shop_door": "Workshop Door", + } + if self.doors is None: + return None + + friendly_name = friendly_door_names.get(name, "unknown") + attributes = self.doors.get(name, {"state": "unknown"}) + state = attributes.get("state", "unknown").lower() + since = attributes.get("last_changed", "unknown").lower() + + # "last_update": "2020-07-04T18:11:34.2981419Z" + ts = parse(since) + tz_info = ts.tzinfo + now = datetime.datetime.now(tz_info) + delta = (now - ts).total_seconds() + duration = datetime_utils.describe_duration_briefly(int(delta)) + + now = datetime.datetime.now() + is_night = now.hour <= 7 or now.hour >= 21 + width = 0 + if is_night and state == "open": + color = "border-color: #ff0000;" + width = 15 + globals.put("ratago_triggered", True) + else: + color = "" + width = 0 + globals.put("ratago_triggered", False) + return f""" + +
+ {friendly_name}
+ +
+ {state}

+ for {duration} +
+""" + + +# Test +#x = garage_door_renderer({"Test": 1}) +#x.periodic_render("Poll MyQ") +#x.periodic_render("Update Page") diff --git a/myq_trigger.py b/ratago_trigger.py similarity index 63% rename from myq_trigger.py rename to ratago_trigger.py index db09f33..a0fc310 100644 --- a/myq_trigger.py +++ b/ratago_trigger.py @@ -6,10 +6,10 @@ import trigger from typing import List, Optional, Tuple -class myq_trigger(trigger.trigger): +class ratago_trigger(trigger.trigger): def get_triggered_page_list(self) -> Optional[List[Tuple[str, int]]]: - if globals.get("myq_triggered"): + if globals.get("ratago_triggered", False): print("****** MyQ garage door is open page trigger ******") - return [(kiosk_constants.myq_pagename, trigger.trigger.PRIORITY_HIGH)] + return [(kiosk_constants.ratago_pagename, trigger.trigger.PRIORITY_HIGH)] else: return None diff --git a/renderer_catalog.py b/renderer_catalog.py index 81373e0..7cd2168 100644 --- a/renderer_catalog.py +++ b/renderer_catalog.py @@ -12,7 +12,7 @@ import google_news_rss_renderer import health_renderer import local_photos_mirror_renderer import mynorthwest_rss_renderer -import myq_renderer +import ratago_renderer import reddit_renderer import seattletimes_rss_renderer import stevens_renderer @@ -42,8 +42,8 @@ __registry = [ # stranger_renderer.stranger_events_renderer( # {"Fetch Events": (hours * 12), "Shuffle Events": (always)} # ), - myq_renderer.garage_door_renderer( - {"Poll MyQ": (minutes * 5), "Update Page": (always)} + ratago_renderer.garage_door_renderer( + {"Poll Home Assistant": (always), "Update Page": (always)} ), bellevue_city_calendar_renderer.bellevue_city_calendar_renderer( { diff --git a/trigger_catalog.py b/trigger_catalog.py index 534e215..cc4fc1f 100644 --- a/trigger_catalog.py +++ b/trigger_catalog.py @@ -2,12 +2,12 @@ import camera_trigger import gcal_trigger -import myq_trigger +import ratago_trigger import recipe_renderer_and_trigger __registry = [ camera_trigger.any_camera_trigger(), - myq_trigger.myq_trigger(), + ratago_trigger.ratago_trigger(), gcal_trigger.gcal_trigger(), recipe_renderer_and_trigger.RecipeTrigger(), ] -- 2.44.0