etc...
#!/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):
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
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
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)
<HR style="border-top:3px solid white;">
"""
)
- 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('<TABLE BORDER=0 WIDTH=100%><TR valign="top">')
f.write(
f.write("</UL></FONT></TD></TR></TABLE></DIV>\n")
else:
logger.debug(
- f"{num_lines} lines (max={max_length} chars): one column"
+ f"{num_lines} lines: one column mode"
)
f.write(f"<FONT><UL>{contents}</UL></FONT>")
f.write("</DIV>")
#!/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__)
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(
- '<TD BGCOLOR="#007010" HEIGHT=100 WIDTH=33% STYLE="text-size:70%; vertical-align: middle;">\n'
- )
- txt_color="#ffffff"
- else:
- # BAD!
- f.write(
- '<TD BGCOLOR="#990000" HEIGHT=100 WIDTH=33% CLASS="invalid" STYLE="text-size:70%; vertical-align:middle;">\n'
- )
- txt_color="#000000"
- f.write(f" <CENTER><FONT SIZE=-1 COLOR={txt_color}>\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}<BR>\n<B>{duration}</B> old.\n")
- f.write("</FONT></CENTER>\n</TD>\n\n")
- n += 1
- if n % 3 == 0:
- f.write("</TR>\n<TR>\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(
- """
-<HTML>
-<HEAD>
-<STYLE>
-@-webkit-keyframes invalid {
- from { background-color: #ff6400; }
- to { background-color: #ff0000; }
- padding-right: 25px;
- padding-left: 25px;
-}
-@-moz-keyframes invalid {
- from { background-color: #ff6400; }
- to { background-color: #ff0000; }
- padding-right: 25px;
- padding-left: 25px;
-}
-@-o-keyframes invalid {
- from { background-color: #ff6400; }
- to { background-color: #ff0000; }
- padding-right: 25px;
- padding-left: 25px;
-}
-@keyframes invalid {
- from { background-color: #ff6400; }
- to { background-color: #ff0000; }
- padding-right: 25px;
- padding-left: 25px;
-}
-.invalid {
- -webkit-animation: invalid 1s infinite; /* Safari 4+ */
- -moz-animation: invalid 1s infinite; /* Fx 5+ */
- -o-animation: invalid 1s infinite; /* Opera 12+ */
- animation: invalid 1s infinite; /* IE 10+ */
-}
-</STYLE>
-<meta http-equiv="cache-control" content="max-age=0" />
-<meta http-equiv="cache-control" content="no-cache" />
-<meta http-equiv="expires" content="0" />
-<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
-<meta http-equiv="pragma" content="no-cache" />
-</HEAD>
-<BODY>
-<H1>Periodic Cronjob Health Report</H1>
-<HR>
-<CENTER>
-<TABLE BORDER=0 WIDTH=99% style="font-size:16pt">
-<TR>
-"""
- )
-
- def write_footer(self, f: file_writer.file_writer) -> None:
- f.write(
- """
-</TR>
-</TABLE>
-</BODY>
-</HTML>"""
- )
-
-
#test = periodic_health_renderer({"Test", 123})
#test.periodic_render("Test")
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')
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():
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]
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:
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
# 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,
# 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}.')
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)
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'<!--#include virtual="{filename}"-->')
+
+
def emit_wrapped(f,
filename: str,
*,
p75 = np.percentile(latency, 75)
p90 = np.percentile(latency, 90)
p99 = np.percentile(latency, 99)
- except IndexError:
- pass
- f.write(
+ f.write(
f'''
<TR>
<TD {style}>{name} </TD>
<TD {style}> p25={p25:5.2f}, p50={p50:5.2f}, p75={p75:5.2f}, p90={p90:5.2f}, p99={p99:5.2f}</TD>
</TR>
'''
- )
- f.write('</TABLE>')
+ )
+ except IndexError:
+ pass
+ f.write('</TABLE>')
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(
# 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)}
),
"Shuffle News": (always),
},
'bellevuewa.gov',
- [ '/calendar/events.xml' ],
+ ['/calendar/events.xml'],
'Bellevue City Calendar'
),
bellevue_reporter_rss_renderer.bellevue_reporter_rss_renderer(
{'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)},
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"
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")
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))
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(
#!/usr/bin/env python3
import datetime
-import re
from typing import Dict, List, Optional
import xml
# 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,
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
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
import file_writer
import renderer
import kiosk_secrets as secrets
-import random
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:
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"
# "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"""
-<h1>Weather at {text_location}:</h1>
+
+ with file_writer.file_writer(f"weather-{self.file_prefix}_3_10800.html") as f:
+ f.write(f"""
+<h1>Upcoming weather at {text_location}:</h1>
<hr>
+<script src="/kiosk/Chart.js"></script>""")
+ f.write("""
+<script>
+function makePrecipChart(name, xValues, yValues) {
+ const config = {
+ type: 'line',
+ data: {
+ labels: xValues,
+ datasets: [
+ {
+ fill: true,
+ lineTension: 0,
+ backgroundColor: "rgba(0,0,255,0.1)",
+ borderColor: "rgba(0,0,255,0.9)",
+ data: yValues
+ }
+ ]
+ },
+ options: {
+ plugins: {
+ legend: {
+ display: false,
+ },
+ },
+ scales: {
+ x: {
+ grid: {
+ display: false,
+ }
+ },
+ y: {
+ display: false,
+ beginAtZero: true,
+ min: 0.0,
+ max: 10.0,
+ grid: {
+ display: false,
+ },
+ }
+ }
+ },
+ };
+ return new Chart(name, config);
+}
+</script>
<center>
-<table width=99% cellspacing=10 border=0>
- <tr>"""
- )
+""")
count = parsed_json["cnt"]
ts = {}
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"])
# },
# 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('<td style="vertical-align:top;font-size:75%%"><P STYLE="padding:8px;">%s</P></td>' % text)
- # f.write('</tr></table>')
- # f.close()
- # return True
-
- # f.write("<table border=0 cellspacing=10>\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("</div>")
+ f.write('<div STYLE="overflow:hidden; width:100%; height:500px">')
+ 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(
- '<td width=%d%% style="vertical-align:top;">\n' % (100 / num_days)
+ f'<div style="width:{width}; height:500px; float:left"><table>\n'
)
- f.write("<table border=0>\n")
# Date
f.write(
+ "</font></center></b></td></tr>\n"
)
- # Icon
+ # Conditions icon
f.write(
' <tr><td colspan=3 height=100><center><img src="/kiosk/images/weather/%s" height=125></center></td></tr>\n'
% self.pick_icon(conditions[date], rain[date], snow[date])
color = "#000099"
if lows[date] <= 32.5:
color = "#009999"
- f.write(
- ' <tr><td width=33%% align=left><font color="%s"><b>%d°F </b></font></td>\n'
- % (color, int(lows[date]))
- )
-
- # Total precip
- precip *= 0.0393701
- if precip > 0.025:
- f.write(
- ' <td width=33%%><center><b><font style="background-color:#dfdfff; color:#003355">%3.1f"</font></b></center></td>\n'
- % precip
- )
+ f.write(f'''
+<tr>
+ <td width=33% align=left>
+ <font color="{color}">
+ <b>{int(lows[date])}°F </b>
+ </font>
+ </td>
+''')
+
+ # Total aggregate_precip in inches
+ aggregate_precip *= 0.0393701
+ if aggregate_precip > 0.025:
+ f.write(f'''
+ <td width=33%>
+ <center>
+ <font style="background-color:#dfdfff; color:#003355">
+ <b>{aggregate_precip:3.1f}"</b>
+ </font>
+ </center>
+ </td>
+''')
else:
f.write(" <td width=33%> </td>\n")
- # High temp
+ # High temp + precip chart
color = "#800000"
if highs[date] >= 80:
color = "#AA0000"
- f.write(
- ' <td align=right><font color="%s"><b> %d°F</b></font></td></tr>\n'
- % (color, int(highs[date]))
- )
-
- # Text "description"
- f.write(
- '<tr><td colspan=3 style="vertical-align:top;font-size:75%%">%s</td></tr>\n'
- % self.describe_weather(
- highs[date],
- lows[date],
- wind[date],
- conditions[date],
- rain[date],
- snow[date],
- )
- )
- f.write("</table>\n</td>\n")
- f.write("</tr></table></center>")
+ f.write(f'''
+ <td align=right>
+ <font color="{color}">
+ <b> {int(highs[date])}°F</b>
+ </font>
+ </td>
+</tr>
+<tr>
+ <td colspan=3 style="vertical-align:top;">
+ <canvas id="myChart{n}" style="width:100%;max-width:300px"></canvas>
+ </td>
+</tr>
+<script>
+var yValues{n} = ''')
+ f.write(precip[date].__repr__())
+ f.write(f''';
+var xValues{n} = [ 3, 6, 9, 12, 15, 18, 21, 24 ];
+makePrecipChart("myChart{n}", xValues{n}, yValues{n});
+</script>
+</table>
+</div>
+''')
+ f.write("</div></center>")
return True
-# x = weather_renderer({"Stevens": 1000}, "stevens")
-# x.periodic_render("Stevens")
+#x = weather_renderer({"Stevens": 1000}, "stevens")
+#x.periodic_render("Stevens")