-
Notifications
You must be signed in to change notification settings - Fork 31
/
config_provider.py
357 lines (286 loc) · 14.1 KB
/
config_provider.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
import json
import logging
from configparser import ConfigParser
logger = logging.getLogger()
class ConfigProvider:
def update(self):
"""
This function is called every loop cycle. You can use it to update and cache your configuration from a remote
source.
"""
pass
def get_powermeter_target_point(self):
"""
The target power for powermeter in watts
"""
pass
def get_powermeter_max_point(self):
"""
The maximum power of your powermeter for the normal "regulation loop".
If your powermeter jumps over this point, the limit will be increased instantly. it is like a "super high priority limit change".
If you defined ON_GRID_USAGE_JUMP_TO_LIMIT_PERCENT > 0, then the limit will jump to the defined percent when reaching this point.
"""
pass
def get_powermeter_min_point(self):
"""
The minimum power of your powermeter for the normal "regulation loop".
if your powermeter jumps under this point and ON_GRID_FEED_FAST_LIMIT_DECREASE = true, the limit will be reduced instantly. it is like a "super high priority limit change".
"""
pass
def on_grid_usage_jump_to_limit_percent(self):
"""
If the powermeter jumps over the max point, the limit will be increased to this percent of the powermeter value.
"""
pass
def on_grid_feed_fast_limit_decrease(self):
"""
If the powermeter falls below the min point, the limit will be immediatelly decreased.
"""
pass
def get_powermeter_tolerance(self):
"""
The tolerance for the powermeter in watts. If the powermeter value is in the range of target_point - tolerance and target_point + tolerance, the limit will not be changed.
"""
pass
def get_min_wattage_in_percent(self, inverter_idx):
"""
The minimum limit in percent, e.g. 5% of your inverter power rating.
"""
pass
def get_normal_wattage(self, inverter_idx):
"""
Maximum limit in watts when battery is high (above HOY_BATTERY_THRESHOLD_NORMAL_LIMIT_IN_V)
"""
pass
def get_reduce_wattage(self, inverter_idx):
"""
Maximum limit in watts when battery is low (below HOY_BATTERY_THRESHOLD_REDUCE_LIMIT_IN_V)
"""
pass
def get_battery_priority(self, inverter_idx):
"""
Define priority of the inverters (possible values: 1 (high) ... 5 (low); default = 1). Same priorities are also possible.
example 1 (default):
inverter 1 = 1000W, priority = 1 // inverter 2 = 500W, priority = 1:
set limit of 1100W -> inverter 1 is set to 733W and inverter 2 is set to 367W
set limit of 300W -> inverter 1 is set to 200W and inverter 2 is set to 100W
example 2:
inverter 1 = 1000W, priority = 1 // inverter 2 = 500W, priority = 2:
set limit of 1100W -> inverter 1 is set to 1000W and inverter 2 is set to 100W
set limit of 300W -> inverter 1 is set to 300W and inverter 2 is powered off
"""
pass
class ConfigFileConfigProvider(ConfigProvider):
"""
This class reads the configuration from the fixed config file.
"""
def __init__(self, config: ConfigParser):
self.config = config
def get_powermeter_target_point(self):
return self.config.getint('CONTROL', 'POWERMETER_TARGET_POINT')
def get_powermeter_max_point(self):
return self.config.getint('CONTROL', 'POWERMETER_MAX_POINT')
def get_powermeter_min_point(self):
return self.config.getint('CONTROL', 'POWERMETER_MIN_POINT')
def get_powermeter_tolerance(self):
return self.config.getint('CONTROL', 'POWERMETER_TOLERANCE')
def on_grid_usage_jump_to_limit_percent(self):
return self.config.getint('COMMON', 'ON_GRID_USAGE_JUMP_TO_LIMIT_PERCENT')
def on_grid_feed_fast_limit_decrease(self):
return self.config.getboolean('COMMON', 'ON_GRID_FEED_FAST_LIMIT_DECREASE')
def get_min_wattage_in_percent(self, inverter_idx):
return self.config.getint('INVERTER_' + str(inverter_idx + 1), 'HOY_MIN_WATT_IN_PERCENT')
def get_normal_wattage(self, inverter_idx):
return self.config.getint('INVERTER_' + str(inverter_idx + 1), 'HOY_BATTERY_NORMAL_WATT')
def get_reduce_wattage(self, inverter_idx):
return self.config.getint('INVERTER_' + str(inverter_idx + 1), 'HOY_BATTERY_REDUCE_WATT')
def get_battery_priority(self, inverter_idx):
return self.config.getint('INVERTER_' + str(inverter_idx + 1), 'HOY_BATTERY_PRIORITY')
class ConfigProviderChain(ConfigProvider):
"""
This class is a chain of config providers. It will call all the providers in the order they are given and return the
first non-None value.
This is useful if you want to combine multiple config sources, e.g. a config file and a MQTT topic.
"""
def __init__(self, providers):
self.providers = providers
def update(self):
for provider in self.providers:
provider.update()
def __getattribute__(self, name):
if name in ['update', 'providers']:
return object.__getattribute__(self, name)
def method(*args, **kwargs):
for provider in self.providers:
f = getattr(provider, name)
if callable(f):
value = f(*args, **kwargs)
if value is not None:
return value
return None
return method
class OverridingConfigProvider(ConfigProvider):
"""
This class is a config provider that allows to override the config values from code.
This can be used as a base class for config providers that allow to change the configuration
using a push mechanism, e.g. MQTT or a REST API.
"""
def __init__(self):
self.common_config = {}
self.inverter_config = []
@staticmethod
def cast_value(is_inverter_value, key, value):
if is_inverter_value:
if key in ['min_watt_in_percent', 'normal_watt', 'reduce_watt', 'battery_priority']:
return int(value)
else:
logger.error(f"Unknown inverter key {key}")
else:
if key in ['powermeter_target_point', 'powermeter_max_point', 'powermeter_min_point', 'powermeter_tolerance', 'on_grid_usage_jump_to_limit_percent']:
return int(value)
elif key in ['on_grid_feed_fast_limit_decrease']:
return bool(value)
else:
logger.error(f"Unknown common key {key}")
def set_common_value(self, name, value):
if value is None:
if name in self.common_config:
del self.common_config[name]
logger.info(f"Unset common config value {name}")
else:
cast_value = self.cast_value(False, name, value)
self.common_config[name] = cast_value
logger.info(f"Set common config value {name} to {cast_value}")
def set_inverter_value(self, inverter_idx: int, name: str, value):
if value is None:
if inverter_idx < len(self.inverter_config) and name in self.inverter_config[inverter_idx]:
del self.inverter_config[inverter_idx][name]
logger.info(f"Unset inverter {inverter_idx} config value {name}")
else:
while len(self.inverter_config) <= inverter_idx:
self.inverter_config.append({})
cast_value = self.cast_value(True, name, value)
self.inverter_config[inverter_idx][name] = cast_value
logger.info(f"Set inverter {inverter_idx} config value {name} to {cast_value}")
def get_powermeter_target_point(self):
return self.common_config.get('powermeter_target_point')
def get_powermeter_max_point(self):
return self.common_config.get('powermeter_max_point')
def get_powermeter_min_point(self):
return self.common_config.get('powermeter_min_point')
def get_powermeter_tolerance(self):
return self.common_config.get('powermeter_tolerance')
def on_grid_usage_jump_to_limit_percent(self):
return self.common_config.get('on_grid_usage_jump_to_limit_percent')
def on_grid_feed_fast_limit_decrease(self):
return self.common_config.get('on_grid_feed_fast_limit_decrease')
def get_min_wattage_in_percent(self, inverter_idx):
if inverter_idx >= len(self.inverter_config):
return None
return self.inverter_config[inverter_idx].get('min_watt_in_percent')
def get_normal_wattage(self, inverter_idx):
if inverter_idx >= len(self.inverter_config):
return None
return self.inverter_config[inverter_idx].get('normal_watt')
def get_reduce_wattage(self, inverter_idx):
if inverter_idx >= len(self.inverter_config):
return None
return self.inverter_config[inverter_idx].get('reduce_watt')
def get_battery_priority(self, inverter_idx):
if inverter_idx >= len(self.inverter_config):
return None
return self.inverter_config[inverter_idx].get('battery_priority')
class MqttHandler(OverridingConfigProvider):
"""
Config provider that subscribes to a MQTT topic and updates the configuration from the messages.
"""
def __init__(self, mqtt_broker, mqtt_port, client_id, mqtt_username, mqtt_password, topic_prefix, log_level):
super().__init__()
self.mqtt_broker = mqtt_broker
self.mqtt_port = mqtt_port
self.mqtt_username = mqtt_username
self.mqtt_password = mqtt_password
self.topic_prefix = topic_prefix
self.set_topic = f"{self.topic_prefix}/set"
self.reset_topic = f"{self.topic_prefix}/reset"
self.log_level = log_level
import paho.mqtt.client as mqtt
self.mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=client_id)
self.mqtt_client.will_set(f"{self.topic_prefix}/status", payload="offline", qos=1, retain=True)
self.mqtt_client.on_connect = self.on_connect
self.mqtt_client.on_message = self.on_message
if self.mqtt_username is not None:
self.mqtt_client.username_pw_set(self.mqtt_username, self.mqtt_password)
self.mqtt_client.connect(self.mqtt_broker, self.mqtt_port)
self.mqtt_client.loop_start()
def update(self):
# Publish all config values to MQTT
for key, value in self.common_config.items():
self.mqtt_client.publish(f"{self.topic_prefix}/state/{key}", payload=value, qos=1, retain=True)
for inverter_idx, inverter_config in enumerate(self.inverter_config):
for key, value in inverter_config.items():
self.mqtt_client.publish(f"{self.topic_prefix}/state/inverter/{inverter_idx}/{key}", payload=value, qos=1, retain=True)
def on_connect(self, client, userdata, flags, reason_code, properties):
print("Connected with result code " + str(reason_code))
client.subscribe(f"{self.set_topic}/#")
client.subscribe(f"{self.reset_topic}/#")
client.publish(f"{self.topic_prefix}/status", payload="online", qos=1, retain=True)
def on_message(self, client, userdata, msg):
try:
self.handle_message(msg)
except Exception as e:
logger.error(f"Error handling message {msg.topic}: {e}")
def handle_message(self, msg):
if msg.topic.startswith(self.set_topic):
topic_suffix = msg.topic[len(self.set_topic) + 1:]
logger.info(f"Received set message for config value {topic_suffix} with payload {msg.payload}")
def set_common_value(name):
self.set_common_value(name, msg.payload)
def set_inverter_value(inverter_idx, name):
self.set_inverter_value(inverter_idx, name, msg.payload)
elif msg.topic.startswith(self.reset_topic):
topic_suffix = msg.topic[len(self.reset_topic) + 1:]
logger.info(f"Received reset message for config value {topic_suffix}")
def set_common_value(name):
self.set_common_value(name, None)
def set_inverter_value(inverter_idx, name):
self.set_inverter_value(inverter_idx, name, None)
else:
logger.error(f"Invalid topic {msg.topic}")
return
if topic_suffix.startswith("inverter/"):
inverter_topic_suffix = topic_suffix[len("inverter/"):]
index_config_start_pos = inverter_topic_suffix.index("/")
if index_config_start_pos == -1:
logger.error(f"Invalid inverter config topic {msg.topic}")
return
inverter = int(inverter_topic_suffix[:index_config_start_pos])
key = inverter_topic_suffix[index_config_start_pos + 1:]
set_inverter_value(inverter, key)
else:
set_common_value(topic_suffix)
def cast_value_for_publish(self, value):
if type(value) == bool:
return "true" if value else "false"
return value
def publish_state(self, key, value):
self.mqtt_client.publish(f"{self.topic_prefix}/state/{key}", payload=self.cast_value_for_publish(value))
def publish_inverter_state(self, inverter_idx, key, value):
self.mqtt_client.publish(f"{self.topic_prefix}/state/inverter/{inverter_idx}/{key}", payload=self.cast_value_for_publish(value))
def publish_log_record(self, record: logging.LogRecord):
if self.log_level is None or record.levelno < self.log_level:
return
# Create a dictionary with the log record details
log_message = {
'name': record.name,
'level': record.levelname,
'msg': record.getMessage(),
'exc_info': record.exc_info
}
# Convert the dictionary to a JSON-formatted string
json_payload = json.dumps(log_message)
# Publish the JSON-formatted log message to the MQTT topic
self.mqtt_client.publish(f"{self.topic_prefix}/log", payload=json_payload)
def __del__(self):
logger.info("Disconnecting MQTT client")
self.mqtt_client.disconnect()