Mercurial > code > home > repos > homeauto
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()