Browse Source

RPi: send value irradiation to VZ and MQTT

to send all meassured and calculated values to Volkszaehler and to mqtt
change some logging levels for better differentiation
change frequency as part of AC phase
pull/626/head
Knuti_in_Päse 2 years ago
parent
commit
96d64faf62
  1. 28
      tools/rpi/ahoy.yml.example
  2. 2
      tools/rpi/hoymiles/__init__.py
  3. 6
      tools/rpi/hoymiles/__main__.py
  4. 36
      tools/rpi/hoymiles/decoders/__init__.py
  5. 126
      tools/rpi/hoymiles/outputs.py

28
tools/rpi/ahoy.yml.example

@ -44,9 +44,7 @@ ahoy:
- serial: 114172220003 - serial: 114172220003
url: 'http://localhost/middleware/' url: 'http://localhost/middleware/'
channels: channels:
- type: 'temperature' - type: 'ac_frequency0'
uid: 'ad578a40-1d97-11ed-8e8b-fda01a416575'
- type: 'frequency'
uid: '' uid: ''
- type: 'ac_power0' - type: 'ac_power0'
uid: '7ca5ac50-1e41-11ed-927f-610c4cb2c69e' uid: '7ca5ac50-1e41-11ed-927f-610c4cb2c69e'
@ -54,18 +52,42 @@ ahoy:
uid: '9a38e2e0-1d94-11ed-b539-25f8607ac030' uid: '9a38e2e0-1d94-11ed-b539-25f8607ac030'
- type: 'ac_current0' - type: 'ac_current0'
uid: 'a9a4daf0-1e41-11ed-b68c-eb73eef3d21d' uid: 'a9a4daf0-1e41-11ed-b68c-eb73eef3d21d'
- type: 'ac_reactive_power0'
uid: ''
- type: 'dc_power0' - type: 'dc_power0'
uid: '38eb3ca0-1e53-11ed-b830-792e70a592fa' uid: '38eb3ca0-1e53-11ed-b830-792e70a592fa'
- type: 'dc_voltage0' - type: 'dc_voltage0'
uid: '' uid: ''
- type: 'dc_current0' - type: 'dc_current0'
uid: '' uid: ''
- type: 'dc_energy_total0'
uid: ''
- type: 'dc_energy_daily0'
uid: 'c2a93ea0-9a4e-11ed-8000-7d82e3ac8959'
- type: 'dc_irradiation0'
uid: 'c2d887a0-9a4e-11ed-a7ac-0dab944fd82d'
- type: 'dc_power1' - type: 'dc_power1'
uid: '51c0e9d0-1e53-11ed-b574-8bc81547eb8f' uid: '51c0e9d0-1e53-11ed-b574-8bc81547eb8f'
- type: 'dc_voltage1' - type: 'dc_voltage1'
uid: '' uid: ''
- type: 'dc_current1' - type: 'dc_current1'
uid: '' uid: ''
- type: 'dc_energy_total1'
uid: ''
- type: 'dc_energy_daily1'
uid: 'c3c04df0-9a4e-11ed-82c6-a15a9aba54a3'
- type: 'dc_irradiation1'
uid: 'c3f3efd0-9a4e-11ed-9a77-3fd3187e6237'
- type: 'temperature'
uid: 'ad578a40-1d97-11ed-8e8b-fda01a416575'
- type: 'powerfactor'
uid: ''
- type: 'yield_total'
uid: ''
- type: 'yield_today'
uid: 'c4a76dd0-9a4e-11ed-b79f-2de013d39150'
- type: 'efficiency'
uid: 'c4d8e9c0-9a4e-11ed-9d9e-9737749e4b45'
dtu: dtu:
serial: 99978563001 serial: 99978563001

2
tools/rpi/hoymiles/__init__.py

@ -159,7 +159,7 @@ class ResponseDecoder(ResponseDecoderFactory):
command = self.request_command command = self.request_command
c_datetime = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f") c_datetime = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f")
logging.debug(f'{c_datetime} model_decoder: {model}Decode{command.upper()}') logging.info(f'{c_datetime} model_decoder: {model}Decode{command.upper()}')
model_decoders = __import__('hoymiles.decoders') model_decoders = __import__('hoymiles.decoders')
if hasattr(model_decoders, f'{model}Decode{command.upper()}'): if hasattr(model_decoders, f'{model}Decode{command.upper()}'):

6
tools/rpi/hoymiles/__main__.py

@ -17,7 +17,7 @@ from suntimes import SunTimes
import argparse import argparse
import yaml import yaml
from yaml.loader import SafeLoader from yaml.loader import SafeLoader
import paho.mqtt.client # import paho.mqtt.client
import hoymiles import hoymiles
import logging import logging
@ -174,7 +174,7 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
# get decoder object # get decoder object
result = decoder.decode() result = decoder.decode()
if hoymiles.HOYMILES_DEBUG_LOGGING: if hoymiles.HOYMILES_DEBUG_LOGGING:
logging.debug(f'{c_datetime} Decoded: {result.__dict__()}') logging.info(f'{c_datetime} Decoded: {result.__dict__()}')
# check decoder object for output # check decoder object for output
if isinstance(result, hoymiles.decoders.StatusResponse): if isinstance(result, hoymiles.decoders.StatusResponse):
@ -284,7 +284,7 @@ if __name__ == '__main__':
logging.error("Could not load config file. Try --help") logging.error("Could not load config file. Try --help")
sys.exit(2) sys.exit(2)
except yaml.YAMLError as e_yaml: except yaml.YAMLError as e_yaml:
logging.error('Failed to load config file {global_config.config_file}: {e_yaml}') logging.error(f'Failed to load config file {global_config.config_file}: {e_yaml}')
sys.exit(1) sys.exit(1)
# read AHOY configuration file and prepare logging # read AHOY configuration file and prepare logging

36
tools/rpi/hoymiles/decoders/__init__.py

@ -93,7 +93,8 @@ class Response:
class StatusResponse(Response): class StatusResponse(Response):
"""Inverter StatusResponse object""" """Inverter StatusResponse object"""
e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor', 'reactive_power', 'irradiation'] phase_keys = ['voltage','current','power','reactive_power','frequency']
string_keys = ['voltage','current','power','energy_total','energy_daily', 'irradiation']
temperature = None temperature = None
frequency = None frequency = None
powerfactor = None powerfactor = None
@ -125,7 +126,7 @@ class StatusResponse(Response):
p_exists = False p_exists = False
phase_id = len(phases) phase_id = len(phases)
phase = {} phase = {}
for key in self.e_keys: for key in self.phase_keys:
prop = f'ac_{key}_{phase_id}' prop = f'ac_{key}_{phase_id}'
if hasattr(self, prop): if hasattr(self, prop):
p_exists = True p_exists = True
@ -149,7 +150,7 @@ class StatusResponse(Response):
s_exists = False s_exists = False
string_id = len(strings) string_id = len(strings)
string = {} string = {}
for key in self.e_keys: for key in self.string_keys:
prop = f'dc_{key}_{string_id}' prop = f'dc_{key}_{string_id}'
if hasattr(self, prop): if hasattr(self, prop):
s_exists = True s_exists = True
@ -170,14 +171,27 @@ class StatusResponse(Response):
data['phases'] = self.phases data['phases'] = self.phases
data['strings'] = self.strings data['strings'] = self.strings
data['temperature'] = self.temperature data['temperature'] = self.temperature
data['frequency'] = self.frequency
data['powerfactor'] = self.powerfactor data['powerfactor'] = self.powerfactor
data['event_count'] = self.event_count
data['time'] = self.time_rx
data['energy_total'] = 0.0 data['yield_total'] = 0.0
data['yield_today'] = 0.0
for string in data['strings']:
data['yield_total'] += string['energy_total']
data['yield_today'] += string['energy_daily']
ac_sum_power = 0.0
for phase in data['phases']:
ac_sum_power += phase['power']
dc_sum_power = 0.0
for string in data['strings']: for string in data['strings']:
data['energy_total'] += string['energy_total'] dc_sum_power += string['power']
if dc_sum_power != 0:
data['efficiency'] = round(ac_sum_power * 100 / dc_sum_power, 2)
else:
data['efficiency'] = 0.0
data['event_count'] = self.event_count
data['time'] = self.time_rx
return data return data
@ -469,7 +483,7 @@ class Hm300Decode0B(StatusResponse):
""" Phase 1 watts """ """ Phase 1 watts """
return self.unpack('>H', 18)[0]/10 return self.unpack('>H', 18)[0]/10
@property @property
def frequency(self): def ac_frequency_0(self):
""" Grid frequency in Hertz """ """ Grid frequency in Hertz """
return self.unpack('>H', 16)[0]/100 return self.unpack('>H', 16)[0]/100
@property @property
@ -568,7 +582,7 @@ class Hm600Decode0B(StatusResponse):
""" Phase 1 watts """ """ Phase 1 watts """
return self.unpack('>H', 30)[0]/10 return self.unpack('>H', 30)[0]/10
@property @property
def frequency(self): def ac_frequency_0(self):
""" Grid frequency in Hertz """ """ Grid frequency in Hertz """
return self.unpack('>H', 28)[0]/100 return self.unpack('>H', 28)[0]/100
@property @property
@ -729,7 +743,7 @@ class Hm1200Decode0B(StatusResponse):
""" Phase 1 watts """ """ Phase 1 watts """
return self.unpack('>H', 50)[0]/10 return self.unpack('>H', 50)[0]/10
@property @property
def frequency(self): def ac_frequency_0(self):
""" Grid frequency in Hertz """ """ Grid frequency in Hertz """
return self.unpack('>H', 48)[0]/100 return self.unpack('>H', 48)[0]/100
@property @property

126
tools/rpi/hoymiles/outputs.py

@ -10,11 +10,6 @@ import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from hoymiles.decoders import StatusResponse, HardwareInfoResponse from hoymiles.decoders import StatusResponse, HardwareInfoResponse
try:
from influxdb_client import InfluxDBClient
except ModuleNotFoundError:
pass
class OutputPluginFactory: class OutputPluginFactory:
def __init__(self, **params): def __init__(self, **params):
""" """
@ -59,10 +54,19 @@ class InfluxOutputPlugin(OutputPluginFactory):
""" """
super().__init__(**params) super().__init__(**params)
try:
from influxdb_client import InfluxDBClient
except ModuleNotFoundError:
ErrorText1 = f'Module "influxdb_client" for INFLUXDB necessary.'
ErrorText2 = f'Install module with command: python3 -m pip install influxdb_client'
print(ErrorText1, ErrorText2)
logging.error(ErrorText1)
logging.error(ErrorText2)
exit()
self._bucket = params.get('bucket', 'hoymiles/autogen') self._bucket = params.get('bucket', 'hoymiles/autogen')
self._org = params.get('org', '') self._org = params.get('org', '')
self._measurement = params.get('measurement', self._measurement = params.get('measurement', f'inverter,host={socket.gethostname()}')
f'inverter,host={socket.gethostname()}')
client = InfluxDBClient(url, token, bucket=self._bucket) client = InfluxDBClient(url, token, bucket=self._bucket)
self.api = client.write_api() self.api = client.write_api()
@ -105,6 +109,7 @@ class InfluxOutputPlugin(OutputPluginFactory):
data_stack.append(f'{measurement},phase={phase_id},type=current value={phase["current"]} {ctime}') data_stack.append(f'{measurement},phase={phase_id},type=current value={phase["current"]} {ctime}')
data_stack.append(f'{measurement},phase={phase_id},type=power value={phase["power"]} {ctime}') data_stack.append(f'{measurement},phase={phase_id},type=power value={phase["power"]} {ctime}')
data_stack.append(f'{measurement},phase={phase_id},type=Q_AC value={phase["reactive_power"]} {ctime}') data_stack.append(f'{measurement},phase={phase_id},type=Q_AC value={phase["reactive_power"]} {ctime}')
data_stack.append(f'{measurement},phase={phase_id},type=frequency value={phase["frequency"]:.3f} {ctime}')
phase_id = phase_id + 1 phase_id = phase_id + 1
# DC Data # DC Data
@ -115,28 +120,23 @@ class InfluxOutputPlugin(OutputPluginFactory):
data_stack.append(f'{measurement},string={string_id},type=power value={string["power"]:.2f} {ctime}') data_stack.append(f'{measurement},string={string_id},type=power value={string["power"]:.2f} {ctime}')
data_stack.append(f'{measurement},string={string_id},type=YieldDay value={string["energy_daily"]:.2f} {ctime}') data_stack.append(f'{measurement},string={string_id},type=YieldDay value={string["energy_daily"]:.2f} {ctime}')
data_stack.append(f'{measurement},string={string_id},type=YieldTotal value={string["energy_total"]/1000:.4f} {ctime}') data_stack.append(f'{measurement},string={string_id},type=YieldTotal value={string["energy_total"]/1000:.4f} {ctime}')
if 'irradiation' in string: data_stack.append(f'{measurement},string={string_id},type=Irradiation value={string["irradiation"]:.2f} {ctime}')
data_stack.append(f'{measurement},string={string_id},type=Irradiation value={string["irradiation"]:.2f} {ctime}')
string_id = string_id + 1 string_id = string_id + 1
# Global # Global
if data['event_count'] is not None: if data['event_count'] is not None:
data_stack.append(f'{measurement},type=total_events value={data["event_count"]} {ctime}') data_stack.append(f'{measurement},type=total_events value={data["event_count"]} {ctime}')
if data['powerfactor'] is not None: if data['powerfactor'] is not None:
data_stack.append(f'{measurement},type=pf value={data["powerfactor"]:f} {ctime}') data_stack.append(f'{measurement},type=PF_AC value={data["powerfactor"]:f} {ctime}')
data_stack.append(f'{measurement},type=frequency value={data["frequency"]:.3f} {ctime}')
data_stack.append(f'{measurement},type=Temp value={data["temperature"]:.2f} {ctime}') data_stack.append(f'{measurement},type=Temp value={data["temperature"]:.2f} {ctime}')
if data['energy_total'] is not None: if data['yield_total'] is not None:
data_stack.append(f'{measurement},type=total value={data["energy_total"]/1000:.3f} {ctime}') data_stack.append(f'{measurement},type=YieldTotal value={data["yield_total"]/1000:.3f} {ctime}')
if data['yield_today'] is not None:
data_stack.append(f'{measurement},type=YieldToday value={data["yield_today"]/1000:.3f} {ctime}')
data_stack.append(f'{measurement},type=Efficiency value={data["efficiency"]:.2f} {ctime}')
self.api.write(self._bucket, self._org, data_stack) self.api.write(self._bucket, self._org, data_stack)
try:
import paho.mqtt.client
except ModuleNotFoundError:
pass
class MqttOutputPlugin(OutputPluginFactory): class MqttOutputPlugin(OutputPluginFactory):
""" Mqtt output plugin """ """ Mqtt output plugin """
client = None client = None
@ -164,6 +164,16 @@ class MqttOutputPlugin(OutputPluginFactory):
""" """
super().__init__(**params) super().__init__(**params)
try:
import paho.mqtt.client
except ModuleNotFoundError:
ErrorText1 = f'Module "paho.mqtt.client" for MQTT-output necessary.'
ErrorText2 = f'Install module with command: python3 -m pip install paho-mqtt'
print(ErrorText1, ErrorText2)
logging.error(ErrorText1)
logging.error(ErrorText2)
exit()
mqtt_client = paho.mqtt.client.Client() mqtt_client = paho.mqtt.client.Client()
if config.get('useTLS',False): if config.get('useTLS',False):
mqtt_client.tls_set() mqtt_client.tls_set()
@ -196,33 +206,41 @@ class MqttOutputPlugin(OutputPluginFactory):
# AC Data # AC Data
phase_id = 0 phase_id = 0
phase_sum_power = 0
for phase in data['phases']: for phase in data['phases']:
self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power'])
self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage']) self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage'])
self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current']) self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current'])
self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power'])
self.client.publish(f'{topic}/emeter/{phase_id}/Q_AC', phase['reactive_power']) self.client.publish(f'{topic}/emeter/{phase_id}/Q_AC', phase['reactive_power'])
self.client.publish(f'{topic}/emeter/{phase_id}/frequency', phase['frequency'])
phase_id = phase_id + 1 phase_id = phase_id + 1
phase_sum_power += phase['power']
# DC Data # DC Data
string_id = 0 string_id = 0
string_sum_power = 0
for string in data['strings']: for string in data['strings']:
self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage']) self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage'])
self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current'])
self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power']) self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power'])
self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldDay', string['energy_daily']) self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldDay', string['energy_daily'])
self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldTotal', string['energy_total']/1000) self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldTotal', string['energy_total']/1000)
if 'irradiation' in string: self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation'])
self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation'])
string_id = string_id + 1 string_id = string_id + 1
string_sum_power += string['power']
# Global # Global
if data['event_count'] is not None:
self.client.publish(f'{topic}/total_events', data['event_count'])
if data['powerfactor'] is not None: if data['powerfactor'] is not None:
self.client.publish(f'{topic}/pf', data['powerfactor']) self.client.publish(f'{topic}/PF_AC', data['powerfactor'])
self.client.publish(f'{topic}/frequency', data['frequency'])
self.client.publish(f'{topic}/Temp', data['temperature']) self.client.publish(f'{topic}/Temp', data['temperature'])
if data['energy_total'] is not None: if data['yield_total'] is not None:
self.client.publish(f'{topic}/total', data['energy_total']/1000) self.client.publish(f'{topic}/YieldTotal', data['yield_total']/1000)
if data['yield_today'] is not None:
self.client.publish(f'{topic}/YieldToday', data['yield_today']/1000)
self.client.publish(f'{topic}/Efficiency', data['efficiency'])
elif isinstance(response, HardwareInfoResponse): elif isinstance(response, HardwareInfoResponse):
self.client.publish(f'{topic}/Firmware/Version',\ self.client.publish(f'{topic}/Firmware/Version',\
@ -236,12 +254,6 @@ class MqttOutputPlugin(OutputPluginFactory):
else: else:
raise ValueError('Data needs to be instance of StatusResponse or a instance of HardwareInfoResponse') raise ValueError('Data needs to be instance of StatusResponse or a instance of HardwareInfoResponse')
try:
import requests
import time
except ModuleNotFoundError:
pass
class VzInverterOutput: class VzInverterOutput:
def __init__(self, config, session): def __init__(self, config, session):
self.session = session self.session = session
@ -274,7 +286,8 @@ class VzInverterOutput:
self.try_publish(ts, f'ac_voltage{phase_id}', phase['voltage']) self.try_publish(ts, f'ac_voltage{phase_id}', phase['voltage'])
self.try_publish(ts, f'ac_current{phase_id}', phase['current']) self.try_publish(ts, f'ac_current{phase_id}', phase['current'])
self.try_publish(ts, f'ac_power{phase_id}', phase['power']) self.try_publish(ts, f'ac_power{phase_id}', phase['power'])
self.try_publish(ts, f'ac_Q{phase_id}', phase['reactive_power']) self.try_publish(ts, f'ac_reactive_power{phase_id}', phase['reactive_power'])
self.try_publish(ts, f'ac_frequency{phase_id}', phase['frequency'])
phase_id = phase_id + 1 phase_id = phase_id + 1
# DC Data # DC Data
@ -283,33 +296,39 @@ class VzInverterOutput:
self.try_publish(ts, f'dc_voltage{string_id}', string['voltage']) self.try_publish(ts, f'dc_voltage{string_id}', string['voltage'])
self.try_publish(ts, f'dc_current{string_id}', string['current']) self.try_publish(ts, f'dc_current{string_id}', string['current'])
self.try_publish(ts, f'dc_power{string_id}', string['power']) self.try_publish(ts, f'dc_power{string_id}', string['power'])
self.try_publish(ts, f'dc_YieldDay{string_id}', string['energy_daily']) self.try_publish(ts, f'dc_energy_daily{string_id}', string['energy_daily'])
self.try_publish(ts, f'dc_YieldTotal{string_id}', string['energy_total']) self.try_publish(ts, f'dc_energy_total{string_id}', string['energy_total'])
if 'irradiation' in string: self.try_publish(ts, f'dc_irradiation{string_id}', string['irradiation'])
self.try_publish(ts, f'dc_Irradiation{string_id}', string['irradiation'])
string_id = string_id + 1 string_id = string_id + 1
# Global # Global
if data['event_count'] is not None:
self.try_publish(ts, f'event_count', data['event_count'])
if data['powerfactor'] is not None: if data['powerfactor'] is not None:
self.try_publish(ts, f'powerfactor', data['powerfactor']) self.try_publish(ts, f'powerfactor', data['powerfactor'])
self.try_publish(ts, f'frequency', data['frequency']) self.try_publish(ts, f'temperature', data['temperature'])
if data['yield_total'] is not None:
self.try_publish(ts, f'Temp', data['temperature']) self.try_publish(ts, f'yield_total', data['yield_total'])
if data['energy_total'] is not None: if data['yield_today'] is not None:
self.try_publish(ts, f'total', data['energy_total']) self.try_publish(ts, f'yield_today', data['yield_today'])
self.try_publish(ts, f'efficiency', data['efficiency'])
def try_publish(self, ts, ctype, value): def try_publish(self, ts, ctype, value):
if not ctype in self.channels: if not ctype in self.channels:
logging.warning(f'ctype \"{ctype}\" not found in ahoy.yml')
return return
uid = self.channels[ctype] uid = self.channels[ctype]
url = f'{self.baseurl}/data/{uid}.json?operation=add&ts={ts}&value={value}' url = f'{self.baseurl}/data/{uid}.json?operation=add&ts={ts}&value={value}'
try: try:
r = self.session.get(url) r = self.session.get(url)
if r.status_code != 200: if r.status_code == 404:
raise ValueError('Could not send request (%s)' % url) logging.critical('VZ-DB not reachable, please check "middleware"')
except requests.exceptions.ConnectionError as e: if r.status_code == 400:
raise ValueError('Could not send request (%s)' % e) logging.critical('UUID not configured in VZ-DB')
elif r.status_code != 200:
raise ValueError(f'Transmit result {url}')
except ConnectionError as e:
raise ValueError(f'Could not connect VZ-DB {type(e)} {e.keys()}')
class VolkszaehlerOutputPlugin(OutputPluginFactory): class VolkszaehlerOutputPlugin(OutputPluginFactory):
def __init__(self, config, **params): def __init__(self, config, **params):
@ -318,6 +337,17 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory):
""" """
super().__init__(**params) super().__init__(**params)
try:
import requests
import time
except ModuleNotFoundError:
ErrorText1 = f'Module "requests" and "time" for VolkszaehlerOutputPlugin necessary.'
ErrorText2 = f'Install module with command: python3 -m pip install requests'
print(ErrorText1, ErrorText2)
logging.error(ErrorText1)
logging.error(ErrorText2)
exit(1)
self.session = requests.Session() self.session = requests.Session()
self.inverters = dict() self.inverters = dict()

Loading…
Cancel
Save