From da3a11e9fcea80a7700eb54605512d331a9ec612 Mon Sep 17 00:00:00 2001 From: Scott Date: Sat, 4 Dec 2021 20:35:41 -0800 Subject: [PATCH] Fix cameras, improve weather, delegate health renderer to a helper, etc... --- camera_trigger.py | 24 +-- gkeep_renderer.py | 9 +- health_renderer.py | 144 +----------------- kiosk.py | 48 ++++-- renderer_catalog.py | 10 +- stranger_renderer.py | 10 +- urbanist_renderer.py | 7 +- weather_renderer.py | 350 +++++++++++++++++-------------------------- 8 files changed, 203 insertions(+), 399 deletions(-) diff --git a/camera_trigger.py b/camera_trigger.py index 41dc809..8582889 100644 --- a/camera_trigger.py +++ b/camera_trigger.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 -from datetime import datetime -import glob +import logging import os import time from typing import List, Tuple, Optional import trigger -import utils + + +logger = logging.getLogger(__file__) class any_camera_trigger(trigger.trigger): @@ -58,7 +59,7 @@ class any_camera_trigger(trigger.trigger): ts = os.stat(filename).st_ctime age = now - ts if ts != self.last_trigger_timestamp[camera] and age < 10: - print(f'Camera: {camera}, age {age}') + logger.info(f'{camera} is triggered; {filename} touched {age}s ago (@{ts}') self.last_trigger_timestamp[camera] = ts num_cameras_with_recent_triggers += 1 @@ -83,26 +84,27 @@ class any_camera_trigger(trigger.trigger): self.triggers_in_the_past_seven_min[camera] <= 4 or num_cameras_with_recent_triggers > 1 ): + logger.info(f'{camera} has {self.triggers_in_the_past_seven_min[camera]} triggers in the past 7d.') + logger.info(f'{num_cameras_with_recent_triggers} cameras are triggered right now.') + age = now - self.last_trigger_timestamp[camera] priority = self.choose_priority(camera, int(age)) - print( - f"{utils.timestamp()}: *** {camera}[{priority}] CAMERA TRIGGER ***" - ) + logger.info(f'*** CAMERA TRIGGER (hidden/{camera}.html @ {priority}) ***') triggers.append( ( - f"hidden/{camera}.html", + f"hidden/unwrapped_{camera}.html", priority, ) ) else: - print(f"{utils.timestamp()}: Camera {camera} too spammy, squelching it") + logger.info(f'{camera} is too spammy; {self.triggers_in_the_past_seven_min[camera]} events in the past 7m. Ignoring it.') except Exception as e: - print(e) - pass + logger.exception(e) if len(triggers) == 0: return None else: + logger.info('There are active camera triggers!') return triggers diff --git a/gkeep_renderer.py b/gkeep_renderer.py index b639ed2..443abc1 100644 --- a/gkeep_renderer.py +++ b/gkeep_renderer.py @@ -69,12 +69,9 @@ class gkeep_renderer(renderer.abstaining_renderer): individual_lines = contents.split("\n") num_lines = len(individual_lines) - max_length = 0 contents = "" for x in individual_lines: length = len(x) - if length > max_length: - max_length = length leading_spaces = len(x) - len(x.lstrip(" ")) leading_spaces //= 2 leading_spaces = int(leading_spaces) @@ -107,9 +104,9 @@ class gkeep_renderer(renderer.abstaining_renderer):
""" ) - if num_lines >= 12 and max_length < 120: + if num_lines >= 12: logger.debug( - f"{num_lines} lines (max={max_length} chars): two columns" + f"{num_lines} lines: two column mode" ) f.write('') f.write( @@ -129,7 +126,7 @@ class gkeep_renderer(renderer.abstaining_renderer): f.write("
\n") else: logger.debug( - f"{num_lines} lines (max={max_length} chars): one column" + f"{num_lines} lines: one column mode" ) f.write(f"") f.write("") diff --git a/health_renderer.py b/health_renderer.py index 5416af2..cfa6a8a 100644 --- a/health_renderer.py +++ b/health_renderer.py @@ -1,14 +1,11 @@ #!/usr/bin/env python3 import logging -import os -import time +import subprocess from typing import Dict -import constants import file_writer import renderer -import utils logger = logging.getLogger(__file__) @@ -20,141 +17,12 @@ class periodic_health_renderer(renderer.abstaining_renderer): def periodic_render(self, key: str) -> bool: with file_writer.file_writer("periodic-health_6_300.html") as f: - timestamps = "/timestamps/" - days = constants.seconds_per_day - hours = constants.seconds_per_hour - mins = constants.seconds_per_minute - minutes = mins - limits = { - timestamps + "last_http_probe_wannabe_house": mins * 10, - timestamps + "last_http_probe_meerkat_cabin": mins * 10, - timestamps + "last_http_probe_dns_house": mins * 10, - timestamps + "last_http_probe_rpi_cabin": mins * 10, - timestamps + "last_http_probe_rpi_house": mins * 10, - timestamps + "last_http_probe_therm_house": mins * 10, - timestamps + "last_rsnapshot_hourly": hours * 24, - timestamps + "last_rsnapshot_daily": days * 3, - timestamps + "last_rsnapshot_weekly": days * 14, - timestamps + "last_rsnapshot_monthly": days * 70, - timestamps + "last_zfssnapshot_hourly": hours * 5, - timestamps + "last_zfssnapshot_daily": hours * 36, - timestamps + "last_zfssnapshot_weekly": days * 9, - timestamps + "last_zfssnapshot_monthly": days * 70, - timestamps + "last_zfssnapshot_cleanup": hours * 24, - timestamps + "last_zfs_scrub": days * 9, - timestamps + "last_backup_zfs_scrub": days * 9, - timestamps + "last_cabin_zfs_scrub": days * 9, - timestamps + "last_zfsxfer_backup.house": hours * 36, - timestamps + "last_zfsxfer_ski.dyn.guru.org": days * 7, - timestamps + "last_photos_sync": hours * 8, - timestamps + "last_disk_selftest_short": days * 14, - timestamps + "last_disk_selftest_long": days * 31, - timestamps + "last_backup_disk_selftest_short": days * 14, - timestamps + "last_backup_disk_selftest_long": days * 31, - timestamps + "last_cabin_disk_selftest_short": days * 14, - timestamps + "last_cabin_disk_selftest_long": days * 31, - timestamps + "last_cabin_rpi_ping": mins * 20, - timestamps + "last_healthy_wifi": mins * 10, - timestamps + "last_healthy_network": mins * 10, - timestamps + "last_scott_sync": days * 2, - } - self.write_header(f) - - now = time.time() - n = 0 - for filepath, limit_sec in sorted(limits.items()): - ts = os.stat(filepath).st_mtime - age = now - ts - logger.debug(f"{filepath} -- age: {age}, limit {limit_sec}") - if age < limits[filepath]: - # OK - f.write( - '\n' - ) - txt_color="#ffffff" - else: - # BAD! - f.write( - '\n' - ) - txt_color="#000000" - f.write(f"
\n") - - name = filepath.replace(timestamps, "") - name = name.replace("last_", "") - name = name.replace("_", " ") - duration = utils.describe_duration_briefly(int(age)) - - logger.debug(f"{name} is {duration} old.") - f.write(f"{name}
\n{duration} old.\n") - f.write("
\n\n\n") - n += 1 - if n % 3 == 0: - f.write("\n\n\n") - self.write_footer(f) + command = "/home/pi/bin/cronhealth.py --kiosk_mode" + p = subprocess.Popen(command, shell=True, bufsize=0, stdout=subprocess.PIPE) + for line in iter(p.stdout.readline, b''): + f.write(line.decode("utf-8")) + p.stdout.close() return True - def write_header(self, f: file_writer.file_writer) -> None: - f.write( - """ - - - - - - - - - - -

Periodic Cronjob Health Report

-
-
- - -""" - ) - - def write_footer(self, f: file_writer.file_writer) -> None: - f.write( - """ - -
- -""" - ) - - #test = periodic_health_renderer({"Test", 123}) #test.periodic_render("Test") diff --git a/kiosk.py b/kiosk.py index 514fbac..99d40c3 100755 --- a/kiosk.py +++ b/kiosk.py @@ -102,7 +102,7 @@ def guess_page(command: str, page_chooser: chooser.chooser) -> str: 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('gmaps-seattle-unwrapped', 'traffic') page = page.replace('gomenu', 'dinner menu') page = page.replace('WSJNews', 'news') page = page.replace('telma', 'telma cabin') @@ -111,6 +111,7 @@ def guess_page(command: str, page_chooser: chooser.chooser) -> str: logger.debug(f'normalize_page output: {page}') return page + logger.info(f'No exact match for f{command}; trying to guess...') best_page = None best_score = None for page in page_chooser.get_page_list(): @@ -118,12 +119,15 @@ def guess_page(command: str, page_chooser: chooser.chooser) -> str: score = SequenceMatcher(None, command, npage).ratio() if best_score is None or score > best_score: best_page = page + best_score = score assert best_page is not None + logger.info(f'Best guess for f{command} => {best_page} (score = {best_score})') return best_page def process_command(command: str, page_history: List[str], page_chooser) -> str: - logger.debug(f'Parsing verbal command: {command}') + command = command.lower() + logger.info(f'Parsing verbal command: {command}') page = None if 'hold' in command: page = page_history[0] @@ -186,7 +190,7 @@ def process_command(command: str, page_history: List[str], page_chooser) -> str: elif 'twitter' in command: page = 'twitter_10_3600.html' elif 'traffic' in command: - page = 'wsdot-bridges_3_none.html' + page = 'gmaps-seattle-unwrapped_5_none.html' elif 'front' in command and 'door' in command: page = 'hidden/frontdoor.html' elif 'driveway' in command: @@ -196,7 +200,7 @@ def process_command(command: str, page_history: List[str], page_chooser) -> str: else: page = guess_page(command, page_chooser) assert page is not None - logger.debug(f'Chose page {page}') + logger.info(f'Parsed to {page}') return page @@ -256,7 +260,7 @@ def thread_change_current(command_queue: Queue) -> None: # Set current.shtml to the right page. try: with open(current_file, "w") as f: - emit_wrapped( + emit( f, page, override_refresh_sec = constants.emergency_refresh_period_sec, @@ -275,7 +279,7 @@ def thread_change_current(command_queue: Queue) -> None: # Fix this hack... maybe read the webserver logs and see if it # actually was picked up? - time.sleep(0.95) + time.sleep(0.999) os.remove(emergency_file) logger.debug(f'chooser: ...and removed {emergency_file}.') @@ -287,7 +291,7 @@ def thread_change_current(command_queue: Queue) -> None: swap_page_target = now + constants.refresh_period_sec try: with open(current_file, "w") as f: - emit_wrapped(f, page) + emit(f, page) logger.debug(f'chooser: Wrote {current_file}.') except Exception as e: logger.exception(e) @@ -298,6 +302,23 @@ def thread_change_current(command_queue: Queue) -> None: time.sleep(0.5) +def emit(f, + filename: str, + *, + override_refresh_sec: int = None, + command: str = None): + if 'unwrapped' not in filename: + logger.debug(f'Emitting {filename} wrapped.') + emit_wrapped(f, filename, override_refresh_sec=override_refresh_sec, command=command) + else: + logger.debug(f'Emitting {filename} raw.') + emit_raw(f, filename) + + +def emit_raw(f, filename: str): + f.write(f'') + + def emit_wrapped(f, filename: str, *, @@ -583,9 +604,7 @@ def renderer_update_internal_stats_page( p75 = np.percentile(latency, 75) p90 = np.percentile(latency, 90) p99 = np.percentile(latency, 99) - except IndexError: - pass - f.write( + f.write( f''' {name}  @@ -594,8 +613,10 @@ f'''  p25={p25:5.2f}, p50={p50:5.2f}, p75={p75:5.2f}, p90={p90:5.2f}, p99={p99:5.2f} ''' - ) - f.write('') + ) + except IndexError: + pass + f.write('') def thread_invoke_renderers() -> None: @@ -603,6 +624,9 @@ def thread_invoke_renderers() -> None: render_counts: collections.Counter = collections.Counter() last_render: Dict[str, datetime] = {} + # Touch the internal render page now to signal that we're alive. + renderer_update_internal_stats_page(last_render, render_counts, render_times) + # Main renderer loop while True: logger.info( diff --git a/renderer_catalog.py b/renderer_catalog.py index 4c27000..00a5834 100644 --- a/renderer_catalog.py +++ b/renderer_catalog.py @@ -36,9 +36,9 @@ oauth = gdata_oauth.OAuth(secrets.google_client_secret) # frequency in the renderer thread of ~once a minute. It just means that # everytime it check these will be stale and happen. __registry = [ - stranger_renderer.stranger_events_renderer( - {"Fetch Events": (hours * 12), "Shuffle Events": (always)} - ), +# stranger_renderer.stranger_events_renderer( +# {"Fetch Events": (hours * 12), "Shuffle Events": (always)} +# ), myq_renderer.garage_door_renderer( {"Poll MyQ": (minutes * 5), "Update Page": (always)} ), @@ -48,7 +48,7 @@ __registry = [ "Shuffle News": (always), }, 'bellevuewa.gov', - [ '/calendar/events.xml' ], + ['/calendar/events.xml'], 'Bellevue City Calendar' ), bellevue_reporter_rss_renderer.bellevue_reporter_rss_renderer( @@ -61,7 +61,7 @@ __registry = [ {'Fetch News': (hours * 2), 'Shuffle News': (always)}, 'www.theurbanist.org', ['/feed/'], - 'TheUrbanist', + 'The Urbanist', ), mynorthwest_rss_renderer.mynorthwest_rss_renderer( {"Fetch News": (hours * 1), "Shuffle News": (always)}, diff --git a/stranger_renderer.py b/stranger_renderer.py index fb34d2f..0c566d9 100644 --- a/stranger_renderer.py +++ b/stranger_renderer.py @@ -23,6 +23,7 @@ class stranger_events_renderer(renderer.abstaining_renderer): super().__init__(name_to_timeout_dict) self.feed_site = "everout.com" self.events = grab_bag.grab_bag() + self.pfilter = profanity_filter.ProfanityFilter() def debug_prefix(self) -> str: return "stranger" @@ -92,11 +93,7 @@ class stranger_events_renderer(renderer.abstaining_renderer): def fetch_events(self) -> bool: self.events.clear() - feed_uris = [ - "/seattle/events/?page=1", - "/seattle/events/?page=2", - "/seattle/events/?page=3", - ] + feed_uris = [] now = datetime.datetime.now() ts = now + datetime.timedelta(1) tomorrow = datetime.datetime.strftime(ts, "%Y-%m-%d") @@ -116,7 +113,6 @@ class stranger_events_renderer(renderer.abstaining_renderer): feed_uris.append(f"/seattle/events/?start-date={next_sun}&page=1") feed_uris.append(f"/seattle/events/?start-date={next_sun}&page=2") - filter = profanity_filter.ProfanityFilter() for uri in feed_uris: try: logger.debug("fetching 'https://%s%s'" % (self.feed_site, uri)) @@ -135,7 +131,7 @@ class stranger_events_renderer(renderer.abstaining_renderer): soup = BeautifulSoup(raw, "html.parser") for x in soup.find_all("div", class_="row event list-item mb-3 py-3"): text = x.get_text() - if filter.contains_bad_word(text): + if self.pfilter.contains_bad_word(text): continue raw_str = str(x) raw_str = raw_str.replace( diff --git a/urbanist_renderer.py b/urbanist_renderer.py index 437e3e6..15d0c06 100644 --- a/urbanist_renderer.py +++ b/urbanist_renderer.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import datetime -import re from typing import Dict, List, Optional import xml @@ -12,7 +11,7 @@ import generic_news_rss_renderer as gnrss # https://www.theurbanist.org/feed/ class urbanist_renderer(gnrss.generic_news_rss_renderer): - """Read the TheUrbanist feed.""" + """Read the The Urbanist feed.""" def __init__( self, @@ -44,7 +43,7 @@ class urbanist_renderer(gnrss.generic_news_rss_renderer): def item_is_interesting_for_headlines( self, title: str, description: str, item: xml.etree.ElementTree.Element ) -> bool: - return 'the urbanist' not in description.lower() + return self.find_pubdate(item) is not None and 'urbanist' not in title.lower() def do_details(self) -> bool: return True @@ -52,7 +51,7 @@ class urbanist_renderer(gnrss.generic_news_rss_renderer): def item_is_interesting_for_article( self, title: str, description: str, item: xml.etree.ElementTree.Element ) -> bool: - return len(description) > 20 + return len(description) > 30 # Test diff --git a/weather_renderer.py b/weather_renderer.py index f10f1ce..4a5663c 100644 --- a/weather_renderer.py +++ b/weather_renderer.py @@ -10,7 +10,6 @@ import urllib.parse import file_writer import renderer import kiosk_secrets as secrets -import random class weather_renderer(renderer.abstaining_renderer): @@ -20,63 +19,9 @@ class weather_renderer(renderer.abstaining_renderer): super().__init__(name_to_timeout_dict) self.file_prefix = file_prefix - def debug_prefix(self) -> str: - return f"weather({self.file_prefix})" - def periodic_render(self, key: str) -> bool: return self.fetch_weather() - def describe_time(self, index: int) -> str: - if index <= 1: - return "overnight" - elif index <= 3: - return "morning" - elif index <= 5: - return "afternoon" - else: - return "evening" - - def describe_wind(self, mph: float) -> str: - if mph <= 0.3: - return "calm" - elif mph <= 5.0: - return "light" - elif mph < 15.0: - return "breezy" - elif mph <= 25.0: - return "gusty" - else: - return "heavy" - - def describe_magnitude(self, mm: float) -> str: - if mm < 2.0: - return "light" - elif mm < 10.0: - return "moderate" - else: - return "heavy" - - def describe_precip(self, rain: float, snow: float) -> str: - if rain == 0.0 and snow == 0.0: - return "no precipitation" - magnitude = rain + snow - if rain > 0 and snow > 0: - return f"a {self.describe_magnitude(magnitude)} mix of rain and snow" - elif rain > 0: - return f"{self.describe_magnitude(magnitude)} rain" - elif snow > 0: - return f"{self.describe_magnitude(magnitude)} snow" - return "rain" - - def fix_caps(self, s: str) -> str: - r = "" - s = s.lower() - for x in s.split("."): - x = x.strip() - r += x.capitalize() + ". " - r = r.replace(". .", ".") - return r - def pick_icon( self, conditions: List[str], rain: List[float], snow: List[float] ) -> str: @@ -135,89 +80,6 @@ class weather_renderer(renderer.abstaining_renderer): return "partlysunny.gif" return "clear.gif" - def describe_weather( - self, - high: float, - low: float, - wind: List[float], - conditions: List[str], - rain: List[float], - snow: List[float], - ) -> str: - # High temp: 65 - # Low temp: 44 - # -onight------ -morning----- -afternoon-- -evening---- - # 12a-3a 3a-6a 6a-9a 9a-12p 12p-3p 3p-6p 6p-9p 9p-12p - # Wind: [12.1 3.06 3.47 4.12 3.69 3.31 2.73 2.1] - # Conditions: [Clouds Clouds Clouds Clouds Clouds Clouds Clear Clear] - # Rain: [0.4 0.2 0 0 0 0 0 0] - # Snow: [0 0 0 0 0 0 0 0] - high = int(high) - low = int(low) - count = min(len(wind), len(conditions), len(rain), len(snow)) - descr = "" - - lcondition = "" - lwind = "" - lprecip = "" - ltime = "" - for x in range(0, count): - time = self.describe_time(x) - current = "" - chunks = 0 - - txt = conditions[x] - if txt == "Clouds": - txt = "cloudy" - elif txt == "Rain": - txt = "rainy" - - if txt != lcondition: - if txt != "Snow" and txt != "Rain": - current += txt - chunks += 1 - lcondition = txt - - txt = self.describe_wind(wind[x]) - if txt != lwind: - if len(current) > 0: - current += " with " - current += txt + " winds" - lwind = txt - chunks += 1 - - txt = self.describe_precip(rain[x], snow[x]) - if txt != lprecip: - if len(current) > 0: - if chunks > 1: - current += " and " - else: - current += " with " - chunks += 1 - current += txt - lprecip = txt - - if len(current): - if ltime != time: - if random.randint(0, 3) == 0: - if time != "overnight": - descr += current + " in the " + time + ". " - descr += current + " overnight. " - else: - if time != "overnight": - descr += "In the " - descr += time + ", " + current + ". " - else: - current = current.replace("cloudy", "clouds") - descr += current + " developing. " - ltime = time - if ltime == "overnight" or ltime == "morning": - descr += "Conditions continuing the rest of the day. " - descr = descr.replace("with breezy winds", "and breezy") - descr = descr.replace("Clear developing", "Skies clearing") - descr = self.fix_caps(descr) - return descr - def fetch_weather(self) -> bool: if self.file_prefix == "stevens": text_location = "Stevens Pass, WA" @@ -254,15 +116,58 @@ class weather_renderer(renderer.abstaining_renderer): # "dt_txt":"2017-01-30 18:00:00" # }, # {"dt":1485810000,.... - with file_writer.file_writer("weather-%s_3_10800.html" % self.file_prefix) as f: - f.write( - f""" -

Weather at {text_location}:

+ + with file_writer.file_writer(f"weather-{self.file_prefix}_3_10800.html") as f: + f.write(f""" +

Upcoming weather at {text_location}:


+""") + f.write(""" +
- - """ - ) +""") count = parsed_json["cnt"] ts = {} @@ -272,31 +177,32 @@ class weather_renderer(renderer.abstaining_renderer): conditions: Dict[str, List[str]] = {} rain: Dict[str, List[float]] = {} snow: Dict[str, List[float]] = {} + precip: Dict[str, List[float]] = {} + for x in range(0, count): data = parsed_json["list"][x] dt = data["dt_txt"] # 2019-10-07 18:00:00 - date = dt.split(" ")[0] - time = dt.split(" ")[1] + (date, time) = dt.split(' ') wind[date] = [] conditions[date] = [] - highs[date] = -99999 - lows[date] = +99999 + highs[date] = None + lows[date] = None rain[date] = [] snow[date] = [] + precip[date] = [] ts[date] = 0 for x in range(0, count): data = parsed_json["list"][x] dt = data["dt_txt"] # 2019-10-07 18:00:00 - date = dt.split(" ")[0] - time = dt.split(" ")[1] + (date, time) = dt.split(' ') _ = data["dt"] if _ > ts[date]: ts[date] = _ temp = data["main"]["temp"] - if highs[date] < temp: + if highs[date] is None or highs[date] < temp: highs[date] = temp - if temp < lows[date]: + if lows[date] is None or temp < lows[date]: lows[date] = temp wind[date].append(data["wind"]["speed"]) conditions[date].append(data["weather"][0]["main"]) @@ -331,43 +237,42 @@ class weather_renderer(renderer.abstaining_renderer): # }, # u'wind': {u'speed': 6.31, u'deg': 10.09}} - # Next 5 half-days - # for x in xrange(0, 5): - # fcast = parsed_json['forecast']['txt_forecast']['forecastday'][x] - # text = fcast['fcttext'] - # text = re.subn(r' ([0-9]+)F', r' \1°F', text)[0] - # f.write('' % text) - # f.write('

%s

') - # f.close() - # return True - - # f.write("\n") - days_seen = {} + days_seen = set() for date in sorted(highs.keys()): - today = datetime.fromtimestamp(ts[date]) - formatted_date = today.strftime("%a %e %b") + day = datetime.fromtimestamp(ts[date]) + formatted_date = day.strftime("%a %e %b") if formatted_date in days_seen: continue - days_seen[formatted_date] = True - num_days = len(list(days_seen.keys())) + days_seen.add(formatted_date) + total = len(days_seen) + + days_seen = set() + for n, date in enumerate(sorted(highs.keys())): + if n % 3 == 0: + if n > 0: + f.write("") + f.write('
') + remaining = total - n + if remaining >= 3: + width = "33%" + else: + width = f'{100/remaining}%' - days_seen = {} - for date in sorted(highs.keys()): - precip = 0.0 - for _ in rain[date]: - precip += _ - for _ in snow[date]: - precip += _ + precip[date] = [] + aggregate_precip = 0.0 + for r, s in zip(rain[date], snow[date]): + aggregate = r + s + aggregate_precip += aggregate + precip[date].append(aggregate) - today = datetime.fromtimestamp(ts[date]) - formatted_date = today.strftime("%a %e %b") + day = datetime.fromtimestamp(ts[date]) + formatted_date = day.strftime("%a %e %b") if formatted_date in days_seen: continue - days_seen[formatted_date] = True + days_seen.add(formatted_date) f.write( - '
\n") - f.write("
\n' % (100 / num_days) + f'
\n' ) - f.write("
\n") # Date f.write( @@ -376,7 +281,7 @@ class weather_renderer(renderer.abstaining_renderer): + "\n" ) - # Icon + # Conditions icon f.write( ' \n' % self.pick_icon(conditions[date], rain[date], snow[date]) @@ -386,46 +291,59 @@ class weather_renderer(renderer.abstaining_renderer): color = "#000099" if lows[date] <= 32.5: color = "#009999" - f.write( - ' \n' - % (color, int(lows[date])) - ) - - # Total precip - precip *= 0.0393701 - if precip > 0.025: - f.write( - ' \n' - % precip - ) + f.write(f''' + + +''') + + # Total aggregate_precip in inches + aggregate_precip *= 0.0393701 + if aggregate_precip > 0.025: + f.write(f''' + +''') else: f.write(" \n") - # High temp + # High temp + precip chart color = "#800000" if highs[date] >= 80: color = "#AA0000" - f.write( - ' \n' - % (color, int(highs[date])) - ) - - # Text "description" - f.write( - '\n' - % self.describe_weather( - highs[date], - lows[date], - wind[date], - conditions[date], - rain[date], - snow[date], - ) - ) - f.write("
%d°F  
%3.1f"
+ + {int(lows[date])}°F   + + +
+ + {aggregate_precip:3.1f}" + +
+
   %d°F
%s
\n
") + f.write(f''' + + +   {int(highs[date])}°F + + + + + + + + + + + +''') + f.write("
") return True -# x = weather_renderer({"Stevens": 1000}, "stevens") -# x.periodic_render("Stevens") +#x = weather_renderer({"Stevens": 1000}, "stevens") +#x.periodic_render("Stevens") -- 2.45.2