From 86f7e14f34b43ed8eb8cf5eaf113a4ecca976327 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sat, 19 Mar 2022 22:37:32 -0700 Subject: [PATCH] Various changes including adding new stevens renderer. --- chooser.py | 8 +-- file_writer.py | 6 +- gcal_renderer.py | 18 ++--- gkeep_renderer.py | 4 +- kiosk.py | 28 ++++---- kiosk_constants.py | 19 ++++++ myq_renderer.py | 4 +- myq_trigger.py | 4 +- renderer_catalog.py | 9 ++- stevens_renderer.py | 160 ++++++++++++++++++++++++++++++-------------- 10 files changed, 170 insertions(+), 90 deletions(-) create mode 100644 kiosk_constants.py diff --git a/chooser.py b/chooser.py index 3514c97..813eaff 100644 --- a/chooser.py +++ b/chooser.py @@ -10,7 +10,7 @@ from typing import Any, Callable, List, Optional, Set, Tuple import datetime_utils -import constants +import kiosk_constants import trigger @@ -29,8 +29,8 @@ class chooser(ABC): filenames = [] pages = [ f - for f in os.listdir(constants.pages_dir) - if os.path.isfile(os.path.join(constants.pages_dir, f)) + for f in os.listdir(kiosk_constants.pages_dir) + if os.path.isfile(os.path.join(kiosk_constants.pages_dir, f)) ] for page in pages: result = re.match(valid_filename, page) @@ -38,7 +38,7 @@ class chooser(ABC): if result.group(3) != "none": freshness_requirement = int(result.group(3)) last_modified = int( - os.path.getmtime(os.path.join(constants.pages_dir, page)) + os.path.getmtime(os.path.join(kiosk_constants.pages_dir, page)) ) age = now - last_modified if age > freshness_requirement: diff --git a/file_writer.py b/file_writer.py index fced449..beac7bb 100644 --- a/file_writer.py +++ b/file_writer.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 -import constants import os from uuid import uuid4 +import kiosk_constants class file_writer: """Helper context to write a pages file.""" def __init__(self, filename: str, *, transformations=[]): temp = "temp-" + str(uuid4()) - self.temp_filename = os.path.join(constants.pages_dir, temp) - self.full_filename = os.path.join(constants.pages_dir, filename) + self.temp_filename = os.path.join(kiosk_constants.pages_dir, temp) + self.full_filename = os.path.join(kiosk_constants.pages_dir, filename) self.xforms = [file_writer.remove_tricky_unicode] self.xforms.extend(transformations) self.f = None diff --git a/gcal_renderer.py b/gcal_renderer.py index edb9546..1e026cd 100644 --- a/gcal_renderer.py +++ b/gcal_renderer.py @@ -13,7 +13,7 @@ from dateutil.parser import parse import gdata_oauth import pytz -import constants +import kiosk_constants import file_writer import globals import renderer @@ -282,9 +282,9 @@ f""" x = int(delta.total_seconds()) if x > 0: identifier = "id%d" % count - days = divmod(x, constants.seconds_per_day) - hours = divmod(days[1], constants.seconds_per_hour) - minutes = divmod(hours[1], constants.seconds_per_minute) + days = divmod(x, kiosk_constants.seconds_per_day) + hours = divmod(days[1], kiosk_constants.seconds_per_hour) + minutes = divmod(hours[1], kiosk_constants.seconds_per_minute) g.write( f'
  • %d days, %02d:%02d until %s
  • \n' % ( @@ -349,7 +349,7 @@ var fn = setInterval(function() { return False def look_for_triggered_events(self) -> bool: - with file_writer.file_writer(constants.gcal_imminent_pagename) as f: + with file_writer.file_writer(kiosk_constants.gcal_imminent_pagename) as f: f.write("

    Imminent Upcoming Calendar Events:

    \n
    \n") f.write("
    \n") now = datetime.datetime.now(pytz.timezone("US/Pacific")) @@ -360,10 +360,10 @@ var fn = setInterval(function() { continue delta = eventstamp - now x = int(delta.total_seconds()) - if x > -120 and x < 4 * constants.seconds_per_minute: - days = divmod(x, constants.seconds_per_day) - hours = divmod(days[1], constants.seconds_per_hour) - minutes = divmod(hours[1], constants.seconds_per_minute) + if x > -120 and x < 4 * kiosk_constants.seconds_per_minute: + days = divmod(x, kiosk_constants.seconds_per_day) + hours = divmod(days[1], kiosk_constants.seconds_per_hour) + minutes = divmod(hours[1], kiosk_constants.seconds_per_minute) eventstamp = event.start_time name = event.friendly_name() calendar = event.calendar diff --git a/gkeep_renderer.py b/gkeep_renderer.py index d7765c8..f910e30 100644 --- a/gkeep_renderer.py +++ b/gkeep_renderer.py @@ -7,7 +7,7 @@ from typing import Dict import gkeepapi # type: ignore -import constants +import kiosk_constants import file_writer import renderer import kiosk_secrets as secrets @@ -132,7 +132,7 @@ class gkeep_renderer(renderer.abstaining_renderer): f.write("") else: logger.debug(f"Note is empty, deleting {filename}.") - _ = os.path.join(constants.pages_dir, filename) + _ = os.path.join(kiosk_constants.pages_dir, filename) try: os.remove(_) except: diff --git a/kiosk.py b/kiosk.py index b1bdb1c..01b7949 100755 --- a/kiosk.py +++ b/kiosk.py @@ -24,7 +24,7 @@ import config import datetime_utils import file_utils -import constants +import kiosk_constants import file_writer import renderer_catalog import chooser @@ -142,9 +142,9 @@ def process_command(command: str, page_history: List[str], page_chooser) -> str: break elif 'internal' in command: if 'render' in command: - page = constants.render_stats_pagename + page = kiosk_constants.render_stats_pagename else: - page = constants.render_stats_pagename + page = kiosk_constants.render_stats_pagename elif 'weather' in command: if 'telma' in command or 'cabin' in command: page = 'weather-telma_3_10800.html' @@ -222,8 +222,8 @@ def thread_change_current(command_queue: Queue) -> None: page_chooser = chooser.weighted_random_chooser_with_triggers( trigger_catalog.get_triggers(), [filter_news_during_dinnertime] ) - current_file = os.path.join(constants.pages_dir, "current.shtml") - emergency_file = os.path.join(constants.pages_dir, "reload_immediately.html") + current_file = os.path.join(kiosk_constants.pages_dir, "current.shtml") + emergency_file = os.path.join(kiosk_constants.pages_dir, "reload_immediately.html") # Main chooser loop while True: @@ -255,7 +255,7 @@ def thread_change_current(command_queue: Queue) -> None: if triggered: if page != page_history[0] or (swap_page_target - now) < 10.0: logger.info(f'chooser: An emergency page reload to {page} is needed at this time.') - swap_page_target = now + constants.emergency_refresh_period_sec + swap_page_target = now + kiosk_constants.emergency_refresh_period_sec # Set current.shtml to the right page. try: @@ -263,7 +263,7 @@ def thread_change_current(command_queue: Queue) -> None: emit( f, page, - override_refresh_sec = constants.emergency_refresh_period_sec, + override_refresh_sec = kiosk_constants.emergency_refresh_period_sec, command = command ) logger.debug(f'chooser: Wrote {current_file}.') @@ -288,7 +288,7 @@ def thread_change_current(command_queue: Queue) -> None: logger.info( f'chooser: Nominal choice of {page} as the next to show.' ) - swap_page_target = now + constants.refresh_period_sec + swap_page_target = now + kiosk_constants.refresh_period_sec try: with open(current_file, "w") as f: emit(f, page) @@ -346,9 +346,9 @@ def emit_wrapped(f, return float(override_refresh_sec * 1000.0) now = datetime.now(tz=pytz.timezone("US/Pacific")) if now.hour < 6: - return float(constants.refresh_period_night_sec * 1000.0) + return float(kiosk_constants.refresh_period_night_sec * 1000.0) else: - return float(constants.refresh_period_sec * 1000.0) + return float(kiosk_constants.refresh_period_sec * 1000.0) age = file_utils.describe_file_ctime(f"pages/{filename}") bgcolor = pick_background_color() @@ -513,7 +513,7 @@ def emit_wrapped(f, setInterval(check_reload, 500); -""" % constants.root_url) +""" % kiosk_constants.root_url) f.write(f'') f.write( """ @@ -573,7 +573,7 @@ def renderer_update_internal_stats_page( logger.info( 'renderer: Updating internal render statistics page.' ) - with file_writer.file_writer(constants.render_stats_pagename) as f: + with file_writer.file_writer(kiosk_constants.render_stats_pagename) as f: f.write( '''
    @@ -669,7 +669,7 @@ f''' # Update a page about internal stats of renderers. renderer_update_internal_stats_page(last_render, render_counts, render_times) logger.info('renderer: having a little nap...') - time.sleep(constants.render_period_sec) + time.sleep(kiosk_constants.render_period_sec) @bootstrap.initialize @@ -729,7 +729,7 @@ def main() -> None: # Have a little break and then check to make sure all threads are still alive. logger.debug('watchdog: having a little nap.') - time.sleep(constants.check_threads_period_sec) + time.sleep(kiosk_constants.check_threads_period_sec) if __name__ == "__main__": diff --git a/kiosk_constants.py b/kiosk_constants.py new file mode 100644 index 0000000..ce04f7e --- /dev/null +++ b/kiosk_constants.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +hostname = 'kiosk.house' +pages_dir = '/var/www/html/kiosk' +root_url = f'http://{hostname}/kiosk' + +refresh_period_sec = 22.0 +emergency_refresh_period_sec = 45.0 +refresh_period_night_sec = 600.0 +render_period_sec = 30.0 +check_threads_period_sec = 60.0 + +seconds_per_minute = 60 +seconds_per_hour = seconds_per_minute * 60 +seconds_per_day = seconds_per_hour * 24 + +myq_pagename = "myq_4_300.html" +render_stats_pagename = 'internal/render-stats_1_1000.html' +gcal_imminent_pagename = "hidden/gcal-imminent_0_none.html" diff --git a/myq_renderer.py b/myq_renderer.py index 0377963..2379a70 100644 --- a/myq_renderer.py +++ b/myq_renderer.py @@ -9,7 +9,7 @@ from typing import Dict, Optional import datetime_utils -import constants +import kiosk_constants import file_writer import renderer import kiosk_secrets as secrets @@ -43,7 +43,7 @@ class garage_door_renderer(renderer.abstaining_renderer): return len(self.doors) > 0 def update_page(self) -> bool: - with file_writer.file_writer(constants.myq_pagename) as f: + with file_writer.file_writer(kiosk_constants.myq_pagename) as f: f.write( f"""

    Garage Door Status

    diff --git a/myq_trigger.py b/myq_trigger.py index fd25669..db09f33 100644 --- a/myq_trigger.py +++ b/myq_trigger.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import constants +import kiosk_constants import globals import trigger from typing import List, Optional, Tuple @@ -10,6 +10,6 @@ class myq_trigger(trigger.trigger): def get_triggered_page_list(self) -> Optional[List[Tuple[str, int]]]: if globals.get("myq_triggered"): print("****** MyQ garage door is open page trigger ******") - return [(constants.myq_pagename, trigger.trigger.PRIORITY_HIGH)] + return [(kiosk_constants.myq_pagename, trigger.trigger.PRIORITY_HIGH)] else: return None diff --git a/renderer_catalog.py b/renderer_catalog.py index 00a5834..c62b926 100644 --- a/renderer_catalog.py +++ b/renderer_catalog.py @@ -2,7 +2,7 @@ import bellevue_city_calendar_renderer import bellevue_reporter_rss_renderer -import constants +import kiosk_constants import cnn_rss_renderer import gdata_oauth import gcal_renderer @@ -14,8 +14,8 @@ import mynorthwest_rss_renderer import myq_renderer import reddit_renderer import seattletimes_rss_renderer +import stevens_renderer import kiosk_secrets as secrets -import stranger_renderer import stock_renderer import twitter_renderer import urbanist_renderer @@ -25,7 +25,7 @@ import wsj_rss_renderer seconds = 1 minutes = 60 -hours = constants.seconds_per_hour +hours = kiosk_constants.seconds_per_hour always = seconds * 1 @@ -93,6 +93,9 @@ __registry = [ ["/rss/RSSMarketsMain.xml", "/rss/WSJcomUSBusiness.xml"], "WSJBusiness", ), + stevens_renderer.stevens_renderer( + {'Unused': (minutes * 30)} + ), google_news_rss_renderer.google_news_rss_renderer( {"Fetch News": (minutes * 30), "Shuffle News": (always)}, "news.google.com", diff --git a/stevens_renderer.py b/stevens_renderer.py index bba0603..0916aee 100644 --- a/stevens_renderer.py +++ b/stevens_renderer.py @@ -1,58 +1,116 @@ #!/usr/bin/env python3 -import http.client -from typing import List, Dict -import xml.etree.ElementTree as ET +import datetime +import json +import logging +import requests +from typing import Dict -import renderer +import datetime_utils import file_writer +import renderer + + +logger = logging.getLogger(__file__) + +class stevens_renderer(renderer.abstaining_renderer): + URL = 'https://wsdot.com/Travel/Real-time/Service/api/MountainPass/Details/10' -class stevens_pass_conditions_renderer(renderer.debuggable_abstaining_renderer): - """Renders a page about Stevens Pass conditions.""" - - def __init__( - self, name_to_timeout_dict: Dict[str, int], feed_site: str, feed_uris: List[str] - ) -> None: - super(stevens_pass_conditions_renderer, self).__init__( - name_to_timeout_dict, False - ) - self.feed_site = feed_site - self.feed_uris = feed_uris - - def debug_prefix(self) -> str: - return "stevens" - - def periodic_render(self, key: str) -> bool: - with file_writer.file_writer("stevens-conditions_1_86400.html") as f: - for uri in self.feed_uris: - self.conn = http.client.HTTPSConnection(self.feed_site) - self.conn.request("GET", uri, None, {"Accept-Charset": "utf-8"}) - response = self.conn.getresponse() - if response.status == 200: - raw = response.read() - rss = ET.fromstring(raw) - channel = rss[0] - for item in channel.getchildren(): - if item.tag == "title": - f.write(f"

    {item.text}


    ") - f.write( - '' - ) - elif item.tag == "item": - for x in item.getchildren(): - if x.tag == "description": - text = x.text - if text is not None: - text = text.replace( - "Stevens Pass US2
    ", "" - ) - text = text.replace("

    ", "
    ") - text = text.replace( - "Elevation Meters:1238
    ", "" - ) - else: - text = "" - f.write(f"

    \n{text}\n") - return True + def __init__(self, name_to_timeout_dict: Dict[str, int]) -> None: + super().__init__(name_to_timeout_dict) + + def render_conditions(mp: Dict[str, str], conditions: Dict[str, str]) -> str: + ret = f''' +

    + + + + + + + + + + + +''' + if 'restrictionOne' in conditions or 'restrictionTwo' in conditions: + ret += ''' + + +
    temperature:{conditions['temperature']}°{conditions['temperatureUnit'][0]}
    weather:{conditions['weather']}
    road:{conditions['roadCondition']}
    restrictions:''' + if 'restrictionOne' in conditions: + ret += f''' + {conditions['restrictionOne']['travelDirectionName']}: + {conditions['restrictionOne']['publicPage']}
    ''' + if 'restrictionTwo' in conditions: + ret += f''' + {conditions['restrictionTwo']['travelDirectionName']}: + {conditions['restrictionTwo']['publicPage']}''' + ret += '
    ' + return ret + + def render_forecast(forecasts: Dict[str, str]) -> str: + ret = '' + fc = forecasts['forecast']['forecastData'] + for n, f in enumerate(fc): + color = '' + if n % 2 == 0: + color = ' BGCOLOR="#dfefff"' + ret += f''' + + {f['periodText']} + {f['forecastText']} +''' + ret += '
    ' + return ret + + def render_image(cameras: Dict[str, str]) -> str: + for camera in cameras: + if camera['cameraId'] == 8063: + return f''' +
    + +
    + {camera['cameraLabel']} ({camera['direction']}) +
    ''' + return '' + + def periodic_render(self, unused: str) -> bool: + page = requests.get(stevens_renderer.URL) + if page.status_code == 200: + contents = json.loads(page.content) + mp = contents['mountainPass'] + conditions = contents['condition'] + cameras = contents['cameras'] + forecasts = contents['stationForecasts'][0] + now = datetime_utils.now_pacific() + tss = conditions['displayDate'] + tss = tss.replace('Z', '+00:00') + ts = datetime.datetime.strptime(tss, '%Y-%m-%dT%H:%M:%S.%f%z') + tss = datetime_utils.describe_timedelta_briefly(now - ts) + with file_writer.file_writer('stevens-conditions_5_3000.html') as f: + f.write(f''' +

    Stevens Pass Conditions ~{tss} ago:

    +
    + + + + + + + + +
    + {stevens_renderer.render_conditions(mp, conditions)} + + {stevens_renderer.render_image(cameras)} +
    + {stevens_renderer.render_forecast(forecasts)} +
    ''') + return True return False + +test = stevens_renderer({"Test", 123}) +test.periodic_render("Test") -- 2.45.2