diff service/wifi/wifi.py @ 1728:81aa0873b48d

port to skaffold, starlette, etc
author drewp@bigasterisk.com
date Fri, 30 Jun 2023 22:03:55 -0700
parents f88ff1021ee0
children
line wrap: on
line diff
--- a/service/wifi/wifi.py	Tue Jun 20 23:26:24 2023 -0700
+++ b/service/wifi/wifi.py	Fri Jun 30 22:03:55 2023 -0700
@@ -10,53 +10,44 @@
 Todo: this should be the one polling and writing to mongo, not entrancemusic
 
 """
-from collections import defaultdict
 import datetime
-import json
 import logging
-import sys
 import time
 import traceback
+from dataclasses import dataclass
 from typing import List
 
-import ago
-from cyclone.httpclient import fetch
-import cyclone.web
-from cycloneerr import PrettyErrorHandler
+import background_loop
 from dateutil import tz
-import docopt
-from patchablegraph import (
-    CycloneGraphEventsHandler,
-    CycloneGraphHandler,
-    PatchableGraph,
-)
+from patchablegraph import PatchableGraph
+from patchablegraph.handler import GraphEvents, StaticGraph
 from prometheus_client import Counter, Gauge, Summary
-from prometheus_client.exposition import generate_latest
-from prometheus_client.registry import REGISTRY
-from pymongo import DESCENDING, MongoClient as Connection
+from pymongo import DESCENDING
+from pymongo import MongoClient as Connection
 from pymongo.collection import Collection
-import pystache
-from rdflib import ConjunctiveGraph, Literal, Namespace, RDF
-from standardservice.logsetup import log
-from twisted.internet import reactor, task
-from twisted.internet.defer import ensureDeferred, inlineCallbacks
+from rdflib import RDF, ConjunctiveGraph, Literal, Namespace
+from starlette.applications import Starlette
+from starlette.routing import Route
+from starlette_exporter import PrometheusMiddleware, handle_metrics
 
 from scrape import SeenNode, Wifi
 
+logging.basicConfig()
+log = logging.getLogger()
+
 AST = Namespace("http://bigasterisk.com/")
 DEV = Namespace("http://projects.bigasterisk.com/device/")
 ROOM = Namespace("http://projects.bigasterisk.com/room/")
 
-
-class Index(PrettyErrorHandler, cyclone.web.RequestHandler):
+# class Index(PrettyErrorHandler, cyclone.web.RequestHandler):
 
-    def get(self):
-        age = time.time() - self.settings.poller.lastPollTime
-        if age > 10:
-            raise ValueError("poll data is stale. age=%s" % age)
+#     def get(self):
+#         age = time.time() - self.settings.poller.lastPollTime
+#         if age > 10:
+#             raise ValueError("poll data is stale. age=%s" % age)
 
-        self.set_header("Content-Type", "text/html")
-        self.write(open("index.html").read())
+#         self.set_header("Content-Type", "text/html")
+#         self.write(open("index.html").read())
 
 
 def whenConnected(mongo, macThatIsNowConnected):
@@ -72,46 +63,40 @@
     return lastArrive['created']
 
 
-def connectedAgoString(conn):
-    return ago.human(conn.astimezone(tz.tzutc()).replace(tzinfo=None))
+# class Table(PrettyErrorHandler, cyclone.web.RequestHandler):
 
-
-class Table(PrettyErrorHandler, cyclone.web.RequestHandler):
+#     def get(self):
 
-    def get(self):
-
-        def rowDict(row):
-            row['cls'] = "signal" if row.get('connected') else "nosignal"
-            if 'name' not in row:
-                row['name'] = row.get('clientHostname', '-')
-            if 'signal' not in row:
-                row['signal'] = 'yes' if row.get('connected') else 'no'
+#         def rowDict(row):
+#             row['cls'] = "signal" if row.get('connected') else "nosignal"
+#             if 'name' not in row:
+#                 row['name'] = row.get('clientHostname', '-')
+#             if 'signal' not in row:
+#                 row['signal'] = 'yes' if row.get('connected') else 'no'
 
-            try:
-                conn = whenConnected(self.settings.mongo, row.get('mac', '??'))
-                row['connectedAgo'] = connectedAgoString(conn)
-            except ValueError:
-                row['connectedAgo'] = 'yes' if row.get('connected') else ''
-            row['router'] = row.get('ssid', '')
-            return row
+#             try:
+#                 conn = whenConnected(self.settings.mongo, row.get('mac', '??'))
+#                 row['connectedAgo'] = connectedAgoString(conn)
+#             except ValueError:
+#                 row['connectedAgo'] = 'yes' if row.get('connected') else ''
+#             row['router'] = row.get('ssid', '')
+#             return row
 
-        self.set_header("Content-Type", "application/xhtml+xml")
-        self.write(
-            pystache.render(
-                open("table.mustache").read(),
-                dict(rows=sorted(map(rowDict, self.settings.poller.lastAddrs),
-                                 key=lambda a: (not a.get('connected'), a.get('name'))))))
-
+#         self.set_header("Content-Type", "application/xhtml+xml")
+#         self.write(
+#             pystache.render(
+#                 open("table.mustache").read(),
+#                 dict(rows=sorted(map(rowDict, self.settings.poller.lastAddrs),
+#                                  key=lambda a: (not a.get('connected'), a.get('name'))))))
 
-class Json(PrettyErrorHandler, cyclone.web.RequestHandler):
+# class Json(PrettyErrorHandler, cyclone.web.RequestHandler):
 
-    def get(self):
-        self.set_header("Content-Type", "application/json")
-        age = time.time() - self.settings.poller.lastPollTime
-        if age > 10:
-            raise ValueError("poll data is stale. age=%s" % age)
-        self.write(json.dumps({"wifi": self.settings.poller.lastAddrs, "dataAge": age}))
-
+#     def get(self):
+#         self.set_header("Content-Type", "application/json")
+#         age = time.time() - self.settings.poller.lastPollTime
+#         if age > 10:
+#             raise ValueError("poll data is stale. age=%s" % age)
+#         self.write(json.dumps({"wifi": self.settings.poller.lastAddrs, "dataAge": age}))
 
 POLL = Summary('poll', 'Time in HTTP poll requests')
 POLL_SUCCESSES = Counter('poll_successes', 'poll success count')
@@ -120,24 +105,26 @@
 MAC_ON_WIFI = Gauge('connected', 'mac addr is currently connected', ['mac'])
 
 
-class Poller(object):
+@dataclass
+class Poller:
+    wifi: Wifi
+    mongo: Collection
+    masterGraph: PatchableGraph
 
-    def __init__(self, wifi: Wifi, mongo: Collection):
-        self.wifi = wifi
-        self.mongo = mongo
+    def __post_init__(self):
         self.lastAddrs = []  # List[SeenNode]
         self.lastWithSignal = []
         self.lastPollTime = 0
 
-    @POLL.time()
-    async def poll(self):
-        try:
-            newAddrs = await self.wifi.getPresentMacAddrs()
-            self.onNodes(newAddrs)
-            POLL_SUCCESSES.inc()
-        except Exception as e:
-            log.error("poll error: %r\n%s", e, traceback.format_exc())
-            POLL_ERRORS.inc()
+    async def poll(self, first_run):
+        with POLL.time():
+            try:
+                newAddrs = await self.wifi.getPresentMacAddrs()
+                self.onNodes(newAddrs)
+                POLL_SUCCESSES.inc()
+            except Exception as e:
+                log.error("poll error: %r\n%s", e, traceback.format_exc())
+                POLL_ERRORS.inc()
 
     def onNodes(self, newAddrs: List[SeenNode]):
         now = int(time.time())
@@ -148,7 +135,7 @@
         for action in actions:
             log.info("action: %s", action)
             action['created'] = datetime.datetime.now(tz.gettz('UTC'))
-            mongo.save(action)
+            self.mongo.insert_one(action)
             MAC_ON_WIFI.labels(mac=action['address'].lower()).set(1 if action['action'] == 'arrive' else 0)
         if now // 3600 > self.lastPollTime // 3600:
             log.info('hourly writes')
@@ -159,7 +146,7 @@
         self.lastAddrs = newAddrs
         self.lastPollTime = now
 
-        self.updateGraph(masterGraph)
+        self.updateGraph(self.masterGraph)
 
     def computeActions(self, newWithSignal):
         actions = []
@@ -216,77 +203,42 @@
                 g.add((s, p, o, ctx))
 
             try:
-                conn = whenConnected(mongo, dev.mac)
+                conn = whenConnected(self.mongo, dev.mac)
             except ValueError:
                 traceback.print_exc()
                 pass
             else:
-                g.add((dev.uri, ROOM['connectedAgo'], Literal(connectedAgoString(conn)), ctx))
                 g.add((dev.uri, ROOM['connected'], Literal(conn), ctx))
         masterGraph.setToGraph(g)
 
 
-class RemoteSuspend(PrettyErrorHandler, cyclone.web.RequestHandler):
-
-    def post(self):
-        # windows is running shutter (https://www.den4b.com/products/shutter)
-        fetch('http://DESKTOP-GOU4AC4:8011/action', postdata={'id': 'Sleep'})
+# class RemoteSuspend(PrettyErrorHandler, cyclone.web.RequestHandler):
 
-
-class Metrics(cyclone.web.RequestHandler):
-
-    def get(self):
-        self.add_header('content-type', 'text/plain')
-        self.write(generate_latest(REGISTRY))
+#     def post(self):
+#         # windows is running shutter (https://www.den4b.com/products/shutter)
+#         fetch('http://DESKTOP-GOU4AC4:8011/action', postdata={'id': 'Sleep'})
 
 
-if __name__ == '__main__':
-    args = docopt.docopt('''
-Usage:
-  wifi.py [options]
-
-Options:
-  -v, --verbose  more logging
-  --port=<n>     serve on port [default: 9070]
-  --poll=<freq>  poll frequency [default: .2]
-''')
-    if args['--verbose']:
-        from twisted.python import log as twlog
-        twlog.startLogging(sys.stdout)
-        log.setLevel(10)
-        log.setLevel(logging.DEBUG)
-
+def main():
+    log.setLevel(logging.INFO)
+    masterGraph = PatchableGraph()
     mongo = Connection('mongodb.default.svc.cluster.local', 27017, tz_aware=True)['visitor']['visitor']
 
     config = ConjunctiveGraph()
     config.parse(open('private_config.n3'), format='n3')
 
-    masterGraph = PatchableGraph()
     wifi = Wifi(config)
-    poller = Poller(wifi, mongo)
-    task.LoopingCall(lambda: ensureDeferred(poller.poll())).start(1 / float(args['--poll']))
+    poller = Poller(wifi, mongo, masterGraph)
+    loop = background_loop.loop_forever(poller.poll, 10)
 
-    reactor.listenTCP(
-        int(args['--port']),
-        cyclone.web.Application(
-            [
-                (r"/", Index),
-                (r"/build/(bundle\.js)", cyclone.web.StaticFileHandler, {
-                    "path": 'build'
-                }),
-                (r'/json', Json),
-                (r'/graph/wifi', CycloneGraphHandler, {
-                    'masterGraph': masterGraph
-                }),
-                (r'/graph/wifi/events', CycloneGraphEventsHandler, {
-                    'masterGraph': masterGraph
-                }),
-                (r'/table', Table),
-                (r'/remoteSuspend', RemoteSuspend),
-                (r'/metrics', Metrics),
-                #(r'/activity', Activity),
-            ],
-            wifi=wifi,
-            poller=poller,
-            mongo=mongo))
-    reactor.run()
+    app = Starlette(routes=[
+        Route('/graph/wifi', StaticGraph(masterGraph)),
+        Route('/graph/wifi/events', GraphEvents(masterGraph)),
+    ],)
+
+    app.add_middleware(PrometheusMiddleware, app_name='environment')
+    app.add_route("/metrics", handle_metrics)
+    return app
+
+
+app = main()