view service/powerEagle/reader.py @ 1714:4cbe3df8f48f

rewrite to use starlette/etc
author drewp@bigasterisk.com
date Sun, 24 Apr 2022 02:15:30 -0700
parents e8654a3bd1c7
children fb082013fa24
line wrap: on
line source

import asyncio
import binascii
import json
import logging
from typing import Dict

import aiohttp
from patchablegraph import PatchableGraph
from patchablegraph.handler import GraphEvents, StaticGraph
from prometheus_client import Gauge, Summary
from rdflib import Literal, Namespace
from starlette.applications import Starlette
from starlette.routing import Route
from starlette_exporter import PrometheusMiddleware, handle_metrics

import background_loop
from private_config import cloudId, deviceIp, installId, macId, periodSec

ROOM = Namespace("http://projects.bigasterisk.com/room/")

logging.basicConfig(level=logging.INFO)
log = logging.getLogger()

authPlain = cloudId + ':' + installId
auth = binascii.b2a_base64(authPlain.encode('ascii')).strip(b'=\n')

STAT_UPDATE_UP = Gauge('background_loop_up', 'not erroring')
STAT_UPDATE_CALLS = Summary('background_loop_calls', 'calls')


class Poller(object):

    def __init__(self, out: Dict[str, Gauge], graph):
        self.out = out
        self.graph = graph

    async def poll(self, first: bool):
        url = (f'http://{deviceIp}/cgi-bin/cgi_manager')

        async with aiohttp.ClientSession() as session:
            async with session.post(url,
                                    headers={'Authorization': 'Basic %s' % auth.decode('ascii')},
                                    data=(f'''<LocalCommand>
                                                <Name>get_usage_data</Name>
                                                <MacId>0x{macId}</MacId>
                                            </LocalCommand>
                                            <LocalCommand>
                                                <Name>get_price_blocks</Name>
                                                <MacId>0x{macId}</MacId>
                                            </LocalCommand>'''),
                                    timeout=10) as response:

                ret = json.loads(await response.text())
                log.debug(f"response body {ret}")
                if ret['demand_units'] != 'kW':
                    raise ValueError
                if ret['summation_units'] != 'kWh':
                    raise ValueError

                demandW = float(ret['demand']) * 1000
                self.out['w'].set(demandW)

                sd = float(ret['summation_delivered'])
                if sd > 0:  # Sometimes nan
                    self.out['kwh'].set(sd)

                if 'price' in ret:
                    self.out['price'].set(float(ret['price']))

                self.graph.patchObject(context=ROOM['powerEagle'],
                                       subject=ROOM['housePower'],
                                       predicate=ROOM['instantDemandWatts'],
                                       newObject=Literal(demandW))


def main():
    masterGraph = PatchableGraph()

    out = {
        'w': Gauge('house_power_w', 'house power demand'),
        'kwh': Gauge('house_power_kwh', 'house power sum delivered'),
        'price': Gauge('house_power_price', 'house power price'),
    }

    p = Poller(out, masterGraph)

    # todo: background_loop isn't trying to maintain a goal of periodSec
    asyncio.create_task(background_loop.loop_forever(p.poll, periodSec, STAT_UPDATE_UP, STAT_UPDATE_CALLS))

    app = Starlette(debug=True,
                    routes=[
                        Route('/graph/power', StaticGraph(masterGraph)),
                        Route('/graph/power/events', GraphEvents(masterGraph)),
                    ])

    app.add_middleware(PrometheusMiddleware, app_name='power_eagle')
    app.add_route("/metrics", handle_metrics)
    return app


app = main()