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. 122
      tools/rpi/hoymiles/outputs.py

28
tools/rpi/ahoy.yml.example

@ -44,9 +44,7 @@ ahoy:
- serial: 114172220003
url: 'http://localhost/middleware/'
channels:
- type: 'temperature'
uid: 'ad578a40-1d97-11ed-8e8b-fda01a416575'
- type: 'frequency'
- type: 'ac_frequency0'
uid: ''
- type: 'ac_power0'
uid: '7ca5ac50-1e41-11ed-927f-610c4cb2c69e'
@ -54,18 +52,42 @@ ahoy:
uid: '9a38e2e0-1d94-11ed-b539-25f8607ac030'
- type: 'ac_current0'
uid: 'a9a4daf0-1e41-11ed-b68c-eb73eef3d21d'
- type: 'ac_reactive_power0'
uid: ''
- type: 'dc_power0'
uid: '38eb3ca0-1e53-11ed-b830-792e70a592fa'
- type: 'dc_voltage0'
uid: ''
- type: 'dc_current0'
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'
uid: '51c0e9d0-1e53-11ed-b574-8bc81547eb8f'
- type: 'dc_voltage1'
uid: ''
- type: 'dc_current1'
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:
serial: 99978563001

2
tools/rpi/hoymiles/__init__.py

@ -159,7 +159,7 @@ class ResponseDecoder(ResponseDecoderFactory):
command = self.request_command
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')
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 yaml
from yaml.loader import SafeLoader
import paho.mqtt.client
# import paho.mqtt.client
import hoymiles
import logging
@ -174,7 +174,7 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
# get decoder object
result = decoder.decode()
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
if isinstance(result, hoymiles.decoders.StatusResponse):
@ -284,7 +284,7 @@ if __name__ == '__main__':
logging.error("Could not load config file. Try --help")
sys.exit(2)
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)
# read AHOY configuration file and prepare logging

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

@ -93,7 +93,8 @@ class Response:
class StatusResponse(Response):
"""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
frequency = None
powerfactor = None
@ -125,7 +126,7 @@ class StatusResponse(Response):
p_exists = False
phase_id = len(phases)
phase = {}
for key in self.e_keys:
for key in self.phase_keys:
prop = f'ac_{key}_{phase_id}'
if hasattr(self, prop):
p_exists = True
@ -149,7 +150,7 @@ class StatusResponse(Response):
s_exists = False
string_id = len(strings)
string = {}
for key in self.e_keys:
for key in self.string_keys:
prop = f'dc_{key}_{string_id}'
if hasattr(self, prop):
s_exists = True
@ -170,14 +171,27 @@ class StatusResponse(Response):
data['phases'] = self.phases
data['strings'] = self.strings
data['temperature'] = self.temperature
data['frequency'] = self.frequency
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']:
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
@ -469,7 +483,7 @@ class Hm300Decode0B(StatusResponse):
""" Phase 1 watts """
return self.unpack('>H', 18)[0]/10
@property
def frequency(self):
def ac_frequency_0(self):
""" Grid frequency in Hertz """
return self.unpack('>H', 16)[0]/100
@property
@ -568,7 +582,7 @@ class Hm600Decode0B(StatusResponse):
""" Phase 1 watts """
return self.unpack('>H', 30)[0]/10
@property
def frequency(self):
def ac_frequency_0(self):
""" Grid frequency in Hertz """
return self.unpack('>H', 28)[0]/100
@property
@ -729,7 +743,7 @@ class Hm1200Decode0B(StatusResponse):
""" Phase 1 watts """
return self.unpack('>H', 50)[0]/10
@property
def frequency(self):
def ac_frequency_0(self):
""" Grid frequency in Hertz """
return self.unpack('>H', 48)[0]/100
@property

122
tools/rpi/hoymiles/outputs.py

@ -10,11 +10,6 @@ import logging
from datetime import datetime, timezone
from hoymiles.decoders import StatusResponse, HardwareInfoResponse
try:
from influxdb_client import InfluxDBClient
except ModuleNotFoundError:
pass
class OutputPluginFactory:
def __init__(self, **params):
"""
@ -59,10 +54,19 @@ class InfluxOutputPlugin(OutputPluginFactory):
"""
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._org = params.get('org', '')
self._measurement = params.get('measurement',
f'inverter,host={socket.gethostname()}')
self._measurement = params.get('measurement', f'inverter,host={socket.gethostname()}')
client = InfluxDBClient(url, token, bucket=self._bucket)
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=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=frequency value={phase["frequency"]:.3f} {ctime}')
phase_id = phase_id + 1
# DC Data
@ -115,7 +120,6 @@ 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=YieldDay value={string["energy_daily"]:.2f} {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}')
string_id = string_id + 1
@ -123,20 +127,16 @@ class InfluxOutputPlugin(OutputPluginFactory):
if data['event_count'] is not None:
data_stack.append(f'{measurement},type=total_events value={data["event_count"]} {ctime}')
if data['powerfactor'] is not None:
data_stack.append(f'{measurement},type=pf value={data["powerfactor"]:f} {ctime}')
data_stack.append(f'{measurement},type=frequency value={data["frequency"]:.3f} {ctime}')
data_stack.append(f'{measurement},type=PF_AC value={data["powerfactor"]:f} {ctime}')
data_stack.append(f'{measurement},type=Temp value={data["temperature"]:.2f} {ctime}')
if data['energy_total'] is not None:
data_stack.append(f'{measurement},type=total value={data["energy_total"]/1000:.3f} {ctime}')
if data['yield_total'] is not None:
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)
try:
import paho.mqtt.client
except ModuleNotFoundError:
pass
class MqttOutputPlugin(OutputPluginFactory):
""" Mqtt output plugin """
client = None
@ -164,6 +164,16 @@ class MqttOutputPlugin(OutputPluginFactory):
"""
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()
if config.get('useTLS',False):
mqtt_client.tls_set()
@ -196,33 +206,41 @@ class MqttOutputPlugin(OutputPluginFactory):
# AC Data
phase_id = 0
phase_sum_power = 0
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}/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}/frequency', phase['frequency'])
phase_id = phase_id + 1
phase_sum_power += phase['power']
# DC Data
string_id = 0
string_sum_power = 0
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}/current', string['current'])
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}/YieldTotal', string['energy_total']/1000)
if 'irradiation' in string:
self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation'])
string_id = string_id + 1
string_sum_power += string['power']
# Global
if data['event_count'] is not None:
self.client.publish(f'{topic}/total_events', data['event_count'])
if data['powerfactor'] is not None:
self.client.publish(f'{topic}/pf', data['powerfactor'])
self.client.publish(f'{topic}/frequency', data['frequency'])
self.client.publish(f'{topic}/PF_AC', data['powerfactor'])
self.client.publish(f'{topic}/Temp', data['temperature'])
if data['energy_total'] is not None:
self.client.publish(f'{topic}/total', data['energy_total']/1000)
if data['yield_total'] is not None:
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):
self.client.publish(f'{topic}/Firmware/Version',\
@ -236,12 +254,6 @@ class MqttOutputPlugin(OutputPluginFactory):
else:
raise ValueError('Data needs to be instance of StatusResponse or a instance of HardwareInfoResponse')
try:
import requests
import time
except ModuleNotFoundError:
pass
class VzInverterOutput:
def __init__(self, config, 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_current{phase_id}', phase['current'])
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
# 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_current{string_id}', string['current'])
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_YieldTotal{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_energy_daily{string_id}', string['energy_daily'])
self.try_publish(ts, f'dc_energy_total{string_id}', string['energy_total'])
self.try_publish(ts, f'dc_irradiation{string_id}', string['irradiation'])
string_id = string_id + 1
# Global
if data['event_count'] is not None:
self.try_publish(ts, f'event_count', data['event_count'])
if data['powerfactor'] is not None:
self.try_publish(ts, f'powerfactor', data['powerfactor'])
self.try_publish(ts, f'frequency', data['frequency'])
self.try_publish(ts, f'Temp', data['temperature'])
if data['energy_total'] is not None:
self.try_publish(ts, f'total', data['energy_total'])
self.try_publish(ts, f'temperature', data['temperature'])
if data['yield_total'] is not None:
self.try_publish(ts, f'yield_total', data['yield_total'])
if data['yield_today'] is not None:
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):
if not ctype in self.channels:
logging.warning(f'ctype \"{ctype}\" not found in ahoy.yml')
return
uid = self.channels[ctype]
url = f'{self.baseurl}/data/{uid}.json?operation=add&ts={ts}&value={value}'
try:
r = self.session.get(url)
if r.status_code != 200:
raise ValueError('Could not send request (%s)' % url)
except requests.exceptions.ConnectionError as e:
raise ValueError('Could not send request (%s)' % e)
if r.status_code == 404:
logging.critical('VZ-DB not reachable, please check "middleware"')
if r.status_code == 400:
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):
def __init__(self, config, **params):
@ -318,6 +337,17 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory):
"""
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.inverters = dict()

Loading…
Cancel
Save