Browse Source

Merge branch 'main' into development03

pull/658/head
lumapu 2 years ago
parent
commit
a25aad3bb3
  1. 33
      tools/rpi/ahoy.yml.example
  2. 3
      tools/rpi/hoymiles/__init__.py
  3. 119
      tools/rpi/hoymiles/__main__.py
  4. 82
      tools/rpi/hoymiles/decoders/__init__.py
  5. 177
      tools/rpi/hoymiles/outputs.py

33
tools/rpi/ahoy.yml.example

@ -28,6 +28,11 @@ ahoy:
password: 'password' password: 'password'
useTLS: False useTLS: False
insecureTLS: False #set True for e.g. self signed certificates. insecureTLS: False #set True for e.g. self signed certificates.
QoS: 0
Retain: True
last_will:
topic: Appelweg_PV/114181807700 # defaults to 'hoymiles/{serial}'
payload: "LAST-WILL-MESSAGE: Please check my HOST and Process!"
# Influx2 output # Influx2 output
influxdb: influxdb:
@ -44,9 +49,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 +57,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

3
tools/rpi/hoymiles/__init__.py

@ -158,6 +158,9 @@ class ResponseDecoder(ResponseDecoderFactory):
model = self.inverter_model model = self.inverter_model
command = self.request_command command = self.request_command
c_datetime = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f")
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()}'):
device = getattr(model_decoders, f'{model}Decode{command.upper()}') device = getattr(model_decoders, f'{model}Decode{command.upper()}')

119
tools/rpi/hoymiles/__main__.py

@ -17,10 +17,31 @@ 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 hoymiles import hoymiles
import logging import logging
################################################################################
""" Signal Handler """
################################################################################
# from signal import signal, Signals, SIGINT, SIGTERM, SIGKILL, SIGHUP
from signal import *
def signal_handler(sig_num, frame):
signame = Signals(sig_num).name
logging.info(f'Stop by Signal {signame} ({sig_num})')
print (f'Stop by Signal <{signame}> ({sig_num}) at: {time.strftime("%d.%m.%Y %H:%M:%S")}')
if mqtt_client:
mqtt_client.disco()
sys.exit(0)
signal(SIGINT, signal_handler) # Interrupt from keyboard (CTRL + C)
signal(SIGTERM, signal_handler) # Signal Handler from terminating processes
signal(SIGHUP, signal_handler) # Hangup detected on controlling terminal or death of controlling process
# signal(SIGKILL, signal_handler) # Signal Handler SIGKILL and SIGSTOP cannot be caught, blocked, or ignored!!
################################################################################
################################################################################
class InfoCommands(IntEnum): class InfoCommands(IntEnum):
InverterDevInform_Simple = 0 # 0x00 InverterDevInform_Simple = 0 # 0x00
InverterDevInform_All = 1 # 0x01 InverterDevInform_All = 1 # 0x01
@ -81,8 +102,8 @@ def main_loop(ahoy_config):
sunset = SunsetHandler(ahoy_config.get('sunset')) sunset = SunsetHandler(ahoy_config.get('sunset'))
dtu_ser = ahoy_config.get('dtu', {}).get('serial') dtu_ser = ahoy_config.get('dtu', {}).get('serial')
loop_interval = ahoy_config.get('interval', 1) loop_interval = ahoy_config.get('interval', 1)
try: try:
do_init = True do_init = True
while True: while True:
@ -92,7 +113,7 @@ def main_loop(ahoy_config):
for inverter in inverters: for inverter in inverters:
if hoymiles.HOYMILES_DEBUG_LOGGING: if hoymiles.HOYMILES_DEBUG_LOGGING:
logging.debug(f'Poll inverter {inverter["serial"]}') logging.info(f'Poll inverter name={inverter["name"]} ser={inverter["serial"]}')
poll_inverter(inverter, dtu_ser, do_init, 3) poll_inverter(inverter, dtu_ser, do_init, 3)
do_init = False do_init = False
@ -161,6 +182,8 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
c_datetime = datetime.now() c_datetime = datetime.now()
if hoymiles.HOYMILES_DEBUG_LOGGING: if hoymiles.HOYMILES_DEBUG_LOGGING:
logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response)) logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
# prepare decoder object
decoder = hoymiles.ResponseDecoder(response, decoder = hoymiles.ResponseDecoder(response,
request=com.request, request=com.request,
inverter_ser=inverter_ser, inverter_ser=inverter_ser,
@ -168,20 +191,22 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
dtu_ser=dtu_ser, dtu_ser=dtu_ser,
strings=inverter_strings strings=inverter_strings
) )
result = decoder.decode()
if isinstance(result, hoymiles.decoders.StatusResponse):
data = result.__dict__()
# get decoder object
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
if isinstance(result, hoymiles.decoders.StatusResponse):
data = result.__dict__()
if 'event_count' in data: if 'event_count' in data:
if event_message_index[inv_str] < data['event_count']: if event_message_index[inv_str] < data['event_count']:
event_message_index[inv_str] = data['event_count'] event_message_index[inv_str] = data['event_count']
command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.AlarmData, alarm_id=event_message_index[inv_str])) command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.AlarmData, alarm_id=event_message_index[inv_str]))
if mqtt_client: if mqtt_client:
# mqtt_send_status(mqtt_client, inverter_ser, data, topic=inverter.get('mqtt', {}).get('topic', None))
mqtt_client.store_status(result, topic=inverter.get('mqtt', {}).get('topic', None)) mqtt_client.store_status(result, topic=inverter.get('mqtt', {}).get('topic', None))
if influx_client: if influx_client:
@ -190,49 +215,11 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
if volkszaehler_client: if volkszaehler_client:
volkszaehler_client.store_status(result) volkszaehler_client.store_status(result)
def mqtt_send_status(broker, inverter_ser, data, topic=None): # check decoder object for output
""" if isinstance(result, hoymiles.decoders.HardwareInfoResponse):
Publish StatusResponse object if mqtt_client:
mqtt_client.store_status(result, topic=inverter.get('mqtt', {}).get('topic', None))
:param paho.mqtt.client.Client broker: mqtt-client instance
:param str inverter_ser: inverter serial
:param hoymiles.StatusResponse data: decoded inverter StatusResponse
:param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser})
:type topic: str
"""
if not topic:
topic = f'hoymiles/{inverter_ser}'
# Global Head
if data['time'] is not None:
broker.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S"))
# AC Data
phase_id = 0
for phase in data['phases']:
broker.publish(f'{topic}/emeter/{phase_id}/power', phase['power'])
broker.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage'])
broker.publish(f'{topic}/emeter/{phase_id}/current', phase['current'])
broker.publish(f'{topic}/emeter/{phase_id}/Q_AC', phase['reactive_power'])
phase_id = phase_id + 1
# DC Data
string_id = 0
for string in data['strings']:
broker.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage'])
broker.publish(f'{topic}/emeter-dc/{string_id}/current', string['current'])
broker.publish(f'{topic}/emeter-dc/{string_id}/power', string['power'])
broker.publish(f'{topic}/emeter-dc/{string_id}/YieldDay', string['energy_daily'])
broker.publish(f'{topic}/emeter-dc/{string_id}/YieldTotal', string['energy_total']/1000)
string_id = string_id + 1
# Global
if data['powerfactor'] is not None:
broker.publish(f'{topic}/pf', data['powerfactor'])
broker.publish(f'{topic}/frequency', data['frequency'])
broker.publish(f'{topic}/temperature', data['temperature'])
if data['energy_total'] is not None:
broker.publish(f'{topic}/total', data['energy_total']/1000)
def mqtt_on_command(client, userdata, message): def mqtt_on_command(client, userdata, message):
""" """
@ -318,32 +305,30 @@ 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
ahoy_config = dict(cfg.get('ahoy', {})) ahoy_config = dict(cfg.get('ahoy', {}))
init_logging(ahoy_config) init_logging(ahoy_config)
# Prepare for multiple transceivers, makes them configurable (currently
# only one supported)
for radio_config in ahoy_config.get('nrf', [{}]):
hmradio = hoymiles.HoymilesNRF(**radio_config)
event_message_index = {}
command_queue = {}
mqtt_command_topic_subs = []
if global_config.log_transactions: if global_config.log_transactions:
hoymiles.HOYMILES_TRANSACTION_LOGGING=True hoymiles.HOYMILES_TRANSACTION_LOGGING=True
if global_config.verbose: if global_config.verbose:
hoymiles.HOYMILES_DEBUG_LOGGING=True hoymiles.HOYMILES_DEBUG_LOGGING=True
# Prepare for multiple transceivers, makes them configurable
for radio_config in ahoy_config.get('nrf', [{}]):
hmradio = hoymiles.HoymilesNRF(**radio_config)
# create MQTT - client object
mqtt_client = None mqtt_client = None
mqtt_config = ahoy_config.get('mqtt', {}) mqtt_config = ahoy_config.get('mqtt', {})
if mqtt_config and not mqtt_config.get('disabled', False): if mqtt_config and not mqtt_config.get('disabled', False):
from .outputs import MqttOutputPlugin from .outputs import MqttOutputPlugin
mqtt_client = MqttOutputPlugin(mqtt_config) mqtt_client = MqttOutputPlugin(mqtt_config)
# create INFLUX - client object
influx_client = None influx_client = None
influx_config = ahoy_config.get('influxdb', {}) influx_config = ahoy_config.get('influxdb', {})
if influx_config and not influx_config.get('disabled', False): if influx_config and not influx_config.get('disabled', False):
@ -355,23 +340,24 @@ if __name__ == '__main__':
bucket=influx_config.get('bucket', None), bucket=influx_config.get('bucket', None),
measurement=influx_config.get('measurement', 'hoymiles')) measurement=influx_config.get('measurement', 'hoymiles'))
# create VOLKSZAEHLER - client object
volkszaehler_client = None volkszaehler_client = None
volkszaehler_config = ahoy_config.get('volkszaehler', {}) volkszaehler_config = ahoy_config.get('volkszaehler', {})
if volkszaehler_config and not volkszaehler_config.get('disabled', False): if volkszaehler_config and not volkszaehler_config.get('disabled', False):
from .outputs import VolkszaehlerOutputPlugin from .outputs import VolkszaehlerOutputPlugin
volkszaehler_client = VolkszaehlerOutputPlugin( volkszaehler_client = VolkszaehlerOutputPlugin(volkszaehler_config)
volkszaehler_config)
event_message_index = {}
command_queue = {}
mqtt_command_topic_subs = []
g_inverters = [g_inverter.get('serial') for g_inverter in ahoy_config.get('inverters', [])]
for g_inverter in ahoy_config.get('inverters', []): for g_inverter in ahoy_config.get('inverters', []):
g_inverter_ser = g_inverter.get('serial') g_inverter_ser = g_inverter.get('serial')
inv_str = str(g_inverter_ser) inv_str = str(g_inverter_ser)
command_queue[inv_str] = [] command_queue[inv_str] = []
event_message_index[inv_str] = 0 event_message_index[inv_str] = 0
#
# Enables and subscribe inverter to mqtt /command-Topic # Enables and subscribe inverter to mqtt /command-Topic
#
if mqtt_client and g_inverter.get('mqtt', {}).get('send_raw_enabled', False): if mqtt_client and g_inverter.get('mqtt', {}).get('send_raw_enabled', False):
topic_item = ( topic_item = (
str(g_inverter_ser), str(g_inverter_ser),
@ -380,5 +366,6 @@ if __name__ == '__main__':
mqtt_client.subscribe(topic_item[1]) mqtt_client.subscribe(topic_item[1])
mqtt_command_topic_subs.append(topic_item) mqtt_command_topic_subs.append(topic_item)
logging.info(f'Starting main_loop with inverter(s) {g_inverters}') # start main-loop
main_loop(ahoy_config) main_loop(ahoy_config)

82
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']: for string in data['strings']:
data['energy_total'] += string['energy_total'] 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']:
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
@ -343,10 +357,14 @@ class HardwareInfoResponse(UnknownResponse):
}; };
self.response = bytes('\x27\x1a\x07\xe5\x04\x4d\x03\x4a\x00\x68\x00\x00\x00\x00\xe6\xfb', 'latin1') self.response = bytes('\x27\x1a\x07\xe5\x04\x4d\x03\x4a\x00\x68\x00\x00\x00\x00\xe6\xfb', 'latin1')
""" """
fw_version, fw_build_yyyy, fw_build_mmdd, fw_build_hhmm, hw_id = struct.unpack('>HHHHH', self.response[0:10])
def __dict__(self):
""" Base values, availabe in each __dict__ call """
responce_info = self.response responce_info = self.response
logging.debug(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", responce_info)}') logging.info(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", responce_info)}')
fw_version, fw_build_yyyy, fw_build_mmdd, fw_build_hhmm, hw_id = struct.unpack('>HHHHH', self.response[0:10])
fw_version_maj = int((fw_version / 10000)) fw_version_maj = int((fw_version / 10000))
fw_version_min = int((fw_version % 10000) / 100) fw_version_min = int((fw_version % 10000) / 100)
@ -355,10 +373,22 @@ class HardwareInfoResponse(UnknownResponse):
fw_build_dd = int(fw_build_mmdd % 100) fw_build_dd = int(fw_build_mmdd % 100)
fw_build_HH = int(fw_build_hhmm / 100) fw_build_HH = int(fw_build_hhmm / 100)
fw_build_MM = int(fw_build_hhmm % 100) fw_build_MM = int(fw_build_hhmm % 100)
logging.debug(f'Firmware: {fw_version_maj}.{fw_version_min}.{fw_version_pat} '\ logging.info(f'Firmware: {fw_version_maj}.{fw_version_min}.{fw_version_pat} '\
f'build at {fw_build_dd:>02}/{fw_build_mm:>02}/{fw_build_yyyy}T{fw_build_HH:>02}:{fw_build_MM:>02}, '\ f'build at {fw_build_dd:>02}/{fw_build_mm:>02}/{fw_build_yyyy}T{fw_build_HH:>02}:{fw_build_MM:>02}, '\
f'HW revision {hw_id}') f'HW revision {hw_id}')
data = super().__dict__()
data['FW_ver_maj'] = fw_version_maj
data['FW_ver_min'] = fw_version_min
data['FW_ver_pat'] = fw_version_pat
data['FW_build_yy'] = fw_build_yyyy
data['FW_build_mm'] = fw_build_mm
data['FW_build_dd'] = fw_build_dd
data['FW_build_HH'] = fw_build_HH
data['FW_build_MM'] = fw_build_MM
data['FW_HW_ID'] = hw_id
return data
class DebugDecodeAny(UnknownResponse): class DebugDecodeAny(UnknownResponse):
"""Default decoder""" """Default decoder"""
@ -405,10 +435,10 @@ class DebugDecodeAny(UnknownResponse):
# 1121-Series Intervers, 1 MPPT, 1 Phase # 1121-Series Intervers, 1 MPPT, 1 Phase
class Hm300Decode01(HardwareInfoResponse): class Hm300Decode01(HardwareInfoResponse):
""" Firmware version / date """ """ 1121-series Firmware version / date """
class Hm300Decode02(EventsResponse): class Hm300Decode02(EventsResponse):
""" Inverter generic events log """ """ 1121-series Inverter generic events log """
class Hm300Decode0B(StatusResponse): class Hm300Decode0B(StatusResponse):
""" 1121-series mirco-inverters status data """ """ 1121-series mirco-inverters status data """
@ -453,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
@ -469,18 +499,18 @@ class Hm300Decode0C(Hm300Decode0B):
""" 1121-series mirco-inverters status data """ """ 1121-series mirco-inverters status data """
class Hm300Decode11(EventsResponse): class Hm300Decode11(EventsResponse):
""" Inverter generic events log """ """ 1121-series Inverter generic events log """
class Hm300Decode12(EventsResponse): class Hm300Decode12(EventsResponse):
""" Inverter major events log """ """ 1121-series Inverter major events log """
# 1141-Series Inverters, 2 MPPT, 1 Phase # 1141-Series Inverters, 2 MPPT, 1 Phase
class Hm600Decode01(HardwareInfoResponse): class Hm600Decode01(HardwareInfoResponse):
""" Firmware version / date """ """ 1141-Series Firmware version / date """
class Hm600Decode02(EventsResponse): class Hm600Decode02(EventsResponse):
""" Inverter generic events log """ """ 1141-Series Inverter generic events log """
class Hm600Decode0B(StatusResponse): class Hm600Decode0B(StatusResponse):
""" 1141-series mirco-inverters status data """ """ 1141-series mirco-inverters status data """
@ -552,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
@ -576,18 +606,18 @@ class Hm600Decode0C(Hm600Decode0B):
""" 1141-series mirco-inverters status data """ """ 1141-series mirco-inverters status data """
class Hm600Decode11(EventsResponse): class Hm600Decode11(EventsResponse):
""" Inverter generic events log """ """ 1141-Series Inverter generic events log """
class Hm600Decode12(EventsResponse): class Hm600Decode12(EventsResponse):
""" Inverter major events log """ """ 1141-Series Inverter major events log """
# 1161-Series Inverters, 2 MPPT, 1 Phase # 1161-Series Inverters, 2 MPPT, 1 Phase
class Hm1200Decode01(HardwareInfoResponse): class Hm1200Decode01(HardwareInfoResponse):
""" Firmware version / date """ """ 1161-Series Firmware version / date """
class Hm1200Decode02(EventsResponse): class Hm1200Decode02(EventsResponse):
""" Inverter generic events log """ """ 1161-Series Inverter generic events log """
class Hm1200Decode0B(StatusResponse): class Hm1200Decode0B(StatusResponse):
""" 1161-series mirco-inverters status data """ """ 1161-series mirco-inverters status data """
@ -713,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
@ -737,7 +767,7 @@ class Hm1200Decode0C(Hm1200Decode0B):
""" 1161-series mirco-inverters status data """ """ 1161-series mirco-inverters status data """
class Hm1200Decode11(EventsResponse): class Hm1200Decode11(EventsResponse):
""" Inverter generic events log """ """ 1161-Series Inverter generic events log """
class Hm1200Decode12(EventsResponse): class Hm1200Decode12(EventsResponse):
""" Inverter major events log """ """ 1161-Series Inverter major events log """

177
tools/rpi/hoymiles/outputs.py

@ -8,12 +8,7 @@ Hoymiles output plugin library
import socket import socket
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from hoymiles.decoders import StatusResponse 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,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=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
@ -123,20 +127,16 @@ class InfluxOutputPlugin(OutputPluginFactory):
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,15 +164,38 @@ 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()
mqtt_client.tls_insecure_set(config.get('insecureTLS',False)) mqtt_client.tls_insecure_set(config.get('insecureTLS',False))
mqtt_client.username_pw_set(config.get('user', None), config.get('password', None)) mqtt_client.username_pw_set(config.get('user', None), config.get('password', None))
last_will = config.get('last_will', None)
if last_will:
lw_topic = last_will.get('topic', 'last will hoymiles')
lw_payload = last_will.get('payload', 'last will')
mqtt_client.will_set(str(lw_topic), str(lw_payload))
mqtt_client.connect(config.get('host', '127.0.0.1'), config.get('port', 1883)) mqtt_client.connect(config.get('host', '127.0.0.1'), config.get('port', 1883))
mqtt_client.loop_start() mqtt_client.loop_start()
self.client = mqtt_client self.client = mqtt_client
self.qos = config.get('QoS', 0) # Quality of Service
self.ret = config.get('Retain', True) # Retain Message
def disco(self, **params):
self.client.loop_stop() # Stop loop
self.client.disconnect() # disconnect
def store_status(self, response, **params): def store_status(self, response, **params):
""" """
@ -185,51 +208,66 @@ class MqttOutputPlugin(OutputPluginFactory):
:raises ValueError: when response is not instance of StatusResponse :raises ValueError: when response is not instance of StatusResponse
""" """
if not isinstance(response, StatusResponse):
raise ValueError('Data needs to be instance of StatusResponse')
data = response.__dict__() data = response.__dict__()
topic = f'{data.get("inverter_name", "hoymiles")}/{data.get("inverter_ser", None)}' topic = f'{data.get("inverter_name", "hoymiles")}/{data.get("inverter_ser", None)}'
if isinstance(response, StatusResponse):
# Global Head # Global Head
if data['time'] is not None: if data['time'] is not None:
self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S")) self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S"), self.qos, self.ret)
# 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.qos, self.ret)
self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage']) self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current'], self.qos, self.ret)
self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current']) self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power'], self.qos, self.ret)
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.qos, self.ret)
self.client.publish(f'{topic}/emeter/{phase_id}/frequency', phase['frequency'], self.qos, self.ret)
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.qos, self.ret)
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.qos, self.ret)
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.qos, self.ret)
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.qos, self.ret)
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, self.qos, self.ret)
if 'irradiation' in string: self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation'], self.qos, self.ret)
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'], self.qos, self.ret)
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.qos, self.ret)
self.client.publish(f'{topic}/frequency', data['frequency']) self.client.publish(f'{topic}/Temp', data['temperature'], self.qos, self.ret)
if data['yield_total'] is not None:
self.client.publish(f'{topic}/YieldTotal', data['yield_total']/1000, self.qos, self.ret)
if data['yield_today'] is not None:
self.client.publish(f'{topic}/YieldToday', data['yield_today']/1000, self.qos, self.ret)
self.client.publish(f'{topic}/Efficiency', data['efficiency'], self.qos, self.ret)
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)
try: elif isinstance(response, HardwareInfoResponse):
import requests self.client.publish(f'{topic}/Firmware/Version',\
import time f'{data["FW_ver_maj"]}.{data["FW_ver_min"]}.{data["FW_ver_pat"]}', self.qos, self.ret)
except ModuleNotFoundError:
pass self.client.publish(f'{topic}/Firmware/Build_at',\
f'{data["FW_build_dd"]}/{data["FW_build_mm"]}/{data["FW_build_yy"]}T{data["FW_build_HH"]}:{data["FW_build_MM"]}',\
self.qos, self.ret)
self.client.publish(f'{topic}/Firmware/HWPartId',\
f'{data["FW_HW_ID"]}', self.qos, self.ret)
else:
raise ValueError('Data needs to be instance of StatusResponse or a instance of HardwareInfoResponse')
class VzInverterOutput: class VzInverterOutput:
def __init__(self, config, session): def __init__(self, config, session):
@ -237,6 +275,7 @@ class VzInverterOutput:
self.serial = config.get('serial') self.serial = config.get('serial')
self.baseurl = config.get('url', 'http://localhost/middleware/') self.baseurl = config.get('url', 'http://localhost/middleware/')
self.channels = dict() self.channels = dict()
for channel in config.get('channels', []): for channel in config.get('channels', []):
uid = channel.get('uid') uid = channel.get('uid')
ctype = channel.get('type') ctype = channel.get('type')
@ -262,7 +301,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
@ -271,32 +311,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):
@ -305,8 +352,20 @@ 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()
for inverterconfig in config.get('inverters', []): for inverterconfig in config.get('inverters', []):
serial = inverterconfig.get('serial') serial = inverterconfig.get('serial')
output = VzInverterOutput(inverterconfig, self.session) output = VzInverterOutput(inverterconfig, self.session)
@ -320,6 +379,8 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory):
:raises ValueError: when response is not instance of StatusResponse :raises ValueError: when response is not instance of StatusResponse
""" """
# check decoder object for output
if not isinstance(response, StatusResponse): if not isinstance(response, StatusResponse):
raise ValueError('Data needs to be instance of StatusResponse') raise ValueError('Data needs to be instance of StatusResponse')

Loading…
Cancel
Save