forked from Marv2190/venus.dbus-MqttToGridMeter
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathMQTTtoGridMeter.py
295 lines (231 loc) · 12.5 KB
/
MQTTtoGridMeter.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
#!/usr/bin/env python
"""
Changed a lot of a Script originall created by Ralf Zimmermann ([email protected]) in 2020.
The orginal code and its documentation can be found on: https://github.com/RalfZim/venus.dbus-fronius-smartmeter
Used https://github.com/victronenergy/velib_python/blob/master/dbusdummyservice.py as basis for this service.
Links (DSTK_2023-11-27):
https://github.com/victronenergy/venus/wiki/dbus-api
https://www.victronenergy.com/live/ccgx:modbustcp_faq
https://github.com/victronenergy/venus/wiki/howto-add-a-driver-to-Venus
/data/mqtttogrid/vedbus.py
/data/mqtttogrid/ve_utils.py
python -m ensurepip --upgrade
pip install paho-mqtt
"""
import os
import socket
import sys
import time
import logging
import logging.handlers
import platform
import paho.mqtt.client as mqtt
from vedbus import VeDbusService
if sys.version_info.major == 2:
import gobject
import thread # for daemon = True / Python 2.x
else:
from gi.repository import GLib as gobject
import _thread as thread # for daemon = True / Python 3.x
# our own packages
sys.path.insert(1, os.path.join(os.path.dirname(__file__), '../ext/velib_python'))
path_UpdateIndex = '/UpdateIndex'
# MQTT Setup
broker_address = "192.168.40.227"
MQTTNAME = "MQTTtoMeter"
broker_user = "mqtttogridmeter"
broker_pw = "ac9tXVJXTDQSE"
Zaehlersensorpfad = "sensor/hausstrom"
# MQTT:
def on_disconnect(client, userdata, rc): # pylint: disable=unused-argument
logging.info('Unexpected MQTT disconnection rc=%s. Will auto-reconnect', str(rc))
try:
logging.info("Trying to Reconnect")
client.connect(broker_address)
except Exception as e: # pylint: disable=broad-exception-caught
logging.exception("Error in Retrying to Connect with Broker", exc_info=e)
def on_connect(client, userdata, flags, rc): # pylint: disable=unused-argument
if rc == 0:
logging.info("Connected to MQTT Broker!")
ok = client.subscribe(Zaehlersensorpfad+"/#", 0)
logging.debug("subscribed to %s ok=%s", Zaehlersensorpfad, str(ok))
else:
logging.warning(f"Failed to connect, return code {rc}\n")
def on_message(client, userdata, msg): # pylint: disable=unused-argument
try:
if msg.topic == "sensor/hausstrom/hausstrom_sum_active_instantaneous_power":
get_dbus_service().update(powercurr=float(msg.payload))
elif msg.topic == "sensor/hausstrom/hausstrom_l1_active_instantaneous_power":
get_dbus_service().update(power_l1=float(msg.payload))
elif msg.topic == "sensor/hausstrom/hausstrom_l2_active_instantaneous_power":
get_dbus_service().update(power_l2=float(msg.payload))
elif msg.topic == "sensor/hausstrom/hausstrom_l3_active_instantaneous_power":
get_dbus_service().update(power_l3=float(msg.payload))
elif msg.topic == "sensor/hausstrom/hausstrom_positive_active_energy_total":
get_dbus_service().update(totalin=round(float(msg.payload) / 1000, 3))
elif msg.topic == "sensor/hausstrom/solar_energy_to_grid":
get_dbus_service().update(totalout=round(float(msg.payload), 3))
except Exception as e: # pylint: disable=broad-exception-caught
logging.exception("MQTTtoGridMeter crashed during on_message", exc_info=e)
def log_value(value, label, unit=''):
logging.debug(f"{label}: {value:.0f} {unit}")
class DbusDummyService:
def __init__(self, servicename, deviceinstance, paths, productname='MQTTMeter1', connection='HA Hausstrom Dirk'):
self._vedbusservice = VeDbusService(servicename)
self._paths = paths
logging.debug(f"{servicename} / DeviceInstance = {deviceinstance}")
# Create the management objects, as specified in the ccgx dbus-api document
self._vedbusservice.add_path('/Mgmt/ProcessName', __file__)
self._vedbusservice.add_path('/Mgmt/ProcessVersion',
'running on Python ' + platform.python_version())
self._vedbusservice.add_path('/Mgmt/Connection', connection)
# Create the mandatory objects
self._vedbusservice.add_path('/DeviceInstance', deviceinstance)
self._vedbusservice.add_path('/ProductId', 45069) # 45069 = value used in ac_sensor_bridge.cpp of dbus-cgwacs
# DSTK_2022-10-25: from https://github.com/fabian-lauer/dbus-shelly-3em-smartmeter/blob/main/dbus-shelly-3em-smartmeter.py
# self._dbusservice.add_path('/ProductId', 45069) # found on https://www.sascha-curth.de/projekte/005_Color_Control_GX.html#experiment - should be an ET340 Engerie Meter
# found on https://www.sascha-curth.de/projekte/005_Color_Control_GX.html#experiment - should be an ET340 Engerie Meter
self._vedbusservice.add_path('/DeviceType', 345)
self._vedbusservice.add_path('/Role', 'grid')
self._vedbusservice.add_path('/ProductName', productname)
self._vedbusservice.add_path('/FirmwareVersion', 0.1)
self._vedbusservice.add_path('/HardwareVersion', 0)
self._vedbusservice.add_path('/Connected', 0)
self._vedbusservice.add_path('/Position', 0) # DSTK_2022-10-25 bewirkt bei Gridmeter nichts ???
self._vedbusservice.add_path('/UpdateIndex', 0)
self._vedbusservice.add_path("/Serial", 1234)
for path, settings in self._paths.items():
self._vedbusservice.add_path(
path, settings['initial'], gettextcallback=settings['textformat'], writeable=True, onchangecallback=self._handlechangedvalue)
# now _update ios called from on_message:
# gobject.timeout_add(1000, self._update) # pause 1000ms before the next request
self._last_update = 0
sign_of_life_id = gobject.timeout_add(10 * 1000, self._sign_of_life)
logging.debug(f"sign_of_life_id = {sign_of_life_id}")
def update(self,
powercurr=None,
power_l1=None, power_l2=None, power_l3=None,
totalin=None, totalout=None,
gridloss=False):
if gridloss:
logging.warning("Grid lost. exit")
self._vedbusservice['/Connected'] = 0 # does not seem to have any effect. At least no grid lost alarm
os._exit(1) # exit in order to disconnect and destroy the dbusservice object
self._vedbusservice['/Connected'] = 1
self._last_update = time.time()
# see https://github.com/victronenergy/venus/wiki/dbus#grid-and-genset-meter
self._vedbusservice['/Ac/L1/Voltage'] = 230
self._vedbusservice['/Ac/L2/Voltage'] = 230
self._vedbusservice['/Ac/L3/Voltage'] = 230
for i, power in enumerate([power_l1, power_l2, power_l3], start=1):
if power is not None:
self._vedbusservice[f'/Ac/L{i}/Current'] = round(power / 230, 2)
self._vedbusservice[f'/Ac/L{i}/Power'] = power
log_value(power, f"power_l{i}", "W")
# Add L123/Energy/Forward/Reverse hoping this helps to show correct Consumption values in VRM
if totalin is not None:
self._vedbusservice[f'/Ac/L{i}/Energy/Forward'] = round(totalin / 3, 2)
if totalout is not None:
self._vedbusservice[f'/Ac/L{i}/Energy/Reverse'] = round(totalout / 3, 2)
if totalin is not None:
self._vedbusservice['/Ac/Energy/Forward'] = totalin # consumption
log_value(totalin, "totalin", "kWh")
if totalout is not None:
self._vedbusservice['/Ac/Energy/Reverse'] = totalout # feed into grid
log_value(totalout, "totalout", "kWh")
if not powercurr is None:
self._vedbusservice['/Ac/Power'] = powercurr # positive: consumption, negative: feed into grid
log_value(powercurr, "House Consumption", "W")
self.update_dbus_index()
def update_dbus_index(self):
''' increment UpdateIndex - to show that new data is available '''
index = self._vedbusservice[path_UpdateIndex] + 1 # increment index
if index > 255: # maximum value of the index
index = 0 # overflow from 255 to 0
self._vedbusservice[path_UpdateIndex] = index
def _sign_of_life(self):
now = time.time()
last_update_ago_seconds = now - self._last_update
if last_update_ago_seconds > 10:
logging.warning(f"last update was {last_update_ago_seconds} seconds ago.")
self.update(gridloss=True)
else:
logging.debug(f"ok: last update was {last_update_ago_seconds} seconds ago.")
return True # must return True if it wants to be rescheduled
def _handlechangedvalue(self, path, value):
logging.debug(f"someone else updated {path} to {value}")
return True # accept the change
def init_mqtt():
client = mqtt.Client(MQTTNAME) # create new instance
client.username_pw_set(broker_user, broker_pw)
client.on_disconnect = on_disconnect
client.on_connect = on_connect
client.on_message = on_message
client.connect(broker_address) # connect to broker
client.loop_start()
def init_logging():
logging.basicConfig(
format="%(asctime)s,%(msecs)d %(levelname)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
level=logging.DEBUG,
handlers=[
logging.StreamHandler(),
],
)
syslog_handler = logging.handlers.SysLogHandler(address="/dev/log")
syslog_handler.setLevel(level=logging.DEBUG)
syslog_handler.setFormatter(logging.Formatter(
f"{socket.gethostname()} mqtttogrid %(asctime)s,%(msecs)d %(levelname)s %(message)s"))
logging.getLogger().addHandler(syslog_handler)
file_handler = logging.FileHandler(f"{(os.path.dirname(os.path.realpath(__file__)))}/current.log")
file_handler.setLevel(level=logging.INFO)
file_handler.setFormatter(logging.Formatter(f"%(asctime)s,%(msecs)d %(levelname)s %(message)s"))
logging.getLogger().addHandler(file_handler)
def get_dbus_service():
# formatting
def _kwh(p, v): return (str(round(v, 2)) + 'kWh')
def _wh(p, v): return (str(round(v, 2)) + 'Wh')
def _a(p, v): return (str(round(v, 2)) + 'A')
def _w(p, v): return (str(int(round(v, 0))) + 'W')
def _v(p, v): return (str(round(v, 1)) + 'V')
def _hz(p, v): return (str(round(v, 2)) + 'Hz')
if get_dbus_service.dbusservice is None:
get_dbus_service.dbusservice = DbusDummyService(
# servicename='com.victronenergy.grid',
servicename='com.victronenergy.grid.cgwacs_edl21_ha',
deviceinstance=31, # = VRM instance ID
paths={
'/Ac/Power': {'initial': None, 'textformat': _w},
'/Ac/L1/Voltage': {'initial': None, 'textformat': _v},
'/Ac/L2/Voltage': {'initial': None, 'textformat': _v},
'/Ac/L3/Voltage': {'initial': None, 'textformat': _v},
'/Ac/L1/Current': {'initial': None, 'textformat': _a},
'/Ac/L2/Current': {'initial': None, 'textformat': _a},
'/Ac/L3/Current': {'initial': None, 'textformat': _a},
'/Ac/L1/Power': {'initial': None, 'textformat': _w},
'/Ac/L2/Power': {'initial': None, 'textformat': _w},
'/Ac/L3/Power': {'initial': None, 'textformat': _w},
'/Ac/Energy/Forward': {'initial': None, 'textformat': _kwh}, # energy bought from the grid
'/Ac/Energy/Reverse': {'initial': None, 'textformat': _kwh}, # energy sold to the grid
'/Ac/L1/Energy/Forward': {'initial': None, 'textformat': _kwh}, # energy bought from the grid
'/Ac/L2/Energy/Forward': {'initial': None, 'textformat': _kwh}, # energy bought from the grid
'/Ac/L3/Energy/Forward': {'initial': None, 'textformat': _kwh}, # energy bought from the grid
'/Ac/L1/Energy/Reverse': {'initial': None, 'textformat': _kwh}, # energy sold to the grid
'/Ac/L2/Energy/Reverse': {'initial': None, 'textformat': _kwh}, # energy sold to the grid
'/Ac/L3/Energy/Reverse': {'initial': None, 'textformat': _kwh}, # energy sold to the grid
})
logging.info('Connected to dbus')
return get_dbus_service.dbusservice
get_dbus_service.dbusservice = None
def main():
init_logging()
init_mqtt()
thread.daemon = True # allow the program to quit
from dbus.mainloop.glib import DBusGMainLoop
# Have a mainloop, so we can send/receive asynchronous calls to and from dbus
DBusGMainLoop(set_as_default=True)
logging.debug('Switching over to gobject.MainLoop() (= event based)')
mainloop = gobject.MainLoop()
mainloop.run()
if __name__ == "__main__":
main()