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. 125
      tools/rpi/hoymiles/__main__.py
  4. 82
      tools/rpi/hoymiles/decoders/__init__.py
  5. 213
      tools/rpi/hoymiles/outputs.py

33
tools/rpi/ahoy.yml.example

@ -28,6 +28,11 @@ ahoy:
password: 'password'
useTLS: False
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
influxdb:
@ -44,9 +49,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 +57,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

3
tools/rpi/hoymiles/__init__.py

@ -158,6 +158,9 @@ class ResponseDecoder(ResponseDecoderFactory):
model = self.inverter_model
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')
if hasattr(model_decoders, f'{model}Decode{command.upper()}'):
device = getattr(model_decoders, f'{model}Decode{command.upper()}')

125
tools/rpi/hoymiles/__main__.py

@ -17,10 +17,31 @@ from suntimes import SunTimes
import argparse
import yaml
from yaml.loader import SafeLoader
import paho.mqtt.client
import hoymiles
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):
InverterDevInform_Simple = 0 # 0x00
InverterDevInform_All = 1 # 0x01
@ -81,8 +102,8 @@ def main_loop(ahoy_config):
sunset = SunsetHandler(ahoy_config.get('sunset'))
dtu_ser = ahoy_config.get('dtu', {}).get('serial')
loop_interval = ahoy_config.get('interval', 1)
try:
do_init = True
while True:
@ -92,7 +113,7 @@ def main_loop(ahoy_config):
for inverter in inverters:
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)
do_init = False
@ -161,6 +182,8 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
c_datetime = datetime.now()
if hoymiles.HOYMILES_DEBUG_LOGGING:
logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
# prepare decoder object
decoder = hoymiles.ResponseDecoder(response,
request=com.request,
inverter_ser=inverter_ser,
@ -168,71 +191,35 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
dtu_ser=dtu_ser,
strings=inverter_strings
)
# get decoder object
result = decoder.decode()
if isinstance(result, hoymiles.decoders.StatusResponse):
data = result.__dict__()
if hoymiles.HOYMILES_DEBUG_LOGGING:
logging.info(f'{c_datetime} Decoded: {result.__dict__()}')
if hoymiles.HOYMILES_DEBUG_LOGGING:
logging.debug(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_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]))
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:
influx_client.store_status(result)
influx_client.store_status(result)
if volkszaehler_client:
volkszaehler_client.store_status(result)
def mqtt_send_status(broker, inverter_ser, data, topic=None):
"""
Publish StatusResponse object
volkszaehler_client.store_status(result)
: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
"""
# check decoder object for output
if isinstance(result, hoymiles.decoders.HardwareInfoResponse):
if mqtt_client:
mqtt_client.store_status(result, topic=inverter.get('mqtt', {}).get('topic', None))
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):
"""
@ -318,32 +305,30 @@ 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
ahoy_config = dict(cfg.get('ahoy', {}))
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:
hoymiles.HOYMILES_TRANSACTION_LOGGING=True
if global_config.verbose:
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_config = ahoy_config.get('mqtt', {})
if mqtt_config and not mqtt_config.get('disabled', False):
from .outputs import MqttOutputPlugin
mqtt_client = MqttOutputPlugin(mqtt_config)
# create INFLUX - client object
influx_client = None
influx_config = ahoy_config.get('influxdb', {})
if influx_config and not influx_config.get('disabled', False):
@ -355,23 +340,24 @@ if __name__ == '__main__':
bucket=influx_config.get('bucket', None),
measurement=influx_config.get('measurement', 'hoymiles'))
# create VOLKSZAEHLER - client object
volkszaehler_client = None
volkszaehler_config = ahoy_config.get('volkszaehler', {})
if volkszaehler_config and not volkszaehler_config.get('disabled', False):
from .outputs import VolkszaehlerOutputPlugin
volkszaehler_client = VolkszaehlerOutputPlugin(
volkszaehler_config)
volkszaehler_client = VolkszaehlerOutputPlugin(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', []):
g_inverter_ser = g_inverter.get('serial')
inv_str = str(g_inverter_ser)
command_queue[inv_str] = []
event_message_index[inv_str] = 0
#
# Enables and subscribe inverter to mqtt /command-Topic
#
if mqtt_client and g_inverter.get('mqtt', {}).get('send_raw_enabled', False):
topic_item = (
str(g_inverter_ser),
@ -380,5 +366,6 @@ if __name__ == '__main__':
mqtt_client.subscribe(topic_item[1])
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)

82
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['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
@ -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')
"""
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
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_min = int((fw_version % 10000) / 100)
@ -355,10 +373,22 @@ class HardwareInfoResponse(UnknownResponse):
fw_build_dd = int(fw_build_mmdd % 100)
fw_build_HH = 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'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):
"""Default decoder"""
@ -405,10 +435,10 @@ class DebugDecodeAny(UnknownResponse):
# 1121-Series Intervers, 1 MPPT, 1 Phase
class Hm300Decode01(HardwareInfoResponse):
""" Firmware version / date """
""" 1121-series Firmware version / date """
class Hm300Decode02(EventsResponse):
""" Inverter generic events log """
""" 1121-series Inverter generic events log """
class Hm300Decode0B(StatusResponse):
""" 1121-series mirco-inverters status data """
@ -453,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
@ -469,18 +499,18 @@ class Hm300Decode0C(Hm300Decode0B):
""" 1121-series mirco-inverters status data """
class Hm300Decode11(EventsResponse):
""" Inverter generic events log """
""" 1121-series Inverter generic events log """
class Hm300Decode12(EventsResponse):
""" Inverter major events log """
""" 1121-series Inverter major events log """
# 1141-Series Inverters, 2 MPPT, 1 Phase
class Hm600Decode01(HardwareInfoResponse):
""" Firmware version / date """
""" 1141-Series Firmware version / date """
class Hm600Decode02(EventsResponse):
""" Inverter generic events log """
""" 1141-Series Inverter generic events log """
class Hm600Decode0B(StatusResponse):
""" 1141-series mirco-inverters status data """
@ -552,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
@ -576,18 +606,18 @@ class Hm600Decode0C(Hm600Decode0B):
""" 1141-series mirco-inverters status data """
class Hm600Decode11(EventsResponse):
""" Inverter generic events log """
""" 1141-Series Inverter generic events log """
class Hm600Decode12(EventsResponse):
""" Inverter major events log """
""" 1141-Series Inverter major events log """
# 1161-Series Inverters, 2 MPPT, 1 Phase
class Hm1200Decode01(HardwareInfoResponse):
""" Firmware version / date """
""" 1161-Series Firmware version / date """
class Hm1200Decode02(EventsResponse):
""" Inverter generic events log """
""" 1161-Series Inverter generic events log """
class Hm1200Decode0B(StatusResponse):
""" 1161-series mirco-inverters status data """
@ -713,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
@ -737,7 +767,7 @@ class Hm1200Decode0C(Hm1200Decode0B):
""" 1161-series mirco-inverters status data """
class Hm1200Decode11(EventsResponse):
""" Inverter generic events log """
""" 1161-Series Inverter generic events log """
class Hm1200Decode12(EventsResponse):
""" Inverter major events log """
""" 1161-Series Inverter major events log """

213
tools/rpi/hoymiles/outputs.py

@ -8,12 +8,7 @@ Hoymiles output plugin library
import socket
import logging
from datetime import datetime, timezone
from hoymiles.decoders import StatusResponse
try:
from influxdb_client import InfluxDBClient
except ModuleNotFoundError:
pass
from hoymiles.decoders import StatusResponse, HardwareInfoResponse
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,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=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}')
data_stack.append(f'{measurement},string={string_id},type=Irradiation value={string["irradiation"]:.2f} {ctime}')
string_id = string_id + 1
# Global
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,15 +164,38 @@ 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()
mqtt_client.tls_insecure_set(config.get('insecureTLS',False))
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.loop_start()
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):
"""
@ -185,51 +208,66 @@ class MqttOutputPlugin(OutputPluginFactory):
: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__()
topic = f'{data.get("inverter_name", "hoymiles")}/{data.get("inverter_ser", None)}'
# Global Head
if data['time'] is not None:
self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S"))
# AC Data
phase_id = 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}/Q_AC', phase['reactive_power'])
phase_id = phase_id + 1
# DC Data
string_id = 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
# Global
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}/Temp', data['temperature'])
if data['energy_total'] is not None:
self.client.publish(f'{topic}/total', data['energy_total']/1000)
try:
import requests
import time
except ModuleNotFoundError:
pass
if isinstance(response, StatusResponse):
# Global Head
if data['time'] is not None:
self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S"), self.qos, self.ret)
# AC Data
phase_id = 0
phase_sum_power = 0
for phase in data['phases']:
self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage'], self.qos, self.ret)
self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current'], self.qos, self.ret)
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.qos, self.ret)
self.client.publish(f'{topic}/emeter/{phase_id}/frequency', phase['frequency'], self.qos, self.ret)
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.qos, self.ret)
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.qos, self.ret)
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.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation'], self.qos, self.ret)
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'], self.qos, self.ret)
if data['powerfactor'] is not None:
self.client.publish(f'{topic}/PF_AC', data['powerfactor'], self.qos, self.ret)
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)
elif isinstance(response, HardwareInfoResponse):
self.client.publish(f'{topic}/Firmware/Version',\
f'{data["FW_ver_maj"]}.{data["FW_ver_min"]}.{data["FW_ver_pat"]}', self.qos, self.ret)
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:
def __init__(self, config, session):
@ -237,6 +275,7 @@ class VzInverterOutput:
self.serial = config.get('serial')
self.baseurl = config.get('url', 'http://localhost/middleware/')
self.channels = dict()
for channel in config.get('channels', []):
uid = channel.get('uid')
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_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
@ -271,32 +311,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):
@ -305,8 +352,20 @@ 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()
for inverterconfig in config.get('inverters', []):
serial = inverterconfig.get('serial')
output = VzInverterOutput(inverterconfig, self.session)
@ -320,6 +379,8 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory):
:raises ValueError: when response is not instance of StatusResponse
"""
# check decoder object for output
if not isinstance(response, StatusResponse):
raise ValueError('Data needs to be instance of StatusResponse')

Loading…
Cancel
Save