diff --git a/tools/rpi/README.md b/tools/rpi/README.md index bc8b23c0..d1829706 100644 --- a/tools/rpi/README.md +++ b/tools/rpi/README.md @@ -80,12 +80,79 @@ python3 getting_started.py # to test and see whether RF24 class can be loaded as If there are no error messages on the last step, then the NRF24 Wrapper has been installed successfully. + +Building RF24 Wrapper for Debian 11 (bullseye) 64 bit operating system +---------------------------------------------------------------------- +The description above does not work on Debian 11 (bullseye) 64 bit operating system. +Please check first, if you have Debian 11 (bullseye) 64 bit operating system installed: + - `uname -a` search for aarch64 + - `lsb_release -d` + - `cat /etc/debian_version` + +There are 2 possible solutions to install the RF24 wrapper: + +**__1. Solution:__** +```code +sudo apt install cmake git python3-dev libboost-python-dev python3-pip python3-rpi.gpio + +sudo ln -s $(ls /usr/lib/$(ls /usr/lib/gcc | \ + head -1)/libboost_python3*.so | \ + tail -1) /usr/lib/$(ls /usr/lib/gcc | \ + head -1)/libboost_python3.so + +git clone https://github.com/nRF24/RF24.git +cd RF24 + +rm -rf build Makefile.inc +./configure --driver=SPIDEV +``` +> _edit `Makefile.inc` with your prefered editor e.g. nano or vi_ +> +> old: +>```code +> CPUFLAGS=-marm -march=armv6zk -mtune=arm1176jzf-s -mfpu=vfp -mfloat-abi=hard +> CFLAGS=-marm -march=armv6zk -mtune=arm1176jzf-s -mfpu=vfp -mfloat-abi=hard -Ofast -Wall -pthread +>``` +> new: +>```code +> CPUFLAGS= +> CFLAGS=-Ofast -Wall -pthread +>``` +_continue now_ +```code +make +sudo make install + +cd pyRF24 +rm -r ./build/ ./dist/ ./RF24.egg-info/ ./__pycache__/ #just to make sure there is no old stuff +python3 -m pip install --upgrade pip +python3 -m pip install . +python3 -m pip list #watch for RF24 module - if its there its installed +``` + + +**__2. Solution:__** +```code +sudo apt install git python3-dev libboost-python-dev python3-pip python3-rpi.gpio + +git clone --recurse-submodules https://github.com/nRF24/pyRF24.git +cd pyRF24 +python3 -m pip install . -v # this step takes about 5 minutes on my RPI-4 ! +``` + +If you have problems with your radio module from ahoi, e.g.: cannot interpret received data, +please try to reduce the speed of your radio module! +Add the following parameter to your ahoy.yml configuration file in "nrf" section: +`spispeed: 600000` (0.6 MHz) + + + Required python modules ----------------------- Some modules are not installed by default on a RaspberryPi, therefore add them manually: -``` +```code pip install crcmod pyyaml paho-mqtt SunTimes ``` @@ -112,7 +179,7 @@ Python parameters The application describes itself -``` +```code python3 -m hoymiles --help usage: hoymiles [-h] -c [CONFIG_FILE] [--log-transactions] [--verbose] @@ -180,7 +247,7 @@ Todo - Ability to talk to multiple inverters - MQTT gateway - understand channel hopping -- configurable polling interval +- ~~configurable polling interval~~ done: interval ist configurable in ahoy.yml - commands - picture of setup! - python module diff --git a/tools/rpi/ahoy.service b/tools/rpi/ahoy.service index 394bc09e..c7be5bb2 100644 --- a/tools/rpi/ahoy.service +++ b/tools/rpi/ahoy.service @@ -6,11 +6,9 @@ # WorkingDirectory (absolute path to your private ahoy dir) # To change other config parameter, please consult systemd documentation # -# To activate this service, create a link, enable and start the ahoy.service -# $ mkdir -p $HOME/.config/systemd/user -# $ ln -sf $(pwd)/ahoy/tools/rpi/ahoy.service -t $HOME/.config/systemd/user +# To activate this service, enable and start ahoy.service +# $ systemctl --user enable $(pwd)/ahoy/tools/rpi/ahoy.service # $ systemctl --user status ahoy -# $ systemctl --user enable ahoy # $ systemctl --user start ahoy # $ systemctl --user status ahoy # diff --git a/tools/rpi/ahoy.yml.example b/tools/rpi/ahoy.yml.example index fb033f80..9301067f 100644 --- a/tools/rpi/ahoy.yml.example +++ b/tools/rpi/ahoy.yml.example @@ -31,7 +31,7 @@ ahoy: QoS: 0 Retain: True last_will: - topic: Appelweg_PV/114181807700 # defaults to 'hoymiles/{serial}' + topic: my_DTU_name # Name of DTU - default: hoymiles/{DTU-serial} payload: "LAST-WILL-MESSAGE: Please check my HOST and Process!" # Influx2 output @@ -96,6 +96,7 @@ ahoy: dtu: serial: 99978563001 + name: my_DTU_name inverters: - name: 'balkon' @@ -103,14 +104,14 @@ ahoy: txpower: 'low' # txpower per inverter (min,low,high,max) mqtt: send_raw_enabled: false # allow inject debug data via mqtt - topic: 'hoymiles/114172221234' # defaults to '{inverter-name}/{serial}' + topic: 'hoymiles/114172220003' # defaults to '{inverter-name}/{serial}' strings: # list all available strings - s_name: 'String 1 left' # String 1 name - s_maxpower: 395 # String 1 max power in Wp + s_maxpower: 395 # String 1 max power in inverter - s_name: 'String 2 right' # String 2 name - s_maxpower: 400 # String 2 max power in Wp + s_maxpower: 400 # String 2 max power in inverter - s_name: 'String 3 up' # String 3 name - s_maxpower: 405 # String 3 max power in Wp + s_maxpower: 405 # String 3 max power in inverter - s_name: 'String 4 down' # String 4 name - s_maxpower: 410 # String 4 max power in Wp + s_maxpower: 410 # String 4 max power in inverter diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py index 74ec9fd8..210bed65 100644 --- a/tools/rpi/hoymiles/__init__.py +++ b/tools/rpi/hoymiles/__init__.py @@ -11,8 +11,28 @@ import re from datetime import datetime import logging import crcmod -from RF24 import RF24, RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16 from .decoders import * +from os import environ + +try: + # OSI Layer 2 driver for nRF24L01 on Arduino & Raspberry Pi/Linux Devices + # https://github.com/nRF24/RF24.git + from RF24 import RF24, RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16 + if environ.get('TERM') is not None: + print('Using python Module: RF24') +except ModuleNotFoundError as e: + if environ.get('TERM') is not None: + print(f'{e} - try to use module: RF24') + try: + # Repo for pyRF24 package + # https://github.com/nRF24/pyRF24.git + from pyrf24 import RF24, RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16 + if environ.get('TERM') is not None: + print(f'{e} - Using python Module: pyrf24') + except ModuleNotFoundError as e: + if environ.get('TERM') is not None: + print(f'{e} - exit') + exit() f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus') f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0) @@ -158,15 +178,26 @@ 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()}') + if HOYMILES_DEBUG_LOGGING: + if command.upper() == '01': + model_desc = "Firmware version / date" + elif command.upper() == '02': + model_desc = "Inverter generic events log" + elif command.upper() == '0B': + model_desc = "mirco-inverters status data" + elif command.upper() == '0C': + model_desc = "mirco-inverters status data" + elif command.upper() == '11': + model_desc = "Inverter generic events log" + elif command.upper() == '12': + model_desc = "Inverter major events log" + logging.info(f'model_decoder: {model}Decode{command.upper()} - {model_desc}') model_decoders = __import__('hoymiles.decoders') if hasattr(model_decoders, f'{model}Decode{command.upper()}'): device = getattr(model_decoders, f'{model}Decode{command.upper()}') else: - if HOYMILES_DEBUG_LOGGING: - device = getattr(model_decoders, 'DebugDecodeAny') + device = getattr(model_decoders, 'DebugDecodeAny') return device(self.response, time_rx=self.time_rx, diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index 97773ed1..7de4a1a2 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -33,6 +33,12 @@ def signal_handler(sig_num, frame): if mqtt_client: mqtt_client.disco() + if influx_client: + influx_client.disco() + + if volkszaehler_client: + volkszaehler_client.disco() + sys.exit(0) signal(SIGINT, signal_handler) # Interrupt from keyboard (CTRL + C) @@ -75,7 +81,6 @@ class SunsetHandler: else: logging.info('Sunset disabled.') - def checkWaitForSunrise(self): if not self.suntimes: return @@ -94,6 +99,23 @@ class SunsetHandler: time.sleep(time_to_sleep) logging.info (f'Woke up...') + def sun_status2mqtt(self, dtu_ser, dtu_name): + if not mqtt_client: + return + local_sunrise = self.suntimes.riselocal(datetime.now()).strftime("%d.%m.%YT%H:%M") + local_sunset = self.suntimes.setlocal(datetime.now()).strftime("%d.%m.%YT%H:%M") + local_zone = self.suntimes.setlocal(datetime.now()).tzinfo._key + if self.suntimes: + mqtt_client.info2mqtt({'topic' : f'{dtu_name}/{dtu_ser}'}, \ + {'dis_night_comm' : 'True', \ + 'local_sunrise' : local_sunrise, \ + 'local_sunset' : local_sunset, + 'local_zone' : local_zone}) + else: + mqtt_client.sun_info2mqtt({'sun_topic': f'{dtu_name}/{dtu_ser}'}, \ + {'dis_night_comm': 'False'}) + + def main_loop(ahoy_config): """Main loop""" inverters = [ @@ -101,7 +123,9 @@ def main_loop(ahoy_config): if not inverter.get('disabled', False)] sunset = SunsetHandler(ahoy_config.get('sunset')) - dtu_ser = ahoy_config.get('dtu', {}).get('serial') + dtu_ser = ahoy_config.get('dtu', {}).get('serial', None) + dtu_name = ahoy_config.get('dtu', {}).get('name', 'hoymiles-dtu') + sunset.sun_status2mqtt(dtu_ser, dtu_name) loop_interval = ahoy_config.get('interval', 1) try: @@ -112,6 +136,11 @@ def main_loop(ahoy_config): t_loop_start = time.time() for inverter in inverters: + if not 'name' in inverter: + inverter['name'] = 'hoymiles' + if not 'serial' in inverter: + logging.error("No inverter serial number found in ahoy.yml - exit") + sys.exit(999) if hoymiles.HOYMILES_DEBUG_LOGGING: logging.info(f'Poll inverter name={inverter["name"]} ser={inverter["serial"]}') poll_inverter(inverter, dtu_ser, do_init, 3) @@ -122,8 +151,6 @@ def main_loop(ahoy_config): if time_to_sleep > 0: time.sleep(time_to_sleep) - except KeyboardInterrupt: - sys.exit() except Exception as e: logging.fatal('Exception catched: %s' % e) logging.fatal(traceback.print_exc()) @@ -174,13 +201,14 @@ def poll_inverter(inverter, dtu_ser, do_init, retries): response = com.get_payload() payload_ttl = 0 except Exception as e_all: - logging.error(f'Error while retrieving data: {e_all}') + if hoymiles.HOYMILES_TRANSACTION_LOGGING: + logging.error(f'Error while retrieving data: {e_all}') pass # Handle the response data if any if response: - c_datetime = datetime.now() - if hoymiles.HOYMILES_DEBUG_LOGGING: + if hoymiles.HOYMILES_TRANSACTION_LOGGING: + c_datetime = datetime.now() logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response)) # prepare decoder object @@ -195,7 +223,7 @@ def poll_inverter(inverter, dtu_ser, do_init, retries): # get decoder object result = decoder.decode() if hoymiles.HOYMILES_DEBUG_LOGGING: - logging.info(f'{c_datetime} Decoded: {result.__dict__()}') + logging.info(f'Decoded: {result.__dict__()}') # check decoder object for output if isinstance(result, hoymiles.decoders.StatusResponse): @@ -281,7 +309,13 @@ def init_logging(ahoy_config): lvl = logging.WARNING elif level == 'ERROR': lvl = logging.ERROR + elif level == 'FATAL': + lvl = logging.FATAL + if hoymiles.HOYMILES_TRANSACTION_LOGGING: + lvl = logging.DEBUG logging.basicConfig(filename=fn, format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=lvl) + dtu_name = ahoy_config.get('dtu',{}).get('name','hoymiles-dtu') + logging.info(f'start logging for {dtu_name} with level: {logging.root.level}') if __name__ == '__main__': parser = argparse.ArgumentParser(description='Ahoy - Hoymiles solar inverter gateway', prog="hoymiles") @@ -308,29 +342,29 @@ if __name__ == '__main__': 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) - if global_config.log_transactions: hoymiles.HOYMILES_TRANSACTION_LOGGING=True if global_config.verbose: hoymiles.HOYMILES_DEBUG_LOGGING=True + # read AHOY configuration file and prepare logging + ahoy_config = dict(cfg.get('ahoy', {})) + init_logging(ahoy_config) + # 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', {}) + mqtt_config = ahoy_config.get('mqtt', None) 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', {}) + influx_config = ahoy_config.get('influxdb', None) if influx_config and not influx_config.get('disabled', False): from .outputs import InfluxOutputPlugin influx_client = InfluxOutputPlugin( diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index b46dbe48..bb32fb07 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -99,6 +99,7 @@ class StatusResponse(Response): frequency = None powerfactor = None event_count = None + unpack_error = False def unpack(self, fmt, base): """ @@ -110,6 +111,10 @@ class StatusResponse(Response): :rtype: tuple """ size = struct.calcsize(fmt) + if (len(self.response) < base+size): + self.unpack_error = True + logging.error(f'base: {base} size: {size} len: {len(self.response)} fmt: {fmt} rep: {self.response}') + return [0] return struct.unpack(fmt, self.response[base:base+size]) @property @@ -150,6 +155,7 @@ class StatusResponse(Response): s_exists = False string_id = len(strings) string = {} + string['name'] = self.inv_strings[string_id]['s_name'] for key in self.string_keys: prop = f'dc_{key}_{string_id}' if hasattr(self, prop): @@ -193,7 +199,8 @@ class StatusResponse(Response): data['event_count'] = self.event_count data['time'] = self.time_rx - return data + if not self.unpack_error: + return data class UnknownResponse(Response): """ @@ -321,9 +328,9 @@ class EventsResponse(UnknownResponse): #logging.debug(' payload has valid modbus crc') self.response = self.response[:-2] - status = struct.unpack('>H', self.response[:2])[0] - a_text = self.alarm_codes.get(status, 'N/A') - logging.info (f' Inverter status: {a_text} ({status})') + self.status = struct.unpack('>H', self.response[:2])[0] + self.a_text = self.alarm_codes.get(self.status, 'N/A') + logging.info (f'Inverter status: {self.a_text} ({self.status})') chunk_size = 12 for i_chunk in range(2, len(self.response), chunk_size): @@ -331,9 +338,12 @@ class EventsResponse(UnknownResponse): logging.debug(' '.join([f'{byte:02x}' for byte in chunk]) + ': ') + if (len(chunk[0:6]) < 6): + logging.error(f'length of chunk must be greater or equal 6 bytes: {chunk}') + return + opcode, a_code, a_count, uptime_sec = struct.unpack('>BBHH', chunk[0:6]) a_text = self.alarm_codes.get(a_code, 'N/A') - logging.debug(f' uptime={timedelta(seconds=uptime_sec)} a_count={a_count} opcode={opcode} a_code={a_code} a_text={a_text}') dbg = '' @@ -341,6 +351,14 @@ class EventsResponse(UnknownResponse): dbg += f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk)) logging.debug(dbg) + def __dict__(self): + """ Base values, availabe in each __dict__ call """ + + data = super().__dict__() + data['inv_stat_num'] = self.status + data['inv_stat_txt'] = self.a_text + return data + class HardwareInfoResponse(UnknownResponse): def __init__(self, *args, **params): super().__init__(*args, **params) @@ -361,9 +379,14 @@ class HardwareInfoResponse(UnknownResponse): def __dict__(self): """ Base values, availabe in each __dict__ call """ - responce_info = self.response - logging.info(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", responce_info)}') + data = super().__dict__() + if (len(self.response) != 16): + logging.error(f'HardwareInfoResponse: data length should be 16 bytes - measured {len(self.response)} bytes') + logging.error(f'HardwareInfoResponse: data: {self.response}') + return data + + logging.info(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", self.response[0:16])}') 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)) @@ -377,7 +400,6 @@ class HardwareInfoResponse(UnknownResponse): 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 @@ -468,6 +490,8 @@ class Hm300Decode0B(StatusResponse): """ String 1 irratiation in percent """ if self.inv_strings is None: return None + if self.inv_strings[0]['s_maxpower'] == 0: + return 0.00 return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3) @property @@ -540,6 +564,8 @@ class Hm600Decode0B(StatusResponse): """ String 1 irratiation in percent """ if self.inv_strings is None: return None + if self.inv_strings[0]['s_maxpower'] == 0: + return 0.00 return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3) @property @@ -567,6 +593,8 @@ class Hm600Decode0B(StatusResponse): """ String 2 irratiation in percent """ if self.inv_strings is None: return None + if self.inv_strings[1]['s_maxpower'] == 0: + return 0.00 return round(self.unpack('>H', 12)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3) @property @@ -647,6 +675,8 @@ class Hm1200Decode0B(StatusResponse): """ String 1 irratiation in percent """ if self.inv_strings is None: return None + if self.inv_strings[0]['s_maxpower'] == 0: + return 0.00 return round(self.unpack('>H', 8)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3) @property @@ -674,6 +704,8 @@ class Hm1200Decode0B(StatusResponse): """ String 2 irratiation in percent """ if self.inv_strings is None: return None + if self.inv_strings[1]['s_maxpower'] == 0: + return 0.00 return round(self.unpack('>H', 10)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3) @property @@ -701,6 +733,8 @@ class Hm1200Decode0B(StatusResponse): """ String 3 irratiation in percent """ if self.inv_strings is None: return None + if self.inv_strings[2]['s_maxpower'] == 0: + return 0.00 return round(self.unpack('>H', 30)[0]/10/self.inv_strings[2]['s_maxpower']*100, 3) @property @@ -728,6 +762,8 @@ class Hm1200Decode0B(StatusResponse): """ String 4 irratiation in percent """ if self.inv_strings is None: return None + if self.inv_strings[3]['s_maxpower'] == 0: + return 0.00 return round(self.unpack('>H', 32)[0]/10/self.inv_strings[3]['s_maxpower']*100, 3) @property diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py index 8fb55f3e..11971a85 100644 --- a/tools/rpi/hoymiles/outputs.py +++ b/tools/rpi/hoymiles/outputs.py @@ -9,6 +9,7 @@ import socket import logging from datetime import datetime, timezone from hoymiles.decoders import StatusResponse, HardwareInfoResponse +from hoymiles import HOYMILES_TRANSACTION_LOGGING, HOYMILES_DEBUG_LOGGING class OutputPluginFactory: def __init__(self, **params): @@ -39,6 +40,7 @@ class InfluxOutputPlugin(OutputPluginFactory): def __init__(self, url, token, **params): """ Initialize InfluxOutputPlugin + https://influxdb-client.readthedocs.io/en/stable/api.html#influxdbclient The following targets must be present in your InfluxDB. This does not automatically create anything for You. @@ -68,8 +70,12 @@ class InfluxOutputPlugin(OutputPluginFactory): self._org = params.get('org', '') self._measurement = params.get('measurement', f'inverter,host={socket.gethostname()}') - client = InfluxDBClient(url, token, bucket=self._bucket) - self.api = client.write_api() + with InfluxDBClient(url, token, bucket=self._bucket) as self.client: + self.api = self.client.write_api() + + def disco(self, **params): + self.client.close() # Shutdown the client + return def store_status(self, response, **params): """ @@ -102,6 +108,9 @@ class InfluxOutputPlugin(OutputPluginFactory): # InfluxDB requires nanoseconds ctime = int(utctime.timestamp() * 1e9) + if HOYMILES_DEBUG_LOGGING: + logging.info(f'InfluxDB: utctime: {utctime}') + # AC Data phase_id = 0 for phase in data['phases']: @@ -135,6 +144,9 @@ class InfluxOutputPlugin(OutputPluginFactory): 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}') + if HOYMILES_DEBUG_LOGGING: + #logging.debug(f'INFLUX data to DB: {data_stack}') + pass self.api.write(self._bucket, self._org, data_stack) class MqttOutputPlugin(OutputPluginFactory): @@ -196,6 +208,12 @@ class MqttOutputPlugin(OutputPluginFactory): def disco(self, **params): self.client.loop_stop() # Stop loop self.client.disconnect() # disconnect + return + + def info2mqtt(self, mqtt_topic, mqtt_data): + for mqtt_key in mqtt_data: + self.client.publish(f'{mqtt_topic["topic"]}/{mqtt_key}', mqtt_data[mqtt_key], self.qos, self.ret) + return def store_status(self, response, **params): """ @@ -209,13 +227,18 @@ class MqttOutputPlugin(OutputPluginFactory): """ data = response.__dict__() - topic = f'{data.get("inverter_name", "hoymiles")}/{data.get("inverter_ser", None)}' + topic = params.get('topic', None) + if not topic: + topic = f'{data.get("inverter_name", "hoymiles")}/{data.get("inverter_ser", None)}' + + if HOYMILES_DEBUG_LOGGING: + logging.info(f'MQTT-topic: {topic} data-type: {type(response)}') 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) + self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%YT%H:%M:%S"), self.qos, self.ret) # AC Data phase_id = 0 @@ -233,12 +256,16 @@ class MqttOutputPlugin(OutputPluginFactory): 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) + if 'name' in string: + string_name = string['name'].replace(" ","_") + else: + string_name = string_id + self.client.publish(f'{topic}/emeter-dc/{string_name}/voltage', string['voltage'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_name}/current', string['current'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_name}/power', string['power'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_name}/YieldDay', string['energy_daily'], self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_name}/YieldTotal', string['energy_total']/1000, self.qos, self.ret) + self.client.publish(f'{topic}/emeter-dc/{string_name}/Irradiation', string['irradiation'], self.qos, self.ret) string_id = string_id + 1 string_sum_power += string['power'] @@ -277,9 +304,10 @@ class VzInverterOutput: self.channels = dict() for channel in config.get('channels', []): - uid = channel.get('uid') + uid = channel.get('uid', None) ctype = channel.get('type') - if uid and ctype: + # if uid and ctype: + if ctype: self.channels[ctype] = uid def store_status(self, data, session): @@ -295,6 +323,9 @@ class VzInverterOutput: ts = int(round(data['time'].timestamp() * 1000)) + if HOYMILES_DEBUG_LOGGING: + logging.info(f'Volkszaehler-Timestamp: {ts}') + # AC Data phase_id = 0 for phase in data['phases']: @@ -327,13 +358,24 @@ class VzInverterOutput: 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']) + return def try_publish(self, ts, ctype, value): if not ctype in self.channels: - logging.warning(f'ctype \"{ctype}\" not found in ahoy.yml') + if HOYMILES_DEBUG_LOGGING: + 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}' + if uid == None: + if HOYMILES_DEBUG_LOGGING: + logging.debug(f'ctype \"{ctype}\" has no configured uid-value in ahoy.yml') + return + + if HOYMILES_DEBUG_LOGGING: + logging.debug(f'VZ-url: {url}') + try: r = self.session.get(url) if r.status_code == 404: @@ -344,6 +386,7 @@ class VzInverterOutput: raise ValueError(f'Transmit result {url}') except ConnectionError as e: raise ValueError(f'Could not connect VZ-DB {type(e)} {e.keys()}') + return class VolkszaehlerOutputPlugin(OutputPluginFactory): def __init__(self, config, **params): @@ -364,13 +407,17 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory): exit(1) self.session = requests.Session() - self.inverters = dict() + self.inverters = dict() for inverterconfig in config.get('inverters', []): serial = inverterconfig.get('serial') output = VzInverterOutput(inverterconfig, self.session) self.inverters[serial] = output + def disco(self, **params): + self.session.close() # closing the connection + return + def store_status(self, response, **params): """ Publish StatusResponse object @@ -395,3 +442,4 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory): output.store_status(data, self.session) except ValueError as e: logging.warning('Could not send data to volkszaehler instance: %s' % e) + return