release 1.0
Initial release with code cleanup and some required changes for use in HA integration
This commit is contained in:
parent
072ce0e731
commit
d8bbb6e3c5
73
README.md
73
README.md
|
@ -1,15 +1,15 @@
|
|||
<img src="https://public-aiot-fra-prod.s3.dualstack.eu-central-1.amazonaws.com/anker-power/public/product/anker-power/e9478c2d-e665-4d84-95d7-dd4844f82055/20230719-144818.png" alt="Solarbank E1600 Logo" title="Anker Solix API" align="right" height="100" />
|
||||
<img src="https://public-aiot-fra-prod.s3.dualstack.eu-central-1.amazonaws.com/anker-power/public/product/anker-power/e9478c2d-e665-4d84-95d7-dd4844f82055/20230719-144818.png" alt="Solarbank E1600 Logo" title="Anker Solix Api" align="right" height="100" />
|
||||
|
||||
# Anker Solix API
|
||||
# Anker Solix Api
|
||||
|
||||
[![github licence](https://img.shields.io/badge/Licence-MIT-orange)](https://github.com/thomluther/anker-solix-api/blob/main/LICENSE)
|
||||
![python badge](https://img.shields.io/badge/Made%20with-Python-orange)
|
||||
|
||||
This is an experimental Python library for Anker Solix Power devices (Solarbank, Inverter etc).
|
||||
|
||||
🚨 This is by no means an official Anker API. 🚨
|
||||
🚨 This is by no means an official Anker Api. 🚨
|
||||
|
||||
🚨 It can break at any time, or API request can be removed/added/changed and break some of the endpoint methods used in this API.🚨
|
||||
🚨 It can break at any time, or Api request can be removed/added/changed and break some of the endpoint methods used in this Api.🚨
|
||||
|
||||
# Python Versions
|
||||
|
||||
|
@ -27,13 +27,13 @@ pip install aiohttp
|
|||
|
||||
# Anker Account Information
|
||||
|
||||
Because of the way the Anker Solix API works, one account with email/password cannot be used for the Anker mobile App and this API in parallel.
|
||||
Because of the way the Anker Solix Api works, one account with email/password cannot be used for the Anker mobile App and this Api in parallel.
|
||||
The Anker Cloud allows only one request token per account at any time. Each new authentication request by a client will create a new token and drop a previous token.
|
||||
Therefore usage of this API may kick out your account login in the mobile app.
|
||||
Therefore usage of this Api may kick out your account login in the mobile app.
|
||||
However, starting with Anker mobile app release 2.0, you can share your defined system(s) with 'family members'.
|
||||
Therefore it is recommended to create a second Anker account with a different email address and share your defined system(s) with the second account.
|
||||
Attention: A shared account is only a member of the shared system, and as such currently has no permissions to access or query device details of the shared system.
|
||||
Therefore an API homepage query will neither display any data for a shared account. However, a shared account can receive API scene/site details of shared systems (App system = API site),
|
||||
Therefore an Api homepage query will neither display any data for a shared account. However, a shared account can receive Api scene/site details of shared systems (App system = Api site),
|
||||
which is equivalent to what is displayed in the mobile app on the home screen for the selected system.
|
||||
|
||||
# Usage
|
||||
|
@ -48,13 +48,13 @@ from aiohttp import ClientSession
|
|||
from api import api, errors
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
#_LOGGER.setLevel(logging.DEBUG) # enable for detailed API output
|
||||
#_LOGGER.setLevel(logging.DEBUG) # enable for detailed Api output
|
||||
|
||||
async def main() -> None:
|
||||
"""Create the aiohttp session and run the example."""
|
||||
async with ClientSession() as websession:
|
||||
"""put your code here, example"""
|
||||
myapi = api.API("username@domain.com","password","de",websession, _LOGGER)
|
||||
myapi = api.AnkerSolixApi("username@domain.com","password","de",websession, _LOGGER)
|
||||
await myapi.update_sites()
|
||||
await myapi.update_device_details()
|
||||
print("System Overview:")
|
||||
|
@ -62,7 +62,7 @@ async def main() -> None:
|
|||
print("Device Overview:")
|
||||
print(json.dumps(myapi.devices, indent=2))
|
||||
|
||||
"""run async main"""
|
||||
# run async main
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
asyncio.run(main())
|
||||
|
@ -70,36 +70,36 @@ if __name__ == '__main__':
|
|||
print(f'{type(err)}: {err}')
|
||||
```
|
||||
|
||||
The API class provides 2 main methods:
|
||||
- `API.update_sites()` to query overview data for all accessible sites and store data in API dictionaries `API.sites` and `API.devices` for quick access.
|
||||
This method could be run in regular intervals (30s or more) to fetch new data of the systems
|
||||
- `API.update_device_details()` to query further settings for the device serials as found in the sites query.
|
||||
This method should be run less frequently since this will mostly fetch various device configuration settings and needs multiple queries.
|
||||
The AnkerSolixApi class provides 2 main methods:
|
||||
- `AnkerSolixApi.update_sites()` to query overview data for all accessible sites and store data in dictionaries `AnkerSolixApi.sites` and `AnkerSolixApi.devices` for quick access.
|
||||
This method could be run in regular intervals (30sec or more) to fetch new data of the systems
|
||||
- `AnkerSolixApi.update_device_details()` to query further settings for the device serials as found in the sites query.
|
||||
This method should be run less frequently since this will mostly fetch various device configuration settings and needs multiple queries.
|
||||
It currently is developped for Solarbank devices only, further device types such as Inverters or Power Stations could be added once example data is available.
|
||||
|
||||
Check out `test_api.py` and other python executable tools that may help to leverage and explore the API for your Anker power system.
|
||||
Check out `test_api.py` and other python executable tools that may help to leverage and explore the Api for your Anker power system.
|
||||
The subfolder [`examples`](https://github.com/thomluther/anker-solix-api/tree/main/examples) contains json files with anonymized responses of the
|
||||
`export_system.py` module giving you an idea of how various API responses look like. (Note that the Solarbank was switched off when the data were pulled, so some fields may be empty)
|
||||
Those json files can also be used to develop/debug the API for system constellations not available to the developper.
|
||||
`export_system.py` module giving you an idea of how various Api responses look like. (Note that the Solarbank was switched off when the data were pulled, so some fields may be empty)
|
||||
Those json files can also be used to develop/debug the Api for system constellations not available to the developper.
|
||||
|
||||
# API Tools
|
||||
# AnkerSolixApi Tools
|
||||
|
||||
## test_api.py
|
||||
|
||||
Example exec module that can be used to explore and test API methods or direct enpoint requests with parameters.
|
||||
Example exec module that can be used to explore and test AnkerSolixApi methods or direct enpoint requests with parameters.
|
||||
|
||||
## export_system.py
|
||||
|
||||
Example exec module to use the Anker API for export of defined system data and device details.
|
||||
Example exec module to use the Anker Api for export of defined system data and device details.
|
||||
This module will prompt for the Anker account details if not pre-set in the header.
|
||||
Upon successfull authentication, you can specify a subfolder for the exported JSON files received as API query response, defaulting to your nick name
|
||||
Upon successfull authentication, you can specify a subfolder for the exported JSON files received as Api query response, defaulting to your nick name
|
||||
Optionally you can specify whether personalized information in the response data should be randomized in the files, like SNs, Site IDs, Trace IDs etc.
|
||||
You can review the response files afterwards. They can be used as examples for dedicated data extraction from the devices.
|
||||
Optionally the API class can use the json files for debugging and testing on various system outputs.
|
||||
Optionally the AnkerSolixApi class can use the json files for debugging and testing on various system outputs.
|
||||
|
||||
## solarbank_monitor.py
|
||||
|
||||
Example exec module to use the Anker API for continously querying and displaying important solarbank parameters
|
||||
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.
|
||||
|
@ -107,12 +107,12 @@ Attention: During executiion of this module, the used account cannot be used in
|
|||
|
||||
## energy_csv.py
|
||||
|
||||
Example exec module to use the Anker API for export of daily Solarbank Energy Data.
|
||||
Example exec module to use the Anker Api for export of daily Solarbank Energy Data.
|
||||
This method will prompt for the Anker account details if not pre-set in the header.
|
||||
Then you can specify a start day and the number of days for data extraction from the Anker Cloud.
|
||||
Note: The Solar production and Solarbank discharge can be queried across the full range. The solarbank
|
||||
charge however can be queried only as total for an interval (e.g. day). Therefore when solarbank charge
|
||||
data is also selected for export, an additional API query per day is required.
|
||||
data is also selected for export, an additional Api query per day is required.
|
||||
The received daily values will be exported into a csv file.
|
||||
|
||||
|
||||
|
@ -122,18 +122,17 @@ The received daily values will be exported into a csv file.
|
|||
![last commit](https://img.shields.io/github/last-commit/thomluther/anker-solix-api?color=orange)
|
||||
[![Community Discussion](https://img.shields.io/badge/Home%20Assistant%20Community-Discussion-orange)](https://community.home-assistant.io/t/feature-request-integration-or-addon-for-anker-solix-e1600-solarbank/641086)
|
||||
|
||||
Github is used to host code, to track issues and feature requests, as well as accept pull requests.
|
||||
|
||||
Pull requests are the best way to propose changes to the codebase.
|
||||
|
||||
1. [Check for open features/bugs](https://github.com/thomluther/anker-solix-api/issues)
|
||||
or [initiate a discussion on one](https://github.com/thomluther/anker-solix-api/issues/new).
|
||||
2. [Fork the repository](https://github.com/thomluther/anker-solix-api/fork).
|
||||
3. Install the dev environment: `make init`.
|
||||
4. Enter the virtual environment: `source ./venv/bin/activate`
|
||||
5. Code your new feature or bug fix.
|
||||
6. Write a test that covers your new functionality.
|
||||
7. Update `README.md` with any new documentation.
|
||||
8. Run tests and ensure 100% code coverage: `make coverage`
|
||||
9. Ensure you have no linting errors: `make lint`
|
||||
10. Ensure you have typed your code correctly: `make typing`
|
||||
11. Submit a pull request!
|
||||
1. [Fork the repository](https://github.com/thomluther/anker-solix-api/fork).
|
||||
1. Fork the repo and create your branch from `main`.
|
||||
1. If you've changed something, update the documentation.
|
||||
1. Test your contribution.
|
||||
1. Issue that pull request!
|
||||
|
||||
|
||||
# Acknowledgements / Credits
|
||||
|
@ -144,4 +143,4 @@ The received daily values will be exported into a csv file.
|
|||
|
||||
# Showing Your Appreciation
|
||||
|
||||
If you like this project, please give it a star on [GitHub](https://github.com/thomluther/anker-solix-api)
|
||||
If you like this project, please give it a star on [GitHub](https://github.com/thomluther/anker-solix-api)
|
887
api/api.py
887
api/api.py
File diff suppressed because it is too large
Load Diff
|
@ -1,73 +1,75 @@
|
|||
"""Define package errors."""
|
||||
from typing import Dict, Type
|
||||
"""Define Anker Solix API errors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class AnkerSolixError(Exception):
|
||||
"""Define a base error."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationError(AnkerSolixError):
|
||||
"""Authorization error."""
|
||||
pass
|
||||
|
||||
|
||||
class ConnectError(AnkerSolixError):
|
||||
"""Connection error."""
|
||||
pass
|
||||
|
||||
|
||||
class NetworkError(AnkerSolixError):
|
||||
"""Network error."""
|
||||
pass
|
||||
|
||||
|
||||
class ServerError(AnkerSolixError):
|
||||
"""Server error."""
|
||||
pass
|
||||
|
||||
|
||||
class RequestError(AnkerSolixError):
|
||||
"""Request error."""
|
||||
pass
|
||||
|
||||
|
||||
class VerifyCodeError(AnkerSolixError):
|
||||
"""Verify code error."""
|
||||
pass
|
||||
|
||||
|
||||
class VerifyCodeExpiredError(AnkerSolixError):
|
||||
"""Verification code has expired."""
|
||||
pass
|
||||
|
||||
|
||||
class NeedVerifyCodeError(AnkerSolixError):
|
||||
"""Need verification code error."""
|
||||
pass
|
||||
|
||||
|
||||
class VerifyCodeMaxError(AnkerSolixError):
|
||||
"""Maximum attempts of verications error."""
|
||||
pass
|
||||
|
||||
|
||||
class VerifyCodeNoneMatchError(AnkerSolixError):
|
||||
"""Verify code none match error."""
|
||||
pass
|
||||
|
||||
|
||||
class VerifyCodePasswordError(AnkerSolixError):
|
||||
"""Verify code password error."""
|
||||
pass
|
||||
|
||||
|
||||
class ClientPublicKeyError(AnkerSolixError):
|
||||
"""Define an error for client public key error."""
|
||||
pass
|
||||
|
||||
|
||||
class TokenKickedOutError(AnkerSolixError):
|
||||
"""Define an error for token does not exist because it was kicked out."""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidCredentialsError(AnkerSolixError):
|
||||
"""Define an error for unauthenticated accounts."""
|
||||
pass
|
||||
|
||||
|
||||
class RetryExceeded(AnkerSolixError):
|
||||
"""Define an error for exceeded retry attempts. Please try again in 24 hours."""
|
||||
pass
|
||||
|
||||
ERRORS: Dict[int, Type[AnkerSolixError]] = {
|
||||
|
||||
ERRORS: dict[int, type[AnkerSolixError]] = {
|
||||
401: AuthorizationError,
|
||||
403: AuthorizationError,
|
||||
997: ConnectError,
|
||||
998: NetworkError,
|
||||
999: ServerError,
|
||||
|
@ -83,6 +85,7 @@ ERRORS: Dict[int, Type[AnkerSolixError]] = {
|
|||
26070: ClientPublicKeyError,
|
||||
26084: TokenKickedOutError,
|
||||
26108: InvalidCredentialsError,
|
||||
26156: InvalidCredentialsError,
|
||||
100053: RetryExceeded,
|
||||
}
|
||||
|
||||
|
|
109
energy_csv.py
109
energy_csv.py
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
Example exec module to use the Anker API for export of daily Solarbank Energy Data.
|
||||
"""Example exec module to use the Anker API for export of daily Solarbank Energy Data.
|
||||
|
||||
This method will prompt for the Anker account details if not pre-set in the header.
|
||||
Then you can specify a start day and the number of days for data extraction from the Anker Cloud.
|
||||
Note: The Solar production and Solarbank discharge can be queried across the full range. The solarbank
|
||||
|
@ -9,15 +9,22 @@ The received daily values will be exported into a csv file.
|
|||
"""
|
||||
|
||||
import asyncio
|
||||
from aiohttp import ClientSession
|
||||
import csv
|
||||
from datetime import datetime
|
||||
from api import api
|
||||
from getpass import getpass
|
||||
import json, logging, sys, csv
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from api import api
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
_LOGGER.addHandler(logging.StreamHandler(sys.stdout))
|
||||
#_LOGGER.setLevel(logging.DEBUG) # enable for debug output
|
||||
# _LOGGER.setLevel(logging.DEBUG) # enable for debug output
|
||||
CONSOLE: logging.Logger = logging.getLogger("console")
|
||||
CONSOLE.addHandler(logging.StreamHandler(sys.stdout))
|
||||
CONSOLE.setLevel(logging.INFO)
|
||||
|
||||
# Optional default Anker Account credentials to be used
|
||||
USER = ""
|
||||
|
@ -26,14 +33,15 @@ COUNTRY = ""
|
|||
|
||||
|
||||
async def main() -> None:
|
||||
global USER, PASSWORD, COUNTRY
|
||||
print("Exporting daily Energy data for Anker Solarbank:")
|
||||
"""Run main to export energy history from cloud."""
|
||||
global USER, PASSWORD, COUNTRY # noqa: PLW0603
|
||||
CONSOLE.info("Exporting daily Energy data for Anker Solarbank:")
|
||||
if USER == "":
|
||||
print("\nEnter Anker Account credentials:")
|
||||
CONSOLE.info("\nEnter Anker Account credentials:")
|
||||
USER = input("Username (email): ")
|
||||
if USER == "":
|
||||
return False
|
||||
PASSWORD = getpass("Password: ")
|
||||
PASSWORD = getpass("Password: ")
|
||||
if PASSWORD == "":
|
||||
return False
|
||||
COUNTRY = input("Country ID (e.g. DE): ")
|
||||
|
@ -41,66 +49,87 @@ async def main() -> None:
|
|||
return False
|
||||
try:
|
||||
async with ClientSession() as websession:
|
||||
print("\nTrying authentication...",end="")
|
||||
myapi = api.API(USER,PASSWORD,COUNTRY,websession, _LOGGER)
|
||||
CONSOLE.info("\nTrying authentication...")
|
||||
myapi = api.AnkerSolixApi(USER, PASSWORD, COUNTRY, websession, _LOGGER)
|
||||
if await myapi.async_authenticate():
|
||||
print("OK")
|
||||
CONSOLE.info("OK")
|
||||
else:
|
||||
print("CACHED") # Login validation will be done during first API call
|
||||
CONSOLE.info(
|
||||
"CACHED"
|
||||
) # Login validation will be done during first API call
|
||||
# Refresh the site and device info of the API
|
||||
print("\nUpdating Site Info...", end="")
|
||||
CONSOLE.info("\nUpdating Site Info...")
|
||||
if (await myapi.update_sites()) == {}:
|
||||
print("NO INFO")
|
||||
CONSOLE.info("NO INFO")
|
||||
return False
|
||||
print("OK")
|
||||
print(f"\nDevices: {len(myapi.devices)}")
|
||||
CONSOLE.info("OK")
|
||||
CONSOLE.info(f"\nDevices: {len(myapi.devices)}")
|
||||
_LOGGER.debug(json.dumps(myapi.devices, indent=2))
|
||||
|
||||
|
||||
for sn, device in myapi.devices.items():
|
||||
if device.get("type") == "solarbank":
|
||||
print(f"Found {device.get('name')} SN: {sn}")
|
||||
try:
|
||||
daystr = input("\nEnter start day for daily energy data (yyyy-mm-dd) or enter to skip: ")
|
||||
CONSOLE.info(f"Found {device.get('name')} SN: {sn}")
|
||||
try:
|
||||
daystr = input(
|
||||
"\nEnter start day for daily energy data (yyyy-mm-dd) or enter to skip: "
|
||||
)
|
||||
if daystr == "":
|
||||
print(f"Skipped SN: {sn}, checking for next Solarbank...")
|
||||
CONSOLE.info(
|
||||
f"Skipped SN: {sn}, checking for next Solarbank..."
|
||||
)
|
||||
continue
|
||||
startday = datetime.fromisoformat(daystr)
|
||||
numdays = int(input("How many days to query (1-366): "))
|
||||
daytotals = input("Do you want to include daily total data (e.g. solarbank charge) which require API query per day? (Y/N): ")
|
||||
daytotals = daytotals.upper() in ["Y","YES","TRUE",1]
|
||||
filename = input(f"CSV filename for export (daily_energy_{daystr}.csv): ")
|
||||
daytotals = input(
|
||||
"Do you want to include daily total data (e.g. solarbank charge) which require API query per day? (Y/N): "
|
||||
)
|
||||
daytotals = daytotals.upper() in ["Y", "YES", "TRUE", 1]
|
||||
filename = input(
|
||||
f"CSV filename for export (daily_energy_{daystr}.csv): "
|
||||
)
|
||||
if filename == "":
|
||||
filename = f"daily_energy_{daystr}.csv"
|
||||
except ValueError:
|
||||
return False
|
||||
print(f"Queries may take up to {numdays*daytotals + 2} seconds...please wait...")
|
||||
data = await myapi.energy_daily(siteId=device.get("site_id"),deviceSn=sn,startDay=startday,numDays=numdays,dayTotals=daytotals)
|
||||
CONSOLE.info(
|
||||
f"Queries may take up to {numdays*daytotals + 2} seconds...please wait..."
|
||||
)
|
||||
data = await myapi.energy_daily(
|
||||
siteId=device.get("site_id"),
|
||||
deviceSn=sn,
|
||||
startDay=startday,
|
||||
numDays=numdays,
|
||||
dayTotals=daytotals,
|
||||
)
|
||||
_LOGGER.debug(json.dumps(data, indent=2))
|
||||
# Write csv file
|
||||
if len(data) > 0:
|
||||
with open(filename, 'w', newline='') as csvfile:
|
||||
with open(
|
||||
filename, "w", newline="", encoding="utf-8"
|
||||
) as csvfile:
|
||||
fieldnames = (next(iter(data.values()))).keys()
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
writer.writerows(data.values())
|
||||
print(f"\nCompleted: Successfully exported data to {filename}")
|
||||
CONSOLE.info(
|
||||
f"\nCompleted: Successfully exported data to {filename}"
|
||||
)
|
||||
return True
|
||||
|
||||
print("No data received for device")
|
||||
|
||||
CONSOLE.info("No data received for device")
|
||||
return False
|
||||
print("No accepted Solarbank device found.")
|
||||
CONSOLE.info("No accepted Solarbank device found.")
|
||||
return False
|
||||
|
||||
except Exception as err:
|
||||
print(f'{type(err)}: {err}')
|
||||
CONSOLE.info(f"{type(err)}: {err}")
|
||||
return False
|
||||
|
||||
|
||||
"""run async main"""
|
||||
if __name__ == '__main__':
|
||||
# run async main
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
if not asyncio.run(main()):
|
||||
print("Aborted!")
|
||||
except Exception as err:
|
||||
print(f'{type(err)}: {err}')
|
||||
|
||||
CONSOLE.info("Aborted!")
|
||||
except Exception as exception:
|
||||
CONSOLE.info(f"{type(exception)}: {exception}")
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
SETUP:
|
||||
HM 600 Inverter
|
||||
2x 395 W panels.
|
||||
1x solarbank e1600
|
||||
1 System in Anker App
|
383
export_system.py
383
export_system.py
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
Example exec module to use the Anker API for export of defined system data and device details.
|
||||
"""Example exec module to use the Anker API for export of defined system data and device details.
|
||||
|
||||
This module will prompt for the Anker account details if not pre-set in the header.
|
||||
Upon successfull authentication, you can specify a subfolder for the exported JSON files received as API query response, defaulting to your nick name.
|
||||
Optionally you can specify whether personalized information in the response data should be randomized in the files, like SNs, Site IDs, Trace IDs etc.
|
||||
|
@ -8,102 +8,133 @@ Optionally the API class can use the json files for debugging and testing on var
|
|||
"""
|
||||
|
||||
import asyncio
|
||||
from aiohttp import ClientSession
|
||||
from datetime import datetime
|
||||
from getpass import getpass
|
||||
from contextlib import suppress
|
||||
import json, logging, sys, csv, os, time, string, random
|
||||
from getpass import getpass
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
import time
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from api import api
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
_LOGGER.addHandler(logging.StreamHandler(sys.stdout))
|
||||
#_LOGGER.setLevel(logging.DEBUG) # enable for debug output
|
||||
# _LOGGER.setLevel(logging.DEBUG) # enable for debug output
|
||||
CONSOLE: logging.Logger = logging.getLogger("console")
|
||||
CONSOLE.addHandler(logging.StreamHandler(sys.stdout))
|
||||
CONSOLE.setLevel(logging.INFO)
|
||||
|
||||
# Optional default Anker Account credentials to be used
|
||||
USER = ""
|
||||
PASSWORD = ""
|
||||
COUNTRY = ""
|
||||
|
||||
RANDOMIZE = True # Global flag to save randomize decission
|
||||
RANDOMDATA = {} # Global dict for randomized data, printed at the end
|
||||
RANDOMIZE = True # Global flag to save randomize decission
|
||||
RANDOMDATA = {} # Global dict for randomized data, printed at the end
|
||||
|
||||
|
||||
def randomize(val, key: str = "") -> str:
|
||||
"""Randomize a given string while maintaining its format if format is known for given key name.
|
||||
Reuse same randomization if value was already randomized"""
|
||||
global RANDOMDATA
|
||||
|
||||
Reuse same randomization if value was already randomized
|
||||
"""
|
||||
global RANDOMDATA # noqa: PLW0602
|
||||
if not RANDOMIZE:
|
||||
return str(val)
|
||||
randomstr = RANDOMDATA.get(val,"")
|
||||
randomstr = RANDOMDATA.get(val, "")
|
||||
if not randomstr and val:
|
||||
if "_sn" in key:
|
||||
randomstr = "".join(random.choices(string.ascii_uppercase+string.digits, k=len(val)))
|
||||
randomstr = "".join(
|
||||
random.choices(string.ascii_uppercase + string.digits, k=len(val))
|
||||
)
|
||||
elif "bt_ble_" in key:
|
||||
"""Handle values with and without : """
|
||||
temp = val.replace(":","")
|
||||
randomstr = RANDOMDATA.get(temp) # retry existing randomized value without :
|
||||
# Handle values with and without ':'
|
||||
temp = val.replace(":", "")
|
||||
randomstr = RANDOMDATA.get(
|
||||
temp
|
||||
) # retry existing randomized value without :
|
||||
if not randomstr:
|
||||
randomstr = "".join(random.choices(string.hexdigits.upper(), k=len(temp)))
|
||||
randomstr = "".join(
|
||||
random.choices(string.hexdigits.upper(), k=len(temp))
|
||||
)
|
||||
if ":" in val:
|
||||
RANDOMDATA.update({temp: randomstr}) # save also key value without :
|
||||
randomstr = ':'.join(a+b for a,b in zip(randomstr[::2], randomstr[1::2]))
|
||||
RANDOMDATA.update({temp: randomstr}) # save also key value without :
|
||||
randomstr = ":".join(
|
||||
a + b for a, b in zip(randomstr[::2], randomstr[1::2])
|
||||
)
|
||||
elif "_id" in key:
|
||||
for part in val.split("-"):
|
||||
if randomstr:
|
||||
randomstr = "-".join([randomstr,"".join(random.choices(string.hexdigits.lower(), k=len(part)))])
|
||||
randomstr = "-".join(
|
||||
[
|
||||
randomstr,
|
||||
"".join(
|
||||
random.choices(string.hexdigits.lower(), k=len(part))
|
||||
),
|
||||
]
|
||||
)
|
||||
else:
|
||||
randomstr = "".join(random.choices(string.hexdigits.lower(), k=len(part)))
|
||||
randomstr = "".join(
|
||||
random.choices(string.hexdigits.lower(), k=len(part))
|
||||
)
|
||||
elif "wifi_name" in key:
|
||||
idx = sum(1 for s in RANDOMDATA.values() if 'wifi-network-' in s)
|
||||
randomstr = f'wifi-network-{idx+1}'
|
||||
idx = sum(1 for s in RANDOMDATA.values() if "wifi-network-" in s)
|
||||
randomstr = f"wifi-network-{idx+1}"
|
||||
else:
|
||||
# default randomize format
|
||||
randomstr = "".join(random.choices(string.ascii_letters, k=len(val)))
|
||||
RANDOMDATA.update({val: randomstr})
|
||||
return randomstr
|
||||
|
||||
|
||||
|
||||
def check_keys(data):
|
||||
"""Recursive traversal of complex nested objects to randomize value for certain keys"""
|
||||
if isinstance(data, str) or isinstance(data, int):
|
||||
"""Recursive traversal of complex nested objects to randomize value for certain keys."""
|
||||
if isinstance(data, int | str):
|
||||
return data
|
||||
for k, v in data.copy().items():
|
||||
if isinstance(v, dict):
|
||||
v = check_keys(v)
|
||||
if isinstance(v, list):
|
||||
v = [check_keys(i) for i in v]
|
||||
"""Randomize value for certain keys"""
|
||||
if any(x in k for x in ["_sn","site_id","trace_id","bt_ble_","wifi_name"]):
|
||||
data[k] = randomize(v,k)
|
||||
# Randomize value for certain keys
|
||||
if any(x in k for x in ["_sn", "site_id", "trace_id", "bt_ble_", "wifi_name"]):
|
||||
data[k] = randomize(v, k)
|
||||
return data
|
||||
|
||||
|
||||
def export(filename: str, d: dict = {}) -> None:
|
||||
"""Save dict data to given file"""
|
||||
time.sleep(1) # central delay between multiple requests
|
||||
def export(filename: str, d: dict = None) -> None:
|
||||
"""Save dict data to given file."""
|
||||
if not d:
|
||||
d = {}
|
||||
time.sleep(1) # central delay between multiple requests
|
||||
if len(d) == 0:
|
||||
print(f"WARNING: File {filename} not saved because JSON is empty")
|
||||
CONSOLE.info(f"WARNING: File {filename} not saved because JSON is empty")
|
||||
return
|
||||
elif RANDOMIZE:
|
||||
d = check_keys(d)
|
||||
try:
|
||||
with open(filename, 'w') as file:
|
||||
with open(filename, "w", encoding="utf-8") as file:
|
||||
json.dump(d, file, indent=2)
|
||||
print(f"Saved JSON to file {filename}")
|
||||
CONSOLE.info(f"Saved JSON to file {filename}")
|
||||
except Exception as err:
|
||||
print(f"ERROR: Failed to save JSON to file {filename}")
|
||||
CONSOLE.error(f"ERROR: Failed to save JSON to file {filename}: {err}")
|
||||
return
|
||||
|
||||
|
||||
async def main() -> bool:
|
||||
global USER, PASSWORD, COUNTRY, RANDOMIZE
|
||||
print("Exporting found Anker Solix system data for all assigned sites:")
|
||||
async def main() -> bool: # noqa: C901
|
||||
"""Run main function to export config."""
|
||||
global USER, PASSWORD, COUNTRY, RANDOMIZE # noqa: PLW0603
|
||||
CONSOLE.info("Exporting found Anker Solix system data for all assigned sites:")
|
||||
if USER == "":
|
||||
print("\nEnter Anker Account credentials:")
|
||||
CONSOLE.info("\nEnter Anker Account credentials:")
|
||||
USER = input("Username (email): ")
|
||||
if USER == "":
|
||||
return False
|
||||
PASSWORD = getpass("Password: ")
|
||||
PASSWORD = getpass("Password: ")
|
||||
if PASSWORD == "":
|
||||
return False
|
||||
COUNTRY = input("Country ID (e.g. DE): ")
|
||||
|
@ -111,119 +142,237 @@ async def main() -> bool:
|
|||
return False
|
||||
try:
|
||||
async with ClientSession() as websession:
|
||||
print("\nTrying authentication...",end="")
|
||||
myapi = api.API(USER,PASSWORD,COUNTRY,websession, _LOGGER)
|
||||
CONSOLE.info("\nTrying authentication...")
|
||||
myapi = api.AnkerSolixApi(USER, PASSWORD, COUNTRY, websession, _LOGGER)
|
||||
if await myapi.async_authenticate():
|
||||
print("OK")
|
||||
CONSOLE.info("OK")
|
||||
else:
|
||||
print("CACHED") # Login validation will be done during first API call
|
||||
|
||||
random = input(f"\nDo you want to randomize unique IDs and SNs in exported files? (default: {'YES' if RANDOMIZE else 'NO'}) (Y/N): ")
|
||||
if random != "" or not isinstance(RANDOMIZE,bool):
|
||||
RANDOMIZE = random.upper() in ["Y","YES","TRUE",1]
|
||||
nickname = myapi.nickname.replace("*","#") # avoid filesystem problems with * in user nicknames
|
||||
CONSOLE.info(
|
||||
"CACHED"
|
||||
) # Login validation will be done during first API call
|
||||
|
||||
resp = input(
|
||||
f"\nDo you want to randomize unique IDs and SNs in exported files? (default: {'YES' if RANDOMIZE else 'NO'}) (Y/N): "
|
||||
)
|
||||
if resp != "" or not isinstance(RANDOMIZE, bool):
|
||||
RANDOMIZE = resp.upper() in ["Y", "YES", "TRUE", 1]
|
||||
nickname = myapi.nickname.replace(
|
||||
"*", "#"
|
||||
) # avoid filesystem problems with * in user nicknames
|
||||
folder = input(f"Subfolder for export (default: {nickname}): ")
|
||||
if folder == "":
|
||||
if nickname == "":
|
||||
return False
|
||||
else:
|
||||
folder = nickname
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
|
||||
folder = nickname
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
|
||||
# first update sites in API object
|
||||
print("\nQuerying site information...")
|
||||
CONSOLE.info("\nQuerying site information...")
|
||||
await myapi.update_sites()
|
||||
print(f"Sites: {len(myapi.sites)}, Devices: {len(myapi.devices)}")
|
||||
CONSOLE.info(f"Sites: {len(myapi.sites)}, Devices: {len(myapi.devices)}")
|
||||
_LOGGER.debug(json.dumps(myapi.devices, indent=2))
|
||||
|
||||
|
||||
# Query API using direct endpoints to save full response of each query in json files
|
||||
print("\nExporting homepage...")
|
||||
export(os.path.join(folder,f"homepage.json"), await myapi.request("post", api._API_ENDPOINTS["homepage"],json={}))
|
||||
print("Exporting site list...")
|
||||
export(os.path.join(folder,f"site_list.json"), await myapi.request("post", api._API_ENDPOINTS["site_list"],json={}))
|
||||
print("Exporting bind devices...")
|
||||
export(os.path.join(folder,f"bind_devices.json"), await myapi.request("post", api._API_ENDPOINTS["bind_devices"],json={})) # shows only owner devices
|
||||
print("Exporting user devices...")
|
||||
export(os.path.join(folder,f"user_devices.json"), await myapi.request("post", api._API_ENDPOINTS["user_devices"],json={})) # shows only owner devices
|
||||
print("Exporting charging devices...")
|
||||
export(os.path.join(folder,f"charging_devices.json"), await myapi.request("post", api._API_ENDPOINTS["charging_devices"],json={})) # shows only owner devices
|
||||
print("Exporting auto upgrade settings...")
|
||||
export(os.path.join(folder,f"auto_upgrade.json"), await myapi.request("post", api._API_ENDPOINTS["get_auto_upgrade"],json={})) # shows only owner devices
|
||||
CONSOLE.info("\nExporting homepage...")
|
||||
export(
|
||||
os.path.join(folder, "homepage.json"),
|
||||
await myapi.request("post", api._API_ENDPOINTS["homepage"], json={}),
|
||||
)
|
||||
CONSOLE.info("Exporting site list...")
|
||||
export(
|
||||
os.path.join(folder, "site_list.json"),
|
||||
await myapi.request("post", api._API_ENDPOINTS["site_list"], json={}),
|
||||
)
|
||||
CONSOLE.info("Exporting bind devices...")
|
||||
export(
|
||||
os.path.join(folder, "bind_devices.json"),
|
||||
await myapi.request(
|
||||
"post", api._API_ENDPOINTS["bind_devices"], json={}
|
||||
),
|
||||
) # shows only owner devices
|
||||
CONSOLE.info("Exporting user devices...")
|
||||
export(
|
||||
os.path.join(folder, "user_devices.json"),
|
||||
await myapi.request(
|
||||
"post", api._API_ENDPOINTS["user_devices"], json={}
|
||||
),
|
||||
) # shows only owner devices
|
||||
CONSOLE.info("Exporting charging devices...")
|
||||
export(
|
||||
os.path.join(folder, "charging_devices.json"),
|
||||
await myapi.request(
|
||||
"post", api._API_ENDPOINTS["charging_devices"], json={}
|
||||
),
|
||||
) # shows only owner devices
|
||||
CONSOLE.info("Exporting auto upgrade settings...")
|
||||
export(
|
||||
os.path.join(folder, "auto_upgrade.json"),
|
||||
await myapi.request(
|
||||
"post", api._API_ENDPOINTS["get_auto_upgrade"], json={}
|
||||
),
|
||||
) # shows only owner devices
|
||||
for siteId, site in myapi.sites.items():
|
||||
print(f"\nExporting site specific data for site {siteId}...")
|
||||
print("Exporting scene info...")
|
||||
export(os.path.join(folder,f"scene_{randomize(siteId,'site_id')}.json"), await myapi.request("post", api._API_ENDPOINTS["scene_info"],json={"site_id": siteId}))
|
||||
print("Exporting solar info...")
|
||||
CONSOLE.info(f"\nExporting site specific data for site {siteId}...")
|
||||
CONSOLE.info("Exporting scene info...")
|
||||
export(
|
||||
os.path.join(folder, f"scene_{randomize(siteId,'site_id')}.json"),
|
||||
await myapi.request(
|
||||
"post",
|
||||
api._API_ENDPOINTS["scene_info"],
|
||||
json={"site_id": siteId},
|
||||
),
|
||||
)
|
||||
CONSOLE.info("Exporting solar info...")
|
||||
with suppress(Exception):
|
||||
export(os.path.join(folder,f"solar_info_{randomize(siteId,'site_id')}.json"), await myapi.request("post", api._API_ENDPOINTS["solar_info"],json={"site_id": siteId})) # PARAMETERS UNKNOWN, site id not sufficient
|
||||
print("Exporting site detail...")
|
||||
export(
|
||||
os.path.join(
|
||||
folder, f"solar_info_{randomize(siteId,'site_id')}.json"
|
||||
),
|
||||
await myapi.request(
|
||||
"post",
|
||||
api._API_ENDPOINTS["solar_info"],
|
||||
json={"site_id": siteId},
|
||||
),
|
||||
) # PARAMETERS UNKNOWN, site id not sufficient
|
||||
CONSOLE.info("Exporting site detail...")
|
||||
admin = site.get("site_admin")
|
||||
try:
|
||||
export(os.path.join(folder,f"site_detail_{randomize(siteId,'site_id')}.json"), await myapi.request("post", api._API_ENDPOINTS["site_detail"],json={"site_id": siteId}))
|
||||
except Exception as err:
|
||||
export(
|
||||
os.path.join(
|
||||
folder, f"site_detail_{randomize(siteId,'site_id')}.json"
|
||||
),
|
||||
await myapi.request(
|
||||
"post",
|
||||
api._API_ENDPOINTS["site_detail"],
|
||||
json={"site_id": siteId},
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
if not admin:
|
||||
print("Query requires account of site owner!")
|
||||
print("Exporting wifi list...")
|
||||
CONSOLE.warning("Query requires account of site owner!")
|
||||
CONSOLE.info("Exporting wifi list...")
|
||||
try:
|
||||
export(os.path.join(folder,f"wifi_list_{randomize(siteId,'site_id')}.json"), await myapi.request("post", api._API_ENDPOINTS["wifi_list"],json={"site_id": siteId})) # works only for site owners
|
||||
except Exception as err:
|
||||
export(
|
||||
os.path.join(
|
||||
folder, f"wifi_list_{randomize(siteId,'site_id')}.json"
|
||||
),
|
||||
await myapi.request(
|
||||
"post",
|
||||
api._API_ENDPOINTS["wifi_list"],
|
||||
json={"site_id": siteId},
|
||||
),
|
||||
) # works only for site owners
|
||||
except Exception:
|
||||
if not admin:
|
||||
print("Query requires account of site owner!")
|
||||
print("Exporting site price...")
|
||||
CONSOLE.warning("Query requires account of site owner!")
|
||||
CONSOLE.info("Exporting site price...")
|
||||
try:
|
||||
export(os.path.join(folder,f"price_{randomize(siteId,'site_id')}.json"), await myapi.request("post", api._API_ENDPOINTS["get_site_price"],json={"site_id": siteId})) # works only for site owners
|
||||
except Exception as err:
|
||||
export(
|
||||
os.path.join(
|
||||
folder, f"price_{randomize(siteId,'site_id')}.json"
|
||||
),
|
||||
await myapi.request(
|
||||
"post",
|
||||
api._API_ENDPOINTS["get_site_price"],
|
||||
json={"site_id": siteId},
|
||||
),
|
||||
) # works only for site owners
|
||||
except Exception:
|
||||
if not admin:
|
||||
print("Query requires account of site owner!")
|
||||
print("Exporting device parameter settings...")
|
||||
CONSOLE.warning("Query requires account of site owner!")
|
||||
CONSOLE.info("Exporting device parameter settings...")
|
||||
try:
|
||||
export(os.path.join(folder,f"device_parm_{randomize(siteId,'site_id')}.json"), await myapi.request("post", api._API_ENDPOINTS["get_device_parm"],json={"site_id": siteId, "param_type": "4"})) # works only for site owners
|
||||
except Exception as err:
|
||||
export(
|
||||
os.path.join(
|
||||
folder, "device_parm_{randomize(siteId,'site_id')}.json"
|
||||
),
|
||||
await myapi.request(
|
||||
"post",
|
||||
api._API_ENDPOINTS["get_device_parm"],
|
||||
json={"site_id": siteId, "param_type": "4"},
|
||||
),
|
||||
) # works only for site owners
|
||||
except Exception:
|
||||
if not admin:
|
||||
print("Query requires account of site owner!")
|
||||
CONSOLE.warning("Query requires account of site owner!")
|
||||
for sn, device in myapi.devices.items():
|
||||
print(f"\nExporting device specific data for device {device.get('name','')} SN {sn}...")
|
||||
siteId = device.get('site_id','')
|
||||
admin = site.get('is_admin')
|
||||
print("Exporting power cutoff settings...")
|
||||
CONSOLE.info(
|
||||
f"\nExporting device specific data for device {device.get('name','')} SN {sn}..."
|
||||
)
|
||||
siteId = device.get("site_id", "")
|
||||
admin = site.get("is_admin")
|
||||
CONSOLE.info("Exporting power cutoff settings...")
|
||||
try:
|
||||
export(os.path.join(folder,f"power_cutoff_{randomize(sn,'_sn')}.json"), await myapi.request("post", api._API_ENDPOINTS["get_cutoff"],json={"site_id": siteId, "device_sn": sn})) # works only for site owners
|
||||
except Exception as err:
|
||||
export(
|
||||
os.path.join(
|
||||
folder, f"power_cutoff_{randomize(sn,'_sn')}.json"
|
||||
),
|
||||
await myapi.request(
|
||||
"post",
|
||||
api._API_ENDPOINTS["get_cutoff"],
|
||||
json={"site_id": siteId, "device_sn": sn},
|
||||
),
|
||||
) # works only for site owners
|
||||
except Exception:
|
||||
if not admin:
|
||||
print("Query requires account of site owner!")
|
||||
print("Exporting fittings...")
|
||||
CONSOLE.warning("Query requires account of site owner!")
|
||||
CONSOLE.info("Exporting fittings...")
|
||||
try:
|
||||
export(os.path.join(folder,f"device_fittings_{randomize(sn,'_sn')}.json"), await myapi.request("post", api._API_ENDPOINTS["get_device_fittings"],json={"site_id": siteId, "device_sn": sn})) # works only for site owners
|
||||
except Exception as err:
|
||||
export(
|
||||
os.path.join(
|
||||
folder, f"device_fittings_{randomize(sn,'_sn')}.json"
|
||||
),
|
||||
await myapi.request(
|
||||
"post",
|
||||
api._API_ENDPOINTS["get_device_fittings"],
|
||||
json={"site_id": siteId, "device_sn": sn},
|
||||
),
|
||||
) # works only for site owners
|
||||
except Exception:
|
||||
if not admin:
|
||||
print("Query requires account of site owner!")
|
||||
print("Exporting load...")
|
||||
CONSOLE.warning("Query requires account of site owner!")
|
||||
CONSOLE.info("Exporting load...")
|
||||
try:
|
||||
export(os.path.join(folder,f"device_load_{randomize(sn,'_sn')}.json"), await myapi.request("post", api._API_ENDPOINTS["get_device_load"],json={"site_id": siteId, "device_sn": sn})) # works only for site owners
|
||||
except Exception as err:
|
||||
export(
|
||||
os.path.join(folder, f"device_load_{randomize(sn,'_sn')}.json"),
|
||||
await myapi.request(
|
||||
"post",
|
||||
api._API_ENDPOINTS["get_device_load"],
|
||||
json={"site_id": siteId, "device_sn": sn},
|
||||
),
|
||||
) # works only for site owners
|
||||
except Exception:
|
||||
if not admin:
|
||||
print("Query requires account of site owner!")
|
||||
CONSOLE.warning("Query requires account of site owner!")
|
||||
|
||||
print(f"\nCompleted export of Anker Solix system data for user {USER}")
|
||||
CONSOLE.info(
|
||||
f"\nCompleted export of Anker Solix system data for user {USER}"
|
||||
)
|
||||
if RANDOMIZE:
|
||||
print(f"Folder {os.path.abspath(folder)} contains the randomized JSON files. Pls check and update fields that may contain unrecognized personalized data.")
|
||||
print(f"Following trace or site IDs, SNs and MAC addresses have been randomized in files (from -> to):")
|
||||
print(json.dumps(RANDOMDATA, indent=2))
|
||||
CONSOLE.info(
|
||||
f"Folder {os.path.abspath(folder)} contains the randomized JSON files. Pls check and update fields that may contain unrecognized personalized data."
|
||||
)
|
||||
CONSOLE.info(
|
||||
"Following trace or site IDs, SNs and MAC addresses have been randomized in files (from -> to):"
|
||||
)
|
||||
CONSOLE.info(json.dumps(RANDOMDATA, indent=2))
|
||||
else:
|
||||
print(f"Folder {os.path.abspath(folder)} contains the JSON files.")
|
||||
CONSOLE.info(
|
||||
f"Folder {os.path.abspath(folder)} contains the JSON files."
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as err:
|
||||
print(f'{type(err)}: {err}')
|
||||
CONSOLE.info(f"{type(err)}: {err}")
|
||||
return False
|
||||
|
||||
|
||||
"""run async main"""
|
||||
if __name__ == '__main__':
|
||||
# run async main
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
if not asyncio.run(main()):
|
||||
print("Aborted!")
|
||||
CONSOLE.info("Aborted!")
|
||||
except KeyboardInterrupt:
|
||||
print("Aborted!")
|
||||
except Exception as err:
|
||||
print(f'{type(err)}: {err}')
|
||||
CONSOLE.info("Aborted!")
|
||||
except Exception as exception:
|
||||
CONSOLE.info(f"{type(exception)}: {exception}")
|
||||
|
|
|
@ -1,44 +1,55 @@
|
|||
"""
|
||||
Example exec module to use the Anker API for continously querying and displaying important solarbank parameters
|
||||
"""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
|
||||
|
||||
import asyncio
|
||||
from aiohttp import ClientSession
|
||||
from datetime import datetime, timedelta
|
||||
from getpass import getpass
|
||||
import json, logging, sys, time, os
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from api import api
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
_LOGGER.addHandler(logging.StreamHandler(sys.stdout))
|
||||
#_LOGGER.setLevel(logging.DEBUG) # enable for debug output
|
||||
CONSOLE: logging.Logger = logging.getLogger("console")
|
||||
CONSOLE.addHandler(logging.StreamHandler(sys.stdout))
|
||||
CONSOLE.setLevel(logging.INFO)
|
||||
|
||||
# Optional default Anker Account credentials to be used
|
||||
USER = ""
|
||||
PASSWORD = ""
|
||||
COUNTRY = ""
|
||||
REFRESH = 30 # refresh interval in seconds
|
||||
REFRESH = 30 # default refresh interval in seconds
|
||||
|
||||
|
||||
def clearscreen():
|
||||
"""Clear the terminal screen."""
|
||||
if sys.stdin is sys.__stdin__: # check if not in IDLE shell
|
||||
os.system("cls") if os.name == "nt" else os.system("clear")
|
||||
#print("\033[H\033[2J", end="") # ESC characters to clear screen, system independent?
|
||||
if os.name == "nt":
|
||||
os.system("cls")
|
||||
else:
|
||||
os.system("clear")
|
||||
#CONSOLE.info("\033[H\033[2J", end="") # ESC characters to clear terminal screen, system independent?
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
global USER, PASSWORD, COUNTRY, REFRESH
|
||||
print("Solarbank Monitor:")
|
||||
"""Run Main routine to start Solarbank monitor in a loop."""
|
||||
global USER, PASSWORD, COUNTRY, REFRESH # noqa: PLW0603
|
||||
CONSOLE.info("Solarbank Monitor:")
|
||||
if USER == "":
|
||||
print("\nEnter Anker Account credentials:")
|
||||
CONSOLE.info("\nEnter Anker Account credentials:")
|
||||
USER = input("Username (email): ")
|
||||
if USER == "":
|
||||
return False
|
||||
PASSWORD = getpass("Password: ")
|
||||
PASSWORD = getpass("Password: ")
|
||||
if PASSWORD == "":
|
||||
return False
|
||||
COUNTRY = input("Country ID (e.g. DE): ")
|
||||
|
@ -46,12 +57,13 @@ async def main() -> None:
|
|||
return False
|
||||
try:
|
||||
async with ClientSession() as websession:
|
||||
print("\nTrying authentication...",end="")
|
||||
myapi = api.API(USER,PASSWORD,COUNTRY,websession, _LOGGER)
|
||||
CONSOLE.info("\nTrying authentication...")
|
||||
myapi = api.AnkerSolixApi(USER,PASSWORD,COUNTRY,websession, _LOGGER)
|
||||
if await myapi.async_authenticate():
|
||||
print("OK")
|
||||
CONSOLE.info("OK")
|
||||
else:
|
||||
print("CACHED") # Login validation will be done during first API call
|
||||
# Login validation will be done during first API call
|
||||
CONSOLE.info("CACHED")
|
||||
|
||||
while True:
|
||||
resp = input(f"\nHow many seconds refresh interval should be used? (10-600, default: {REFRESH}): ")
|
||||
|
@ -74,81 +86,81 @@ async def main() -> None:
|
|||
t5 = 6
|
||||
t6 = 10
|
||||
while True:
|
||||
print("\n")
|
||||
CONSOLE.info("\n")
|
||||
now = datetime.now().astimezone()
|
||||
if next_refr <= now:
|
||||
print("Running site refresh...")
|
||||
CONSOLE.info("Running site refresh...")
|
||||
await myapi.update_sites()
|
||||
next_refr = now + timedelta(seconds=REFRESH)
|
||||
if next_dev_refr <= now:
|
||||
print("Running device details refresh...")
|
||||
CONSOLE.info("Running device details refresh...")
|
||||
await myapi.update_device_details()
|
||||
next_dev_refr = next_refr + timedelta(seconds=REFRESH*9)
|
||||
schedules = {}
|
||||
clearscreen()
|
||||
print(f"Solarbank Monitor (refresh {REFRESH} s, details refresh {10*REFRESH} s):")
|
||||
print(f"Sites: {len(myapi.sites)}, Devices: {len(myapi.devices)}")
|
||||
CONSOLE.info(f"Solarbank Monitor (refresh {REFRESH} s, details refresh {10*REFRESH} s):")
|
||||
CONSOLE.info(f"Sites: {len(myapi.sites)}, Devices: {len(myapi.devices)}")
|
||||
for sn, dev in myapi.devices.items():
|
||||
devtype = dev.get('type','unknown')
|
||||
admin = dev.get('is_admin',False)
|
||||
print(f"{'Device':<{col1}}: {(dev.get('name','NoName')):<{col2}} (Admin: {'YES' if admin else 'NO'})")
|
||||
print(f"{'SN':<{col1}}: {sn}")
|
||||
print(f"{'PN':<{col1}}: {dev.get('pn','')}")
|
||||
print(f"{'Type':<{col1}}: {devtype.capitalize()}")
|
||||
CONSOLE.info(f"{'Device':<{col1}}: {(dev.get('name','NoName')):<{col2}} (Admin: {'YES' if admin else 'NO'})")
|
||||
CONSOLE.info(f"{'SN':<{col1}}: {sn}")
|
||||
CONSOLE.info(f"{'PN':<{col1}}: {dev.get('pn','')}")
|
||||
CONSOLE.info(f"{'Type':<{col1}}: {devtype.capitalize()}")
|
||||
if devtype == "solarbank":
|
||||
siteid = dev.get('site_id','')
|
||||
print(f"{'Site ID':<{col1}}: {siteid}")
|
||||
CONSOLE.info(f"{'Site ID':<{col1}}: {siteid}")
|
||||
online = dev.get('wifi_online')
|
||||
print(f"{'Wifi state':<{col1}}: {('Unknown' if online == None else 'Online' if online else 'Offline'):<{col2}} (Charging Status: {dev.get('charging_status','')})")
|
||||
CONSOLE.info(f"{'Wifi state':<{col1}}: {('Unknown' if online is None else 'Online' if online else 'Offline'):<{col2}} (Charging Status: {dev.get('charging_status','')})")
|
||||
upgrade = dev.get('auto_upgrade')
|
||||
print(f"{'SW Version':<{col1}}: {dev.get('sw_version','Unknown'):<{col2}} (Auto-Upgrade: {'Unknown' if upgrade == None else 'Enabled' if upgrade else 'Disabled'})")
|
||||
CONSOLE.info(f"{'SW Version':<{col1}}: {dev.get('sw_version','Unknown'):<{col2}} (Auto-Upgrade: {'Unknown' if upgrade is None else 'Enabled' if upgrade else 'Disabled'})")
|
||||
soc = f"{dev.get('battery_soc','---'):>3} %"
|
||||
print(f"{'State Of Charge':<{col1}}: {soc:<{col2}} (Min SOC: {str(dev.get('power_cutoff','--'))+' %'})")
|
||||
CONSOLE.info(f"{'State Of Charge':<{col1}}: {soc:<{col2}} (Min SOC: {str(dev.get('power_cutoff','--'))+' %'})")
|
||||
unit = dev.get('power_unit','W')
|
||||
print(f"{'Input Power':<{col1}}: {dev.get('input_power',''):>3} {unit}")
|
||||
print(f"{'Charge Power':<{col1}}: {dev.get('charging_power',''):>3} {unit}")
|
||||
print(f"{'Output Power':<{col1}}: {dev.get('output_power',''):>3} {unit}")
|
||||
CONSOLE.info(f"{'Input Power':<{col1}}: {dev.get('input_power',''):>3} {unit}")
|
||||
CONSOLE.info(f"{'Charge Power':<{col1}}: {dev.get('charging_power',''):>3} {unit}")
|
||||
CONSOLE.info(f"{'Output Power':<{col1}}: {dev.get('output_power',''):>3} {unit}")
|
||||
preset = dev.get('set_output_power')
|
||||
if not preset:
|
||||
preset = '---'
|
||||
print(f"{'Output Preset':<{col1}}: {preset:>3} {unit}")
|
||||
"""update schedule with device details refresh and print it"""
|
||||
CONSOLE.info(f"{'Output Preset':<{col1}}: {preset:>3} {unit}")
|
||||
# update schedule with device details refresh and print it
|
||||
if admin:
|
||||
if not schedules.get(sn) and siteid:
|
||||
schedules.update({sn: await myapi.get_device_load(siteId=siteid,deviceSn=sn)})
|
||||
data = schedules.get(sn,{})
|
||||
print(f"{'Schedule':<{col1}}: {now.strftime('%H:%M UTC %z'):<{col2}} (Current Preset: {data.get('current_home_load','')})")
|
||||
print(f"{'ID':<{t1}} {'Start':<{t2}} {'End':<{t3}} {'Discharge':<{t4}} {'Output':<{t5}} {'ChargePrio':<{t6}}")
|
||||
CONSOLE.info(f"{'Schedule':<{col1}}: {now.strftime('%H:%M UTC %z'):<{col2}} (Current Preset: {data.get('current_home_load','')})")
|
||||
CONSOLE.info(f"{'ID':<{t1}} {'Start':<{t2}} {'End':<{t3}} {'Discharge':<{t4}} {'Output':<{t5}} {'ChargePrio':<{t6}}")
|
||||
for slot in (data.get("home_load_data",{})).get("ranges",[]):
|
||||
enabled = slot.get('turn_on')
|
||||
load = slot.get('appliance_loads',[])
|
||||
load = load[0] if len(load) > 0 else {}
|
||||
print(f"{str(slot.get('id','')):>{t1}} {slot.get('start_time',''):<{t2}} {slot.get('end_time',''):<{t3}} {('---' if enabled == None else 'YES' if enabled else 'NO'):^{t4}} {str(load.get('power',''))+' W':>{t5}} {str(slot.get('charge_priority',''))+' %':>{t6}}")
|
||||
CONSOLE.info(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}}")
|
||||
else:
|
||||
print(f"Not a Solarbank device, further details skipped")
|
||||
print("")
|
||||
#print(json.dumps(myapi.devices, indent=2))
|
||||
sys.stdoutf("Not a Solarbank device, further details skipped")
|
||||
CONSOLE.info("")
|
||||
#CONSOLE.info(json.dumps(myapi.devices, indent=2))
|
||||
for sec in range(0,REFRESH):
|
||||
now = datetime.now().astimezone()
|
||||
if sys.stdin is sys.__stdin__:
|
||||
print(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)
|
||||
print(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) # noqa: T201
|
||||
elif sec == 0:
|
||||
# IDLE may be used and does not support cursor placement, skip time progress display
|
||||
print(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)
|
||||
print(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) # noqa: T201
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
except Exception as err:
|
||||
print(f'{type(err)}: {err}')
|
||||
except Exception as exception:
|
||||
CONSOLE.info(f'{type(exception)}: {exception}')
|
||||
return False
|
||||
|
||||
|
||||
"""run async main"""
|
||||
# run async main
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
if not asyncio.run(main()):
|
||||
print("\nAborted!")
|
||||
CONSOLE.info("\nAborted!")
|
||||
except KeyboardInterrupt:
|
||||
print("\nAborted!")
|
||||
CONSOLE.info("\nAborted!")
|
||||
except Exception as err:
|
||||
print(f'{type(err)}: {err}')
|
||||
CONSOLE.info(f'{type(err)}: {err}')
|
||||
|
|
137
test_api.py
137
test_api.py
|
@ -1,103 +1,108 @@
|
|||
"""
|
||||
Example exec module to test the Anker API for various methods or direct endpoint requests with various parameters.
|
||||
"""
|
||||
"""Example exec module to test the Anker API for various methods or direct endpoint requests with various parameters."""
|
||||
|
||||
import asyncio
|
||||
from aiohttp import ClientSession
|
||||
from datetime import datetime
|
||||
from api import api, errors
|
||||
import json, logging, sys
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from api import api
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
_LOGGER.addHandler(logging.StreamHandler(sys.stdout))
|
||||
#_LOGGER.setLevel(logging.DEBUG) # enable for detailed API output
|
||||
# _LOGGER.setLevel(logging.DEBUG) # enable for detailed API output
|
||||
CONSOLE: logging.Logger = logging.getLogger("console")
|
||||
CONSOLE.addHandler(logging.StreamHandler(sys.stdout))
|
||||
CONSOLE.setLevel(logging.INFO)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Create the aiohttp session and run the example."""
|
||||
print("Testing Solix API:")
|
||||
CONSOLE.info("Testing Solix API:")
|
||||
try:
|
||||
async with ClientSession() as websession:
|
||||
myapi = api.API("username@domain.com","password","de",websession, _LOGGER)
|
||||
myapi = api.AnkerSolixApi(
|
||||
"username@domain.com", "password", "de", websession, _LOGGER
|
||||
)
|
||||
|
||||
#show login response
|
||||
'''
|
||||
# show login response
|
||||
"""
|
||||
#new = await myapi.async_authenticate(restart=True) # enforce new login data from server
|
||||
new = await myapi.async_authenticate() # receive new or load cached login data
|
||||
if new:
|
||||
print("Received Login response:")
|
||||
CONSOLE.info("Received Login response:")
|
||||
else:
|
||||
print("Cached Login response:")
|
||||
print(json.dumps(myapi._login_response, indent=2)) # show used login response for API reqests
|
||||
'''
|
||||
CONSOLE.info("Cached Login response:")
|
||||
CONSOLE.info(json.dumps(myapi._login_response, indent=2)) # show used login response for API reqests
|
||||
"""
|
||||
|
||||
# test site api methods
|
||||
|
||||
await myapi.update_sites()
|
||||
await myapi.update_device_details()
|
||||
print("System Overview:")
|
||||
print(json.dumps(myapi.sites, indent=2))
|
||||
print("Device Overview:")
|
||||
print(json.dumps(myapi.devices, indent=2))
|
||||
CONSOLE.info("System Overview:")
|
||||
CONSOLE.info(json.dumps(myapi.sites, indent=2))
|
||||
CONSOLE.info("Device Overview:")
|
||||
CONSOLE.info(json.dumps(myapi.devices, indent=2))
|
||||
|
||||
|
||||
# test api methods
|
||||
'''
|
||||
print(json.dumps(await myapi.get_site_list(), indent=2))
|
||||
print(json.dumps(await myapi.get_homepage(), indent=2))
|
||||
print(json.dumps(await myapi.get_bind_devices(), indent=2))
|
||||
print(json.dumps(await myapi.get_user_devices(), indent=2))
|
||||
print(json.dumps(await myapi.get_charging_devices(), indent=2))
|
||||
print(json.dumps(await myapi.get_auto_upgrade(), indent=2))
|
||||
print(json.dumps(await myapi.get_scene_info(siteId='efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'), indent=2))
|
||||
print(json.dumps(await myapi.get_wifi_list(siteId='efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'), indent=2))
|
||||
print(json.dumps(await myapi.get_solar_info(siteId='efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'), indent=2)) # json parameters unknown: site_id not sifficient, or works only with Anker Inverters?
|
||||
print(json.dumps(await myapi.get_device_parm(siteId="efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c",paramType="4"), indent=2))
|
||||
print(json.dumps(await myapi.get_power_cutoff(siteId="efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c",deviceSn="9JVB42LJK8J0P5RY"), indent=2))
|
||||
print(json.dumps(await myapi.get_device_load(siteId="efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c",deviceSn="9JVB42LJK8J0P5RY"), indent=2))
|
||||
"""
|
||||
CONSOLE.info(json.dumps(await myapi.get_site_list(), indent=2))
|
||||
CONSOLE.info(json.dumps(await myapi.get_homepage(), indent=2))
|
||||
CONSOLE.info(json.dumps(await myapi.get_bind_devices(), indent=2))
|
||||
CONSOLE.info(json.dumps(await myapi.get_user_devices(), indent=2))
|
||||
CONSOLE.info(json.dumps(await myapi.get_charging_devices(), indent=2))
|
||||
CONSOLE.info(json.dumps(await myapi.get_auto_upgrade(), indent=2))
|
||||
CONSOLE.info(json.dumps(await myapi.get_scene_info(siteId='efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'), indent=2))
|
||||
CONSOLE.info(json.dumps(await myapi.get_wifi_list(siteId='efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'), indent=2))
|
||||
CONSOLE.info(json.dumps(await myapi.get_solar_info(siteId='efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'), indent=2)) # json parameters unknown: site_id not sifficient, or works only with Anker Inverters?
|
||||
CONSOLE.info(json.dumps(await myapi.get_device_parm(siteId="efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c",paramType="4"), indent=2))
|
||||
CONSOLE.info(json.dumps(await myapi.get_power_cutoff(siteId="efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c",deviceSn="9JVB42LJK8J0P5RY"), indent=2))
|
||||
CONSOLE.info(json.dumps(await myapi.get_device_load(siteId="efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c",deviceSn="9JVB42LJK8J0P5RY"), indent=2))
|
||||
|
||||
print(json.dumps(await myapi.energy_analysis(siteId="efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c",deviceSn="9JVB42LJK8J0P5RY",rangeType="week",startDay=datetime.fromisoformat("2023-10-10"),endDay=datetime.fromisoformat("2023-10-10"),devType="solar_production"), indent=2))
|
||||
print(json.dumps(await myapi.energy_analysis(siteId="efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c",deviceSn="9JVB42LJK8J0P5RY",rangeType="week",startDay=datetime.fromisoformat("2023-10-10"),endDay=datetime.fromisoformat("2023-10-10"),devType="solarbank"), indent=2))
|
||||
print(json.dumps(await myapi.energy_daily(siteId="efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c",deviceSn="9JVB42LJK8J0P5RY",startDay=datetime.fromisoformat("2024-01-10"),numDays=10), indent=2))
|
||||
print(json.dumps(await myapi.home_load_chart(siteId='efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'), indent=2))
|
||||
'''
|
||||
CONSOLE.info(json.dumps(await myapi.energy_analysis(siteId="efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c",deviceSn="9JVB42LJK8J0P5RY",rangeType="week",startDay=datetime.fromisoformat("2023-10-10"),endDay=datetime.fromisoformat("2023-10-10"),devType="solar_production"), indent=2))
|
||||
CONSOLE.info(json.dumps(await myapi.energy_analysis(siteId="efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c",deviceSn="9JVB42LJK8J0P5RY",rangeType="week",startDay=datetime.fromisoformat("2023-10-10"),endDay=datetime.fromisoformat("2023-10-10"),devType="solarbank"), indent=2))
|
||||
CONSOLE.info(json.dumps(await myapi.energy_daily(siteId="efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c",deviceSn="9JVB42LJK8J0P5RY",startDay=datetime.fromisoformat("2024-01-10"),numDays=10), indent=2))
|
||||
CONSOLE.info(json.dumps(await myapi.home_load_chart(siteId='efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'), indent=2))
|
||||
"""
|
||||
|
||||
# test api endpoints directly
|
||||
'''
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["homepage"],json={})), indent=2))
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["site_list"],json={})), indent=2))
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["bind_devices"],json={})), indent=2))
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["user_devices"],json={})), indent=2))
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["charging_devices"],json={})), indent=2))
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["get_auto_upgrade"],json={})), indent=2))
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["site_detail"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'})), indent=2))
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["wifi_list"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'})), indent=2))
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["get_site_price"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'})), indent=2))
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["solar_info"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'})), indent=2)) # json parameters unknown: site_id not sifficient, or works only with Anker Inverters?
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["get_cutoff"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c', "device_sn": "9JVB42LJK8J0P5RY"})), indent=2))
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["get_device_fittings"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c', "device_sn": "9JVB42LJK8J0P5RY"})), indent=2))
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["get_device_load"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c', "device_sn": "9JVB42LJK8J0P5RY"})), indent=2))
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["get_device_parm"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c', "param_type": "4"})), indent=2))
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["compatible_process"],json={})), indent=2)) # json parameters unknown
|
||||
print(json.dumps((await myapi.request("post", api._API_ENDPOINTS["home_load_chart"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'})), indent=2))
|
||||
'''
|
||||
"""
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["homepage"],json={})), indent=2))
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["site_list"],json={})), indent=2))
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["bind_devices"],json={})), indent=2))
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["user_devices"],json={})), indent=2))
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["charging_devices"],json={})), indent=2))
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["get_auto_upgrade"],json={})), indent=2))
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["site_detail"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'})), indent=2))
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["wifi_list"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'})), indent=2))
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["get_site_price"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'})), indent=2))
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["solar_info"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'})), indent=2)) # json parameters unknown: site_id not sifficient, or works only with Anker Inverters?
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["get_cutoff"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c', "device_sn": "9JVB42LJK8J0P5RY"})), indent=2))
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["get_device_fittings"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c', "device_sn": "9JVB42LJK8J0P5RY"})), indent=2))
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["get_device_load"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c', "device_sn": "9JVB42LJK8J0P5RY"})), indent=2))
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["get_device_parm"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c', "param_type": "4"})), indent=2))
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["compatible_process"],json={})), indent=2)) # json parameters unknown
|
||||
CONSOLE.info(json.dumps((await myapi.request("post", api._API_ENDPOINTS["home_load_chart"],json={"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c'})), indent=2))
|
||||
"""
|
||||
|
||||
# test api from json files
|
||||
'''
|
||||
"""
|
||||
myapi.testDir("examples")
|
||||
await myapi.update_sites(fromFile=True)
|
||||
await myapi.update_device_details(fromFile=True)
|
||||
print(json.dumps(myapi.sites,indent=2))
|
||||
print(json.dumps(myapi.devices,indent=2))
|
||||
'''
|
||||
CONSOLE.info(json.dumps(myapi.sites,indent=2))
|
||||
CONSOLE.info(json.dumps(myapi.devices,indent=2))
|
||||
"""
|
||||
|
||||
except Exception as err:
|
||||
print(f'{type(err)}: {err}')
|
||||
except Exception as exception:
|
||||
CONSOLE.info(f"{type(exception)}: {exception}")
|
||||
|
||||
|
||||
|
||||
"""run async main"""
|
||||
if __name__ == '__main__':
|
||||
# run async main
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except Exception as err:
|
||||
print(f'{type(err)}: {err}')
|
||||
CONSOLE.info(f"{type(err)}: {err}")
|
||||
|
|
Loading…
Reference in New Issue