Browse Source

Merge pull request #626 from PaeserBastelstube/main

RPi: send value irradiation to VZ and MQTT
pull/684/head
Lukas Pusch 2 years ago
committed by GitHub
parent
commit
dda84cd733
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  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