Skip to content

Commit b7d677b

Browse files
authored
Merge pull request #165 from tomquist/add-mqtt-config-subscriber
Support reconfiguring script via MQTT
2 parents 5934c51 + eee0a5f commit b7d677b

5 files changed

Lines changed: 231 additions & 3 deletions

File tree

CHANGELOG.md

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

3+
## V1.87
4+
### script
5+
* Add support for dynamic reconfiguration of config parameters via MQTT
6+
### config
7+
* Add optional section '[MQTT_CONFIG]' to config file. If present, the script will listen for MQTT messages to reconfigure various parameters at runtime.
8+
39
## V1.86
410
### script
511
* Prepare config to support dynamic reconfiguration of various parameters

HoymilesZeroExport.py

Lines changed: 12 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.86"
18+
__version__ = "1.87"
1919

2020
import requests
2121
import time
@@ -30,7 +30,7 @@
3030
from packaging import version
3131
import argparse
3232
import subprocess
33-
from config_provider import ConfigFileConfigProvider
33+
from config_provider import ConfigFileConfigProvider, MqttConfigProvider, ConfigProviderChain
3434

3535
logging.basicConfig(
3636
format='%(asctime)s %(levelname)-8s %(message)s',
@@ -1314,6 +1314,16 @@ def CreateDTU() -> DTU:
13141314
SLOW_APPROX_LIMIT = CastToInt(GetMaxWattFromAllInverters() * config.getint('COMMON', 'SLOW_APPROX_LIMIT_IN_PERCENT') / 100)
13151315

13161316
CONFIG_PROVIDER = ConfigFileConfigProvider(config)
1317+
if config.has_section("MQTT_CONFIG"):
1318+
broker = config.get("MQTT_CONFIG", "MQTT_BROKER")
1319+
port = config.getint("MQTT_CONFIG", "MQTT_PORT", fallback=1883)
1320+
client_id = config.get("MQTT_CONFIG", "MQTT_CLIENT_ID", fallback="HoymilesZeroExport")
1321+
username = config.get("MQTT_CONFIG", "MQTT_USERNAME", fallback=None)
1322+
password = config.get("MQTT_CONFIG", "MQTT_PASSWORD", fallback=None)
1323+
set_topic = config.get("MQTT_CONFIG", "MQTT_SET_TOPIC", fallback="zeropower/set")
1324+
reset_topic = config.get("MQTT_CONFIG", "MQTT_RESET_TOPIC", fallback="zeropower/reset")
1325+
mqtt_config_provider = MqttConfigProvider(broker, port, client_id, username, password, set_topic, reset_topic)
1326+
CONFIG_PROVIDER = ConfigProviderChain([mqtt_config_provider, CONFIG_PROVIDER])
13171327

13181328
try:
13191329
logger.info("---Init---")

HoymilesZeroExport_Config.ini

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

2121
[VERSION]
22-
VERSION = 1.86
22+
VERSION = 1.87
2323

2424
[SELECT_DTU]
2525
# --- define your DTU (only one) ---
@@ -200,6 +200,24 @@ VZL_PORT_INTERMEDIATE = 2081
200200
# you need to specify the uuid of the vzlogger channel for the reading OBIS(16.7.0) (aktuelle Gesamtwirkleistung)
201201
VZL_UUID_INTERMEDIATE = 06ec9562-a490-49fe-92ea-ffe0758d181c
202202

203+
# Uncomment the following section if you want to use MQTT to dynamically reconfigure some settings while the script is running
204+
# [MQTT_CONFIG]
205+
# MQTT_BROKER = localhost
206+
# MQTT_PORT = 1883
207+
# MQTT_CLIENT_ID = HoymilesZeroExport
208+
209+
# The script subscribes to the following topics:
210+
# - zeropower/set/powermeter_target_point: To change the target point of the powermeter
211+
# - zeropower/set/powermeter_max_point: To change the max point of the powermeter
212+
# - zeropower/set/powermeter_tolerance: To change the tolerance of the powermeter
213+
# - zeropower/set/on_grid_usage_jump_to_limit_percent: To change the on grid usage jump to limit percent
214+
# - zeropower/set/inverter/0/min_watt_in_percent: To change the min watt in percent of the first inverter
215+
# - zeropower/set/inverter/0/normal_watt: To change the battery normal watt of the first inverter
216+
# - zeropower/set/inverter/0/reduce_watt: To change the battery reduce watt of the first inverter
217+
# - zeropower/set/inverter/0/battery_priority: To change the battery priority of the first inverter
218+
# MQTT_SET_TOPIC = zeropower/set
219+
# MQTT_RESET_TOPIC = zeropower/reset
220+
203221
[COMMON]
204222
# Number of Inverters
205223
INVERTER_COUNT = 1

config_provider.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import logging
12
from configparser import ConfigParser
23

4+
logger = logging.getLogger()
35

46
class ConfigProvider:
57

@@ -101,3 +103,194 @@ def get_reduce_wattage(self, inverter_idx):
101103

102104
def get_battery_priority(self, inverter_idx):
103105
return self.config.getint('INVERTER_' + str(inverter_idx + 1), 'HOY_BATTERY_PRIORITY')
106+
107+
108+
class ConfigProviderChain(ConfigProvider):
109+
"""
110+
This class is a chain of config providers. It will call all the providers in the order they are given and return the
111+
first non-None value.
112+
113+
This is useful if you want to combine multiple config sources, e.g. a config file and a MQTT topic.
114+
"""
115+
def __init__(self, providers):
116+
self.providers = providers
117+
118+
def update(self):
119+
for provider in self.providers:
120+
provider.update()
121+
122+
def __getattribute__(self, name):
123+
if name in ['update', 'providers']:
124+
return object.__getattribute__(self, name)
125+
126+
def method(*args, **kwargs):
127+
for provider in self.providers:
128+
f = getattr(provider, name)
129+
if callable(f):
130+
value = f(*args, **kwargs)
131+
if value is not None:
132+
return value
133+
return None
134+
return method
135+
136+
class OverridingConfigProvider(ConfigProvider):
137+
"""
138+
This class is a config provider that allows to override the config values from code.
139+
140+
This can be used as a base class for config providers that allow to change the configuration
141+
using a push mechanism, e.g. MQTT or a REST API.
142+
"""
143+
def __init__(self):
144+
self.common_config = {}
145+
self.inverter_config = []
146+
147+
@staticmethod
148+
def cast_value(is_inverter_value, key, value):
149+
if is_inverter_value:
150+
if key in ['min_watt_in_percent', 'normal_watt', 'reduce_watt', 'battery_priority']:
151+
return int(value)
152+
else:
153+
logger.error(f"Unknown inverter key {key}")
154+
else:
155+
if key in ['powermeter_target_point', 'powermeter_max_point', 'powermeter_tolerance', 'on_grid_usage_jump_to_limit_percent']:
156+
return int(value)
157+
else:
158+
logger.error(f"Unknown common key {key}")
159+
160+
def set_common_value(self, name, value):
161+
if value is None:
162+
if name in self.common_config:
163+
del self.common_config[name]
164+
logger.info(f"Unset common config value {name}")
165+
else:
166+
cast_value = self.cast_value(False, name, value)
167+
self.common_config[name] = cast_value
168+
logger.info(f"Set common config value {name} to {cast_value}")
169+
170+
def set_inverter_value(self, inverter_idx: int, name: str, value):
171+
if value is None:
172+
if inverter_idx < len(self.inverter_config) and name in self.inverter_config[inverter_idx]:
173+
del self.inverter_config[inverter_idx][name]
174+
logger.info(f"Unset inverter {inverter_idx} config value {name}")
175+
else:
176+
while len(self.inverter_config) <= inverter_idx:
177+
self.inverter_config.append({})
178+
cast_value = self.cast_value(True, name, value)
179+
self.inverter_config[inverter_idx][name] = cast_value
180+
logger.info(f"Set inverter {inverter_idx} config value {name} to {cast_value}")
181+
182+
def get_powermeter_target_point(self):
183+
return self.common_config.get('powermeter_target_point')
184+
185+
def get_powermeter_max_point(self):
186+
return self.common_config.get('powermeter_max_point')
187+
188+
def get_powermeter_tolerance(self):
189+
return self.common_config.get('powermeter_tolerance')
190+
191+
def on_grid_usage_jump_to_limit_percent(self):
192+
return self.common_config.get('on_grid_usage_jump_to_limit_percent')
193+
194+
def get_min_wattage_in_percent(self, inverter_idx):
195+
if inverter_idx >= len(self.inverter_config):
196+
return None
197+
return self.inverter_config[inverter_idx].get('min_watt_in_percent')
198+
199+
def get_normal_wattage(self, inverter_idx):
200+
if inverter_idx >= len(self.inverter_config):
201+
return None
202+
return self.inverter_config[inverter_idx].get('normal_watt')
203+
204+
def get_reduce_wattage(self, inverter_idx):
205+
if inverter_idx >= len(self.inverter_config):
206+
return None
207+
return self.inverter_config[inverter_idx].get('reduce_watt')
208+
209+
def get_battery_priority(self, inverter_idx):
210+
if inverter_idx >= len(self.inverter_config):
211+
return None
212+
return self.inverter_config[inverter_idx].get('battery_priority')
213+
214+
215+
class MqttConfigProvider(OverridingConfigProvider):
216+
"""
217+
Config provider that subscribes to a MQTT topic and updates the configuration from the messages.
218+
"""
219+
def __init__(self, mqtt_broker, mqtt_port, client_id, mqtt_username, mqtt_password, set_topic, reset_topic):
220+
super().__init__()
221+
self.mqtt_broker = mqtt_broker
222+
self.mqtt_port = mqtt_port
223+
self.mqtt_username = mqtt_username
224+
self.mqtt_password = mqtt_password
225+
self.set_topic = set_topic
226+
self.reset_topic = reset_topic
227+
self.target_point = None
228+
self.max_point = None
229+
self.tolerance = None
230+
self.on_grid_usage_jump_to_limit_percent = None
231+
self.min_wattage_in_percent = []
232+
self.normal_wattage = []
233+
self.reduce_wattage = []
234+
self.battery_priority = []
235+
236+
import paho.mqtt.client as mqtt
237+
self.mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=client_id)
238+
self.mqtt_client.on_connect = self.on_connect
239+
self.mqtt_client.on_message = self.on_message
240+
if self.mqtt_username is not None:
241+
self.mqtt_client.username_pw_set(self.mqtt_username, self.mqtt_password)
242+
self.mqtt_client.connect(self.mqtt_broker, self.mqtt_port)
243+
self.mqtt_client.loop_start()
244+
245+
def on_connect(self, client, userdata, flags, reason_code, properties):
246+
print("Connected with result code " + str(reason_code))
247+
client.subscribe(f"{self.set_topic}/#")
248+
client.subscribe(f"{self.reset_topic}/#")
249+
250+
def on_message(self, client, userdata, msg):
251+
try:
252+
self.handle_message(msg)
253+
except Exception as e:
254+
logger.error(f"Error handling message {msg.topic}: {e}")
255+
256+
def handle_message(self, msg):
257+
if msg.topic.startswith(self.set_topic):
258+
topic_suffix = msg.topic[len(self.set_topic) + 1:]
259+
logger.info(f"Received set message for config value {topic_suffix} with payload {msg.payload}")
260+
261+
def set_common_value(name):
262+
self.set_common_value(name, msg.payload)
263+
264+
def set_inverter_value(inverter_idx, name):
265+
self.set_inverter_value(inverter_idx, name, msg.payload)
266+
267+
elif msg.topic.startswith(self.reset_topic):
268+
topic_suffix = msg.topic[len(self.reset_topic) + 1:]
269+
logger.info(f"Received reset message for config value {topic_suffix}")
270+
271+
def set_common_value(name):
272+
self.set_common_value(name, None)
273+
274+
def set_inverter_value(inverter_idx, name):
275+
self.set_inverter_value(inverter_idx, name, None)
276+
else:
277+
logger.error(f"Invalid topic {msg.topic}")
278+
return
279+
280+
if topic_suffix.startswith("inverter/"):
281+
inverter_topic_suffix = topic_suffix[len("inverter/"):]
282+
283+
index_config_start_pos = inverter_topic_suffix.index("/")
284+
if index_config_start_pos == -1:
285+
logger.error(f"Invalid inverter config topic {msg.topic}")
286+
return
287+
288+
inverter = int(inverter_topic_suffix[:index_config_start_pos])
289+
key = inverter_topic_suffix[index_config_start_pos + 1:]
290+
set_inverter_value(inverter, key)
291+
else:
292+
set_common_value(topic_suffix)
293+
294+
def __del__(self):
295+
logger.info("Disconnecting MQTT client")
296+
self.mqtt_client.disconnect()

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ idna==3.4
44
packaging==23.2
55
requests==2.31.0
66
urllib3==2.1.0
7+
paho-mqtt==2.0.0

0 commit comments

Comments
 (0)