This is the current running state of the kiosk sans the secrets.
authorScott Gasch <[email protected]>
Wed, 8 Feb 2023 17:39:40 +0000 (09:39 -0800)
committerScott Gasch <[email protected]>
Wed, 8 Feb 2023 17:39:40 +0000 (09:39 -0800)
Getting this up-to-date.

18 files changed:
attention.wav [new file with mode: 0644]
bellevue_reporter_rss_renderer.py
chooser.py
file_writer.py
gcal_renderer.py
gcal_trigger.py
ideas [new file with mode: 0644]
kiosk.py
kiosk_constants.py
local_photos_mirror_renderer.py
main.py
myq_renderer.py
renderer.py
renderer_catalog.py
stock_renderer.py
stranger_renderer.py
twitter_renderer.py
weather_renderer.py

diff --git a/attention.wav b/attention.wav
new file mode 100644 (file)
index 0000000..0519d4f
Binary files /dev/null and b/attention.wav differ
index 70002695b9b130d44e6119b2320816fc0357cfc4..4420c80a5ed10ccbfd2b797efc88e99f9306e1c3 100644 (file)
@@ -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(
index ad047548fbdbd6ba7f85fb3599ab16b478c4c6a2..af2f761075c84a93cdb410050d1d06486da3e635 100644 (file)
@@ -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
index beac7bbcd284861e4b3d8bf7bb419c9bdd9a51b5..ffb15e6b2ec4ed44da9308896b0388d006f2dc10 100644 (file)
@@ -5,6 +5,7 @@ from uuid import uuid4
 
 import kiosk_constants
 
+
 class file_writer:
     """Helper context to write a pages file."""
 
index 1e026cd2e11797ae496dabcba5ec3c3e07f7a874..73704aa506f763eb0b07fd3780b2baf0e80d6564 100644 (file)
@@ -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)
index 36e46e62e81a11ea2ab356dc4139201d5b187cca..81e603ee95f54f57bf96aad9f3f74602e9a4ec5a 100644 (file)
@@ -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 (file)
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.
index 67691f2784004a0a00c05cd36bc0e0e8be149a9a..c9f84e9b1755fead9c07b71baa2f8d45bc1f05af 100755 (executable)
--- 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
index ce04f7e85be5bef23488771f3fdd447ca533f43c..39ee43fbccbf3ecb5f7ffb34e0fbfbbc057c95f9 100644 (file)
@@ -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"
index b9ba5c988ea86bd8e25c3d4c554cc848a81830a8..b68df68096e3fa0b7ce0dd9b94bb6bd9ccc5be3e 100644 (file)
@@ -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 95f5e15f24dafbb4f8195727b591a0dc251b5c20..162a9a90958b7526864c413eaee3a31cc581a8f5 100755 (executable)
--- 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
index 72a6a391a4fb63f554d66bb9d212fdbd833cfa59..ebfac233927a8038cda3daca83a2fe23fb984d78 100644 (file)
@@ -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
index fea5a47c87f4b49e5ff80373515850bff6a2cae0..2f835b98f07c6f42b6b03f8813e49941668fec12 100644 (file)
@@ -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__)
index 7bf3c1f00e85f28a61b0bbc5a208e6e5c38a88b8..4963f1d54e56ca33b475ed74eb8c794170ffe1c4 100644 (file)
@@ -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)},
index 40ced0fb94a85fab390ee086ca6cdae2a78bfdd9..60e70432ccbbbfee620119dc32b718d407222da8 100644 (file)
@@ -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("</TR>")
@@ -103,7 +181,7 @@ class stock_quote_renderer(renderer.abstaining_renderer):
 <TD WIDTH=20% HEIGHT=150 BGCOLOR="{cell_color}">
   <!-- Container -->
   <DIV style="position:relative;
-              height:150px;">
+              height:175px;">
     <!-- Symbol {symbol} -->
     <DIV style="position:absolute;
                 bottom:50;
@@ -120,13 +198,15 @@ class stock_quote_renderer(renderer.abstaining_renderer):
     <DIV style="position:absolute;
                 left:10;
                 top:20;
-                font-size:23pt;
-                font-family: helvetica, arial, sans-serif;
-                width:70%">
+                font-size:24pt;
+                font-family:helvetica, arial, sans-serif;
+                font-weight:900;
+                width:80%">
             ${price:.2f}<BR>
             <I>({percent_change:.1f}%)</I><BR>
             <B>${delta:.2f}</B>
     </DIV>
+    <IMG SRC="{chart_filename}" WIDTH=100%>
   </DIV>
 </TD>"""
                 )
@@ -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)
index e9c15142786d3f2a04edfbf707c092254340c8d2..3a37c891abeddec5744d1445412f77bf4a027b7b 100644 (file)
@@ -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
index edbe17e10b603a76ffb2f5d0fd196e8f3aa67b48..288eea16916eaaf1096d3cc6a6890ac015743db2 100644 (file)
@@ -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'<B>#{x["text"]}</B>, '
+        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("<LI><B>%s</B>\n" % text)
+                    text = f'<B>{text}</B>'
+                    text += self.get_hashtags(tweet)
+                    media_url = self.get_media_url(tweet)
+                    if media_url:
+                        text = f'<TABLE WIDTH=100%><TD WITDH=70%>{text}</TD>'
+                        text += f'<TD><IMG SRC="{media_url}" WIDTH=200></TD></TABLE>'
+                    f.write(f"<LI>{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()
index c72a8620cc3f1f9b5ef7f22be8adc0f89a0ea11c..23c5a201e6e44f254c39c112a2d6c59451231ba5 100644 (file)
@@ -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")