Skip to content

Commit 7094361

Browse files
authored
Merge pull request #218 from tomquist/mqtt-powermeter
Add support for MQTT powermeter
2 parents df8a372 + 0974d0f commit 7094361

4 files changed

Lines changed: 150 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## V 1.97
4+
### script
5+
* add support for MQTT meter and intermediate meter
6+
### config
7+
* add `[SELECT_POWERMETER]`: `USE_MQTT`
8+
* add `[SELECT_INTERMEDIATE_METER]`: `USE_MQTT_INTERMEDIATE`
9+
* add section `[MQTT_POWERMETER]`
10+
* add section `[MQTT_INTERMEDIATE_METER]`
11+
312
## V 1.96
413
### script
514
* bugfix: value of HOY_BATTERY_AVERAGE_CNT was ignored

HoymilesZeroExport.py

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

1717
__author__ = "Tobias Kraft"
18-
__version__ = "1.96"
18+
__version__ = "1.97"
1919

2020
import time
2121
from requests.sessions import Session
@@ -33,6 +33,7 @@
3333
import argparse
3434
import subprocess
3535
from config_provider import ConfigFileConfigProvider, MqttHandler, ConfigProviderChain
36+
import json
3637

3738
session = Session()
3839
logging.basicConfig(
@@ -1172,6 +1173,88 @@ def GetPowermeterWatts(self):
11721173
return CastToInt(power)
11731174

11741175

1176+
def extract_json_value(data, path):
1177+
from jsonpath_ng import parse
1178+
jsonpath_expr = parse(path)
1179+
match = jsonpath_expr.find(data)
1180+
if match:
1181+
return int(float(match[0].value))
1182+
else:
1183+
raise ValueError("No match found for the JSON path")
1184+
1185+
1186+
class MqttPowermeter(Powermeter):
1187+
def __init__(
1188+
self,
1189+
broker: str,
1190+
port: int,
1191+
topic_incoming: str,
1192+
json_path_incoming: str = None,
1193+
topic_outgoing: str = None,
1194+
json_path_outgoing: str = None,
1195+
username: str = None,
1196+
password: str = None,
1197+
):
1198+
self.broker = broker
1199+
self.port = port
1200+
self.topic_incoming = topic_incoming
1201+
self.json_path_incoming = json_path_incoming
1202+
self.topic_outgoing = topic_outgoing
1203+
self.json_path_outgoing = json_path_outgoing
1204+
self.username = username
1205+
self.password = password
1206+
self.value_incoming = None
1207+
self.value_outgoing = None
1208+
1209+
# Initialize MQTT client
1210+
import paho.mqtt.client as mqtt
1211+
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
1212+
if self.username and self.password:
1213+
self.client.username_pw_set(self.username, self.password)
1214+
self.client.on_connect = self.on_connect
1215+
self.client.on_message = self.on_message
1216+
1217+
# Connect to the broker
1218+
self.client.connect(self.broker, self.port)
1219+
self.client.loop_start()
1220+
1221+
def on_connect(self, client, userdata, flags, reason_code, properties):
1222+
logger.info(f"Connected with result code {reason_code}")
1223+
# Subscribe to the topics
1224+
client.subscribe(self.topic_incoming)
1225+
logger.info(f"Subscribed to topic {self.topic_incoming}")
1226+
if self.topic_outgoing and self.topic_outgoing != self.topic_incoming:
1227+
client.subscribe(self.topic_outgoing)
1228+
logger.info(f"Subscribed to topic {self.topic_outgoing}")
1229+
1230+
def on_message(self, client, userdata, msg):
1231+
payload = msg.payload.decode()
1232+
try:
1233+
data = json.loads(payload)
1234+
if msg.topic == self.topic_incoming:
1235+
self.value_incoming = extract_json_value(data, self.json_path_incoming) if self.json_path_incoming else int(float(payload))
1236+
logger.info('MQTT: Incoming power: %s Watt', self.value_incoming)
1237+
elif msg.topic == self.topic_outgoing:
1238+
self.value_outgoing = extract_json_value(data, self.json_path_outgoing) if self.json_path_outgoing else int(float(payload))
1239+
logger.info('MQTT: Outgoing power: %s Watt', self.value_outgoing)
1240+
except json.JSONDecodeError:
1241+
print("Failed to decode JSON")
1242+
1243+
def GetPowermeterWatts(self):
1244+
if self.value_incoming is None:
1245+
self.wait_for_message("incoming")
1246+
if self.topic_outgoing and self.value_outgoing is None:
1247+
self.wait_for_message("outgoing")
1248+
1249+
return self.value_incoming - (self.value_outgoing if self.value_outgoing is not None else 0)
1250+
1251+
def wait_for_message(self, message_type, timeout=5):
1252+
start_time = time.time()
1253+
while (message_type == "incoming" and self.value_incoming is None) or (message_type == "outgoing" and self.value_outgoing is None):
1254+
if time.time() - start_time > timeout:
1255+
raise TimeoutError(f"Timeout waiting for MQTT {message_type} message")
1256+
time.sleep(1)
1257+
11751258
def CreatePowermeter() -> Powermeter:
11761259
shelly_ip = config.get('SHELLY', 'SHELLY_IP')
11771260
shelly_user = config.get('SHELLY', 'SHELLY_USER')
@@ -1244,6 +1327,17 @@ def CreatePowermeter() -> Powermeter:
12441327
return AmisReader(
12451328
config.get('AMIS_READER', 'AMIS_READER_IP')
12461329
)
1330+
elif config.getboolean('SELECT_POWERMETER', 'USE_MQTT'):
1331+
return MqttPowermeter(
1332+
config.get('MQTT_POWERMETER', 'MQTT_BROKER', fallback=config.get("MQTT_CONFIG", "MQTT_BROKER", fallback=None)),
1333+
config.getint('MQTT_POWERMETER', 'MQTT_PORT', fallback=config.getint("MQTT_CONFIG", "MQTT_PORT", fallback=1883)),
1334+
config.get('MQTT_POWERMETER', 'MQTT_TOPIC_INCOMING'),
1335+
config.get('MQTT_POWERMETER', 'MQTT_JSON_PATH_INCOMING', fallback=None),
1336+
config.get('MQTT_POWERMETER', 'MQTT_TOPIC_OUTGOING', fallback=None),
1337+
config.get('MQTT_POWERMETER', 'MQTT_JSON_PATH_OUTGOING', fallback=None),
1338+
config.get('MQTT_POWERMETER', 'MQTT_USERNAME', fallback=config.get('MQTT_CONFIG', 'MQTT_USERNAME', fallback=None)),
1339+
config.get('MQTT_POWERMETER', 'MQTT_PASSWORD', fallback=config.get('MQTT_CONFIG', 'MQTT_PASSWORD', fallback=None))
1340+
)
12471341
else:
12481342
raise Exception("Error: no powermeter defined!")
12491343

@@ -1325,7 +1419,18 @@ def CreateIntermediatePowermeter(dtu: DTU) -> Powermeter:
13251419
config.get('INTERMEDIATE_SCRIPT', 'SCRIPT_IP_INTERMEDIATE'),
13261420
config.get('INTERMEDIATE_SCRIPT', 'SCRIPT_USER_INTERMEDIATE'),
13271421
config.get('INTERMEDIATE_SCRIPT', 'SCRIPT_PASS_INTERMEDIATE')
1328-
)
1422+
)
1423+
elif config.getboolean('SELECT_INTERMEDIATE_METER', 'USE_MQTT_INTERMEDIATE'):
1424+
return MqttPowermeter(
1425+
config.get('INTERMEDIATE_MQTT', 'MQTT_BROKER', fallback=config.get("MQTT_CONFIG", "MQTT_BROKER", fallback=None)),
1426+
config.getint('INTERMEDIATE_MQTT', 'MQTT_PORT', fallback=config.getint("MQTT_CONFIG", "MQTT_PORT", fallback=1883)),
1427+
config.get('INTERMEDIATE_MQTT', 'MQTT_TOPIC_INCOMING'),
1428+
config.get('INTERMEDIATE_MQTT', 'MQTT_JSON_PATH_INCOMING', fallback=None),
1429+
config.get('INTERMEDIATE_MQTT', 'MQTT_TOPIC_OUTGOING', fallback=None),
1430+
config.get('INTERMEDIATE_MQTT', 'MQTT_JSON_PATH_OUTGOING', fallback=None),
1431+
config.get('INTERMEDIATE_MQTT', 'MQTT_USERNAME', fallback=config.get("MQTT_CONFIG", "MQTT_USERNAME", fallback=None)),
1432+
config.get('INTERMEDIATE_MQTT', 'MQTT_PASSWORD', fallback=config.get("MQTT_CONFIG", "MQTT_PASSWORD", fallback=None))
1433+
)
13291434
elif config.getboolean('SELECT_INTERMEDIATE_METER', 'USE_AMIS_READER_INTERMEDIATE'):
13301435
return AmisReader(
13311436
config.get('INTERMEDIATE_AMIS_READER', 'AMIS_READER_IP_INTERMEDIATE')

HoymilesZeroExport_Config.ini

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
# ---------------------------------------------------------------------
2020

2121
[VERSION]
22-
VERSION = 1.95
22+
VERSION = 1.97
2323
[SELECT_DTU]
2424
# --- define your DTU (only one) ---
2525
USE_AHOY = false
@@ -38,6 +38,7 @@ USE_HOMEASSISTANT = false
3838
USE_VZLOGGER = false
3939
USE_SCRIPT = false
4040
USE_AMIS_READER = false
41+
USE_MQTT = false
4142

4243
[AHOY_DTU]
4344
# --- defines for AHOY-DTU ---
@@ -140,6 +141,21 @@ SCRIPT_PASS =
140141
[AMIS_READER]
141142
AMIS_READER_IP = xxx.xxx.xxx.xxx
142143

144+
[MQTT_POWERMETER]
145+
# --- defines for MQTT ---
146+
# If not specified, uses the broker from the [MQTT_CONFIG] section
147+
# MQTT_BROKER = localhost
148+
# MQTT_USERNAME = user
149+
# MQTT_PASSWORD = password
150+
# MQTT_PORT = 1883
151+
MQTT_TOPIC_INCOMING = powermeter/in/power
152+
# Optional: If the data published to the incoming topic is in JSON format, you can specify the JSONPath to the value here
153+
# MQTT_JSON_PATH_INCOMING = $.power.in
154+
# MQTT_TOPIC_OUTGOING = powermeter/out/power
155+
# Optional: If the data published to the outgoing topic is in JSON format, you can specify the JSONPath to the value here
156+
# MQTT_JSON_PATH_OUTGOING = $.power.out
157+
158+
143159
[SELECT_INTERMEDIATE_METER]
144160
# if you have an intermediate meter ("Zwischenzähler") to measure the outputpower of your inverter you can set it here. It is faster than the DTU current_power value
145161
# --- define your intermediate meter - if you don´t have one set the following defines to false to use the value from your DTU---
@@ -157,6 +173,7 @@ USE_HOMEASSISTANT_INTERMEDIATE = false
157173
USE_VZLOGGER_INTERMEDIATE = false
158174
USE_SCRIPT_INTERMEDIATE = false
159175
USE_AMIS_READER_INTERMEDIATE = false
176+
USE_MQTT_INTERMEDIATE = false
160177

161178
[INTERMEDIATE_TASMOTA]
162179
# --- defines for Tasmota Smartmeter Modul---
@@ -231,6 +248,20 @@ SCRIPT_PASS_INTERMEDIATE =
231248
[INTERMEDIATE_AMIS_READER]
232249
AMIS_READER_IP_INTERMEDIATE = xxx.xxx.xxx.xxx
233250

251+
[INTERMEDIATE_MQTT]
252+
# --- defines for MQTT ---
253+
# If not specified, uses the broker from the [MQTT_CONFIG] section
254+
# MQTT_BROKER = localhost
255+
# MQTT_USERNAME = user
256+
# MQTT_PASSWORD = password
257+
# MQTT_PORT = 1883
258+
MQTT_TOPIC_INCOMING = powermeter/in/power
259+
# Optional: If the data published to the incoming topic is in JSON format, you can specify the JSONPath to the value here
260+
# MQTT_JSON_PATH_INCOMING = $.power.in
261+
# MQTT_TOPIC_OUTGOING = powermeter/out/power
262+
# Optional: If the data published to the outgoing topic is in JSON format, you can specify the JSONPath to the value here
263+
# MQTT_JSON_PATH_OUTGOING = $.power.out
264+
234265
# Uncomment the following section if you want to use MQTT to dynamically reconfigure some settings while the script is running
235266
# [MQTT_CONFIG]
236267
# MQTT_BROKER = localhost

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ idna==3.4
44
packaging==23.2
55
requests==2.31.0
66
urllib3==2.1.0
7-
paho-mqtt==2.0.0
7+
paho-mqtt==2.0.0
8+
jsonpath_ng==1.6.1

0 commit comments

Comments
 (0)