diff --git a/tools/rpi/README.md b/tools/rpi/README.md index e4c3bd40..3e5001ea 100644 --- a/tools/rpi/README.md +++ b/tools/rpi/README.md @@ -40,8 +40,27 @@ contact the inverter every second on channel 40, and listen for replies. Whenever it sees a reply, it will decoded and logged to the given log file. - $ sudo python3 ahoy.py --config /home/dtu/ahoy.yml | tee -a log2.log + $ sudo python3 -um hoymiles --log-transactions --verbose --config /home/dtu/ahoy.yml | tee -a log2.log +Python parameters +- `-u` enables python's unbuffered mode +- `-m hoymiles` tells python to load module 'hoymiles' as main app + + +The application describes itself +``` +python -m hoymiles --help +usage: hoymiles [-h] -c [CONFIG_FILE] [--log-transactions] [--verbose] + +Ahoy - Hoymiles solar inverter gateway + +optional arguments: + -h, --help show this help message and exit + -c [CONFIG_FILE], --config-file [CONFIG_FILE] + configuration file + --log-transactions Enable transaction logging output + --verbose Enable debug output +``` Inject payloads via MQTT diff --git a/tools/rpi/ahoy.yml.example b/tools/rpi/ahoy.yml.example index 4e2cb586..5c27c003 100644 --- a/tools/rpi/ahoy.yml.example +++ b/tools/rpi/ahoy.yml.example @@ -3,6 +3,12 @@ ahoy: interval: 0 sunset: true + + # List of available NRF24 transceivers + nrf: + - ce_pin: 22 + cs_pin: 0 + mqtt: disabled: false host: example-broker.local diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py index ba3bd143..cc65107d 100644 --- a/tools/rpi/hoymiles/__init__.py +++ b/tools/rpi/hoymiles/__init__.py @@ -11,8 +11,8 @@ f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus') f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0) -HOYMILES_TRANSACTION_LOGGING=True -HOYMILES_DEBUG_LOGGING=True +HOYMILES_TRANSACTION_LOGGING=False +HOYMILES_DEBUG_LOGGING=False def ser_to_hm_addr(s): """ diff --git a/tools/rpi/ahoy.py b/tools/rpi/hoymiles/__main__.py similarity index 52% rename from tools/rpi/ahoy.py rename to tools/rpi/hoymiles/__main__.py index 19812808..945226b8 100644 --- a/tools/rpi/ahoy.py +++ b/tools/rpi/hoymiles/__main__.py @@ -13,28 +13,6 @@ import paho.mqtt.client import yaml from yaml.loader import SafeLoader -parser = argparse.ArgumentParser(description='Ahoy - Hoymiles solar inverter gateway') -parser.add_argument("-c", "--config-file", nargs="?", - help="configuration file") -global_config = parser.parse_args() - -if global_config.config_file: - with open(global_config.config_file) as yf: - cfg = yaml.load(yf, Loader=SafeLoader) -else: - with open(global_config.config_file) as yf: - cfg = yaml.load('ahoy.yml', Loader=SafeLoader) - -radio = RF24(22, 0, 1000000) -hmradio = hoymiles.HoymilesNRF(device=radio) -mqtt_client = None - -command_queue = {} -mqtt_command_topic_subs = [] - -hoymiles.HOYMILES_TRANSACTION_LOGGING=True -hoymiles.HOYMILES_DEBUG_LOGGING=True - def main_loop(): inverters = [ inverter for inverter in ahoy_config.get('inverters', []) @@ -45,62 +23,66 @@ def main_loop(): print(f'Poll inverter {inverter["serial"]}') poll_inverter(inverter) -def poll_inverter(inverter): +def poll_inverter(inverter, retries=4): inverter_ser = inverter.get('serial') dtu_ser = ahoy_config.get('dtu', {}).get('serial') - if len(command_queue[str(inverter_ser)]) > 0: + # Queue at least status data request + command_queue[str(inverter_ser)].append(hoymiles.compose_set_time_payload()) + + # Putt all queued commands for current inverter on air + while len(command_queue[str(inverter_ser)]) > 0: payload = command_queue[str(inverter_ser)].pop(0) - else: - payload = hoymiles.compose_set_time_payload() - payload_ttl = 4 - while payload_ttl > 0: - payload_ttl = payload_ttl - 1 - com = hoymiles.InverterTransaction( - radio=hmradio, - dtu_ser=dtu_ser, - inverter_ser=inverter_ser, - request=next(hoymiles.compose_esb_packet( - payload, - seq=b'\x80', - src=dtu_ser, - dst=inverter_ser - ))) - response = None - while com.rxtx(): - try: - response = com.get_payload() - payload_ttl = 0 - except Exception as e: - print(f'Error while retrieving data: {e}') - pass + # Send payload {ttl}-times until we get at least one reponse + payload_ttl = retries + while payload_ttl > 0: + payload_ttl = payload_ttl - 1 + com = hoymiles.InverterTransaction( + radio=hmradio, + dtu_ser=dtu_ser, + inverter_ser=inverter_ser, + request=next(hoymiles.compose_esb_packet( + payload, + seq=b'\x80', + src=dtu_ser, + dst=inverter_ser + ))) + response = None + while com.rxtx(): + try: + response = com.get_payload() + payload_ttl = 0 + except Exception as e: + print(f'Error while retrieving data: {e}') + pass - if response: - dt = datetime.now() - print(f'{dt} Payload: ' + hoymiles.hexify_payload(response)) - decoder = hoymiles.ResponseDecoder(response, - request=com.request, - inverter_ser=inverter_ser - ) - result = decoder.decode() - if isinstance(result, hoymiles.decoders.StatusResponse): - data = result.__dict__() - if hoymiles.HOYMILES_DEBUG_LOGGING: - print(f'{dt} 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='') - phase_id = phase_id + 1 - string_id = 0 - for string in data['strings']: - print(f' string{string_id}=voltage:{string["voltage"]}, current:{string["current"]}, power:{string["power"]}, total:{string["energy_total"]/1000}, daily:{string["energy_daily"]}', end='') - string_id = string_id + 1 - print() + # Handle the response data if any + if response: + dt = datetime.now() + print(f'{dt} Payload: ' + hoymiles.hexify_payload(response)) + decoder = hoymiles.ResponseDecoder(response, + request=com.request, + inverter_ser=inverter_ser + ) + result = decoder.decode() + if isinstance(result, hoymiles.decoders.StatusResponse): + data = result.__dict__() + if hoymiles.HOYMILES_DEBUG_LOGGING: + print(f'{dt} 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='') + phase_id = phase_id + 1 + string_id = 0 + for string in data['strings']: + print(f' string{string_id}=voltage:{string["voltage"]}, current:{string["current"]}, power:{string["power"]}, total:{string["energy_total"]/1000}, daily:{string["energy_daily"]}', end='') + string_id = string_id + 1 + print() - if mqtt_client: - mqtt_send_status(mqtt_client, inverter_ser, data, - topic=inverter.get('mqtt', {}).get('topic', None)) + if mqtt_client: + mqtt_send_status(mqtt_client, inverter_ser, data, + topic=inverter.get('mqtt', {}).get('topic', None)) def mqtt_send_status(broker, inverter_ser, data, topic=None): """ Publish StatusResponse object """ @@ -170,8 +152,51 @@ def mqtt_on_command(client, userdata, message): hoymiles.frame_payload(payload[1:])) if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Ahoy - Hoymiles solar inverter gateway', prog="hoymiles") + parser.add_argument("-c", "--config-file", nargs="?", required=True, + help="configuration file") + parser.add_argument("--log-transactions", action="store_true", default=False, + help="Enable transaction logging output") + parser.add_argument("--verbose", action="store_true", default=False, + help="Enable debug output") + global_config = parser.parse_args() + + # 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) + else: + with open('ahoy.yml', 'r') as yf: + cfg = yaml.load(yf, 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}') + sys.exit(1) + ahoy_config = dict(cfg.get('ahoy', {})) + # Prepare for multiple transceivers, makes them configurable (currently + # only one supported) + for radio_config in ahoy_config.get('nrf', [{}]): + radio = RF24( + radio_config.get('ce_pin', 22), + radio_config.get('cs_pin', 0), + radio_config.get('spispeed', 1000000)) + hmradio = hoymiles.HoymilesNRF(device=radio) + + mqtt_client = None + + 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 + mqtt_config = ahoy_config.get('mqtt', []) if not mqtt_config.get('disabled', False): mqtt_client = paho.mqtt.client.Client() @@ -202,9 +227,13 @@ if __name__ == '__main__': loop_interval = ahoy_config.get('interval', 1) try: while True: + t_loop_start = time.time() + main_loop() - if loop_interval: + print('', end='', flush=True) + + if loop_interval > 0 and (time.time() - t_loop_start) < loop_interval: time.sleep(time.time() % loop_interval) except KeyboardInterrupt: