From 53d8c3ad2b0d787bd76d63288445e7bd3be0a933 Mon Sep 17 00:00:00 2001 From: onsmam Date: Tue, 10 May 2022 15:34:13 +0200 Subject: [PATCH 1/4] MQTT integration with Dutch P1 meter DSMR-reader MQTT integration with Dutch P1 meter DSMR-reader specific configuration --- devices/mqttP1Dsmr-reader.py | 235 +++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 devices/mqttP1Dsmr-reader.py diff --git a/devices/mqttP1Dsmr-reader.py b/devices/mqttP1Dsmr-reader.py new file mode 100644 index 0000000..4634136 --- /dev/null +++ b/devices/mqttP1Dsmr-reader.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""P1 meter device for solaredge_meterproxy + + This file is indended to be used with https://github.com/nmakel/solaredge_meterproxy by nmakel + and is to be stored in the devices folder + + It consumes MQTT messages with meterdata from the P1 meter created with https://github.com/marcelrv/p1-reader + +""" + +from __future__ import division +from collections import deque + +import logging +import sys +import time +import json +import paho.mqtt.client as mqtt +from datetime import datetime + +__author__ = ["Marcel Verpaalen"] +__version__ = "1.0" +__copyright__ = "Copyright 2022, Marcel Verpaalen" +__license__ = "GPL" +__credits__ = ["NMakel"] + +class MovingAverage(object): + def __init__(self, size): + """ + Initialize your data structure here. + :type size: int + """ + self.queue = deque(maxlen=size) + + def next(self, val): + """ + :type val: int + :rtype: float + """ + self.queue.append(val) + return sum(self.queue) / len(self.queue) + + +lastValues = {} +logger = logging.getLogger() + +# 1 measurement/message every 5 sec -> 15min demand average +demandAvg = MovingAverage(180) +demandL1Avg = MovingAverage(180) +demandL2Avg = MovingAverage(180) +demandL3Avg = MovingAverage(180) + +def on_connect(client, userdata, flags, rc): + logger.info( + f"MQTT connected to {userdata['host']}:{userdata['port']} - topic: '{userdata['meterValuesTopic']}' with result code {rc}.") + client.subscribe(userdata["meterValuesTopic"]) + if userdata['willTopic'] is not None: + client.publish(userdata['willTopic'], "MeterProxy Connected " + + str(datetime.now().strftime("%d/%m/%Y %H:%M:%S"))) + + +def on_message(client, userdata, message): + global lastValues +# logger.debug("Dump variable %s " % json.dumps( userdata, indent=4, sort_keys=True)) + decoded_message = str(message.payload.decode("utf-8")) + lastValues = json.loads(decoded_message) +# print(lastValues['powerImportedActual'], type(lastValues['powerImportedActual'])) +# print(float(lastValues['powerImportedActual']), type(float(lastValues['powerImportedActual']))) +# print(lastValues['powerExportedActual'], type(lastValues['powerExportedActual'])) + + # Calc net power average + lastValues ['demand_power_active'] = demandAvg.next ( (float(lastValues['powerImportedActual']) * 1000) - (float(lastValues['powerExportedActual']) * 1000) ) + lastValues ['l1_demand_power_active'] = demandL1Avg.next( (float(lastValues['instantaneousActivePowerL1Plus']) * 1000) - (float(lastValues['instantaneousActivePowerL1Min']) * 1000) ) + lastValues ['l2_demand_power_active'] = demandL2Avg.next( (float(lastValues['instantaneousActivePowerL2Plus']) * 1000) - (float(lastValues['instantaneousActivePowerL2Min']) * 1000) ) + lastValues ['l3_demand_power_active'] = demandL3Avg.next( (float(lastValues['instantaneousActivePowerL3Plus']) * 1000) - (float(lastValues['instantaneousActivePowerL3Min']) * 1000) ) + logger.debug(F'Received message in {message.topic}, avg demand: {lastValues ["demand_power_active"]}') + +def on_disconnect(client, userdata, rc): + if rc != 0: + logger.info(F"Unexpected MQTT disconnection, with result code {rc}.") + + +def device(config): + + # Configuration parameters: + # + # host ip or hostname of MQTT server + # port port of MQTT server + # keepalive keepalive time in seconds for MQTT server + # meterValuesTopic MQTT topic to subscribe to to receive meter values + + host = config.get("host", fallback="localhost") + port = config.getint("port", fallback=1883) + keepalive = config.getint("keepalive", fallback=60) + meterValuesTopic = config.get("meterValuesTopic", fallback="meter") + willTopic = config.get("willTopic", fallback=None) + willMsg = config.get("willMsg", fallback="MeterProxy Disconnected") + + topics = { + "host": host, + "port": port, + "meterValuesTopic": meterValuesTopic, + "willTopic": willTopic + } + + try: + client = mqtt.Client(userdata=topics) + client.on_connect = on_connect + client.on_message = on_message + client.on_disconnect = on_disconnect + if willTopic is not None: + client.will_set(willTopic, payload=willMsg, qos=0, retain=False) + client.connect(host, port, keepalive) + client.loop_start() + logger.debug( + f"Started MQTT connection to server - topic: {host}:{port} - {meterValuesTopic}") + except: + logger.critical( + f"MQTT connection failed: {host}:{port} - {meterValuesTopic}") + + return { + "client": client, + "host": host, + "port": port, + "keepalive": keepalive, + "meterValuesTopic": meterValuesTopic, + "willTopic": willTopic, + "willMsg": willMsg + } + + +def values(device): + if not device: + return {} + global lastValues + submitValues = {} + + submitValues['l1n_voltage'] = float(lastValues['instantaneousVoltageL1']) + submitValues['l2n_voltage'] = float(lastValues['instantaneousVoltageL2']) + submitValues['l3n_voltage'] = float(lastValues['instantaneousVoltageL3']) + submitValues['voltage_ln'] = float(lastValues['instantaneousVoltageL1']) + submitValues['frequency'] = 50 + + submitValues ['power_active'] = (float(lastValues['powerImportedActual']) * 1000) - (float(lastValues['powerExportedActual'])* 1000) + submitValues ['l1_power_active']= (float(lastValues['instantaneousActivePowerL1Plus']) * 1000) - (float(lastValues['instantaneousActivePowerL1Min']) * 1000) + submitValues ['l2_power_active']= (float(lastValues['instantaneousActivePowerL2Plus']) * 1000) - (float(lastValues['instantaneousActivePowerL2Min']) * 1000) + submitValues ['l3_power_active']= (float(lastValues['instantaneousActivePowerL3Plus']) * 1000) - (float(lastValues['instantaneousActivePowerL3Min']) * 1000) + + P1Current=False + if (P1Current): + submitValues['l1_current'] = float(lastValues[ 'instantaneousCurrentL1']) + submitValues['l2_current'] = float(lastValues[ 'instantaneousCurrentL2']) + submitValues['l3_current'] = float(lastValues[ 'instantaneousCurrentL3']) + else: + #calculate current as P1 provided current is rounded to integers + submitValues['l1_current'] = abs ( submitValues ['l1_power_active'] ) / float(lastValues['instantaneousVoltageL1']) + submitValues['l2_current'] = abs ( submitValues ['l2_power_active'] ) / float(lastValues['instantaneousVoltageL2']) + submitValues['l3_current'] = abs ( submitValues ['l3_power_active'] ) / float(lastValues['instantaneousVoltageL3']) + + submitValues ['demand_power_active'] = lastValues ['demand_power_active'] + submitValues ['l1_demand_power_active'] = lastValues ['l1_demand_power_active'] + submitValues ['l2_demand_power_active'] = lastValues ['l2_demand_power_active'] + submitValues ['l3_demand_power_active'] = lastValues ['l3_demand_power_active'] + + submitEnergy=True + if (submitEnergy): + submitValues['import_energy_active'] = float(lastValues['electricityImportedT1']) + float(lastValues['electricityImportedT2']) + submitValues['export_energy_active'] = float(lastValues['electricityExportedT1']) + float(lastValues['electricityExportedT2']) + # submitValues ["l1_import_energy_active"] = lastValues['import_energy_active'] + # submitValues ["l1_export_energy_active"] = lastValues["export_energy_active"] + submitValues['energy_active'] = submitValues['import_energy_active'] - submitValues['export_energy_active'] + submitValues["_input"] = lastValues + + logger.debug("Dump values %s " % json.dumps( submitValues, indent=4, sort_keys=True)) + + return submitValues + + # MQTT input is a json with one or more of the below elements + # "energy_active" + # "import_energy_active" + # "power_active" + # "l1_power_active" + # "l2_power_active" + # "l3_power_active" + # "voltage_ln" + # "l1n_voltage" + # "l2n_voltage" + # "l3n_voltage" + # "voltage_ll" + # "l12_voltage" + # "l23_voltage" + # "l31_voltage" + # "frequency" + # "l1_energy_active" + # "l2_energy_active" + # "l3_energy_active" + # "l1_import_energy_active" + # "l2_import_energy_active" + # "l3_import_energy_active" + # "export_energy_active" + # "l1_export_energy_active" + # "l2_export_energy_active" + # "l3_export_energy_active" + # "energy_reactive" + # "l1_energy_reactive" + # "l2_energy_reactive" + # "l3_energy_reactive" + # "energy_apparent" + # "l1_energy_apparent" + # "l2_energy_apparent" + # "l3_energy_apparent" + # "power_factor" + # "l1_power_factor" + # "l2_power_factor" + # "l3_power_factor" + # "power_reactive" + # "l1_power_reactive" + # "l2_power_reactive" + # "l3_power_reactive" + # "power_apparent" + # "l1_power_apparent" + # "l2_power_apparent" + # "l3_power_apparent" + # "l1_current" + # "l2_current" + # "l3_current" + # "demand_power_active" + # "minimum_demand_power_active" + # "maximum_demand_power_active" + # "demand_power_apparent" + # "l1_demand_power_active" + # "l2_demand_power_active" + # "l3_demand_power_active" From 5d55451e7a679c48771c06342261713075aa5cc7 Mon Sep 17 00:00:00 2001 From: onsmam Date: Tue, 10 May 2022 16:00:01 +0200 Subject: [PATCH 2/4] Update mqttP1Dsmr-reader.py --- devices/mqttP1Dsmr-reader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devices/mqttP1Dsmr-reader.py b/devices/mqttP1Dsmr-reader.py index 4634136..775f3fe 100644 --- a/devices/mqttP1Dsmr-reader.py +++ b/devices/mqttP1Dsmr-reader.py @@ -20,11 +20,11 @@ import paho.mqtt.client as mqtt from datetime import datetime -__author__ = ["Marcel Verpaalen"] +__author__ = ["Jacques Mulders"] __version__ = "1.0" __copyright__ = "Copyright 2022, Marcel Verpaalen" __license__ = "GPL" -__credits__ = ["NMakel"] +__credits__ = ["NMakel", "Marcel Verpaalen"] class MovingAverage(object): def __init__(self, size): From 1a97841aaf2cf4f8f8d7cb021af50bcd2efd0480 Mon Sep 17 00:00:00 2001 From: onsmam Date: Wed, 11 May 2022 09:12:05 +0200 Subject: [PATCH 3/4] added DSMR reader readme --- README.md | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 91e3402..169be2e 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,56 @@ The `values()` function is called every `refresh_rate` seconds. It gets passed t Single phase devices should put the single phase values in the generic _and_ first phase specific values, for example: `power_active` and `p1_power_active`, but also `voltage_ln` and `p1n_voltage`. +### DSMR Reader + +if you are already using DSMR reader you can export the data via mqtt. +in DSMR reader go to the configuration screen, MQTT section, (Data source) Telegram: JSON. +the minimum needed; + +``` +[mapping] +# READING FIELD = JSON FIELD +electricity_delivered_1 = electricityImportedT1 +electricity_returned_1 = electricityExportedT1 +electricity_delivered_2 = electricityImportedT2 +electricity_returned_2 = electricityExportedT2 +electricity_currently_delivered = powerImportedActual +electricity_currently_returned = powerExportedActual +phase_voltage_l1 = instantaneousVoltageL1 +phase_voltage_l2 = instantaneousVoltageL2 +phase_voltage_l3 = instantaneousVoltageL3 +phase_power_current_l1 = instantaneousCurrentL1 +phase_power_current_l2 = instantaneousCurrentL2 +phase_power_current_l3 = instantaneousCurrentL3 +phase_currently_delivered_l1 = instantaneousActivePowerL1Plus +phase_currently_delivered_l2 = instantaneousActivePowerL2Plus +phase_currently_delivered_l3 = instantaneousActivePowerL3Plus +phase_currently_returned_l1 = instantaneousActivePowerL1Min +phase_currently_returned_l2 = instantaneousActivePowerL2Min +phase_currently_returned_l3 = instantaneousActivePowerL3Min +``` + +this json message will be send, when activated, on the 'dsmr/json' topic, this topic can be adjusted. +i'm using a [waveshare usb to rs485/modbus module](https://www.waveshare.com/usb-to-rs485.htm) + +config example; + +``` +[server] +device = /dev/ttyUSB0 +baud = 9600 +timeout = 0.1 +log_level = INFO +meters = meter1 + +[meter1] +type=mqttP1Dsmr-reader +host= +port= +meterValuesTopic=dsmr/json +willTopic=dsmr/will +``` ## Contributing -Contributions are more than welcome, especially new device scripts, or modifications which broaden the use case of this tool. \ No newline at end of file +Contributions are more than welcome, especially new device scripts, or modifications which broaden the use case of this tool. From 0792d5a64b89e0ceb52ab006b04eab255655583d Mon Sep 17 00:00:00 2001 From: onsmam Date: Wed, 11 May 2022 09:15:20 +0200 Subject: [PATCH 4/4] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 169be2e..1b4ef34 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ Single phase devices should put the single phase values in the generic _and_ fir ### DSMR Reader -if you are already using DSMR reader you can export the data via mqtt. +if you are already using [DSMR reader](https://dsmr-reader.readthedocs.io/) you can export the data via mqtt. in DSMR reader go to the configuration screen, MQTT section, (Data source) Telegram: JSON. the minimum needed;