From e4eebb67e402aeb75e1f89ce2edf5de74c08814e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan-Jonas=20S=C3=A4mann?= <sprinterfreak@binary-kitchen.de>
Date: Mon, 16 May 2022 12:29:58 +0200
Subject: [PATCH] Improve python code quality

Mostly fix naming convetion, add descriptions. Cleanup code, left behind
from tinkering.
---
 tools/rpi/hoymiles/__init__.py          | 112 ++++++++-------
 tools/rpi/hoymiles/__main__.py          |  48 ++++---
 tools/rpi/hoymiles/decoders/__init__.py | 173 +++++++++++++++---------
 tools/rpi/hoymiles/outputs.py           | 109 +++++++++++----
 4 files changed, 279 insertions(+), 163 deletions(-)

diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py
index af2d2f76..63fe2ebf 100644
--- a/tools/rpi/hoymiles/__init__.py
+++ b/tools/rpi/hoymiles/__init__.py
@@ -1,62 +1,65 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""
+Hoymiles micro-inverters python shared code
+"""
+
 import struct
-import crcmod
-import json
 import time
 import re
 from datetime import datetime
+import json
+import crcmod
 from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16
 from .decoders import *
 
 f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus')
 f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0)
 
-
 HOYMILES_TRANSACTION_LOGGING=False
 HOYMILES_DEBUG_LOGGING=False
 
-def ser_to_hm_addr(s):
+def ser_to_hm_addr(inverter_ser):
     """
-    Calculate the 4 bytes that the HM devices use in their internal messages to 
+    Calculate the 4 bytes that the HM devices use in their internal messages to
     address each other.
 
-    :param str s: inverter serial
+    :param str inverter_ser: inverter serial
     :return: inverter address
     :rtype: bytes
     """
-    bcd = int(str(s)[-8:], base=16)
+    bcd = int(str(inverter_ser)[-8:], base=16)
     return struct.pack('>L', bcd)
 
-def ser_to_esb_addr(s):
+def ser_to_esb_addr(inverter_ser):
     """
     Convert a Hoymiles inverter/DTU serial number into its
     corresponding NRF24 'enhanced shockburst' address byte sequence (5 bytes).
 
     The NRF library expects these in LSB to MSB order, even though the transceiver
     itself will then output them in MSB-to-LSB order over the air.
-    
+
     The inverters use a BCD representation of the last 8
-    digits of their serial number, in reverse byte order, 
+    digits of their serial number, in reverse byte order,
     followed by \x01.
 
-    :param str s: inverter serial
+    :param str inverter_ser: inverter serial
     :return: ESB inverter address
     :rtype: bytes
     """
-    air_order = ser_to_hm_addr(s)[::-1] + b'\x01'
+    air_order = ser_to_hm_addr(inverter_ser)[::-1] + b'\x01'
     return air_order[::-1]
 
-def print_addr(a):
+def print_addr(inverter_ser):
     """
     Debug print addresses
 
-    :param str a: inverter serial
+    :param str inverter_ser: inverter serial
     """
-    print(f"ser# {a} ", end='')
-    print(f" -> HM  {' '.join([f'{x:02x}' for x in ser_to_hm_addr(a)])}", end='')
-    print(f" -> ESB {' '.join([f'{x:02x}' for x in ser_to_esb_addr(a)])}")
-
-# time of last transmission - to calculcate response time
-t_last_tx = 0
+    print(f"ser# {inverter_ser} ", end='')
+    print(f" -> HM  {' '.join([f'{byte:02x}' for byte in ser_to_hm_addr(inverter_ser)])}", end='')
+    print(f" -> ESB {' '.join([f'{byte:02x}' for byte in ser_to_esb_addr(inverter_ser)])}")
 
 class ResponseDecoderFactory:
     """
@@ -115,16 +118,16 @@ class ResponseDecoderFactory:
             raise ValueError('Inverter serial while decoding response')
 
         ser_db = [
-                ('HM300', r'^1121........'),
-                ('HM600', r'^1141........'),
-                ('HM1200', r'^1161........'),
+                ('Hm300', r'^1121........'),
+                ('Hm600', r'^1141........'),
+                ('Hm1200', r'^1161........'),
                 ]
         ser_str = str(self.inverter_ser)
 
         model = None
-        for m, r in ser_db:
-            if re.match(r, ser_str):
-                model = m
+        for s_model, r_match in ser_db:
+            if re.match(r_match, ser_str):
+                model = s_model
                 break
 
         if len(model):
@@ -162,12 +165,12 @@ class ResponseDecoder(ResponseDecoderFactory):
         model = self.inverter_model
         command = self.request_command
 
-        model_decoders = __import__(f'hoymiles.decoders')
-        if hasattr(model_decoders, f'{model}_Decode{command.upper()}'):
-            device = getattr(model_decoders, f'{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()}')
         else:
             if HOYMILES_DEBUG_LOGGING:
-                device = getattr(model_decoders, f'DEBUG_DecodeAny')
+                device = getattr(model_decoders, 'DebugDecodeAny')
 
         return device(self.response,
                 time_rx=self.time_rx,
@@ -188,6 +191,8 @@ class InverterPacketFragment:
         :type ch_rx: int
         :param ch_tx: channel where request was sent
         :type ch_tx: int
+
+        :raises BufferError: when data gets lost on SPI bus
         """
 
         if not time_rx:
@@ -255,11 +260,11 @@ class InverterPacketFragment:
         :return: log line received frame
         :rtype: str
         """
-        dt = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f")
+        c_datetime = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f")
         size = len(self.frame)
         channel = f' channel {self.ch_rx}' if self.ch_rx else ''
         raw = " ".join([f"{b:02x}" for b in self.frame])
-        return f"{dt} Received {size} bytes{channel}: {raw}"
+        return f"{c_datetime} Received {size} bytes{channel}: {raw}"
 
 class HoymilesNRF:
     """Hoymiles NRF24 Interface"""
@@ -330,6 +335,7 @@ class HoymilesNRF:
 
             has_payload, pipe_number = self.radio.available_pipe()
             if has_payload:
+
                 # Data in nRF24 buffer, read it
                 self.rx_error = 0
                 self.rx_channel_ack = True
@@ -342,9 +348,11 @@ class HoymilesNRF:
                         ch_rx=self.rx_channel, ch_tx=self.tx_channel,
                         time_rx=datetime.now()
                         )
-                yield(fragment)
+
+                yield fragment
 
             else:
+
                 # No data in nRF rx buffer, search and wait
                 # Channel lock in (not currently used)
                 self.rx_error = self.rx_error + 1
@@ -407,7 +415,7 @@ def frame_payload(payload):
 
     return payload
 
-def compose_esb_fragment(fragment, seq=b'\80', src=99999999, dst=1, **params):
+def compose_esb_fragment(fragment, seq=b'\x80', src=99999999, dst=1, **params):
     """
     Build standart ESB request fragment
 
@@ -423,20 +431,19 @@ def compose_esb_fragment(fragment, seq=b'\80', src=99999999, dst=1, **params):
     :raises ValueError: if fragment size larger 16 byte
     """
     if len(fragment) > 17:
-        raise ValueError(f'ESB fragment exeeds mtu ({mtu}): Fragment size {len(fragment)} bytes')
+        raise ValueError(f'ESB fragment exeeds mtu: Fragment size {len(fragment)} bytes')
 
-    p = b''
-    p = p + b'\x15'
-    p = p + ser_to_hm_addr(dst)
-    p = p + ser_to_hm_addr(src)
-    p = p + seq
+    packet = b'\x15'
+    packet = packet + ser_to_hm_addr(dst)
+    packet = packet + ser_to_hm_addr(src)
+    packet = packet + seq
 
-    p = p + fragment
+    packet = packet + fragment
 
-    crc8 = f_crc8(p)
-    p = p + struct.pack('B', crc8)
+    crc8 = f_crc8(packet)
+    packet = packet + struct.pack('B', crc8)
 
-    return p
+    return packet
 
 def compose_esb_packet(packet, mtu=17, **params):
     """
@@ -449,7 +456,7 @@ def compose_esb_packet(packet, mtu=17, **params):
     """
     for i in range(0, len(packet), mtu):
         fragment = compose_esb_fragment(packet[i:i+mtu], **params)
-        yield(fragment)
+        yield fragment
 
 def compose_set_time_payload(timestamp=None):
     """
@@ -480,6 +487,7 @@ class InverterTransaction:
     inverter_addr = None
     dtu_ser = None
     req_type = None
+    time_rx = None
 
     radio = None
 
@@ -538,15 +546,15 @@ class InverterTransaction:
         if not self.radio:
             return False
 
-        if not len(self.tx_queue):
+        if len(self.tx_queue) == 0:
             return False
 
         packet = self.tx_queue.pop(0)
 
         if HOYMILES_TRANSACTION_LOGGING:
-            dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
-            print(f'{dt} Transmit {len(packet)} | {hexify_payload(packet)}')
-        
+            c_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
+            print(f'{c_datetime} Transmit {len(packet)} | {hexify_payload(packet)}')
+
         self.radio.transmit(packet)
 
         wait = False
@@ -554,7 +562,7 @@ class InverterTransaction:
             for response in self.radio.receive():
                 if HOYMILES_TRANSACTION_LOGGING:
                     print(response)
-        
+
                 self.frame_append(response)
                 wait = True
         except TimeoutError:
@@ -654,9 +662,9 @@ class InverterTransaction:
         :return: log line of payload for transmission
         :rtype: str
         """
-        dt = self.request_time.strftime("%Y-%m-%d %H:%M:%S.%f")
+        c_datetime = self.request_time.strftime("%Y-%m-%d %H:%M:%S.%f")
         size = len(self.request)
-        return f'{dt} Transmit | {hexify_payload(self.request)}'
+        return f'{c_datetime} Transmit | {hexify_payload(self.request)}'
 
 def hexify_payload(byte_var):
     """
diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py
index d97e1091..f74828da 100644
--- a/tools/rpi/hoymiles/__main__.py
+++ b/tools/rpi/hoymiles/__main__.py
@@ -1,17 +1,21 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 
+"""
+Hoymiles micro-inverters main application
+"""
+
 import sys
 import struct
 import re
 import time
 from datetime import datetime
 import argparse
-import hoymiles
-from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16
-import paho.mqtt.client
 import yaml
 from yaml.loader import SafeLoader
+import paho.mqtt.client
+from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16
+import hoymiles
 
 def main_loop():
     """Main loop"""
@@ -61,14 +65,14 @@ def poll_inverter(inverter, retries=4):
                 try:
                     response = com.get_payload()
                     payload_ttl = 0
-                except Exception as e:
-                    print(f'Error while retrieving data: {e}')
+                except Exception as e_all:
+                    print(f'Error while retrieving data: {e_all}')
                     pass
 
         # Handle the response data if any
         if response:
-            dt = datetime.now()
-            print(f'{dt} Payload: ' + hoymiles.hexify_payload(response))
+            c_datetime = datetime.now()
+            print(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
             decoder = hoymiles.ResponseDecoder(response,
                     request=com.request,
                     inverter_ser=inverter_ser
@@ -77,7 +81,7 @@ def poll_inverter(inverter, retries=4):
             if isinstance(result, hoymiles.decoders.StatusResponse):
                 data = result.__dict__()
                 if hoymiles.HOYMILES_DEBUG_LOGGING:
-                    print(f'{dt} Decoded: {data["temperature"]}', end='')
+                    print(f'{c_datetime} Decoded: {data["temperature"]}', end='')
                     phase_id = 0
                     for phase in data['phases']:
                         print(f' phase{phase_id}=voltage:{phase["voltage"]}, current:{phase["current"]}, power:{phase["power"]}, frequency:{data["frequency"]}', end='')
@@ -185,17 +189,17 @@ if __name__ == '__main__':
 
     # Load ahoy.yml config file
     try:
-        if isinstance(global_config.config_file, str) == True:
-            with open(global_config.config_file, 'r') as yf:
-                cfg = yaml.load(yf, Loader=SafeLoader)
+        if isinstance(global_config.config_file, str):
+            with open(global_config.config_file, 'r') as fh_yaml:
+                cfg = yaml.load(fh_yaml, Loader=SafeLoader)
         else:
-            with open('ahoy.yml', 'r') as yf:
-                cfg = yaml.load(yf, Loader=SafeLoader)
+            with open('ahoy.yml', 'r') as fh_yaml:
+                cfg = yaml.load(fh_yaml, Loader=SafeLoader)
     except FileNotFoundError:
         print("Could not load config file. Try --help")
         sys.exit(2)
-    except yaml.YAMLError as ye:
-        print('Failed to load config frile {global_config.config_file}: {ye}')
+    except yaml.YAMLError as e_yaml:
+        print('Failed to load config frile {global_config.config_file}: {e_yaml}')
         sys.exit(1)
 
     ahoy_config = dict(cfg.get('ahoy', {}))
@@ -241,18 +245,18 @@ if __name__ == '__main__':
     if not radio.begin():
         raise RuntimeError('Can\'t open radio')
 
-    inverters = [inverter.get('serial') for inverter in ahoy_config.get('inverters', [])]
-    for inverter in ahoy_config.get('inverters', []):
-        inverter_ser = inverter.get('serial')
-        command_queue[str(inverter_ser)] = []
+    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')
+        command_queue[str(g_inverter_ser)] = []
 
         #
         # Enables and subscribe inverter to mqtt /command-Topic
         #
-        if mqtt_client and inverter.get('mqtt', {}).get('send_raw_enabled', False):
+        if mqtt_client and g_inverter.get('mqtt', {}).get('send_raw_enabled', False):
             topic_item = (
-                    str(inverter_ser),
-                    inverter.get('mqtt', {}).get('topic', f'hoymiles/{inverter_ser}') + '/command'
+                    str(g_inverter_ser),
+                    g_inverter.get('mqtt', {}).get('topic', f'hoymiles/{g_inverter_ser}') + '/command'
                     )
             mqtt_client.subscribe(topic_item[1])
             mqtt_command_topic_subs.append(topic_item)
diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py
index cd0e87a0..59bc2292 100644
--- a/tools/rpi/hoymiles/decoders/__init__.py
+++ b/tools/rpi/hoymiles/decoders/__init__.py
@@ -1,12 +1,18 @@
 #!/usr/bin/python3
 # -*- coding: utf-8 -*-
+
+"""
+Hoymiles Micro-Inverters decoder library
+"""
+
 import struct
-import crcmod
 from datetime import datetime, timedelta
+import crcmod
 
 f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus')
 
 class Response:
+    """ All Response Shared methods """
     inverter_ser = None
     inverter_name = None
     dtu_ser = None
@@ -28,6 +34,7 @@ class Response:
             self.time_rx = datetime.now()
 
     def __dict__(self):
+        """ Base values, availabe in each __dict__ call """
         return {
                 'inverter_ser': self.inverter_ser,
                 'inverter_name': self.inverter_name,
@@ -36,9 +43,8 @@ class Response:
 class StatusResponse(Response):
     """Inverter StatusResponse object"""
     e_keys  = ['voltage','current','power','energy_total','energy_daily','powerfactor']
-
-    def __init__(self, *args, **params):
-        super().__init__(*args, **params)
+    temperature = None
+    frequency = None
 
     def unpack(self, fmt, base):
         """
@@ -120,9 +126,6 @@ class UnknownResponse(Response):
     Debugging helper for unknown payload format
     """
 
-    def __init__(self, *args, **params):
-        super().__init__(*args, **params)
-
     @property
     def hex_ascii(self):
         """
@@ -131,7 +134,7 @@ class UnknownResponse(Response):
         :return: hexlifierd byte string
         :rtype: str
         """
-        return ' '.join([f'{b:02x}' for b in self.response])
+        return ' '.join([f'{byte:02x}' for byte in self.response])
 
     @property
     def valid_crc(self):
@@ -153,13 +156,13 @@ class UnknownResponse(Response):
 
         res = self.response
 
-        r = len(res) % 16
-        res = res[:r*-1]
+        rem = len(res) % 16
+        res = res[:rem*-1]
 
         vals = None
         if len(res) % 16 == 0:
-            n = len(res)/4
-            vals = struct.unpack(f'>{int(n)}L', res)
+            rlen = len(res)/4
+            vals = struct.unpack(f'>{int(rlen)}L', res)
 
         return vals
 
@@ -171,13 +174,13 @@ class UnknownResponse(Response):
 
         res = self.response[2:]
 
-        r = len(res) % 16
-        res = res[:r*-1]
+        rem = len(res) % 16
+        res = res[:rem*-1]
 
         vals = None
         if len(res) % 16 == 0:
-            n = len(res)/4
-            vals = struct.unpack(f'>{int(n)}L', res)
+            rlen = len(res)/4
+            vals = struct.unpack(f'>{int(rlen)}L', res)
 
         return vals
 
@@ -189,13 +192,13 @@ class UnknownResponse(Response):
 
         res = self.response[4:]
 
-        r = len(res) % 16
-        res = res[:r*-1]
+        rem = len(res) % 16
+        res = res[:rem*-1]
 
         vals = None
         if len(res) % 16 == 0:
-            n = len(res)/4
-            vals = struct.unpack(f'>{int(n)}L', res)
+            rlen = len(res)/4
+            vals = struct.unpack(f'>{int(rlen)}L', res)
 
         return vals
 
@@ -207,13 +210,13 @@ class UnknownResponse(Response):
 
         res = self.response[6:]
 
-        r = len(res) % 16
-        res = res[:r*-1]
+        rem = len(res) % 16
+        res = res[:rem*-1]
 
         vals = None
         if len(res) % 16 == 0:
-            n = len(res)/4
-            vals = struct.unpack(f'>{int(n)}L', res)
+            rlen = len(res)/4
+            vals = struct.unpack(f'>{int(rlen)}L', res)
 
         return vals
 
@@ -225,13 +228,13 @@ class UnknownResponse(Response):
 
         res = self.response
 
-        r = len(res) % 4
-        res = res[:r*-1]
+        rem = len(res) % 4
+        res = res[:rem*-1]
 
         vals = None
         if len(res) % 4 == 0:
-            n = len(res)/2
-            vals = struct.unpack(f'>{int(n)}H', res)
+            rlen = len(res)/2
+            vals = struct.unpack(f'>{int(rlen)}H', res)
 
         return vals
 
@@ -243,17 +246,18 @@ class UnknownResponse(Response):
 
         res = self.response[1:]
 
-        r = len(res) % 4
-        res = res[:r*-1]
+        rem = len(res) % 4
+        res = res[:rem*-1]
 
         vals = None
         if len(res) % 4 == 0:
-            n = len(res)/2
-            vals = struct.unpack(f'>{int(n)}H', res)
+            rlen = len(res)/2
+            vals = struct.unpack(f'>{int(rlen)}H', res)
 
         return vals
 
 class EventsResponse(UnknownResponse):
+    """ Hoymiles micro-inverter event log decode helper """
 
     alarm_codes = {
             1: 'Inverter start',
@@ -337,10 +341,10 @@ class EventsResponse(UnknownResponse):
         status = self.response[:2]
 
         chunk_size = 12
-        for c in range(2, len(self.response), chunk_size):
-            chunk = self.response[c:c+chunk_size]
+        for i_chunk in range(2, len(self.response), chunk_size):
+            chunk = self.response[i_chunk:i_chunk+chunk_size]
 
-            print(' '.join([f'{b:02x}' for b in chunk]) + ': ')
+            print(' '.join([f'{byte:02x}' for byte in chunk]) + ': ')
 
             opcode, a_code, a_count, uptime_sec = struct.unpack('>BBHH', chunk[0:6])
             a_text = self.alarm_codes.get(a_code, 'N/A')
@@ -351,7 +355,7 @@ class EventsResponse(UnknownResponse):
                 print(f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk)))
             print(end='', flush=True)
 
-class DEBUG_DecodeAny(UnknownResponse):
+class DebugDecodeAny(UnknownResponse):
     """Default decoder"""
 
     def __init__(self, *args, **params):
@@ -415,206 +419,247 @@ class DEBUG_DecodeAny(UnknownResponse):
 
 
 # 1121-Series Intervers, 1 MPPT, 1 Phase
-class HM300_Decode0B(StatusResponse):
-    def __init__(self, *args, **params):
-        super().__init__(*args, **params)
+class Hm300Decode0B(StatusResponse):
+    """ 1121-series mirco-inverters status data """
 
     @property
     def dc_voltage_0(self):
+        """ String 1 VDC """
         return self.unpack('>H', 2)[0]/10
     @property
     def dc_current_0(self):
+        """ String 1 ampere """
         return self.unpack('>H', 4)[0]/100
     @property
     def dc_power_0(self):
+        """ String 1 watts """
         return self.unpack('>H', 6)[0]/10
     @property
     def dc_energy_total_0(self):
+        """ String 1 total energy in Wh """
         return self.unpack('>L', 8)[0]
     @property
     def dc_energy_daily_0(self):
+        """ String 1 daily energy in Wh """
         return self.unpack('>H', 12)[0]
 
-
     @property
     def ac_voltage_0(self):
+        """ Phase 1 VAC """
         return self.unpack('>H', 14)[0]/10
     @property
     def ac_current_0(self):
+        """ Phase 1 ampere """
         return self.unpack('>H', 22)[0]/100
     @property
     def ac_power_0(self):
+        """ Phase 1 watts """
         return self.unpack('>H', 18)[0]/10
     @property
     def frequency(self):
+        """ Grid frequency in Hertz """
         return self.unpack('>H', 16)[0]/100
     @property
     def temperature(self):
+        """ Inverter temperature in °C """
         return self.unpack('>H', 26)[0]/10
 
-class HM300_Decode11(EventsResponse):
-    def __init__(self, *args, **params):
-        super().__init__(*args, **params)
+class Hm300Decode11(EventsResponse):
+    """ Inverter generic events log """
 
-class HM300_Decode12(EventsResponse):
-    def __init__(self, *args, **params):
-        super().__init__(*args, **params)
+class Hm300Decode12(EventsResponse):
+    """ Inverter major events log """
 
 
 # 1141-Series Inverters, 2 MPPT, 1 Phase
-class HM600_Decode0B(StatusResponse):
-    def __init__(self, *args, **params):
-        super().__init__(*args, **params)
+class Hm600Decode0B(StatusResponse):
+    """ 1141-series mirco-inverters status data """
 
     @property
     def dc_voltage_0(self):
+        """ String 1 VDC """
         return self.unpack('>H', 2)[0]/10
     @property
     def dc_current_0(self):
+        """ String 1 ampere """
         return self.unpack('>H', 4)[0]/100
     @property
     def dc_power_0(self):
+        """ String 1 watts """
         return self.unpack('>H', 6)[0]/10
     @property
     def dc_energy_total_0(self):
+        """ String 1 total energy in Wh """
         return self.unpack('>L', 14)[0]
     @property
     def dc_energy_daily_0(self):
+        """ String 1 daily energy in Wh """
         return self.unpack('>H', 22)[0]
 
     @property
     def dc_voltage_1(self):
+        """ String 2 VDC """
         return self.unpack('>H', 8)[0]/10
     @property
     def dc_current_1(self):
+        """ String 2 ampere """
         return self.unpack('>H', 10)[0]/100
     @property
     def dc_power_1(self):
+        """ String 2 watts """
         return self.unpack('>H', 12)[0]/10
     @property
     def dc_energy_total_1(self):
+        """ String 2 total energy in Wh """
         return self.unpack('>L', 18)[0]
     @property
     def dc_energy_daily_1(self):
+        """ String 2 daily energy in Wh """
         return self.unpack('>H', 24)[0]
 
     @property
     def ac_voltage_0(self):
+        """ Phase 1 VAC """
         return self.unpack('>H', 26)[0]/10
     @property
     def ac_current_0(self):
+        """ Phase 1 ampere """
         return self.unpack('>H', 34)[0]/10
     @property
     def ac_power_0(self):
+        """ Phase 1 watts """
         return self.unpack('>H', 30)[0]/10
     @property
     def frequency(self):
+        """ Grid frequency in Hertz """
         return self.unpack('>H', 28)[0]/100
     @property
     def temperature(self):
+        """ Inverter temperature in °C """
         return self.unpack('>H', 38)[0]/10
     @property
     def alarm_count(self):
+        """ Event counter """
         return self.unpack('>H', 40)[0]
 
-class HM600_Decode11(EventsResponse):
-    def __init__(self, *args, **params):
-        super().__init__(*args, **params)
+class Hm600Decode11(EventsResponse):
+    """ Inverter generic events log """
 
-class HM600_Decode12(EventsResponse):
-    def __init__(self, *args, **params):
-        super().__init__(*args, **params)
+class Hm600Decode12(EventsResponse):
+    """ Inverter major events log """
 
 
 # 1161-Series Inverters, 4 MPPT, 1 Phase
-class HM1200_Decode0B(StatusResponse):
-    def __init__(self, *args, **params):
-        super().__init__(*args, **params)
+class Hm1200Decode0B(StatusResponse):
+    """ 1161-series mirco-inverters status data """
 
     @property
     def dc_voltage_0(self):
+        """ String 1 VDC """
         return self.unpack('>H', 2)[0]/10
     @property
     def dc_current_0(self):
+        """ String 1 ampere """
         return self.unpack('>H', 4)[0]/100
     @property
     def dc_power_0(self):
+        """ String 1 watts """
         return self.unpack('>H', 8)[0]/10
     @property
     def dc_energy_total_0(self):
+        """ String 1 total energy in Wh """
         return self.unpack('>L', 12)[0]
     @property
     def dc_energy_daily_0(self):
+        """ String 1 daily energy in Wh """
         return self.unpack('>H', 20)[0]
 
     @property
     def dc_voltage_1(self):
+        """ String 2 VDC """
         return self.unpack('>H', 2)[0]/10
     @property
     def dc_current_1(self):
+        """ String 2 ampere """
         return self.unpack('>H', 4)[0]/100
     @property
     def dc_power_1(self):
+        """ String 2 watts """
         return self.unpack('>H', 10)[0]/10
     @property
     def dc_energy_total_1(self):
+        """ String 2 total energy in Wh """
         return self.unpack('>L', 16)[0]
     @property
     def dc_energy_daily_1(self):
+        """ String 2 daily energy in Wh """
         return self.unpack('>H', 22)[0]
 
     @property
     def dc_voltage_2(self):
+        """ String 3 VDC """
         return self.unpack('>H', 24)[0]/10
     @property
     def dc_current_2(self):
+        """ String 3 ampere """
         return self.unpack('>H', 26)[0]/100
     @property
     def dc_power_2(self):
+        """ String 3 watts """
         return self.unpack('>H', 30)[0]/10
     @property
     def dc_energy_total_2(self):
+        """ String 3 total energy in Wh """
         return self.unpack('>L', 34)[0]
     @property
     def dc_energy_daily_2(self):
+        """ String 3 daily energy in Wh """
         return self.unpack('>H', 42)[0]
 
     @property
     def dc_voltage_3(self):
+        """ String 4 VDC """
         return self.unpack('>H', 24)[0]/10
     @property
     def dc_current_3(self):
+        """ String 4 ampere """
         return self.unpack('>H', 28)[0]/100
     @property
     def dc_power_3(self):
+        """ String 4 watts """
         return self.unpack('>H', 32)[0]/10
     @property
     def dc_energy_total_3(self):
+        """ String 4 total energy in Wh """
         return self.unpack('>L', 38)[0]
     @property
     def dc_energy_daily_3(self):
+        """ String 4 daily energy in Wh """
         return self.unpack('>H', 44)[0]
 
     @property
     def ac_voltage_0(self):
+        """ Phase 1 VAC """
         return self.unpack('>H', 46)[0]/10
     @property
     def ac_current_0(self):
+        """ Phase 1 ampere """
         return self.unpack('>H', 54)[0]/100
     @property
     def ac_power_0(self):
+        """ Phase 1 watts """
         return self.unpack('>H', 50)[0]/10
     @property
     def frequency(self):
+        """ Grid frequency in Hertz """
         return self.unpack('>H', 48)[0]/100
     @property
     def temperature(self):
+        """ Inverter temperature in °C """
         return self.unpack('>H', 58)[0]/10
 
-class HM1200_Decode11(EventsResponse):
-    def __init__(self, *args, **params):
-        super().__init__(*args, **params)
+class Hm1200Decode11(EventsResponse):
+    """ Inverter generic events log """
 
-class HM1200_Decode12(EventsResponse):
-    def __init__(self, *args, **params):
-        super().__init__(*args, **params)
+class Hm1200Decode12(EventsResponse):
+    """ Inverter major events log """
diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py
index f1b6a2d1..7b942846 100644
--- a/tools/rpi/hoymiles/outputs.py
+++ b/tools/rpi/hoymiles/outputs.py
@@ -1,4 +1,9 @@
 #!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""
+Hoymiles output plugin library
+"""
 
 import socket
 from datetime import datetime, timezone
@@ -11,15 +16,46 @@ except ModuleNotFoundError:
 
 class OutputPluginFactory:
     def __init__(self, **params):
-        """Initialize output plugin"""
+        """
+        Initialize output plugin
+
+        :param inverter_ser: The inverter serial
+        :type inverter_ser: str
+        :param inverter_name: The configured name for the inverter
+        :type inverter_name: str
+        """
 
-        self.inverter_ser = params.get('inverter_ser', 0)
+        self.inverter_ser = params.get('inverter_ser', '')
+        self.inverter_name = params.get('inverter_name', None)
 
-    def store_status(self, data):
+    def store_status(self, response, **params):
+        """
+        Default function
+
+        :raises NotImplementedError: when the plugin does not implement store status data
+        """
         raise NotImplementedError('The current output plugin does not implement store_status')
 
 class InfluxOutputPlugin(OutputPluginFactory):
+    """ Influx2 output plugin """
+    api = None
+
     def __init__(self, url, token, **params):
+        """
+        Initialize InfluxOutputPlugin
+
+        The following targets must be present in your InfluxDB. This does not
+        automatically create anything for You.
+
+        :param str url: The url to connect this client to. Like http://localhost:8086
+        :param str token: Influx2 access token which is allowed to write to bucket
+        :param org: Influx2 org, the token belongs to
+        :type org: str
+        :param bucket: Influx2 bucket to store data in (also known as retention policy)
+        :type bucket: str
+        :param measurement: Default measurement-prefix to use
+        :type measurement: str
+        """
         super().__init__(**params)
 
         self._bucket = params.get('bucket', 'hoymiles/autogen')
@@ -30,20 +66,20 @@ class InfluxOutputPlugin(OutputPluginFactory):
         client = InfluxDBClient(url, token, bucket=self._bucket)
         self.api = client.write_api()
 
-    def store_status(self, response):
+    def store_status(self, response, **params):
         """
         Publish StatusResponse object
 
-        :param influxdb.InfluxDBClient influx_client: A connected instance to Influx database
-        :param str inverter_ser: inverter serial
-        :param hoymiles.StatusResponse data: decoded inverter StatusResponse
-        :type response: hoymiles.StatusResponse
-        :param measurement: Influx measurement name
-        :type measurement: str
+        :param hoymiles.decoders.StatusResponse response: StatusResponse object
+        :type response: hoymiles.decoders.StatusResponse
+        :param measurement: Custom influx measurement name
+        :type measurement: str or None
+
+        :raises ValueError: when response is not instance of StatusResponse
         """
 
         if not isinstance(response, StatusResponse):
-            raise RuntimeError('Data needs to be instance of StatusResponse')
+            raise ValueError('Data needs to be instance of StatusResponse')
 
         data = response.__dict__()
 
@@ -89,7 +125,30 @@ except ModuleNotFoundError:
     pass
 
 class MqttOutputPlugin(OutputPluginFactory):
+    """ Mqtt output plugin """
+    client = None
+
     def __init__(self, *args, **params):
+        """
+        Initialize MqttOutputPlugin
+
+        :param host: Broker ip or hostname (defaults to: 127.0.0.1)
+        :type host: str
+        :param port: Broker port
+        :type port: int (defaults to: 1883)
+        :param user: Optional username to login to the broker
+        :type user: str or None
+        :param password: Optional passwort to login to the broker
+        :type password: str or None
+        :param topic: Topic prefix to use (defaults to: hoymiles/{inverter_ser})
+        :type topic: str
+
+        :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
+        """
         super().__init__(*args, **params)
 
         mqtt_client = paho.mqtt.client.Client()
@@ -103,36 +162,36 @@ class MqttOutputPlugin(OutputPluginFactory):
         """
         Publish StatusResponse object
 
-        :param paho.mqtt.client.Client broker: mqtt-client instance
-        :param str inverter_ser: inverter serial
-        :param hoymiles.StatusResponse data: decoded inverter StatusResponse
+        :param hoymiles.decoders.StatusResponse response: StatusResponse object
         :param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser})
         :type topic: str
+
+        :raises ValueError: when response is not instance of StatusResponse
         """
 
         if not isinstance(response, StatusResponse):
-            raise RuntimeError('Data needs to be instance of StatusResponse')
+            raise ValueError('Data needs to be instance of StatusResponse')
 
         data = response.__dict__()
 
-        topic = params.get('topic', f'hoymiles/{inverter_ser}')
+        topic = params.get('topic', f'hoymiles/{data["inverter_ser"]}')
 
         # AC Data
         phase_id = 0
         for phase in data['phases']:
-            self.mqtt_client.publish(f'{topic}/emeter/{phase_id}/power', phase['power'])
-            self.mqtt_client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage'])
-            self.mqtt_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}/voltage', phase['voltage'])
+            self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current'])
             phase_id = phase_id + 1
 
         # DC Data
         string_id = 0
         for string in data['strings']:
-            self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000)
-            self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power'])
-            self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage'])
-            self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current'])
+            self.client.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000)
+            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}/current', string['current'])
             string_id = string_id + 1
         # Global
-        self.mqtt_client.publish(f'{topic}/frequency', data['frequency'])
-        self.mqtt_client.publish(f'{topic}/temperature', data['temperature'])
+        self.client.publish(f'{topic}/frequency', data['frequency'])
+        self.client.publish(f'{topic}/temperature', data['temperature'])