From fe0774367d9ee7c7968dbe275cd1c8dfff38f12b Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Wed, 8 Feb 2023 09:39:40 -0800 Subject: [PATCH] This is the current running state of the kiosk sans the secrets. Getting this up-to-date. --- attention.wav | Bin 0 -> 14332 bytes bellevue_reporter_rss_renderer.py | 22 ++-- chooser.py | 2 +- file_writer.py | 1 + gcal_renderer.py | 2 +- gcal_trigger.py | 2 +- ideas | 6 ++ kiosk.py | 6 +- kiosk_constants.py | 2 + local_photos_mirror_renderer.py | 6 +- main.py | 2 +- myq_renderer.py | 2 +- renderer.py | 2 +- renderer_catalog.py | 11 +- stock_renderer.py | 166 ++++++++++++++++++++++-------- stranger_renderer.py | 2 +- twitter_renderer.py | 68 +++++++++--- weather_renderer.py | 4 +- 18 files changed, 220 insertions(+), 86 deletions(-) create mode 100644 attention.wav create mode 100644 ideas diff --git a/attention.wav b/attention.wav new file mode 100644 index 0000000000000000000000000000000000000000..0519d4fe5d8f0b7b95d24b46972a5779251dcab4 GIT binary patch literal 14332 zcmd_xTTc@~6bJBaZ2>_L5HS#q8VrgTys@Qq3?bXrp6Tw+>~0(TduiI4bN*-cb?4HM$Fn`iFjvl8 zK6mZbbaN%cFc#$OLx#C~)xubrN`@bs9(#n=j9X_8IBlYhx6c%D#bPmEGINkC6-&p< zW)5-XVmV(CI_#(vD*39=5l6L9&DVr#9d%+IU%y-LI3^tPHN+d(MxoJnB7TBx5}JI? z@n%Ph(Bf;2x3X>XZQgdNo$ZKpcsr#|wky)*?UuTOJ&|57E3pAbgd212I0Jp*{?UP* zfqSm-;HX4IBheuBMpOD zxOuhwCs(HXO-mN#d+26`swvMyXC;)wnBVElK&y%|Gi&ccYfdxjXrH=JcA9ajh0?Q7 zo?#*7ER<)TuQ?0pS(}?RU!S1axy*ek`O)3%XJAhM{5X64Q~` z#prXR+fV=V(We95Q_#PfKBu5hRsBzcyn{ZI)2d27pI#McO-Q$nUeju=OLsc5YFcNY zy^m}PI=gAlq_!TNDYRBmo0i^st%*tZ(VIYdCDP1l_bX3EXQkSFB+Q!0^x^EFx1o!kWOx4wj(Y*Nw6L$2`&Yn$Bcb=SAzoHC7tQ33 z!D86YjipqUTge;nrR#)+W!&r`XVWwB+P$)_;;vrH0t*L|P26DNY+jEArt>0-}Q z-FyMFui(tBr1Q3?nn&;KL)bQ)z-igdm+5S)IJ2ubu2=g6bgv+tqXj2;3KmEA6#lZU zWiz{m<64Ks)?@2z6C`*N7DqQIAhy@DSgtp)*qdzF#y!E~usFKg0rNGan|+(g^$r$& zhvR?0FLX51F1mFBOS(iwD!u|Hq8mssuAD1h{0f=)2D5HwlPKFWUIr7< z-6$AWx@YlwHWNQ!mY59Dbqw8 zX4!=#tx4-R#$yYP(;{0)WV)V&p^J4Ue9jLIkJO;PLl-<3hlV{PCFr}yKQ-mQbDQ}C DJZq6g literal 0 HcmV?d00001 diff --git a/bellevue_reporter_rss_renderer.py b/bellevue_reporter_rss_renderer.py index 7000269..4420c80 100644 --- a/bellevue_reporter_rss_renderer.py +++ b/bellevue_reporter_rss_renderer.py @@ -60,15 +60,19 @@ class bellevue_reporter_rss_renderer(gnrss.generic_news_rss_renderer): @staticmethod def looks_like_spam(title: str, description: str) -> bool: return ( - 'marketplace' in description - or 'national-marketplace' in description - or re.search('[Ww]eed', 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 - or re.search('[Tt]op.[Rr]ated', title) is not None - or re.search('[Ll]ose [Ww]eight', title) is not None - or re.search('[Ll]ose [Ww]eight', description) is not None + description is not None + and title is not None + and ( + 'marketplace' in description + or 'national-marketplace' in description + or re.search('[Ww]eed', 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 + or re.search('[Tt]op.[Rr]ated', title) is not None + or re.search('[Ll]ose [Ww]eight', title) is not None + or re.search('[Ll]ose [Ww]eight', description) is not None + ) ) def item_is_interesting_for_headlines( diff --git a/chooser.py b/chooser.py index ad04754..af2f761 100644 --- a/chooser.py +++ b/chooser.py @@ -8,7 +8,7 @@ import re import time from typing import Any, Callable, List, Optional, Set, Tuple -from pyutilz.datetimez import datetime_utils +from pyutils.datetimes import datetime_utils import kiosk_constants import trigger diff --git a/file_writer.py b/file_writer.py index beac7bb..ffb15e6 100644 --- a/file_writer.py +++ b/file_writer.py @@ -5,6 +5,7 @@ from uuid import uuid4 import kiosk_constants + class file_writer: """Helper context to write a pages file.""" diff --git a/gcal_renderer.py b/gcal_renderer.py index 1e026cd..73704aa 100644 --- a/gcal_renderer.py +++ b/gcal_renderer.py @@ -360,7 +360,7 @@ var fn = setInterval(function() { continue delta = eventstamp - now x = int(delta.total_seconds()) - if x > -120 and x < 4 * kiosk_constants.seconds_per_minute: + if x > 0 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) diff --git a/gcal_trigger.py b/gcal_trigger.py index 36e46e6..81e603e 100644 --- a/gcal_trigger.py +++ b/gcal_trigger.py @@ -2,7 +2,7 @@ from typing import List, Optional, Tuple -import constants +import kiosk_constants as constants import globals import trigger diff --git a/ideas b/ideas new file mode 100644 index 0000000..dbde541 --- /dev/null +++ b/ideas @@ -0,0 +1,6 @@ +dynamic weights for pages +make the renderers declare what pages they wrote +make the renderers have tags and use search in chooser to filter by tag +split triggers away from page chooser; main chooser thread in kiosk.py gets triggers + ..from the page queue just like voice commands and therefore doesn't have to poll + ..the chooser as often; can probably be a blocking read with a longer timeout. diff --git a/kiosk.py b/kiosk.py index 67691f2..c9f84e9 100755 --- a/kiosk.py +++ b/kiosk.py @@ -19,12 +19,12 @@ import numpy as np import pvporcupine import pytz -from pyutilz import ( +from pyutils import ( bootstrap, config, ) -from pyutilz.datetimez import datetime_utils -from pyutilz.files import file_utils +from pyutils.datetimes import datetime_utils +from pyutils.files import file_utils import kiosk_constants import file_writer diff --git a/kiosk_constants.py b/kiosk_constants.py index ce04f7e..39ee43f 100644 --- a/kiosk_constants.py +++ b/kiosk_constants.py @@ -17,3 +17,5 @@ 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" + +static_content_url_filepath = "/home/pi/kiosk_static_url.txt" diff --git a/local_photos_mirror_renderer.py b/local_photos_mirror_renderer.py index b9ba5c9..b68df68 100644 --- a/local_photos_mirror_renderer.py +++ b/local_photos_mirror_renderer.py @@ -16,7 +16,7 @@ class local_photos_mirror_renderer(renderer.abstaining_renderer): album_whitelist = frozenset( [ - "8-Mile Lake Hike", + "Autumn at Kubota", "Bangkok and Phuket, 2003", "Barn", "Blue Angels... Seafair", @@ -39,9 +39,9 @@ class local_photos_mirror_renderer(renderer.abstaining_renderer): "Portland, ME 2021", "Prague and Munich 2019", "Random", + "SFO 2014", "Scott and Lynn", "Sculpture Place", - "SFO 2014", "Skiing with Alex", "Sonoma", "Trip to California, '16", @@ -49,8 +49,8 @@ class local_photos_mirror_renderer(renderer.abstaining_renderer): "Trip to East Coast '16", "Turkey 2022", "Tuscany 2008", - "Yosemite 2010", "WA Roadtrip, 2021", + "Yosemite 2010", "Zoo", ] ) diff --git a/main.py b/main.py index 95f5e15..162a9a9 100755 --- a/main.py +++ b/main.py @@ -20,7 +20,7 @@ import pytz import datetime_utils import file_utils -import constants +import kiosk_constants as constants import renderer_catalog import chooser import listen diff --git a/myq_renderer.py b/myq_renderer.py index 72a6a39..ebfac23 100644 --- a/myq_renderer.py +++ b/myq_renderer.py @@ -7,7 +7,7 @@ from dateutil.parser import parse import pymyq # type: ignore from typing import Dict, Optional -from pyutilz.datetimez import datetime_utils +from pyutils.datetimes import datetime_utils import kiosk_constants import file_writer diff --git a/renderer.py b/renderer.py index fea5a47..2f835b9 100644 --- a/renderer.py +++ b/renderer.py @@ -5,7 +5,7 @@ import logging import time from typing import Dict, Optional, Set -from pyutilz.decorator_utils import invocation_logged +from pyutils.decorator_utils import invocation_logged logger = logging.getLogger(__file__) diff --git a/renderer_catalog.py b/renderer_catalog.py index 7bf3c1f..4963f1d 100644 --- a/renderer_catalog.py +++ b/renderer_catalog.py @@ -115,14 +115,19 @@ __registry = [ "ABHYX", "SPAB", "SPHD", + "SCHD", + "BCD", "GC=F", - "VDC", + "VYM", "VYMI", + "VDC", "VNQ", "VNQI", ], - { "BTC-USD": "BTC", - "GC=F": "GOLD" }, + { + "BTC-USD": "BTC", + "GC=F": "GOLD" + }, ), seattletimes_rss_renderer.seattletimes_rss_renderer( {"Fetch News": (hours * 4), "Shuffle News": (always)}, diff --git a/stock_renderer.py b/stock_renderer.py index 40ced0f..60e7043 100644 --- a/stock_renderer.py +++ b/stock_renderer.py @@ -1,11 +1,14 @@ #!/usr/bin/env python3 import logging -from typing import Dict, List, Optional, Tuple +import os +from typing import Any, Dict, List, Optional, Tuple import yfinance as yf # type: ignore +import plotly.graph_objects as go import file_writer +import kiosk_constants import renderer @@ -24,50 +27,117 @@ class stock_quote_renderer(renderer.abstaining_renderer): super().__init__(name_to_timeout_dict) self.symbols = symbols self.display_subs = display_subs + self.info_cache: Dict[yf.ticker.Ticker, Dict] = {} - @staticmethod - def get_ticker_name(ticker: yf.ticker.Ticker) -> str: + def cache_info(self, ticker: yf.ticker.Ticker) -> Dict: + if ticker in self.info_cache: + return self.info_cache[ticker] + i = ticker.get_info() + self.info_cache[ticker] = i + return i + + def get_ticker_name(self, ticker: yf.Ticker) -> str: """Get friendly name of a ticker.""" - info = ticker.get_info() + info = self.cache_info(ticker) if "shortName" in info: return info["shortName"] return ticker @staticmethod - def get_price(ticker: yf.ticker.Ticker) -> Optional[float]: - """Get most recent price of a ticker.""" - keys = [ - "bid", - "ask", - "regularMarketPrice", - "lastMarket", - "open", - "previousClose", - ] - info = ticker.get_info() + def get_item_from_dict(keys: List[str], dictionary: Dict[str, Any]) -> Optional[Any]: + result = None for key in keys: - if key in info and info[key] is not None and info[key] != 0.0: - print(f"Price: picked {key}, ${info[key]}.") - return float(info[key]) + result = dictionary.get(key, None) + if result: + return result return None + def get_price(self, ticker: yf.Ticker) -> Optional[float]: + """Get most recent price of a ticker.""" + + # First try fast_info + price = stock_quote_renderer.get_item_from_dict( + ['last_price', + 'open', + 'previous_close'], + ticker.fast_info) + if price: + return price + + # Next try info + price = stock_quote_renderer.get_item_from_dict( + ['bid', + 'ask', + 'lastMarket'], + self.cache_info(ticker)) + if price: + return price + + # Finally, fall back on history + hist = ticker.history(period="1d").to_dict()['Close'] + latest = None + latest_price = None + for k, v in hist.items(): + if latest is None or k > latest: + price = hist[k] + if price is not None: + latest = k + latest_price = price + print(f"Price: fell back on latest close {latest_price} at {latest}") + return latest_price + @staticmethod + def make_chart(symbol: str, ticker: yf.Ticker, period: str) -> str: + base_filename = f'stock_chart_{symbol}.png' + output_filename = os.path.join(kiosk_constants.pages_dir, base_filename) + transparent = go.Layout( + paper_bgcolor='rgba(0,0,0,0)', + plot_bgcolor='rgba(0,0,0,0)', + xaxis_rangeslider_visible=False, + ) + hist = ticker.history(period=period, interval="1wk") + chart = go.Figure( + data=go.Candlestick( + open=hist['Open'], + high=hist['High'], + low=hist['Low'], + close=hist['Close'], + ), + layout=transparent, + ) + chart.update_xaxes(visible=False, showticklabels=False) + chart.update_yaxes(side="right") + chart.write_image(output_filename, format="png", width=600, height=350) + print(f"Write {output_filename}...") + return base_filename + + def get_last_close( + self, + ticker: yf.Ticker + ) -> float: + last_close = stock_quote_renderer.get_item_from_dict( + ['previous_close', + 'open'], + ticker.fast_info) + if last_close: + return last_close + + last_close = stock_quote_renderer.get_item_from_dict( + ['preMarketPrice'], + self.cache_info(ticker)) + if last_close: + return last_close + return self.get_price(ticker) + def get_change_and_delta( - ticker: yf.ticker.Ticker, price: float + self, + ticker: yf.Ticker, + price: float ) -> Tuple[float, float]: """Given the current price, look up opening price and compute delta.""" - keys = [ - "previousClose", - "open", - ] - info = ticker.get_info() - for key in keys: - if key in info and info[key] is not None: - print(f"Change: picked {key}, ${info[key]}.") - old_price = float(info[key]) - delta = price - old_price - return (delta / old_price * 100.0, delta) - return (0.0, 0.0) + last_price = self.get_last_close(ticker) + delta = price - last_price + return (delta / last_price * 100.0, delta) def periodic_render(self, key: str) -> bool: """Write an up-to-date stock page.""" @@ -77,20 +147,28 @@ class stock_quote_renderer(renderer.abstaining_renderer): symbols_finished = 0 for symbol in self.symbols: ticker = yf.Ticker(symbol) - # print(ticker.get_info()) if ticker is None: logger.debug(f"Unknown symbol {symbol} -- ignored.") continue - name = stock_quote_renderer.get_ticker_name(ticker) - price = stock_quote_renderer.get_price(ticker) + name = self.get_ticker_name(ticker) + if name is None: + logger.debug(f'Bad name for {symbol} -- skipped.') + continue + price = self.get_price(ticker) if price is None: logger.debug(f"No price information for {symbol} -- skipped.") continue - (percent_change, delta) = stock_quote_renderer.get_change_and_delta( + (percent_change, delta) = self.get_change_and_delta( ticker, price ) - # print(f"delta: {delta}, change: {percent_change}") - cell_color = "#b00000" if percent_change < 0 else "#009000" + chart_filename = stock_quote_renderer.make_chart(symbol, ticker, "1y") + print(f"delta: {delta}, change: {percent_change}") + if percent_change < 0: + cell_color = "#b00000" + elif percent_change > 0: + cell_color = "#009000" + else: + cell_color = "#a0a0a0" if symbols_finished % 4 == 0: if symbols_finished > 0: f.write("") @@ -103,7 +181,7 @@ class stock_quote_renderer(renderer.abstaining_renderer):
+ height:175px;">
+ font-size:24pt; + font-family:helvetica, arial, sans-serif; + font-weight:900; + width:80%"> ${price:.2f}
({percent_change:.1f}%)
${delta:.2f}
+
""" ) @@ -134,5 +214,5 @@ class stock_quote_renderer(renderer.abstaining_renderer): return True # Test -#x = stock_quote_renderer({}, ["MSFT", "GOOG", "BTC-USD", "ABHYX", "GC=F", "VNQ"], { "BTC-USD": "BTC", "GC=F": "GOLD" }) -#x.periodic_render(None) +x = stock_quote_renderer({}, ["MSFT", "GOOG", "BTC-USD", "ABHYX", "GC=F", "VNQ"], { "BTC-USD": "BTC", "GC=F": "GOLD" }) +x.periodic_render(None) diff --git a/stranger_renderer.py b/stranger_renderer.py index e9c1514..3a37c89 100644 --- a/stranger_renderer.py +++ b/stranger_renderer.py @@ -7,7 +7,7 @@ import re from typing import Dict from bs4 import BeautifulSoup # type: ignore -from pyutilz import profanity_filter +from scottutilz import profanity_filter import file_writer import grab_bag diff --git a/twitter_renderer.py b/twitter_renderer.py index edbe17e..288eea1 100644 --- a/twitter_renderer.py +++ b/twitter_renderer.py @@ -1,17 +1,21 @@ #!/usr/bin/env python3 +import logging import random import re -from typing import Dict, List +from typing import Dict, List, Optional import tweepy # type: ignore -from pyutilz import profanity_filter +from scottutilz import profanity_filter import file_writer import renderer import kiosk_secrets as secrets +logger = logging.getLogger(__name__) + + class twitter_renderer(renderer.abstaining_renderer): def __init__(self, name_to_timeout_dict: Dict[str, int]) -> None: super().__init__(name_to_timeout_dict) @@ -60,19 +64,45 @@ class twitter_renderer(renderer.abstaining_renderer): def fetch_tweets(self) -> bool: try: tweets = self.api.home_timeline(tweet_mode="extended", count=200) - except: + except Exception as e: + logger.exception(e) print("Exception while fetching tweets!") return False for tweet in tweets: + #j = tweet._json + #import json + #print(json.dumps(j, indent=4, sort_keys=True)) + #print("------") + author = tweet.author.name author_handle = tweet.author.screen_name self.handles_by_author[author] = author_handle if author not in self.tweets_by_author: self.tweets_by_author[author] = [] - l = self.tweets_by_author[author] - l.append(tweet) + x = self.tweets_by_author[author] + x.append(tweet) return True + def get_hashtags(self, tweet) -> str: + ret = ' ' + if 'entities' in tweet._json: + entities = tweet._json['entities'] + if 'hashtags' in entities: + for x in entities['hashtags']: + ret += f'#{x["text"]}, ' + ret = re.sub(', $', '', ret) + return ret + + def get_media_url(self, tweet) -> Optional[str]: + if 'entities' in tweet._json: + entities = tweet._json['entities'] + if 'media' in entities: + media = entities['media'] + for x in media: + if 'media_url_https' in x: + return x['media_url_https'] + return None + def shuffle_tweets(self) -> bool: authors = list(self.tweets_by_author.keys()) author = random.choice(authors) @@ -94,7 +124,13 @@ class twitter_renderer(renderer.abstaining_renderer): ): already_seen.add(text) text = self.linkify(text) - f.write("
  • %s\n" % text) + text = f'{text}' + text += self.get_hashtags(tweet) + media_url = self.get_media_url(tweet) + if media_url: + text = f'' + text += f'
    {text}
    ' + f.write(f"
  • {text}") count += 1 length += len(text) if count > 3 or length > 270: @@ -104,13 +140,13 @@ class twitter_renderer(renderer.abstaining_renderer): # Test -# t = twitter_renderer( -# {"Fetch Tweets" : 1, -# "Shuffle Tweets" : 1}) -# x = "bla bla bla https://t.co/EjWnT3UA9U bla bla" -# x = t.linkify(x) -# print(x) -# if t.fetch_tweets() == 0: -# print("Error fetching tweets, none fetched.") -# else: -# t.shuffle_tweets() +t = twitter_renderer( + {"Fetch Tweets" : 1, + "Shuffle Tweets" : 1}) +#x = "bla bla bla https://t.co/EjWnT3UA9U bla bla" +#x = t.linkify(x) +#print(x) +if t.fetch_tweets() == 0: + print("Error fetching tweets, none fetched.") +else: + t.shuffle_tweets() diff --git a/weather_renderer.py b/weather_renderer.py index c72a862..23c5a20 100644 --- a/weather_renderer.py +++ b/weather_renderer.py @@ -361,5 +361,5 @@ makePrecipChart("myChart{n}", xValues{n}, yValues{n}); 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