anker-solix-api/solarbank_monitor.py

285 lines
14 KiB
Python
Raw Normal View History

2024-02-20 22:16:16 +00:00
#!/usr/bin/env python
"""Example exec module to use the Anker API for continously querying and
displaying important solarbank parameters This module will prompt for the Anker
account details if not pre-set in the header. Upon successfull authentication,
you will see the solarbank parameters displayed and refreshed at reqular
interval.
Note: When the system owning account is used, more details for the solarbank
can be queried and displayed.
Attention: During executiion of this module, the used account cannot be used in
the Anker App since it will be kicked out on each refresh.
""" # noqa: D205
2024-01-30 11:58:09 +00:00
import asyncio
2024-02-26 00:13:20 +00:00
from datetime import datetime, timedelta
2024-02-10 17:57:33 +00:00
import json
import logging
import os
import sys
import time
from aiohttp import ClientSession
2024-02-20 13:35:04 +00:00
from aiohttp.client_exceptions import ClientError
from api import api, errors
2024-02-26 00:13:20 +00:00
import common
2024-01-30 11:58:09 +00:00
_LOGGER: logging.Logger = logging.getLogger(__name__)
_LOGGER.addHandler(logging.StreamHandler(sys.stdout))
2024-02-20 13:35:04 +00:00
# _LOGGER.setLevel(logging.DEBUG) # enable for debug output
CONSOLE: logging.Logger = logging.getLogger("console")
CONSOLE.addHandler(logging.StreamHandler(sys.stdout))
CONSOLE.setLevel(logging.INFO)
2024-01-30 11:58:09 +00:00
2024-02-20 13:35:04 +00:00
REFRESH = 30 # default refresh interval in seconds
INTERACTIVE = True
2024-01-30 11:58:09 +00:00
def clearscreen():
"""Clear the terminal screen."""
2024-01-30 11:58:09 +00:00
if sys.stdin is sys.__stdin__: # check if not in IDLE shell
if os.name == "nt":
os.system("cls")
else:
os.system("clear")
2024-02-20 13:35:04 +00:00
# CONSOLE.info("\033[H\033[2J", end="") # ESC characters to clear terminal screen, system independent?
2024-01-30 11:58:09 +00:00
2024-02-20 13:35:04 +00:00
def get_subfolders(folder: str) -> list:
"""Get the full pathnames of all subfolders for given folder as list."""
if os.path.isdir(folder):
return [os.path.abspath(f) for f in os.scandir(folder) if f.is_dir()]
return []
async def main() -> ( # noqa: C901 # pylint: disable=too-many-locals,too-many-branches,too-many-statements
None
):
"""Run Main routine to start Solarbank monitor in a loop."""
2024-02-26 00:13:20 +00:00
global REFRESH # pylint: disable=global-statement # noqa: PLW0603
CONSOLE.info("Solarbank Monitor:")
2024-02-20 13:35:04 +00:00
# get list of possible example and export folders to test the monitor against
exampleslist = get_subfolders(
os.path.join(os.path.dirname(__file__), "examples")
) + get_subfolders(os.path.join(os.path.dirname(__file__), "exports"))
if INTERACTIVE:
2024-02-20 13:35:04 +00:00
if exampleslist:
CONSOLE.info("\nSelect the input source for the monitor:")
CONSOLE.info("(0) Real time from Anker cloud")
for idx, filename in enumerate(exampleslist, start=1):
CONSOLE.info("(%s) %s", idx, filename)
selection = input(f"Input Source number (0-{len(exampleslist)}): ")
if (
not selection.isdigit()
or int(selection) < 0
or int(selection) > len(exampleslist)
):
2024-01-30 11:58:09 +00:00
return False
2024-02-20 13:35:04 +00:00
if (selection := int(selection)) == 0:
use_file = False
else:
use_file = True
testfolder = exampleslist[selection - 1]
2024-02-20 22:10:57 +00:00
else:
use_file = False
2024-01-30 11:58:09 +00:00
try:
async with ClientSession() as websession:
myapi = api.AnkerSolixApi(
common.user(), common.password(), common.country(), websession, _LOGGER
)
2024-02-20 13:35:04 +00:00
if use_file:
# set the correct test folder for Api
myapi.testDir(testfolder)
elif await myapi.async_authenticate():
CONSOLE.info("Anker Cloud authentication: OK")
2024-01-30 11:58:09 +00:00
else:
# Login validation will be done during first API call
2024-02-20 13:35:04 +00:00
CONSOLE.info("Anker Cloud authentication: CACHED")
2024-01-30 11:58:09 +00:00
while True:
2024-02-20 13:35:04 +00:00
resp = input(
f"\nHow many seconds refresh interval should be used? (10-600, default: {REFRESH}): "
)
2024-01-30 11:58:09 +00:00
if not resp:
break
if resp.isdigit() and 10 <= int(resp) <= 600:
2024-01-30 11:58:09 +00:00
REFRESH = int(resp)
break
# Run loop to update Solarbank parameters
now = datetime.now().astimezone()
next_refr = now
next_dev_refr = now
col1 = 15
col2 = 23
col3 = 14
2024-01-30 11:58:09 +00:00
t1 = 2
t2 = 5
t3 = 5
2024-03-27 10:53:59 +00:00
t4 = 6
2024-01-30 11:58:09 +00:00
t5 = 6
t6 = 10
2024-03-26 00:15:38 +00:00
t7 = 6
t8 = 6
2024-01-30 11:58:09 +00:00
while True:
CONSOLE.info("\n")
2024-01-30 11:58:09 +00:00
now = datetime.now().astimezone()
if next_refr <= now:
CONSOLE.info("Running site refresh...")
2024-02-20 13:35:04 +00:00
await myapi.update_sites(fromFile=use_file)
2024-01-30 11:58:09 +00:00
next_refr = now + timedelta(seconds=REFRESH)
if next_dev_refr <= now:
CONSOLE.info("Running device details refresh...")
2024-02-20 13:35:04 +00:00
await myapi.update_device_details(fromFile=use_file)
next_dev_refr = next_refr + timedelta(seconds=REFRESH * 9)
# schedules = {}
2024-01-30 11:58:09 +00:00
clearscreen()
2024-02-20 13:35:04 +00:00
CONSOLE.info(
"Solarbank Monitor (refresh %s s, details refresh %s s):",
REFRESH,
10 * REFRESH,
2024-02-20 13:35:04 +00:00
)
if use_file:
CONSOLE.info("Using input source folder: %s", myapi.testDir())
2024-03-01 13:54:25 +00:00
if len(myapi.sites) > 0:
2024-03-26 00:15:38 +00:00
update_time = (
(next(iter(myapi.sites.values()))).get("solarbank_info") or {}
).get("updated_time") or "Unknown"
2024-03-01 13:54:25 +00:00
else:
update_time = "Unknown"
2024-02-20 13:35:04 +00:00
CONSOLE.info(
2024-03-01 13:54:25 +00:00
f"Sites: {len(myapi.sites)}, Devices: {len(myapi.sites)}, Data-Updated: {update_time}"
2024-02-20 13:35:04 +00:00
)
# pylint: disable=logging-fstring-interpolation
2024-01-30 11:58:09 +00:00
for sn, dev in myapi.devices.items():
2024-02-20 13:35:04 +00:00
devtype = dev.get("type", "Unknown")
admin = dev.get("is_admin", False)
CONSOLE.info(
f"{'Device':<{col1}}: {(dev.get('name','NoName')):<{col2}} {'Alias':<{col3}}: {dev.get('alias','Unknown')}"
2024-02-20 13:35:04 +00:00
)
CONSOLE.info(
f"{'Serialnumber':<{col1}}: {sn:<{col2}} {'Admin':<{col3}}: {'YES' if admin else 'NO'}"
2024-02-20 13:35:04 +00:00
)
siteid = dev.get("site_id", "")
CONSOLE.info(f"{'Site ID':<{col1}}: {siteid}")
2024-03-26 00:15:38 +00:00
for fsn, fitting in (dev.get("fittings") or {}).items():
CONSOLE.info(
2024-03-01 13:54:25 +00:00
f"{'Accessory':<{col1}}: {fitting.get('device_name',''):<{col2}} {'Serialnumber':<{col3}}: {fsn}"
)
CONSOLE.info(
f"{'Wifi SSID':<{col1}}: {dev.get('wifi_name',''):<{col2}}"
)
online = dev.get("wifi_online")
2024-02-20 13:35:04 +00:00
CONSOLE.info(
f"{'Wifi state':<{col1}}: {('Unknown' if online is None else 'Online' if online else 'Offline'):<{col2}} {'Signal':<{col3}}: {dev.get('wifi_signal','---'):>4} %"
2024-02-20 13:35:04 +00:00
)
2024-01-30 11:58:09 +00:00
if devtype == "solarbank":
upgrade = dev.get("auto_upgrade")
2024-02-20 13:35:04 +00:00
CONSOLE.info(
f"{'SW Version':<{col1}}: {dev.get('sw_version','Unknown'):<{col2}} {'Auto-Upgrade':<{col3}}: {'Unknown' if upgrade is None else 'Enabled' if upgrade else 'Disabled'}"
2024-02-20 13:35:04 +00:00
)
CONSOLE.info(
2024-03-07 15:47:52 +00:00
f"{'Cloud Status':<{col1}}: {dev.get('status_desc','Unknown'):<{col2}} {'Status code':<{col3}}: {str(dev.get('status','-'))}"
2024-02-20 13:35:04 +00:00
)
CONSOLE.info(
f"{'Charge Status':<{col1}}: {dev.get('charging_status_desc','Unknown'):<{col2}} {'Status code':<{col3}}: {str(dev.get('charging_status','-'))}"
2024-02-20 13:35:04 +00:00
)
soc = f"{dev.get('battery_soc','---'):>4} %"
2024-02-20 13:35:04 +00:00
CONSOLE.info(
f"{'State Of Charge':<{col1}}: {soc:<{col2}} {'Min SOC':<{col3}}: {str(dev.get('power_cutoff','--')):>4} %"
2024-02-20 13:35:04 +00:00
)
energy = f"{dev.get('battery_energy','----'):>4} Wh"
2024-02-20 13:35:04 +00:00
CONSOLE.info(
f"{'Battery Energy':<{col1}}: {energy:<{col2}} {'Capacity':<{col3}}: {str(dev.get('battery_capacity','----')):>4} Wh"
2024-02-20 13:35:04 +00:00
)
unit = dev.get("power_unit", "W")
CONSOLE.info(
f"{'Solar Power':<{col1}}: {dev.get('input_power',''):>4} {unit:<{col2-5}} {'Output Power':<{col3}}: {dev.get('output_power',''):>4} {unit}"
2024-02-20 13:35:04 +00:00
)
preset = dev.get("set_output_power") or "---"
2024-02-26 00:13:20 +00:00
site_preset = dev.get("set_system_output_power") or "---"
2024-02-20 13:35:04 +00:00
CONSOLE.info(
f"{'Charge Power':<{col1}}: {dev.get('charging_power',''):>4} {unit:<{col2-5}} {'Device Preset':<{col3}}: {preset:>4} {unit}"
2024-02-20 13:35:04 +00:00
)
# update schedule with device details refresh and print it
2024-01-30 11:58:09 +00:00
if admin:
2024-02-20 13:35:04 +00:00
data = dev.get("schedule", {})
CONSOLE.info(
2024-03-01 13:54:25 +00:00
f"{'Schedule (Now)':<{col1}}: {now.strftime('%H:%M:%S UTC %z'):<{col2}} {'System Preset':<{col3}}: {str(site_preset).replace('W',''):>4} W"
2024-02-20 13:35:04 +00:00
)
CONSOLE.info(
2024-03-27 10:53:59 +00:00
f"{'ID':<{t1}} {'Start':<{t2}} {'End':<{t3}} {'Export':<{t4}} {'Output':<{t5}} {'ChargePrio':<{t6}} {'SB1':>{t7}} {'SB2':>{t8}} Name"
2024-02-20 13:35:04 +00:00
)
# for slot in (data.get("home_load_data",{})).get("ranges",[]):
for slot in data.get("ranges", []):
enabled = slot.get("turn_on")
load = slot.get("appliance_loads", [])
2024-01-30 11:58:09 +00:00
load = load[0] if len(load) > 0 else {}
2024-03-26 00:15:38 +00:00
solarbanks = slot.get("device_power_loads", [])
sb1 = str(
solarbanks[0].get("power")
if len(solarbanks) > 0
else "---"
)
sb2 = str(
solarbanks[1].get("power")
if len(solarbanks) > 1
else "---"
)
2024-02-20 13:35:04 +00:00
CONSOLE.info(
2024-03-26 00:15:38 +00:00
f"{str(slot.get('id','')):>{t1}} {slot.get('start_time',''):<{t2}} {slot.get('end_time',''):<{t3}} {('---' if enabled is None else 'YES' if enabled else 'NO'):^{t4}} {str(load.get('power',''))+' W':>{t5}} {str(slot.get('charge_priority',''))+' %':>{t6}} {sb1+' W':>{t7}} {sb2+' W':>{t8}} {str(load.get('name',''))}"
2024-02-20 13:35:04 +00:00
)
elif devtype == "inverter":
upgrade = dev.get("auto_upgrade")
CONSOLE.info(
f"{'SW Version':<{col1}}: {dev.get('sw_version','Unknown'):<{col2}} {'Auto-Upgrade':<{col3}}: {'Unknown' if upgrade is None else 'Enabled' if upgrade else 'Disabled'}"
2024-02-20 13:35:04 +00:00
)
CONSOLE.info(
f"{'Status':<{col1}}: {dev.get('status_desc','Unknown'):<{col2}} {'Status code':<{col3}}: {str(dev.get('status','-'))}"
2024-02-20 13:35:04 +00:00
)
unit = dev.get("power_unit", "W")
CONSOLE.info(
f"{'AC Power':<{col1}}: {dev.get('generate_power',''):>3} {unit}"
)
2024-01-30 11:58:09 +00:00
else:
2024-02-20 13:35:04 +00:00
CONSOLE.warning(
"Neither Solarbank nor Inverter device, further details will be skipped"
)
CONSOLE.info("")
2024-04-16 17:07:59 +00:00
CONSOLE.info("Api Requests: %s", myapi.request_count)
2024-02-10 17:57:33 +00:00
CONSOLE.debug(json.dumps(myapi.devices, indent=2))
2024-03-26 00:15:38 +00:00
for sec in range(REFRESH):
2024-02-03 01:01:42 +00:00
now = datetime.now().astimezone()
2024-01-30 11:58:09 +00:00
if sys.stdin is sys.__stdin__:
2024-02-20 13:35:04 +00:00
print( # noqa: T201
f"Site refresh: {int((next_refr-now).total_seconds()):>3} sec, Device details refresh: {int((next_dev_refr-now).total_seconds()):>3} sec (CTRL-C to abort)",
end="\r",
flush=True,
)
2024-01-30 11:58:09 +00:00
elif sec == 0:
# IDLE may be used and does not support cursor placement, skip time progress display
2024-02-20 13:35:04 +00:00
print( # noqa: T201
f"Site refresh: {int((next_refr-now).total_seconds()):>3} sec, Device details refresh: {int((next_dev_refr-now).total_seconds()):>3} sec (CTRL-C to abort)",
end="",
flush=True,
)
2024-01-30 11:58:09 +00:00
time.sleep(1)
return False
2024-02-20 13:35:04 +00:00
except (ClientError, errors.AnkerSolixError) as err:
CONSOLE.error("%s: %s", type(err), err)
2024-01-30 11:58:09 +00:00
return False
# run async main
2024-02-20 13:35:04 +00:00
if __name__ == "__main__":
2024-01-30 11:58:09 +00:00
try:
if not asyncio.run(main()):
CONSOLE.warning("\nAborted!")
2024-01-30 11:58:09 +00:00
except KeyboardInterrupt:
CONSOLE.warning("\nAborted!")
except Exception as exception: # pylint: disable=broad-exception-caught
2024-02-20 22:10:57 +00:00
CONSOLE.exception("%s: %s", type(exception), exception)