diff --git a/bin/glowprom b/bin/glowprom
index 67cb1cd..750ac54 100644
--- a/bin/glowprom
+++ b/bin/glowprom
@@ -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()
diff --git a/glowprom/__init__.py b/glowprom/__init__.py
index 07071b4..b4803dc 100644
--- a/glowprom/__init__.py
+++ b/glowprom/__init__.py
@@ -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"
diff --git a/glowprom/prometheus.py b/glowprom/prometheus.py
new file mode 100644
index 0000000..5f9d1c3
--- /dev/null
+++ b/glowprom/prometheus.py
@@ -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 .
+
+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}
+"""
diff --git a/glowprom/server.py b/glowprom/server.py
new file mode 100644
index 0000000..fd12982
--- /dev/null
+++ b/glowprom/server.py
@@ -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 .
+
+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("""
+
+
Glow Prometheus
+
+Glow Prometheus
+Metrics
+
+""".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)
diff --git a/tests/__init__.py b/tests/__init__.py
index 69cf3da..00b41ee 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -16,3 +16,5 @@
from .test_arguments import TestArguments
from .test_mqtt import TestMQTT
+from .test_prometheus import TestPrometheus
+from .test_server import TestServer
diff --git a/tests/test_message.txt b/tests/test_message.txt
new file mode 100644
index 0000000..10e3f04
--- /dev/null
+++ b/tests/test_message.txt
@@ -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"}
diff --git a/tests/test_prometheus.py b/tests/test_prometheus.py
new file mode 100644
index 0000000..f72724f
--- /dev/null
+++ b/tests/test_prometheus.py
@@ -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 .
+
+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)
diff --git a/tests/test_server.py b/tests/test_server.py
new file mode 100644
index 0000000..542673d
--- /dev/null
+++ b/tests/test_server.py
@@ -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 .
+
+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"))