Browse Source

Merge branch 'dev03-sysinfos' of https://github.com/DanielR92/ahoy into dev03-sysinfos

pull/426/head
DanielR92 2 years ago
parent
commit
a7e51d489e
  1. 31
      .github/ISSUE_TEMPLATE/report-ahoy.md
  2. 33
      .github/ISSUE_TEMPLATE/report.yaml
  3. 6
      tools/rpi/ahoy.yml.example
  4. 18
      tools/rpi/hoymiles/__init__.py
  5. 71
      tools/rpi/hoymiles/__main__.py
  6. 61
      tools/rpi/hoymiles/decoders/__init__.py
  7. 3
      tools/rpi/hoymiles/outputs.py

31
.github/ISSUE_TEMPLATE/report-ahoy.md

@ -33,18 +33,29 @@ connected between +3.3V and GND (Pin 1 & 2) of the NRF Module
* [ ] Image of the your wiring attached
### Connection diagram I used:
| nRF24L01+ Pin | ESP8266/32 GPIO |
| nRF24L01+ Pin | ESP8266 GPIO |
| ------------- | -------------- |
| Pin 1 GND [*] | GND |
| Pin 2 +3.3V | +3.3V |
| Pin 3 CE | GPIO2 CE D4 |
| Pin 4 CSN | GPIO15 CS D8 |
| Pin 5 SCK | GPIO14 SCLK D5 |
| Pin 6 MOSI | GPIO13 MOSI D7 |
| Pin 7 MISO | GPIO12 MISO D6 |
| Pin 8 IRQ | GPIO0 IRQ D3 |
| nRF24L01+ Pin | ESP32 GPIO |
| ------------- | --------------- |
| Pin 1 GND [] | GND |
| Pin 2 +3.3V | +3.3V |
| Pin 3 CE | GPIO_2/_4 CE |
| Pin 4 CSN | GPIO15/_5 CS |
| Pin 5 SCK | GPIO14/18 SCLK |
| Pin 6 MOSI | GPIO13/23 MOSI |
| Pin 7 MISO | GPIO12/19 MISO |
| Pin 8 IRQ | GPIO_0/0 IRQ |
| Pin 1 GND [*] | GND |
| Pin 2 +3.3V | +3.3V |
| Pin 3 CE | GPIO4 CE D4 |
| Pin 4 CSN | GPIO5 CS D5 |
| Pin 5 SCK | GPIO18 SCLK D18 |
| Pin 6 MOSI | GPIO23 MOSI D23 |
| Pin 7 MISO | GPIO19 MISO D19 |
| Pin 8 IRQ | GPIO0 IRQ D0 |
Note: [] GND Pin 1 has a square mark on the nRF24L01+ module
Note: [*] GND Pin 1 has a square mark on the nRF24L01+ module
## Software
* [ ] AhoyDTU

33
.github/ISSUE_TEMPLATE/report.yaml

@ -81,18 +81,29 @@ body:
description: Tell us which connection diagram you used?
value: |
## Connection diagram I used:
| nRF24L01+ Pin | ESP8266/32 GPIO |
| nRF24L01+ Pin | ESP8266 GPIO |
| ------------- | -------------- |
| Pin 1 GND [*] | GND |
| Pin 2 +3.3V | +3.3V |
| Pin 3 CE | GPIO2 CE D4 |
| Pin 4 CSN | GPIO15 CS D8 |
| Pin 5 SCK | GPIO14 SCLK D5 |
| Pin 6 MOSI | GPIO13 MOSI D7 |
| Pin 7 MISO | GPIO12 MISO D6 |
| Pin 8 IRQ | GPIO0 IRQ D3 |
| nRF24L01+ Pin | ESP32 GPIO |
| ------------- | --------------- |
| Pin 1 GND [] | GND |
| Pin 2 +3.3V | +3.3V |
| Pin 3 CE | GPIO_2/_4 CE |
| Pin 4 CSN | GPIO15/_5 CS |
| Pin 5 SCK | GPIO14/18 SCLK |
| Pin 6 MOSI | GPIO13/23 MOSI |
| Pin 7 MISO | GPIO12/19 MISO |
| Pin 8 IRQ | GPIO_0/0 IRQ |
Note: [] GND Pin 1 has a square mark on the nRF24L01+ module
| Pin 1 GND [*] | GND |
| Pin 2 +3.3V | +3.3V |
| Pin 3 CE | GPIO4 CE D4 |
| Pin 4 CSN | GPIO5 CS D5 |
| Pin 5 SCK | GPIO18 SCLK D18 |
| Pin 6 MOSI | GPIO23 MOSI D23 |
| Pin 7 MISO | GPIO19 MISO D19 |
| Pin 8 IRQ | GPIO0 IRQ D0 |
Note: [*] GND Pin 1 has a square mark on the nRF24L01+ module
validations:
required: true
- type: checkboxes

6
tools/rpi/ahoy.yml.example

@ -2,6 +2,12 @@
ahoy:
interval: 5
logging:
filename: 'hoymiles.log'
# DEBUG, INFO, WARNING, ERROR, FATAL
level: 'INFO'
sunset:
disabled: false
latitude: 51.799118

18
tools/rpi/hoymiles/__init__.py

@ -9,7 +9,7 @@ import struct
import time
import re
from datetime import datetime
import json
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 *
@ -51,16 +51,6 @@ def ser_to_esb_addr(inverter_ser):
air_order = ser_to_hm_addr(inverter_ser)[::-1] + b'\x01'
return air_order[::-1]
def print_addr(inverter_ser):
"""
Debug print addresses
:param str inverter_ser: inverter serial
"""
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:
"""
Prepare payload decoder
@ -584,7 +574,7 @@ class InverterTransaction:
if HOYMILES_TRANSACTION_LOGGING:
c_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
print(f'{c_datetime} Transmit {len(packet)} | {hexify_payload(packet)}')
logging.debug(f'{c_datetime} Transmit {len(packet)} | {hexify_payload(packet)}')
self.radio.transmit(packet, txpower=self.txpower)
@ -592,14 +582,14 @@ class InverterTransaction:
try:
for response in self.radio.receive():
if HOYMILES_TRANSACTION_LOGGING:
print(response)
logging.debug(response)
self.frame_append(response)
wait = True
except TimeoutError:
pass
except BufferError as e:
print(f'Buffer error {e}')
logging.warning(f'Buffer error {e}')
pass
return wait

71
tools/rpi/hoymiles/__main__.py

@ -19,6 +19,7 @@ import yaml
from yaml.loader import SafeLoader
import paho.mqtt.client
import hoymiles
import logging
class InfoCommands(IntEnum):
InverterDevInform_Simple = 0 # 0x00
@ -48,25 +49,24 @@ class SunsetHandler:
longitude = sunset_config.get('longitude')
altitude = sunset_config.get('altitude')
self.suntimes = SunTimes(longitude=longitude, latitude=latitude, altitude=altitude)
self.nextSunset = self.suntimes.setutc(datetime.now())
print (f'Todays sunset is at {self.nextSunset}')
self.nextSunset = self.suntimes.setutc(datetime.utcnow())
logging.info (f'Todays sunset is at {self.nextSunset} UTC')
def checkWaitForSunrise(self):
if not self.suntimes:
return
# if the sunset already happened for today
now = datetime.now()
now = datetime.utcnow()
if self.nextSunset < now:
# wait until the sun rises tomorrow
tomorrow = now + timedelta(days=1)
nextSunrise = self.suntimes.riseutc(tomorrow)
self.nextSunset = self.suntimes.setutc(tomorrow)
time_to_sleep = (nextSunrise - datetime.now()).total_seconds()
print (f'Waiting for sunrise at {nextSunrise} ({time_to_sleep} seconds)')
time_to_sleep = int((nextSunrise - datetime.now()).total_seconds())
logging.info (f'Waiting for sunrise at {nextSunrise} UTC ({time_to_sleep} seconds)')
if time_to_sleep > 0:
time.sleep(time_to_sleep)
print (f'Woke up... next sunset is at {self.nextSunset}')
return
logging.info (f'Woke up... next sunset is at {self.nextSunset} UTC')
def main_loop(ahoy_config):
"""Main loop"""
@ -75,6 +75,7 @@ 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')
loop_interval = ahoy_config.get('interval', 1)
try:
@ -86,12 +87,10 @@ def main_loop(ahoy_config):
for inverter in inverters:
if hoymiles.HOYMILES_DEBUG_LOGGING:
print(f'Poll inverter {inverter["serial"]}')
poll_inverter(inverter, do_init)
logging.debug(f'Poll inverter {inverter["serial"]}')
poll_inverter(inverter, dtu_ser, do_init, 3)
do_init = False
print('', end='', flush=True)
if loop_interval > 0:
time_to_sleep = loop_interval - (time.time() - t_loop_start)
if time_to_sleep > 0:
@ -100,12 +99,12 @@ def main_loop(ahoy_config):
except KeyboardInterrupt:
sys.exit()
except Exception as e:
print ('Exception catched: %s' % e)
traceback.print_exc()
logging.fatal('Exception catched: %s' % e)
logging.fatal(traceback.print_exc())
raise
def poll_inverter(inverter, do_init, retries=4):
def poll_inverter(inverter, dtu_ser, do_init, retries):
"""
Send/Receive command_queue, initiate status poll on inverter
@ -114,7 +113,6 @@ def poll_inverter(inverter, do_init, retries=4):
:type retries: int
"""
inverter_ser = inverter.get('serial')
dtu_ser = ahoy_config.get('dtu', {}).get('serial')
# Queue at least status data request
inv_str = str(inverter_ser)
@ -148,14 +146,14 @@ def poll_inverter(inverter, do_init, retries=4):
response = com.get_payload()
payload_ttl = 0
except Exception as e_all:
print(f'Error while retrieving data: {e_all}')
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:
print(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
decoder = hoymiles.ResponseDecoder(response,
request=com.request,
inverter_ser=inverter_ser
@ -165,18 +163,18 @@ def poll_inverter(inverter, do_init, retries=4):
data = result.__dict__()
if hoymiles.HOYMILES_DEBUG_LOGGING:
print(f'{c_datetime} Decoded: temp={data["temperature"]}, total={data["energy_total"]/1000:.3f}', end='')
dbg = f'{c_datetime} Decoded: temp={data["temperature"]}, total={data["energy_total"]/1000:.3f}'
if data['powerfactor'] is not None:
print(f', pf={data["powerfactor"]}', end='')
dbg += f', pf={data["powerfactor"]}'
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='')
dbg += f' phase{phase_id}=voltage:{phase["voltage"]}, current:{phase["current"]}, power:{phase["power"]}, frequency:{data["frequency"]}'
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='')
dbg += f' string{string_id}=voltage:{string["voltage"]}, current:{string["current"]}, power:{string["power"]}, total:{string["energy_total"]/1000}, daily:{string["energy_daily"]}'
string_id = string_id + 1
print()
logging.debug(dbg)
if 'event_count' in data:
if event_message_index[inv_str] < data['event_count']:
@ -257,7 +255,7 @@ def mqtt_on_command(client, userdata, message):
inverter_ser = next(
item[0] for item in mqtt_command_topic_subs if item[1] == message.topic)
except StopIteration:
print('Unexpedtedly received mqtt message for {message.topic}')
logging.warning('Unexpedtedly received mqtt message for {message.topic}')
if inverter_ser:
p_message = message.payload.decode('utf-8').lower()
@ -275,14 +273,31 @@ def mqtt_on_command(client, userdata, message):
command_queue[str(inverter_ser)].append(
hoymiles.frame_payload(payload[1:]))
def init_logging(ahoy_config):
log_config = ahoy_config.get('logging')
fn = 'hoymiles.log'
lvl = logging.ERROR
if log_config:
fn = log_config.get('filename', fn)
level = log_config.get('level', 'ERROR')
if level == 'DEBUG':
lvl = logging.DEBUG
elif level == 'INFO':
lvl = logging.INFO
elif level == 'WARNING':
lvl = logging.WARNING
elif level == 'ERROR':
lvl = logging.ERROR
logging.basicConfig(filename=fn, format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=lvl)
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")
help="Enable transaction logging output (loglevel must be DEBUG)")
parser.add_argument("--verbose", action="store_true", default=False,
help="Enable debug output")
help="Enable detailed debug output (loglevel must be DEBUG)")
global_config = parser.parse_args()
# Load ahoy.yml config file
@ -294,13 +309,14 @@ if __name__ == '__main__':
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")
logging.error("Could not load config file. Try --help")
sys.exit(2)
except yaml.YAMLError as e_yaml:
print('Failed to load config frile {global_config.config_file}: {e_yaml}')
logging.error('Failed to load config file {global_config.config_file}: {e_yaml}')
sys.exit(1)
ahoy_config = dict(cfg.get('ahoy', {}))
init_logging(ahoy_config)
# Prepare for multiple transceivers, makes them configurable (currently
# only one supported)
@ -367,4 +383,5 @@ 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}')
main_loop(ahoy_config)

61
tools/rpi/hoymiles/decoders/__init__.py

@ -8,6 +8,7 @@ Hoymiles Micro-Inverters decoder library
import struct
from datetime import datetime, timedelta
import crcmod
import logging
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus')
f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0)
@ -44,18 +45,20 @@ def print_table_unpack(s_fmt, payload, cw=6):
l_hexlified = [f'{byte:02x}' for byte in payload]
print(f'{"Pos": <{cw}}', end='')
print(''.join([f'{num: >{cw}}' for num in range(0, len(payload))]))
print(f'{"Hex": <{cw}}', end='')
print(''.join([f'{byte: >{cw}}' for byte in l_hexlified]))
dbg = f'{"Pos": <{cw}}'
dbg += ''.join([f'{num: >{cw}}' for num in range(0, len(payload))])
logging.debug(dbg)
dbg = f'{"Hex": <{cw}}'
dbg += ''.join([f'{byte: >{cw}}' for byte in l_hexlified])
logging.debug(dbg)
l_fmt = struct.calcsize(s_fmt)
if len(payload) >= l_fmt:
for offset in range(0, l_fmt):
print(f'{s_fmt: <{cw}}', end='')
print(' ' * cw * offset, end='')
print(''.join(
[f'{num[0]: >{cw*l_fmt}}' for num in g_unpack(s_fmt, payload[offset:])]))
dbg = f'{s_fmt: <{cw}}'
dbg += ' ' * cw * offset
dbg += ''.join([f'{num[0]: >{cw*l_fmt}}' for num in g_unpack(s_fmt, payload[offset:])])
logging.debug(dbg)
class Response:
""" All Response Shared methods """
@ -299,27 +302,28 @@ class EventsResponse(UnknownResponse):
crc_valid = self.validate_crc_m()
if crc_valid:
#print(' payload has valid modbus crc')
#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')
print (f' Inverter status: {a_text} ({status})')
logging.info (f' Inverter status: {a_text} ({status})')
chunk_size = 12
for i_chunk in range(2, len(self.response), chunk_size):
chunk = self.response[i_chunk:i_chunk+chunk_size]
print(' '.join([f'{byte:02x}' for byte in chunk]) + ': ')
logging.debug(' '.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')
print(f' uptime={timedelta(seconds=uptime_sec)} a_count={a_count} opcode={opcode} a_code={a_code} a_text={a_text}')
logging.debug(f' uptime={timedelta(seconds=uptime_sec)} a_count={a_count} opcode={opcode} a_code={a_code} a_text={a_text}')
dbg = ''
for fmt in ['BBHHHHH']:
print(f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk)))
print(end='', flush=True)
dbg += f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk))
logging.debug(dbg)
class HardwareInfoResponse(UnknownResponse):
def __init__(self, *args, **params):
@ -340,8 +344,7 @@ class HardwareInfoResponse(UnknownResponse):
fw_version_pat = int((fw_version % 100))
fw_build_mm = int(fw_build_mmdd / 100)
fw_build_dd = int(fw_build_mmdd % 100)
print()
print(f'Firmware: {fw_version_maj}.{fw_version_min}.{fw_version_pat} build at {fw_build_dd}/{fw_build_mm}/{fw_build_yyyy}, HW revision {hw_id}')
logging.debug(f'Firmware: {fw_version_maj}.{fw_version_min}.{fw_version_pat} build at {fw_build_dd}/{fw_build_mm}/{fw_build_yyyy}, HW revision {hw_id}')
class DebugDecodeAny(UnknownResponse):
"""Default decoder"""
@ -351,40 +354,40 @@ class DebugDecodeAny(UnknownResponse):
crc8_valid = self.validate_crc8()
if crc8_valid:
print(' payload has valid crc8')
logging.debug(' payload has valid crc8')
self.response = self.response[:-1]
crc_valid = self.validate_crc_m()
if crc_valid:
print(' payload has valid modbus crc')
logging.debug(' payload has valid modbus crc')
self.response = self.response[:-2]
l_payload = len(self.response)
print(f' payload has {l_payload} bytes')
logging.debug(f' payload has {l_payload} bytes')
print()
print('Field view: int')
logging.debug()
logging.debug('Field view: int')
print_table_unpack('>B', self.response)
print()
print('Field view: shorts')
logging.debug()
logging.debug('Field view: shorts')
print_table_unpack('>H', self.response)
print()
print('Field view: longs')
logging.debug()
logging.debug('Field view: longs')
print_table_unpack('>L', self.response)
try:
if len(self.response) > 2:
print(' type utf-8 : ' + self.response.decode('utf-8'))
logging.debug(' type utf-8 : ' + self.response.decode('utf-8'))
except UnicodeDecodeError:
print(' type utf-8 : utf-8 decode error')
logging.debug(' type utf-8 : utf-8 decode error')
try:
if len(self.response) > 2:
print(' type ascii : ' + self.response.decode('ascii'))
logging.debug(' type ascii : ' + self.response.decode('ascii'))
except UnicodeDecodeError:
print(' type ascii : ascii decode error')
logging.debug(' type ascii : ascii decode error')
# 1121-Series Intervers, 1 MPPT, 1 Phase

3
tools/rpi/hoymiles/outputs.py

@ -6,6 +6,7 @@ Hoymiles output plugin library
"""
import socket
import logging
from datetime import datetime, timezone
from hoymiles.decoders import StatusResponse
@ -310,4 +311,4 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory):
try:
output.store_status(data, self.session)
except ValueError as e:
print('Could not send data to volkszaehler instance: %s' % e)
logging.warning('Could not send data to volkszaehler instance: %s' % e)

Loading…
Cancel
Save