From b9f6beb178dab3846e979a6ade4770c3abd2d30c Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Mon, 18 Sep 2023 23:29:34 -0700 Subject: [PATCH 01/16] Fuck twitter. --- renderer_catalog.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/renderer_catalog.py b/renderer_catalog.py index 1f38b4f..b5fdebd 100644 --- a/renderer_catalog.py +++ b/renderer_catalog.py @@ -18,7 +18,7 @@ import stevens_renderer import kiosk_secrets as secrets import recipe_renderer_and_trigger import stock_renderer -import twitter_renderer +#import twitter_renderer import urbanist_renderer import weather_renderer import wsj_rss_renderer @@ -159,9 +159,9 @@ __registry = [ reddit_renderer.lifeprotips_reddit_renderer( {"Scrape": (hours * 6), "Shuffle": (always)} ), - twitter_renderer.twitter_renderer( - {"Fetch Tweets": (minutes * 15), "Shuffle Tweets": (always)} - ), +# twitter_renderer.twitter_renderer( +# {"Fetch Tweets": (minutes * 15), "Shuffle Tweets": (always)} +# ), recipe_renderer_and_trigger.RecipeRenderer( "/home/pi/.recipe_url", {"Maybe Render Recipe Page": (always)} ), -- 2.45.2 From 7b613cf08ba9c2ba716af397401c0102d33b9588 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Fri, 15 Dec 2023 18:02:29 -0800 Subject: [PATCH 02/16] Profanity filter, please. --- generic_news_rss_renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generic_news_rss_renderer.py b/generic_news_rss_renderer.py index 3d8956b..2acf193 100644 --- a/generic_news_rss_renderer.py +++ b/generic_news_rss_renderer.py @@ -55,7 +55,7 @@ class generic_news_rss_renderer(renderer.abstaining_renderer): pass def should_profanity_filter(self) -> bool: - return False + return True def find_title(self, item: ET.Element) -> Optional[str]: return item.findtext("title") -- 2.45.2 From c377a67d1c1babf15404917e3b2e40005fae1613 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Fri, 15 Dec 2023 18:05:35 -0800 Subject: [PATCH 03/16] All you fuckers. --- renderer_catalog.py | 58 +++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/renderer_catalog.py b/renderer_catalog.py index b5fdebd..81373e0 100644 --- a/renderer_catalog.py +++ b/renderer_catalog.py @@ -7,7 +7,8 @@ import cnn_rss_renderer import gdata_oauth import gcal_renderer import google_news_rss_renderer -import gkeep_renderer + +# import gkeep_renderer import health_renderer import local_photos_mirror_renderer import mynorthwest_rss_renderer @@ -17,8 +18,9 @@ import seattletimes_rss_renderer import stevens_renderer import kiosk_secrets as secrets import recipe_renderer_and_trigger -import stock_renderer -#import twitter_renderer + +# import stock_renderer +# import twitter_renderer import urbanist_renderer import weather_renderer import wsj_rss_renderer @@ -104,27 +106,27 @@ __registry = [ health_renderer.periodic_health_renderer( {"Update Perioidic Job Health": (seconds * 45)} ), - stock_renderer.stock_quote_renderer( - {"Update Prices": (minutes * 10)}, - [ - "MSFT", - "SPY", - "BTC-USD", - "IEMG", - "ABHYX", - "SPAB", - "SPHD", - "SCHD", - "BCD", - "GC=F", - "VYM", - "VYMI", - "VDC", - "VNQ", - "VNQI", - ], - {"BTC-USD": "BTC", "GC=F": "GOLD"}, - ), + # stock_renderer.stock_quote_renderer( + # {"Update Prices": (minutes * 10)}, + # [ + # "MSFT", + # "SPY", + # "BTC-USD", + # "IEMG", + # "ABHYX", + # "SPAB", + # "SPHD", + # "SCHD", + # "BCD", + # "GC=F", + # "VYM", + # "VYMI", + # "VDC", + # "VNQ", + # "VNQI", + # ], + # {"BTC-USD": "BTC", "GC=F": "GOLD"}, + # ), seattletimes_rss_renderer.seattletimes_rss_renderer( {"Fetch News": (hours * 4), "Shuffle News": (always)}, "www.seattletimes.com", @@ -141,7 +143,7 @@ __registry = [ local_photos_mirror_renderer.local_photos_mirror_renderer( {"Index Photos": (hours * 24), "Choose Photo": (always)} ), - gkeep_renderer.gkeep_renderer({"Update": (minutes * 10)}), + # gkeep_renderer.gkeep_renderer({"Update": (minutes * 10)}), gcal_renderer.gcal_renderer( {"Render Upcoming Events": (hours * 2), "Look For Triggered Events": (always)}, oauth, @@ -159,9 +161,9 @@ __registry = [ reddit_renderer.lifeprotips_reddit_renderer( {"Scrape": (hours * 6), "Shuffle": (always)} ), -# twitter_renderer.twitter_renderer( -# {"Fetch Tweets": (minutes * 15), "Shuffle Tweets": (always)} -# ), + # twitter_renderer.twitter_renderer( + # {"Fetch Tweets": (minutes * 15), "Shuffle Tweets": (always)} + # ), recipe_renderer_and_trigger.RecipeRenderer( "/home/pi/.recipe_url", {"Maybe Render Recipe Page": (always)} ), -- 2.45.2 From 86d6fe82b8475837f9e8e46254f2bf8780fd863e Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sun, 7 Jan 2024 10:34:52 -0800 Subject: [PATCH 04/16] Changes to listen. --- listen.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/listen.py b/listen.py index 61a82cf..4d0ebee 100755 --- a/listen.py +++ b/listen.py @@ -19,12 +19,12 @@ class HotwordListener(object): keyword_paths, sensitivities, input_device_index=None, - library_path=pvporcupine.LIBRARY_PATH, - model_path=pvporcupine.MODEL_PATH, +# library_path=pvporcupine.LIBRARY_PATH, +# model_path=pvporcupine.MODEL_PATH, ): self._queue = command_queue - self._library_path = library_path - self._model_path = model_path +# self._library_path = library_path +# self._model_path = model_path self._keyword_paths = keyword_paths self._sensitivities = sensitivities self._input_device_index = input_device_index @@ -40,8 +40,6 @@ class HotwordListener(object): audio_stream = None try: porcupine = pvporcupine.create( - library_path=self._library_path, - model_path=self._model_path, keyword_paths=self._keyword_paths, sensitivities=self._sensitivities, ) -- 2.45.2 From 429de391ecf48e70b6b81fa7b239c5720c7da371 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sun, 7 Jan 2024 11:12:55 -0800 Subject: [PATCH 05/16] 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.45.2 From b821981cf3468e2e8d6c9c51c7ef40a46b0f3fc5 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sun, 7 Jan 2024 11:15:31 -0800 Subject: [PATCH 06/16] This globals thing is a POS --- ratago_trigger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ratago_trigger.py b/ratago_trigger.py index a0fc310..a54573c 100644 --- a/ratago_trigger.py +++ b/ratago_trigger.py @@ -8,7 +8,7 @@ from typing import List, Optional, Tuple class ratago_trigger(trigger.trigger): def get_triggered_page_list(self) -> Optional[List[Tuple[str, int]]]: - if globals.get("ratago_triggered", False): + if globals.get("ratago_triggered"): print("****** MyQ garage door is open page trigger ******") return [(kiosk_constants.ratago_pagename, trigger.trigger.PRIORITY_HIGH)] else: -- 2.45.2 From 31f8cd8af0c0ece5f0c2ab77bcd4efeb746bb970 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sun, 7 Jan 2024 11:44:27 -0800 Subject: [PATCH 07/16] Typo --- ratago_renderer.py | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/ratago_renderer.py b/ratago_renderer.py index 4e33ed6..a0a296a 100644 --- a/ratago_renderer.py +++ b/ratago_renderer.py @@ -4,7 +4,7 @@ import datetime import logging import json from dateutil.parser import parse -from typing import Dict, Optional +from typing import Any, Dict, Optional import requests from pyutils.datetimes import datetime_utils @@ -19,11 +19,11 @@ import renderer logger = logging.getLogger(__name__) -class garage_door_renderer(renderer.abstaining_renderer): +class ratago_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 = { + self.doors: Dict[str, Dict[str, Any]] = { "cover.ratgdo_middle_door_door": {"state": "unknown"}, "cover.ratgdo_near_house_door": {"state": "unknown"}, "cover.ratgdo_shop_door": {"state": "unknown"}, @@ -41,7 +41,7 @@ class garage_door_renderer(renderer.abstaining_renderer): raise Exception("Unknown operaiton") def poll_home_assistant(self) -> bool: - key = secrets.HOMEASSISTANT_API_KEY + key = secrets.homeassistant_api_key headers = { "Authorization": f"Bearer {key}", "Content-Type": "application/json", @@ -55,7 +55,6 @@ class garage_door_renderer(renderer.abstaining_renderer): ) 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'") @@ -75,18 +74,17 @@ class garage_door_renderer(renderer.abstaining_renderer): """ ) - - html = self.do_door("cover.ratago_near_house_door") + html = self.do_door('cover.ratgdo_near_house_door') if html is None: return False f.write(html) - html = self.do_door("cover.ratago_middle_door_door") + html = self.do_door('cover.ratgdo_middle_door_door') if html is None: return False f.write(html) - html = self.do_door("cover.ratago_shop_door") + html = self.do_door('cover.ratgdo_shop_door') if html is None: return False f.write(html) @@ -115,20 +113,20 @@ class garage_door_renderer(renderer.abstaining_renderer): "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"}) + attributes = self.doors[name] #.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)) + if since != "unknown": + 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)) + else: + duration = "unknown" now = datetime.datetime.now() is_night = now.hour <= 7 or now.hour >= 21 @@ -156,6 +154,6 @@ class garage_door_renderer(renderer.abstaining_renderer): # Test -#x = garage_door_renderer({"Test": 1}) -#x.periodic_render("Poll MyQ") +#x = ratago_renderer({"Test": 1}) +#x.periodic_render("Poll Home Assistant") #x.periodic_render("Update Page") -- 2.45.2 From e4f1ef0564515ec672b3e33d6ec97a4d8b10017d Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sun, 7 Jan 2024 12:02:35 -0800 Subject: [PATCH 08/16] Typo --- renderer_catalog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer_catalog.py b/renderer_catalog.py index 7cd2168..14ba52e 100644 --- a/renderer_catalog.py +++ b/renderer_catalog.py @@ -42,7 +42,7 @@ __registry = [ # stranger_renderer.stranger_events_renderer( # {"Fetch Events": (hours * 12), "Shuffle Events": (always)} # ), - ratago_renderer.garage_door_renderer( + ratago_renderer.ratago_renderer( {"Poll Home Assistant": (always), "Update Page": (always)} ), bellevue_city_calendar_renderer.bellevue_city_calendar_renderer( -- 2.45.2 From d540ccc1458226470aeb35709326d99545848b8c Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sun, 7 Jan 2024 12:54:01 -0800 Subject: [PATCH 09/16] Fix wakeword. --- kiosk_secrets.py | 4 ++++ listen.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/kiosk_secrets.py b/kiosk_secrets.py index 8fbb4c7..e43c354 100644 --- a/kiosk_secrets.py +++ b/kiosk_secrets.py @@ -18,3 +18,7 @@ twitter_consumer_key = "" twitter_consumer_secret = "" twitter_access_token = "" twitter_access_token_secret = "" + +# wakeword detection +pvporcupine_key = "" + diff --git a/listen.py b/listen.py index 4d0ebee..62012b5 100755 --- a/listen.py +++ b/listen.py @@ -9,6 +9,9 @@ import pyaudio import speech_recognition as sr from pyutils import logging_utils +import kiosk_secrets as secrets + + logger = logging.getLogger(__name__) @@ -42,6 +45,7 @@ class HotwordListener(object): porcupine = pvporcupine.create( keyword_paths=self._keyword_paths, sensitivities=self._sensitivities, + access_key=secrets.pvporcupine_key, ) recognizer = sr.Recognizer() pa = pyaudio.PyAudio() -- 2.45.2 From 6fd89b4dad0f69270773edaa4d5ab1096bc2b02f Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sun, 24 Mar 2024 12:13:47 -0700 Subject: [PATCH 10/16] Add invw.org --- generic_news_rss_renderer.py | 4 ++- invw_rss_renderer.py | 56 ++++++++++++++++++++++++++++++++++++ kiosk_secrets.py | 3 ++ listen.py | 3 ++ renderer_catalog.py | 7 +++++ 5 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 invw_rss_renderer.py diff --git a/generic_news_rss_renderer.py b/generic_news_rss_renderer.py index 2acf193..f1261cb 100644 --- a/generic_news_rss_renderer.py +++ b/generic_news_rss_renderer.py @@ -236,8 +236,10 @@ class generic_news_rss_renderer(renderer.abstaining_renderer): if response.status != 200: logger.error( - f"Unexpected status {response.status} while fetching {url}; giving up." + f"Unexpected status {response.status} while fetching {url}: {response.reason}; giving up." ) + print(dir(response)) + print(response.headers) return False raw = response.read() diff --git a/invw_rss_renderer.py b/invw_rss_renderer.py new file mode 100644 index 0000000..0cbbeaf --- /dev/null +++ b/invw_rss_renderer.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 + +import xml +from typing import Dict, List, Optional + +import generic_news_rss_renderer as gnrssr + + +class invw_rss_renderer(gnrssr.generic_news_rss_renderer): + def __init__( + self, + name_to_timeout_dict: Dict[str, int], + feed_site: str, + feed_uris: List[str], + page_title: str, + ): + super().__init__( + name_to_timeout_dict, feed_site, feed_uris, page_title + ) + self.debug = True + + def debug_prefix(self) -> str: + return f"invw({self.page_title})" + + def get_headlines_page_prefix(self) -> str: + return f"invw-{self.page_title}" + + def get_details_page_prefix(self) -> str: + return f"invw-details-{self.page_title}" + + def find_image(self, item: xml.etree.ElementTree.Element) -> Optional[str]: + return None + + def should_use_https(self) -> bool: + return True + + def item_is_interesting_for_headlines( + self, title: str, description: str, item: xml.etree.ElementTree.Element + ) -> bool: + return True + + def item_is_interesting_for_article( + self, title: str, description: str, item: xml.etree.ElementTree.Element + ) -> bool: + return True + +# Test +#x = invw_rss_renderer( +# {"Fetch News" : 1, +# "Shuffle News" : 1}, +# "www.invw.org", +# [ "/feed/" ], +# "Test" ) +#if x.fetch_news() == 0: +# print("Error fetching news, no items fetched.") +#x.shuffle_news() diff --git a/kiosk_secrets.py b/kiosk_secrets.py index 8fbb4c7..87f0643 100644 --- a/kiosk_secrets.py +++ b/kiosk_secrets.py @@ -18,3 +18,6 @@ twitter_consumer_key = "" twitter_consumer_secret = "" twitter_access_token = "" twitter_access_token_secret = "" + +# pvporcupine access key +pvporcupine_access_key="" diff --git a/listen.py b/listen.py index 4d0ebee..1501451 100755 --- a/listen.py +++ b/listen.py @@ -9,6 +9,8 @@ import pyaudio import speech_recognition as sr from pyutils import logging_utils +from kiosk_secrets import pvporcupine_key + logger = logging.getLogger(__name__) @@ -40,6 +42,7 @@ class HotwordListener(object): audio_stream = None try: porcupine = pvporcupine.create( + access_key=pvporcupine_key, keyword_paths=self._keyword_paths, sensitivities=self._sensitivities, ) diff --git a/renderer_catalog.py b/renderer_catalog.py index 14ba52e..bd5ec10 100644 --- a/renderer_catalog.py +++ b/renderer_catalog.py @@ -24,6 +24,7 @@ import recipe_renderer_and_trigger import urbanist_renderer import weather_renderer import wsj_rss_renderer +import invw_rss_renderer seconds = 1 @@ -96,6 +97,12 @@ __registry = [ ["/rss/RSSMarketsMain.xml", "/rss/WSJcomUSBusiness.xml"], "WSJBusiness", ), + invw_rss_renderer.invw_rss_renderer( + {"Fetch News": (hours * 4), "Shuffle News": (always)}, + "www.invw.org", + ["/feed/"], + "invw.org", + ), stevens_renderer.stevens_renderer({"Unused": (minutes * 30)}), google_news_rss_renderer.google_news_rss_renderer( {"Fetch News": (minutes * 30), "Shuffle News": (always)}, -- 2.45.2 From 5a39376421fcdf7e7ea040685db8095b4e2092d8 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Mon, 25 Mar 2024 11:50:34 -0700 Subject: [PATCH 11/16] Update cabin camera URL. --- pages/wenatchee-cams_3_none.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/wenatchee-cams_3_none.html b/pages/wenatchee-cams_3_none.html index 19ba826..5f47da8 100644 --- a/pages/wenatchee-cams_3_none.html +++ b/pages/wenatchee-cams_3_none.html @@ -28,7 +28,7 @@ border="0" height="392" width="512" - src="http://10.0.0.233:8080/rirF9tp7MdjNFl2atzoEvU2BFDyWin/jpeg/WHyDnbh4sv/c3XfOJfFDn/s.jpg" + src="http://10.0.0.233:8080/rirF9tp7MdjNFl2atzoEvU2BFDyWin/jpeg/WHyDnbh4sv/GhZkOhWlTi/s.jpg" \>
-- 2.45.2 From 8510d0d1979e5171a35d5249c80ba580f022d54b Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sun, 31 Mar 2024 17:47:49 -0700 Subject: [PATCH 12/16] InvestigateWest! --- invw_rss_renderer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invw_rss_renderer.py b/invw_rss_renderer.py index 0cbbeaf..ea610f3 100644 --- a/invw_rss_renderer.py +++ b/invw_rss_renderer.py @@ -37,12 +37,12 @@ class invw_rss_renderer(gnrssr.generic_news_rss_renderer): def item_is_interesting_for_headlines( self, title: str, description: str, item: xml.etree.ElementTree.Element ) -> bool: - return True + return title != "InvestigateWest" def item_is_interesting_for_article( self, title: str, description: str, item: xml.etree.ElementTree.Element ) -> bool: - return True + return title != "InvestigateWest" and description != "InvestigateWest" # Test #x = invw_rss_renderer( -- 2.45.2 From ea1ee5f817c01c3736a64d73d496cf35cbd383e5 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Mon, 29 Apr 2024 14:12:45 -0700 Subject: [PATCH 13/16] Testosterone and sensitivity. --- bellevue_reporter_rss_renderer.py | 1 + kiosk.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/bellevue_reporter_rss_renderer.py b/bellevue_reporter_rss_renderer.py index fec70e0..862e171 100644 --- a/bellevue_reporter_rss_renderer.py +++ b/bellevue_reporter_rss_renderer.py @@ -64,6 +64,7 @@ class bellevue_reporter_rss_renderer(gnrss.generic_news_rss_renderer): "marketplace" in description or "national-marketplace" in description or re.search("[Ww]eed", title) is not None + or re.search("[Tt]estosterone", title) is not None or re.search("[Cc]annabis", title) is not None or re.search("[Cc]annabis", description) is not None or "THC" in title diff --git a/kiosk.py b/kiosk.py index 14dae57..fe9e174 100755 --- a/kiosk.py +++ b/kiosk.py @@ -725,7 +725,13 @@ def main() -> None: "The hotword detector thread seems to have died; restarting it and hoping for the best." ) keyword_paths = [pvporcupine.KEYWORD_PATHS[x] for x in ["bumblebee"]] - sensitivities = [0.7] * len(keyword_paths) + + # Sensitivity is the parameter that enables trading + # miss rate for the false alarm rate. It is a floating + # point number within [0, 1]. A higher sensitivity + # reduces the miss rate at the cost of increased false + # alarm rate. + sensitivities = [0.4] * len(keyword_paths) listener = listen.HotwordListener( command_queue, keyword_paths, -- 2.45.2 From 4ef7b7a26a178793b135fcaac648260ed1c3a270 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sat, 15 Jun 2024 09:45:43 -0700 Subject: [PATCH 14/16] Remove bellevue reporter, it's all spam. --- renderer_catalog.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/renderer_catalog.py b/renderer_catalog.py index 14ba52e..f14cb9c 100644 --- a/renderer_catalog.py +++ b/renderer_catalog.py @@ -54,12 +54,12 @@ __registry = [ ["/calendar/events.xml"], "Bellevue City Calendar", ), - bellevue_reporter_rss_renderer.bellevue_reporter_rss_renderer( - {"Fetch News": (hours * 1), "Shuffle News": (always)}, - "www.bellevuereporter.com", - ["/feed/"], - "Bellevue Reporter", - ), +# bellevue_reporter_rss_renderer.bellevue_reporter_rss_renderer( +# {"Fetch News": (hours * 1), "Shuffle News": (always)}, +# "www.bellevuereporter.com", +# ["/feed/"], +# "Bellevue Reporter", +# ), urbanist_renderer.urbanist_renderer( {"Fetch News": (hours * 2), "Shuffle News": (always)}, "www.theurbanist.org", -- 2.45.2 From 05b56fa9d94c3dac31c08b28b8e6adfe7d8d2a4a Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sat, 15 Jun 2024 10:15:00 -0700 Subject: [PATCH 15/16] Add downtown bellevue feed. --- downtown_bellevue_rss_renderer.py | 45 +++++++++++++++++++++++++++++++ grab_bag.py | 2 +- renderer_catalog.py | 14 +++++----- 3 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 downtown_bellevue_rss_renderer.py diff --git a/downtown_bellevue_rss_renderer.py b/downtown_bellevue_rss_renderer.py new file mode 100644 index 0000000..804d97a --- /dev/null +++ b/downtown_bellevue_rss_renderer.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +import logging +from typing import List, Dict + +import generic_news_rss_renderer as gnrss + + +logger = logging.getLogger(__name__) + + +class downtown_bellevue_rss_renderer(gnrss.generic_news_rss_renderer): + """Read the Bellevue Reporter's RSS feed.""" + + def __init__( + self, + name_to_timeout_dict: Dict[str, int], + feed_site: str, + feed_uris: List[str], + page_title: str, + ): + super().__init__(name_to_timeout_dict, feed_site, feed_uris, page_title) + + def get_headlines_page_prefix(self) -> str: + return "downtown-bellevue" + + def get_details_page_prefix(self) -> str: + return "downtown-bellevue-details" + + def should_use_https(self) -> bool: + return True + + +# Test +x = downtown_bellevue_rss_renderer( + {"Fetch News" : 1, + "Shuffle News" : 1}, + "downtownbellevue.com", + [ "/feed/" ], + "Test" +) +if x.fetch_news() == 0: + print("Error fetching news, no items fetched.") +else: + x.shuffle_news() diff --git a/grab_bag.py b/grab_bag.py index b1da60c..10272da 100644 --- a/grab_bag.py +++ b/grab_bag.py @@ -26,7 +26,7 @@ class grab_bag(object): def subset(self, count: int) -> Optional[List[str]]: if len(self.contents) < count: return None - return random.sample(self.contents, count) + return random.sample(list(self.contents), count) def size(self) -> int: return len(self.contents) diff --git a/renderer_catalog.py b/renderer_catalog.py index a363954..dfd4c99 100644 --- a/renderer_catalog.py +++ b/renderer_catalog.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import bellevue_city_calendar_renderer -import bellevue_reporter_rss_renderer +import downtown_bellevue_rss_renderer import kiosk_constants import cnn_rss_renderer import gdata_oauth @@ -55,12 +55,12 @@ __registry = [ ["/calendar/events.xml"], "Bellevue City Calendar", ), -# bellevue_reporter_rss_renderer.bellevue_reporter_rss_renderer( -# {"Fetch News": (hours * 1), "Shuffle News": (always)}, -# "www.bellevuereporter.com", -# ["/feed/"], -# "Bellevue Reporter", -# ), + downtown_bellevue_rss_renderer.downtown_bellevue_rss_renderer( + {"Fetch News": (hours * 1), "Shuffle News": (always)}, + "downtownbellevue.com" + ["/feed/"], + "Downtown Bellevue", + ), urbanist_renderer.urbanist_renderer( {"Fetch News": (hours * 2), "Shuffle News": (always)}, "www.theurbanist.org", -- 2.45.2 From 4ee891c0fc29a002291c7c88f8d8fb5d14c28f92 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sat, 15 Jun 2024 10:15:58 -0700 Subject: [PATCH 16/16] Oops --- renderer_catalog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer_catalog.py b/renderer_catalog.py index dfd4c99..5aaca1d 100644 --- a/renderer_catalog.py +++ b/renderer_catalog.py @@ -57,7 +57,7 @@ __registry = [ ), downtown_bellevue_rss_renderer.downtown_bellevue_rss_renderer( {"Fetch News": (hours * 1), "Shuffle News": (always)}, - "downtownbellevue.com" + "downtownbellevue.com", ["/feed/"], "Downtown Bellevue", ), -- 2.45.2