Browse Source

RPi:print HardwareInfoResponse on MQTT channel

print HardwareInfoResponse on MQTT channel
check: HardwareInfoResponse does not print on VZ
pull/603/head
Knuti_in_Päse 2 years ago
parent
commit
67ed21ae2a
  1. 100
      tools/rpi/hoymiles/__main__.py
  2. 90
      tools/rpi/hoymiles/outputs.py

100
tools/rpi/hoymiles/__main__.py

@ -81,8 +81,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 +92,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 +161,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,71 +170,35 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
dtu_ser=dtu_ser, dtu_ser=dtu_ser,
strings=inverter_strings strings=inverter_strings
) )
# get decoder object
result = decoder.decode() result = decoder.decode()
if isinstance(result, hoymiles.decoders.StatusResponse): if hoymiles.HOYMILES_DEBUG_LOGGING:
data = result.__dict__() logging.debug(f'{c_datetime} Decoded: {result.__dict__()}')
if hoymiles.HOYMILES_DEBUG_LOGGING: # check decoder object for output
logging.debug(f'{c_datetime} Decoded: {result.__dict__()}') 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:
influx_client.store_status(result) influx_client.store_status(result)
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):
"""
Publish StatusResponse object
:param paho.mqtt.client.Client broker: mqtt-client instance # check decoder object for output
:param str inverter_ser: inverter serial if isinstance(result, hoymiles.decoders.HardwareInfoResponse):
:param hoymiles.StatusResponse data: decoded inverter StatusResponse if mqtt_client:
:param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser}) mqtt_client.store_status(result, topic=inverter.get('mqtt', {}).get('topic', None))
: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):
""" """
@ -321,29 +287,27 @@ if __name__ == '__main__':
logging.error('Failed to load config file {global_config.config_file}: {e_yaml}') logging.error('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 +319,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 +345,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)

90
tools/rpi/hoymiles/outputs.py

@ -8,7 +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: try:
from influxdb_client import InfluxDBClient from influxdb_client import InfluxDBClient
@ -185,45 +185,56 @@ 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)}'
# Global Head if isinstance(response, StatusResponse):
if data['time'] is not None:
self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S")) # Global Head
if data['time'] is not None:
# AC Data self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S"))
phase_id = 0
for phase in data['phases']: # AC Data
self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) phase_id = 0
self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage']) for phase in data['phases']:
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}/voltage', phase['voltage'])
phase_id = phase_id + 1 self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current'])
self.client.publish(f'{topic}/emeter/{phase_id}/Q_AC', phase['reactive_power'])
# DC Data phase_id = phase_id + 1
string_id = 0
for string in data['strings']: # DC Data
self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage']) string_id = 0
self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) for string in data['strings']:
self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power']) self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage'])
self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldDay', string['energy_daily']) self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current'])
self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldTotal', string['energy_total']/1000) self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power'])
if 'irradiation' in string: self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldDay', string['energy_daily'])
self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation']) self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldTotal', string['energy_total']/1000)
string_id = string_id + 1 if 'irradiation' in string:
self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation'])
# Global string_id = string_id + 1
if data['powerfactor'] is not None:
self.client.publish(f'{topic}/pf', data['powerfactor']) # Global
self.client.publish(f'{topic}/frequency', data['frequency']) if data['powerfactor'] is not None:
self.client.publish(f'{topic}/pf', data['powerfactor'])
self.client.publish(f'{topic}/Temp', data['temperature']) self.client.publish(f'{topic}/frequency', data['frequency'])
if data['energy_total'] is not None:
self.client.publish(f'{topic}/total', data['energy_total']/1000) 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)
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.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.client.publish(f'{topic}/Firmware/HWPartId', f'{data["FW_HW_ID"]}')
else:
raise ValueError('Data needs to be instance of StatusResponse or a instance of HardwareInfoResponse')
try: try:
import requests import requests
@ -237,6 +248,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')
@ -286,6 +298,7 @@ class VzInverterOutput:
if data['energy_total'] is not None: if data['energy_total'] is not None:
self.try_publish(ts, f'total', data['energy_total']) self.try_publish(ts, f'total', data['energy_total'])
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:
return return
@ -307,6 +320,7 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory):
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 +334,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