Skip to content

Commit

Permalink
feat: Convert data to prometheus metrics and serve over http.
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewjw committed Oct 9, 2020
1 parent bb779ac commit 0f2a5bd
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 5 deletions.
9 changes: 4 additions & 5 deletions bin/glowprom
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@

import sys

from glowprom import get_arguments, connect

def on_message(client, userdata, msg):
print(str(msg.payload))
from glowprom import get_arguments, connect, serve, update_stats

def main():
args = get_arguments(sys.argv[1:])

connect(args, on_message)
connect(args, update_stats)

serve(args)

if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions glowprom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@
from .arguments import get_arguments
from .exceptions import InvalidArguments
from .mqtt import connect
from .prometheus import prometheus
from .server import serve, update_stats

__version__ = "0.1.1"
110 changes: 110 additions & 0 deletions glowprom/prometheus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# glowprom
# Copyright (C) 2020 Andrew Wilkinson
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import json

# Taken from https://gist.github.com/ndfred/b373eeafc4f5b0870c1b8857041289a9
# Fields gathered from the ZigBee Smart Energy Standard document
# 0702: Metering
# - 00: Reading Information Set
# - 00: CurrentSummationDelivered: meter reading
# - 01: CurrentSummationReceived
# - 02: CurrentMaxDemandDelivered
# - 07: ReadingSnapshotTime (UTC time)
# - 14: Supply Status (enum): 0x2 is on
# - 02: Meter Status
# - 00: Status (bit map): 10 means power quality event
# - 03: Formatting
# - 00: UnitofMeasure (enum): 00 means kWh, 01 means m3
# - 01: Multiplier
# - 02: Divisor
# - 03: SummationFormatting (bit map):
# 2B means 3 digits after the decimal point, 2 digits before.
# FB means 3 digits after the decimal point, 16 digits before.
# no leading zeros
# - 04: DemandFormatting
# - 06: MeteringDeviceType: 00 means Electric Metering,
# 80 means Mirrored Gas Metering
# - 07: SiteID: MPAN encoded in UTF-8
# - 08: MeterSerialNumber (string)
# - 12: AlternativeUnitofMeasure (enum)
# - 04: Historical Consumption
# - 00: InstantaneousDemand (signed): current consumption
# - 01: CurrentDayConsumptionDelivered
# - 30: CurrentWeekConsumptionDelivered
# - 40: CurrentMonthConsumptionDelivered
# - 0C: Alternative Historical Consumption
# - 01: CurrentDayConsumptionDelivered
# - 30: CurrentWeekConsumptionDelivered
# - 40: CurrentMonthConsumptionDelivered
# 0705: Prepayment
# - 00: Prepayment Information Set
# - 00: PaymentControlConfiguration (bit map)
# - 01: CreditRemaining (signed)
# 0708: Device Management
# - 01: Supplier Control Attribute Set
# - 01: ProviderName (string)


def prometheus(msg):
# Code adapted from
# https://gist.github.com/ndfred/b373eeafc4f5b0870c1b8857041289a9
payload = json.loads(msg.payload)

elecMtr = payload["elecMtr"]["0702"]

elec_consumption = int(elecMtr["04"]["00"], 16)
elec_daily_consumption = int(elecMtr["04"]["01"], 16)
elec_weekly_consumption = int(elecMtr["04"]["30"], 16)
elec_monthly_consumption = int(elecMtr["04"]["40"], 16)
elec_multiplier = int(elecMtr["03"]["01"], 16)
elec_divisor = float(int(elecMtr["03"]["02"], 16))
elec_meter = int(elecMtr["00"]["00"], 16)

gasMtr = payload["gasMtr"]["0702"]

gas_daily_consumption = int(gasMtr["0C"]["01"], 16)
gas_weekly_consumption = int(gasMtr["0C"]["30"], 16)
gas_monthly_consumption = int(gasMtr["0C"]["40"], 16)
gas_multiplier = int(gasMtr["03"]["01"], 16)
gas_divisor = float(int(gasMtr["03"]["02"], 16))
gas_meter = int(gasMtr["00"]["00"], 16)

elec_daily_consumption = elec_daily_consumption * \
elec_multiplier / elec_divisor
elec_weekly_consumption = elec_weekly_consumption * \
elec_multiplier / elec_divisor
electricity_monthly_consumption = elec_monthly_consumption * \
elec_multiplier / elec_divisor
electricity_meter = elec_meter * elec_multiplier / elec_divisor
gas_daily_consumption = gas_daily_consumption * \
gas_multiplier / gas_divisor
gas_weekly_consumption = gas_weekly_consumption * \
gas_multiplier / gas_divisor
gas_monthly_consumption = gas_monthly_consumption * \
gas_multiplier / gas_divisor
gas_meter = gas_meter * gas_multiplier / gas_divisor

return f"""
consumption{{type='electricity',period='daily'}} {elec_daily_consumption}
consumption{{type='electricity',period='weekly'}} {elec_weekly_consumption}
consumption{{type='electricity',period='monthly'}} {elec_monthly_consumption}
meter{{type='electricity'}} {electricity_meter}
consumption{{type='gas',period='daily'}} {gas_daily_consumption}
consumption{{type='gas',period='weekly'}} {gas_weekly_consumption}
consumption{{type='gas',period='monthly'}} {gas_monthly_consumption}
meter{{type='gas'}} {gas_meter}
"""
67 changes: 67 additions & 0 deletions glowprom/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# glowprom
# Copyright (C) 2020 Andrew Wilkinson
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from datetime import datetime
import http.server

from .prometheus import prometheus


STATS = None


class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/":
self.send_index()
elif self.path == "/metrics":
self.send_metrics()
else:
self.send_error(404)

def send_index(self):
self.send_response(200)
self.end_headers()
self.wfile.write("""
<html>
<head><title>Glow Prometheus</title></head>
<body>
<h1>Glow Prometheus</h1>
<p><a href="/metrics">Metrics</a></p>
</body>
</html>""".encode("utf8"))

def send_metrics(self):
if STATS is None:
self.send_response(404)
self.end_headers()
else:
self.send_response(200)
self.end_headers()
self.wfile.write(STATS.encode("utf8"))


def serve(args): # pragma: no cover
server = http.server.HTTPServer(args.bind, Handler)
server.serve_forever()


def update_stats(client, userdata, msg):
global STATS
if msg is None:
STATS = None
else:
STATS = prometheus(msg)
2 changes: 2 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@

from .test_arguments import TestArguments
from .test_mqtt import TestMQTT
from .test_prometheus import TestPrometheus
from .test_server import TestServer
1 change: 1 addition & 0 deletions tests/test_message.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"elecMtr":{"0702":{"03":{"01":"00000001","04":"00","02":"000003E8","07":"1023497978063","03":"2B","08":"","00":"00","06":"00"},"00":{"07":"00000000","01":"000000000000","00":"0000004D64CC","14":"02","02":"000000000000"},"04":{"01":"001F23","40":"011C3C","30":"004A12","00":"000009F3"},"02":{"00":"00"}},"0705":{"00":{"01":"00000000","00":"0C94"}},"0708":{"01":{"01":"NPOWER NORTHERN"}}},"gasMtr":{"0702":{"03":{"01":"00000001","12":"00","02":"000003E8","07":"7443608110","03":"2B","08":"","00":"01","06":"80"},"00":{"00":"00000022D37B","14":"02"},"0C":{"01":"009A1C","40":"0440B4","30":"015DF5"},"02":{"00":"00"}},"0705":{"00":{"01":"00000000","00":"0C94"}},"0708":{"01":{"01":""}}},"ts":"2020-10-06 20:53:16","hversion":"GLOW-IHD-01-1v4-SMETS2","time":"5F7CD93C","zbSoftVer":"1.2.5","gmtime":1602017596,"pan":{"rssi":"BF","status":"joined","nPAN":"00","join":"0","lqi":"8C"},"smetsVer":"SMETS2","ets":"2000-01-01 00:00:00","gid":"0123456789ABCDEF"}
35 changes: 35 additions & 0 deletions tests/test_prometheus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# glowprom
# Copyright (C) 2020 Andrew Wilkinson
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import os
import unittest

from glowprom import prometheus


MESSAGE_TEXT = open("tests/test_message.txt", "rb").read()


class MockMessage:
payload = MESSAGE_TEXT


class TestPrometheus(unittest.TestCase):
def test_prometheus(self):
prom = prometheus(MockMessage())

self.assertIn("consumption{type='electricity',period='daily'} 7.971",
prom)
77 changes: 77 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# glowprom
# Copyright (C) 2020 Andrew Wilkinson
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import io
import json
from datetime import datetime, timedelta
import unittest

from glowprom.server import Handler, update_stats

MESSAGE_TEXT = open("tests/test_message.txt", "rb").read()


class MockMessage:
payload = MESSAGE_TEXT


class MockHandler(Handler):
def __init__(self):
self.wfile = io.BytesIO()
self.requestline = "GET"
self.client_address = ("127.0.0.1", 8000)
self.request_version = "1.0"
self.command = "GET"


class TestServer(unittest.TestCase):
def setUp(self):
update_stats(None, None, None)

def test_index(self):
handler = MockHandler()
handler.path = "/"
handler.do_GET()

handler.wfile.seek(0)
self.assertTrue("/metrics" in handler.wfile.read().decode("utf8"))

def test_error(self):
handler = MockHandler()
handler.path = "/error"
handler.do_GET()

handler.wfile.seek(0)
self.assertTrue("404" in handler.wfile.read().decode("utf8"))

def test_metrics(self):
update_stats(None, None, MockMessage())

handler = MockHandler()
handler.path = "/metrics"
handler.do_GET()

handler.wfile.seek(0)
self.assertTrue(
"consumption" in handler.wfile.read().decode("utf8"))

def test_metrics_before_update(self):
handler = MockHandler()
handler.path = "/metrics"
handler.do_GET()

handler.wfile.seek(0)
self.assertTrue("404" in handler.wfile.read().decode("utf8"))

0 comments on commit 0f2a5bd

Please sign in to comment.