|
|
@ -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) |
|
|
|