Compare commits

...

2 Commits

Author SHA1 Message Date
Thomas Luther b4c7c07b79 1.8.1 2024-04-24 12:46:25 +02:00
Thomas Luther b41590e888 1.8.1 2024-04-24 12:22:21 +02:00
27 changed files with 315 additions and 174 deletions

View File

@ -63,7 +63,9 @@ async def main() -> None:
common.user(), common.password(), common.country(), websession, _LOGGER
)
await myapi.update_sites()
await myapi.update_site_details()
await myapi.update_device_details()
await myapi.update_device_energy()
print("System Overview:")
print(json.dumps(myapi.sites, indent=2))
print("Device Overview:")
@ -78,14 +80,18 @@ if __name__ == "__main__":
print(f"{type(err)}: {err}")
```
The AnkerSolixApi class provides 2 main methods:
The AnkerSolixApi class provides 4 main methods to query data and cache them into internal dictionaries:
- `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 could be run in regular intervals (60sec or more) to fetch new data of the systems. Note that the system devices update the cloud data only once per minute, therefore less than 60 second intervals do not provide much benefit
- `AnkerSolixApi.update_device_details()` to query further settings for the device serials as found in the sites query or for stand alone devices and store data in dictionary `AnkerSolixApi.devices`
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 and Inverter devices only, further device types such as Portable Power Stations or Power Panels
could be added once example data is available.
- `AnkerSolixApi.update_site_details()` to query further settings for the defined site (power system) and store data in dictionary `AnkerSolixApi.sites` for quick access.
This method should be run less frequently since this will mostly fetch various site configuration settings and needs multiple queries.
- `AnkerSolixApi.update_device_energy()` to query further energy statistics for the devices and store data in dictionary `AnkerSolixApi.devices` for quick access.
This method should be run less frequently since this will fetch 2-4 queries per device. Currently only solarbank devices are supported, but it was noticed, that the energy statistics endpoint (maybe each endpoint) is limited to 25-30 queries per minute.
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 actual example exports with json files using 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)
@ -116,6 +122,10 @@ Optionally you can specify whether personalized information in the response data
You can review the response files afterwards. They can be used as examples for dedicated data extraction from the devices.
Optionally the AnkerSolixApi class can use the json files for debugging and testing on various system outputs.
**Note**:
You should preferrably run the export_system with the owner account of the site. Otherwise only limited information can be exported by shared accounts due to access permissions.
## solarbank_monitor.py
```

View File

@ -149,11 +149,11 @@ _API_ENDPOINTS = {
"compatible_process": "power_service/v1/app/compatible/get_compatible_process", # contains solar_info plus OTA processing codes, works only with owner account
"get_device_fittings": "power_service/v1/app/get_relate_device_fittings", # Device fittings for given site id and device sn. Shows Accessories like Solarbank 0W Switch info
"energy_analysis": "power_service/v1/site/energy_analysis", # Fetch energy data for given time frames
"home_load_chart": "power_service/v1/site/get_home_load_chart", # Fetch data as displayed in home load chart for given site_id and optional device SN (empty if solarbank not connected)
"home_load_chart": "power_service/v1/site/get_home_load_chart", # Fetch data as displayed in home load chart for schedule adjustments for given site_id and optional device SN (empty if solarbank not connected)
"get_upgrade_record": "power_service/v1/app/get_upgrade_record", # get list of firmware update history
"check_upgrade_record": "power_service/v1/app/check_upgrade_record", # show an upgrade record for the device, types 1-3 show different info, only works for owner account
"get_message_unread": "power_service/v1/get_message_unread", # GET method to show if there are unread messages for account
"get_message": "power_service/v1/get_message", # get list of max Messages from certain time, last_time format unknown
"get_upgrade_record": "power_service/v1/app/get_upgrade_record", # get list of firmware update history
"get_mqtt_info": "app/devicemanage/get_user_mqtt_info", # get mqtt server and certificates, not explored or used
}
@ -184,7 +184,7 @@ _API_ENDPOINTS = {
'power_service/v1/app/share_site/delete_inviting_member',
'power_service/v1/app/share_site/get_invited_list',
'power_service/v1/app/share_site/join_site',
'power_service/v1/app/upgrade_event_report',
'power_service/v1/app/upgrade_event_report', # post an entry to upgrade event report
'power_service/v1/app/get_phonecode_list',
'power_service/v1/get_message_not_disturb', # get do not disturb messages settings
'power_service/v1/message_not_disturb', # change do not disurb messages settings
@ -410,7 +410,9 @@ class RequestCounter:
# limit the counter entries to 1 hour when adding new
self.recycle()
def recycle(self, last_time: datetime = datetime.now() - timedelta(hours=1)) -> None:
def recycle(
self, last_time: datetime = datetime.now() - timedelta(hours=1)
) -> None:
"""Remove oldest timestamps from beginning of counter until last_time is reached, default is 1 hour ago."""
self.elements = [x for x in self.elements if x > last_time]
@ -584,7 +586,7 @@ class AnkerSolixApi:
self._logger.error(err)
return {}
def _saveToFile(self, filename: str, data: dict = None) -> bool:
def _saveToFile(self, filename: str, data: dict | None = None) -> bool:
"""Save json data to given file for testing."""
if self.mask_credentials:
masked_filename = filename.replace(
@ -625,9 +627,9 @@ class AnkerSolixApi:
def _update_dev( # noqa: C901
self,
devData: dict,
devType: str = None,
siteId: str = None,
isAdmin: bool = None,
devType: str | None = None,
siteId: str | None = None,
isAdmin: bool | None = None,
) -> str | None:
"""Update the internal device details dictionary with the given data. The device_sn key must be set in the data dict for the update to be applied.
@ -858,7 +860,7 @@ class AnkerSolixApi:
self.devices.update({str(sn): device})
return sn
def testDir(self, subfolder: str = None) -> str:
def testDir(self, subfolder: str | None = None) -> str:
"""Get or set the subfolder for local API test files."""
if not subfolder or subfolder == self._testdir:
return self._testdir
@ -869,35 +871,39 @@ class AnkerSolixApi:
self._logger.info("Set Api test folder to: %s", subfolder)
return self._testdir
def logLevel(self, level: int = None) -> int:
def logLevel(self, level: int | None = None) -> int:
"""Get or set the logger log level."""
if level is not None and isinstance(level, int):
self._logger.setLevel(level)
self._logger.info("Set log level to: %s", level)
return self._logger.getEffectiveLevel()
def requestDelay(self, delay: float = None) -> float:
def requestDelay(self, delay: float | None = None) -> float:
"""Get or set the api request delay in seconds."""
if (
delay is not None
and isinstance(delay, (float,int))
and delay != self._request_delay
and isinstance(delay, (float, int))
and float(delay) != float(self._request_delay)
):
self._request_delay = min(
SolixDefaults.REQUEST_DELAY_MAX,
max(SolixDefaults.REQUEST_DELAY_MIN, delay),
self._request_delay = float(
min(
SolixDefaults.REQUEST_DELAY_MAX,
max(SolixDefaults.REQUEST_DELAY_MIN, delay),
)
)
self._logger.info(
"Set api request delay to %.3f seconds", self._request_delay
)
return self._request_delay
async def _wait_delay(self, delay: float = None) -> None:
async def _wait_delay(self, delay: float | None = None) -> None:
"""Wait at least for the defined Api request delay or for the provided delay in seconds since the last request occured."""
if delay is not None and isinstance(delay, (float,int)):
delay = min(
SolixDefaults.REQUEST_DELAY_MAX,
max(SolixDefaults.REQUEST_DELAY_MIN, delay),
if delay is not None and isinstance(delay, (float, int)):
delay = float(
min(
SolixDefaults.REQUEST_DELAY_MAX,
max(SolixDefaults.REQUEST_DELAY_MIN, delay),
)
)
else:
delay = self._request_delay
@ -934,15 +940,13 @@ class AnkerSolixApi:
"%s",
self.mask_values(data, "user_id", "auth_token", "email", "geo_key"),
)
self._retry_attempt = (
False # clear retry attempt to allow retry for authentication refresh
)
# clear retry attempt to allow retry for authentication refresh
self._retry_attempt = False
else:
self._logger.debug("Fetching new Login credentials from server")
now = datetime.now().astimezone()
self._retry_attempt = (
True # set retry attempt to avoid retry on failed authentication
)
# set retry attempt to avoid retry on failed authentication
self._retry_attempt = True
auth_resp = await self.request(
"post",
_API_LOGIN,
@ -1089,7 +1093,7 @@ class AnkerSolixApi:
# get first the body text for usage in error detail logging if necessary
body_text = await resp.text()
data = {}
resp.raise_for_status() # any response status >= 400
resp.raise_for_status() # any response status >= 400
if (data := await resp.json(content_type=None)) and self.encrypt_body:
# TODO(#70): Test and Support optional encryption for body
# data dict has to be decoded when encrypted
@ -1109,43 +1113,50 @@ class AnkerSolixApi:
)
else:
self._logger.debug("Response Data: %s", data)
self._retry_attempt = False # reset retry flag only when valid token received and not another login request
# reset retry flag only when valid token received and not another login request
self._retry_attempt = False
errors.raise_error(data) # check the Api response status code in the data
# check the Api response status code in the data
errors.raise_error(data)
# valid response at this point, mark login and return data
self._loggedIn = True
return data # noqa: TRY300
except (
ClientError
) as err: # Exception from ClientSession based on standard response status codes
# Exception from ClientSession based on standard response status codes
except ClientError as err:
self._logger.error("Api Request Error: %s", err)
self._logger.error("Response Text: %s", body_text)
# Prepare data dict for Api error lookup
if not data:
data = {}
if not hasattr(data,"code"):
if not hasattr(data, "code"):
data["code"] = resp.status
if not hasattr(data,"msg"):
if not hasattr(data, "msg"):
data["msg"] = body_text
if resp.status in [401,403]:
if resp.status in [401, 403]:
# Unauthorized or forbidden request
if self._retry_attempt:
errors.raise_error(data, prefix=f"Login failed for user {self._email}")
# catch error if Api code not defined
raise errors.AuthorizationError(
f"Login failed for user {self._email}"
) from err
self._logger.warning("Login failed, retrying authentication")
if await self.async_authenticate(restart=True):
return await self.request(
method, endpoint, headers=headers, json=json
)
self._logger.error("Re-Login failed for user %s", self._email)
elif resp.status in [429]:
# reattempt autentication with same credentials if cached token was kicked out
# retry attempt is set if login response data were not cached to fail immediately
if not self._retry_attempt:
self._logger.warning("Login failed, retrying authentication")
if await self.async_authenticate(restart=True):
return await self.request(
method, endpoint, headers=headers, json=json
)
self._logger.error("Re-Login failed for user %s", self._email)
errors.raise_error(
data, prefix=f"Login failed for user {self._email}"
)
# catch error if Api code not defined
raise errors.AuthorizationError(
f"Login failed for user {self._email}"
) from err
if resp.status in [429]:
# Too Many Requests, add stats to message
errors.raise_error(data, prefix=f"Too Many Requests: {self.request_count}")
errors.raise_error(
data, prefix=f"Too Many Requests: {self.request_count}"
)
else:
# raise Anker Solix error if code is known
errors.raise_error(data)
@ -1349,7 +1360,7 @@ class AnkerSolixApi:
return self.sites
async def update_site_details(
self, fromFile: bool = False, exclude: set = None
self, fromFile: bool = False, exclude: set | None = None
) -> dict:
"""Get the latest updates for additional site related details updated less frequently.
@ -1374,7 +1385,7 @@ class AnkerSolixApi:
return self.sites
async def update_device_details(
self, fromFile: bool = False, exclude: set = None
self, fromFile: bool = False, exclude: set | None = None
) -> dict:
"""Get the latest updates for additional device info updated less frequently.
@ -1455,7 +1466,7 @@ class AnkerSolixApi:
return self.devices
async def update_device_energy(self, exclude: set = None) -> dict:
async def update_device_energy(self, exclude: set | None = None) -> dict:
"""Get the energy statistics for given device types from today and yesterday.
Yesterday energy will be queried only once if not available yet, but not updated in subsequent refreshes.
@ -1850,7 +1861,11 @@ class AnkerSolixApi:
return data
async def set_site_price(
self, siteId: str, price: float = None, unit: str = None, co2: float = None
self,
siteId: str,
price: float | None = None,
unit: str | None = None,
co2: float | None = None,
) -> bool:
"""Set the power price, the unit and/or CO2 for a site.
@ -1984,7 +1999,7 @@ class AnkerSolixApi:
self,
siteId: str,
paramType: str = SolixParmType.SOLARBANK_SCHEDULE.value,
deviceSn: str = None,
deviceSn: str | None = None,
fromFile: bool = False,
) -> dict:
r"""Get device parameters (e.g. solarbank schedule). This can be queried for each siteId listed in the homepage info site_list. The paramType is always 4, but can be modified if necessary.
@ -2040,7 +2055,7 @@ class AnkerSolixApi:
paramData: dict,
paramType: str = SolixParmType.SOLARBANK_SCHEDULE.value,
command: int = 17,
deviceSn: str = None,
deviceSn: str | None = None,
) -> dict:
"""Set device parameters (e.g. solarbank schedule).
@ -2073,9 +2088,9 @@ class AnkerSolixApi:
siteId: str,
deviceSn: str,
all_day: bool = False,
preset: int = None,
export: bool = None,
charge_prio: int = None,
preset: int | None = None,
export: bool | None = None,
charge_prio: int | None = None,
set_slot: SolarbankTimeslot = None,
insert_slot: SolarbankTimeslot = None,
) -> bool:
@ -2611,10 +2626,10 @@ class AnkerSolixApi:
)
return resp.get("data", {})
async def get_upgrade_record(
async def check_upgrade_record(
self, recordType: int = 2, fromFile: bool = False
) -> dict:
"""Get upgrade record, shows device updates with their last version. Type 0-3 work.
"""Check upgrade record, shows device updates with their last version. Type 0-3 work.
Example data:
{"is_record": true,"device_list": [{
@ -2624,7 +2639,7 @@ class AnkerSolixApi:
data = {"type": recordType}
if fromFile:
resp = self._loadFromFile(
os.path.join(self._testdir, f"upgrade_record_{recordType}.json")
os.path.join(self._testdir, f"check_upgrade_record_{recordType}.json")
)
else:
resp = await self.request(
@ -2632,14 +2647,45 @@ class AnkerSolixApi:
)
return resp.get("data", {})
async def get_upgrade_record(
self, deviceSn: str | None = None, siteId: str | None = None, recordType: int | None = None, fromFile: bool = False
) -> dict:
"""Get upgrade record for a device serial or site ID, shows update history. Type 1 works for solarbank, type 2 for site ID.
Example data:
{"device_sn": "9JVB42LJK8J0P5RY", "site_id": "", "upgrade_record_list": [
{"upgrade_time": "2024-02-29 12:38:23","upgrade_version": "v1.5.6","pre_version": "v1.4.4","upgrade_type": "1","upgrade_desc": "",
"device_sn": "9JVB42LJK8J0P5RY","device_name": "9JVB42LJK8J0P5RY","child_upgrade_records": null},
{"upgrade_time": "2023-12-29 10:23:06","upgrade_version": "v1.4.4","pre_version": "v0.0.6.6","upgrade_type": "1","upgrade_desc": "",
"device_sn": "9JVB42LJK8J0P5RY","device_name": "9JVB42LJK8J0P5RY","child_upgrade_records": null},
{"upgrade_time": "2023-11-02 13:43:09","upgrade_version": "v1.4.1","pre_version": "v0.0.6.5","upgrade_type": "1","upgrade_desc": "",
"device_sn": "9JVB42LJK8J0P5RY","device_name": "9JVB42LJK8J0P5RY","child_upgrade_records": null}]},
"""
if deviceSn:
data = {"device_sn": deviceSn, "type": 1 if recordType is None else recordType}
elif siteId:
data = {"site_id": siteId, "type": 2 if recordType is None else recordType}
else:
recordType = 0 if recordType is None else recordType
data = {"type": recordType}
if fromFile:
resp = self._loadFromFile(
os.path.join(self._testdir, f"get_upgrade_record_{deviceSn if deviceSn else siteId if siteId else recordType}.json")
)
else:
resp = await self.request(
"post", _API_ENDPOINTS["get_upgrade_record"], json=data
)
return resp.get("data", {})
async def energy_analysis(
self,
siteId: str,
deviceSn: str,
rangeType: str = None,
startDay: datetime = None,
endDay: datetime = None,
devType: str = None,
rangeType: str | None = None,
startDay: datetime | None = None,
endDay: datetime | None = None,
devType: str | None = None,
) -> dict:
"""Fetch Energy data for given device and optional time frame.
@ -2770,7 +2816,7 @@ class AnkerSolixApi:
table.update({daystr: entry})
return table
async def home_load_chart(self, siteId: str, deviceSn: str = None) -> dict:
async def home_load_chart(self, siteId: str, deviceSn: str | None = None) -> dict:
"""Get home load chart data.
Example data:

View File

@ -75,6 +75,8 @@ ERRORS: dict[int, type[AnkerSolixError]] = {
401: AuthorizationError,
403: AuthorizationError,
429: RequestLimitError,
502: ConnectError,
504: ConnectError,
997: ConnectError,
998: NetworkError,
999: ServerError,

View File

@ -1,27 +1,27 @@
{
"DILM86K2GJV2NRAI": {
"device_sn": "DILM86K2GJV2NRAI",
"G55HAP9LVQO2LAPM": {
"device_sn": "G55HAP9LVQO2LAPM",
"type": "solarbank",
"site_id": "bdd7d770-bcb7-d22b-cafc-d0b2eddc4e23",
"site_id": "9e40dc42-adac-7fba-dead-a1bba7bff34e",
"is_admin": true,
"device_pn": "A17C0",
"battery_soc": "6",
"battery_soc": "17",
"battery_capacity": "1600",
"battery_energy": "96",
"charging_power": "64",
"battery_energy": "272",
"charging_power": "-190",
"power_unit": "W",
"charging_status": "3",
"charging_status_desc": "charge_bypass",
"charging_status": "2",
"charging_status_desc": "discharge",
"status": "1",
"status_desc": "online",
"wireless_type": "1",
"input_power": "154",
"output_power": "90",
"set_output_power": "0",
"input_power": "0",
"output_power": "190",
"set_output_power": "300",
"power_cutoff": 5,
"alias": "SB E1600",
"set_system_output_power": "0",
"bt_ble_mac": "3CEAAD9FDDF7",
"set_system_output_power": "300",
"bt_ble_mac": "FEDCEB33AA9A",
"name": "Solarbank E1600",
"wifi_online": true,
"charge": false,
@ -29,7 +29,7 @@
"sw_version": "v1.5.6",
"auto_upgrade": false,
"wifi_name": "wifi-network-1",
"wifi_signal": "54",
"wifi_signal": "50",
"power_cutoff_data": [
{
"id": 1,
@ -49,7 +49,7 @@
"solar_info": {
"solar_brand": "ANKER",
"solar_model": "A5143",
"solar_sn": "9XK0FGWY2TWW",
"solar_sn": "YTZL4ZEJ4R5B",
"solar_model_name": "MI80 Microinverter(BLE)"
},
"schedule": {
@ -57,7 +57,7 @@
{
"id": 0,
"start_time": "00:00",
"end_time": "06:30",
"end_time": "07:20",
"turn_on": true,
"appliance_loads": [
{
@ -71,21 +71,21 @@
"power_setting_mode": 1,
"device_power_loads": [
{
"device_sn": "DILM86K2GJV2NRAI",
"device_sn": "G55HAP9LVQO2LAPM",
"power": 150
}
]
},
{
"id": 0,
"start_time": "06:30",
"end_time": "18:30",
"start_time": "07:20",
"end_time": "19:40",
"turn_on": false,
"appliance_loads": [
{
"id": 0,
"name": "Benutzerdefiniert",
"power": 100,
"power": 140,
"number": 1
}
],
@ -93,14 +93,14 @@
"power_setting_mode": 1,
"device_power_loads": [
{
"device_sn": "DILM86K2GJV2NRAI",
"power": 50
"device_sn": "G55HAP9LVQO2LAPM",
"power": 70
}
]
},
{
"id": 0,
"start_time": "18:30",
"start_time": "19:40",
"end_time": "24:00",
"turn_on": true,
"appliance_loads": [
@ -115,7 +115,7 @@
"power_setting_mode": 1,
"device_power_loads": [
{
"device_sn": "DILM86K2GJV2NRAI",
"device_sn": "G55HAP9LVQO2LAPM",
"power": 150
}
]
@ -130,8 +130,8 @@
"display_advanced_mode": 0,
"advanced_mode_min_load": 0
},
"preset_system_output_power": 100,
"preset_allow_export": false,
"preset_system_output_power": 300,
"preset_allow_export": true,
"preset_charge_priority": 10,
"fittings": {}
}

View File

@ -1,8 +1,8 @@
{
"bdd7d770-bcb7-d22b-cafc-d0b2eddc4e23": {
"9e40dc42-adac-7fba-dead-a1bba7bff34e": {
"type": "system",
"site_info": {
"site_id": "bdd7d770-bcb7-d22b-cafc-d0b2eddc4e23",
"site_id": "9e40dc42-adac-7fba-dead-a1bba7bff34e",
"site_name": "BKW",
"site_img": "",
"device_type_list": [
@ -36,17 +36,17 @@
"statistics": [
{
"type": "1",
"total": "135.13",
"total": "183.54",
"unit": "kwh"
},
{
"type": "2",
"total": "134.72",
"total": "182.99",
"unit": "kg"
},
{
"type": "3",
"total": "44.59",
"total": "60.57",
"unit": "\u20ac"
}
],
@ -55,40 +55,40 @@
"solarbank_list": [
{
"device_pn": "A17C0",
"device_sn": "DILM86K2GJV2NRAI",
"device_sn": "G55HAP9LVQO2LAPM",
"device_img": "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",
"battery_power": "6",
"battery_power": "17",
"bind_site_status": "",
"charging_power": "64",
"charging_power": "-190",
"power_unit": "W",
"charging_status": "3",
"charging_status": "2",
"status": "1",
"wireless_type": "1",
"main_version": "",
"photovoltaic_power": "154",
"output_power": "90",
"photovoltaic_power": "0",
"output_power": "190",
"create_time": 1695392386,
"set_load_power": "0",
"set_load_power": "300",
"output_cutoff_data": 5,
"is_display": true,
"alias_name": "SB E1600",
"current_home_load": "0"
"current_home_load": "300"
}
],
"total_charging_power": "64",
"total_charging_power": "-190.0",
"power_unit": "W",
"charging_status": "0",
"total_battery_power": "0.06",
"updated_time": "2024-03-27 11:49:04",
"total_photovoltaic_power": "154",
"total_output_power": "90.00",
"total_battery_power": "0.17",
"updated_time": "2024-04-24 01:33:08",
"total_photovoltaic_power": "0",
"total_output_power": "190.00",
"display_set_power": false,
"is_display_data": true
},
"retain_load": "0W",
"retain_load": "300W",
"updated_time": "01-01-0001 00:00:00",
"power_site_type": 2,
"site_id": "bdd7d770-bcb7-d22b-cafc-d0b2eddc4e23",
"site_id": "9e40dc42-adac-7fba-dead-a1bba7bff34e",
"powerpanel_list": [],
"site_details": {
"has_unread_msg": false,

View File

@ -5,7 +5,7 @@
"main_switch": true,
"device_list": [
{
"device_sn": "DILM86K2GJV2NRAI",
"device_sn": "G55HAP9LVQO2LAPM",
"device_name": "Solarbank E1600",
"auto_upgrade": false,
"alias_name": "SB E1600",
@ -13,5 +13,5 @@
}
]
},
"trace_id": "145dab32b452d3db2a81ce9506ee4010"
"trace_id": "c6f8bd0ae3dd9eb8b3dbd8c84d2afba5"
}

View File

@ -4,10 +4,10 @@
"data": {
"data": [
{
"device_sn": "DILM86K2GJV2NRAI",
"device_sn": "G55HAP9LVQO2LAPM",
"product_code": "A17C0",
"bt_ble_id": "3C:EA:AD:9F:DD:F7",
"bt_ble_mac": "3CEAAD9FDDF7",
"bt_ble_id": "FE:DC:EB:33:AA:9A",
"bt_ble_mac": "FEDCEB33AA9A",
"device_name": "Solarbank E1600",
"alias_name": "SB E1600",
"img_url": "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",
@ -26,5 +26,5 @@
}
]
},
"trace_id": "acff073fd7e842994b11373fd1d3449e"
"trace_id": "8fed8fc1bba815a1efa844983cecef9e"
}

View File

@ -5,5 +5,5 @@
"device_list": null,
"guide_txt": ""
},
"trace_id": "c55301e2c5686c8d75bf494a91d804b6"
"trace_id": "ded0b8ad19ffe254edcf876c633e4735"
}

View File

@ -5,7 +5,7 @@
"ota_complete_status": 2,
"process_skip_type": 2,
"solar_info": {
"solar_sn": "9XK0FGWY2TWW",
"solar_sn": "YTZL4ZEJ4R5B",
"solar_brand": "ANKER",
"solar_model": "A5143",
"brand_id": "3a9930f5-74ef-4e41-a797-04e6b33d3f0f",
@ -15,5 +15,5 @@
"solar_model_name": "MI80 Microinverter(BLE)"
}
},
"trace_id": "7dbfd7538ad24749c1abfcf0fd93de5f"
"trace_id": "077b4ed2b6fccac0a9dfcf8e4fdd85a6"
}

View File

@ -4,5 +4,5 @@
"data": {
"data": []
},
"trace_id": "823ef9f8a8c4fbdbe7e4abd4232f79c4"
"trace_id": "a433dc2acdb4187e340cff4d6bb60f3c"
}

View File

@ -2,11 +2,11 @@
"code": 0,
"msg": "success!",
"data": {
"site_id": "bdd7d770-bcb7-d22b-cafc-d0b2eddc4e23",
"home_load_data": "{\"ranges\":[{\"id\":0,\"start_time\":\"00:00\",\"end_time\":\"06:30\",\"turn_on\":true,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":300,\"number\":1}],\"charge_priority\":10,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"DILM86K2GJV2NRAI\",\"power\":150}]},{\"id\":0,\"start_time\":\"06:30\",\"end_time\":\"18:30\",\"turn_on\":false,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":100,\"number\":1}],\"charge_priority\":10,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"DILM86K2GJV2NRAI\",\"power\":50}]},{\"id\":0,\"start_time\":\"18:30\",\"end_time\":\"24:00\",\"turn_on\":true,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":300,\"number\":1}],\"charge_priority\":10,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"DILM86K2GJV2NRAI\",\"power\":150}]}],\"min_load\":100,\"max_load\":800,\"step\":0,\"is_charge_priority\":1,\"default_charge_priority\":80,\"is_zero_output_tips\":0,\"display_advanced_mode\":0,\"advanced_mode_min_load\":0}",
"current_home_load": "0W",
"site_id": "9e40dc42-adac-7fba-dead-a1bba7bff34e",
"home_load_data": "{\"ranges\":[{\"id\":0,\"start_time\":\"00:00\",\"end_time\":\"07:20\",\"turn_on\":true,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":300,\"number\":1}],\"charge_priority\":10,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"G55HAP9LVQO2LAPM\",\"power\":150}]},{\"id\":0,\"start_time\":\"07:20\",\"end_time\":\"19:40\",\"turn_on\":false,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":140,\"number\":1}],\"charge_priority\":10,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"G55HAP9LVQO2LAPM\",\"power\":70}]},{\"id\":0,\"start_time\":\"19:40\",\"end_time\":\"24:00\",\"turn_on\":true,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":300,\"number\":1}],\"charge_priority\":10,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"G55HAP9LVQO2LAPM\",\"power\":150}]}],\"min_load\":100,\"max_load\":800,\"step\":0,\"is_charge_priority\":1,\"default_charge_priority\":80,\"is_zero_output_tips\":0,\"display_advanced_mode\":0,\"advanced_mode_min_load\":0}",
"current_home_load": "300W",
"parallel_home_load": "",
"parallel_display": false
},
"trace_id": "adbafcecd3cac7f6c9feda1bcbab7aea"
"trace_id": "ece0447b2ec3c1520e8ca17c1a4ce63f"
}

View File

@ -2,7 +2,7 @@
"code": 0,
"msg": "success!",
"data": {
"param_data": "{\"ranges\":[{\"id\":0,\"start_time\":\"00:00\",\"end_time\":\"06:30\",\"turn_on\":true,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":300,\"number\":1}],\"charge_priority\":10,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"DILM86K2GJV2NRAI\",\"power\":150}]},{\"id\":0,\"start_time\":\"06:30\",\"end_time\":\"18:30\",\"turn_on\":false,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":100,\"number\":1}],\"charge_priority\":10,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"DILM86K2GJV2NRAI\",\"power\":50}]},{\"id\":0,\"start_time\":\"18:30\",\"end_time\":\"24:00\",\"turn_on\":true,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":300,\"number\":1}],\"charge_priority\":10,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"DILM86K2GJV2NRAI\",\"power\":150}]}],\"min_load\":100,\"max_load\":800,\"step\":0,\"is_charge_priority\":1,\"default_charge_priority\":80,\"is_zero_output_tips\":0,\"display_advanced_mode\":0,\"advanced_mode_min_load\":0}"
"param_data": "{\"ranges\":[{\"id\":0,\"start_time\":\"00:00\",\"end_time\":\"07:20\",\"turn_on\":true,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":300,\"number\":1}],\"charge_priority\":10,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"G55HAP9LVQO2LAPM\",\"power\":150}]},{\"id\":0,\"start_time\":\"07:20\",\"end_time\":\"19:40\",\"turn_on\":false,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":140,\"number\":1}],\"charge_priority\":10,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"G55HAP9LVQO2LAPM\",\"power\":70}]},{\"id\":0,\"start_time\":\"19:40\",\"end_time\":\"24:00\",\"turn_on\":true,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":300,\"number\":1}],\"charge_priority\":10,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"G55HAP9LVQO2LAPM\",\"power\":150}]}],\"min_load\":100,\"max_load\":800,\"step\":0,\"is_charge_priority\":1,\"default_charge_priority\":80,\"is_zero_output_tips\":0,\"display_advanced_mode\":0,\"advanced_mode_min_load\":0}"
},
"trace_id": "2b3a35dbdeede8bbff7fba8dfeb0a3ff"
"trace_id": "87feb1cf10c4cb773ea4deb7f8eacb84"
}

View File

@ -0,0 +1,41 @@
{
"code": 0,
"msg": "success!",
"data": {
"device_sn": "",
"site_id": "9e40dc42-adac-7fba-dead-a1bba7bff34e",
"upgrade_record_list": [
{
"upgrade_time": "2024-02-29 10:48:23",
"upgrade_version": "v1.5.6",
"pre_version": "v1.4.4",
"upgrade_type": "1",
"upgrade_desc": "",
"device_sn": "G55HAP9LVQO2LAPM",
"device_name": "G55HAP9LVQO2LAPM",
"child_upgrade_records": null
},
{
"upgrade_time": "2023-12-28 15:57:06",
"upgrade_version": "v1.4.4",
"pre_version": "v0.0.6.6",
"upgrade_type": "1",
"upgrade_desc": "",
"device_sn": "G55HAP9LVQO2LAPM",
"device_name": "G55HAP9LVQO2LAPM",
"child_upgrade_records": null
},
{
"upgrade_time": "2023-11-01 09:53:09",
"upgrade_version": "v1.4.1",
"pre_version": "v0.0.6.5",
"upgrade_type": "1",
"upgrade_desc": "",
"device_sn": "G55HAP9LVQO2LAPM",
"device_name": "G55HAP9LVQO2LAPM",
"child_upgrade_records": null
}
]
},
"trace_id": "9b3f6a74d73cbede9dcd7e3fa9f3c6de"
}

View File

@ -4,7 +4,7 @@
"data": {
"site_list": [
{
"site_id": "bdd7d770-bcb7-d22b-cafc-d0b2eddc4e23",
"site_id": "9e40dc42-adac-7fba-dead-a1bba7bff34e",
"site_name": "BKW",
"site_img": "",
"device_type_list": [
@ -21,10 +21,10 @@
"solarbank_list": [
{
"device_pn": "",
"device_sn": "DILM86K2GJV2NRAI",
"device_sn": "G55HAP9LVQO2LAPM",
"device_name": "SB E1600",
"device_img": "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",
"battery_power": "6",
"battery_power": "17",
"bind_site_status": "1",
"charging_power": "",
"power_unit": "",
@ -42,5 +42,5 @@
],
"powerpanel_list": []
},
"trace_id": "ae8abfd363866b4cee17359fd1cd9dca"
"trace_id": "d0ee3c49869cec32cf6cf251521f29dd"
}

View File

@ -4,5 +4,5 @@
"data": {
"has_unread_msg": false
},
"trace_id": "aec8da3373cd0e59d9db5eb52e7adbdc"
"trace_id": "a058ab399db309214abaabf12ddccaad"
}

View File

@ -0,0 +1,11 @@
{
"code": 0,
"msg": "success!",
"data": {
"is_ota_update": false,
"need_retry": false,
"retry_interval": 0,
"device_list": null
},
"trace_id": "2dcfae1f2f344d0d97ac24d74ef8fba9"
}

View File

@ -19,5 +19,5 @@
}
]
},
"trace_id": "7a84aead0e446bbc85fc76ebbcb2bc01"
"trace_id": "8bb7ef048b69dcff25ae45bc3ec3fad2"
}

View File

@ -2,10 +2,10 @@
"code": 0,
"msg": "success!",
"data": {
"site_id": "bdd7d770-bcb7-d22b-cafc-d0b2eddc4e23",
"site_id": "9e40dc42-adac-7fba-dead-a1bba7bff34e",
"price": 0.33,
"site_co2": 0,
"site_price_unit": "\u20ac"
},
"trace_id": "7cfacbfca09e909cf9a8939ebddba1ac"
"trace_id": "5aae22e5adc38af4b9fdc37aa1f89cdb"
}

View File

@ -20,17 +20,17 @@
"statistics": [
{
"type": "1",
"total": "135.13",
"total": "183.54",
"unit": "kwh"
},
{
"type": "2",
"total": "134.72",
"total": "182.99",
"unit": "kg"
},
{
"type": "3",
"total": "44.59",
"total": "60.57",
"unit": "\u20ac"
}
],
@ -39,40 +39,40 @@
"solarbank_list": [
{
"device_pn": "A17C0",
"device_sn": "DILM86K2GJV2NRAI",
"device_sn": "G55HAP9LVQO2LAPM",
"device_name": "SB E1600",
"device_img": "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",
"battery_power": "6",
"battery_power": "17",
"bind_site_status": "",
"charging_power": "90",
"charging_power": "190",
"power_unit": "W",
"charging_status": "3",
"charging_status": "2",
"status": "1",
"wireless_type": "1",
"main_version": "",
"photovoltaic_power": "154",
"output_power": "90",
"photovoltaic_power": "0",
"output_power": "190",
"create_time": 1695392386,
"set_load_power": "",
"output_cutoff_data": 5,
"is_display": true
}
],
"total_charging_power": "64",
"total_charging_power": "0",
"power_unit": "W",
"charging_status": "0",
"total_battery_power": "0.06",
"updated_time": "2024-03-27 11:49:04",
"total_photovoltaic_power": "154",
"total_output_power": "90.00",
"total_battery_power": "0.17",
"updated_time": "2024-04-24 01:33:08",
"total_photovoltaic_power": "0",
"total_output_power": "190.00",
"display_set_power": false,
"is_display_data": true
},
"retain_load": "0W",
"retain_load": "300W",
"updated_time": "01-01-0001 00:00:00",
"power_site_type": 2,
"site_id": "bdd7d770-bcb7-d22b-cafc-d0b2eddc4e23",
"site_id": "9e40dc42-adac-7fba-dead-a1bba7bff34e",
"powerpanel_list": []
},
"trace_id": "e6a2f3f712a7c2498a25ae7c0bafef3e"
"trace_id": "64eeaeab8d80ee6bab68bf981b9cda1d"
}

View File

@ -3,7 +3,7 @@
"msg": "success!",
"data": {
"site_info": {
"site_id": "bdd7d770-bcb7-d22b-cafc-d0b2eddc4e23",
"site_id": "9e40dc42-adac-7fba-dead-a1bba7bff34e",
"site_name": "BKW",
"site_img": "",
"device_type_list": [
@ -23,7 +23,7 @@
"solarbank_list": [
{
"device_pn": "A17C0",
"device_sn": "DILM86K2GJV2NRAI",
"device_sn": "G55HAP9LVQO2LAPM",
"device_name": "SB E1600",
"device_img": "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",
"battery_power": "",
@ -44,5 +44,5 @@
],
"powerpanel_list": []
},
"trace_id": "84caedc36bfe9b3f0ed1c89ceb5c0ee2"
"trace_id": "daee0eafcbcefbcafc9a0adb3519ea6f"
}

View File

@ -4,7 +4,7 @@
"data": {
"site_list": [
{
"site_id": "bdd7d770-bcb7-d22b-cafc-d0b2eddc4e23",
"site_id": "9e40dc42-adac-7fba-dead-a1bba7bff34e",
"site_name": "BKW",
"site_img": "",
"device_type_list": [
@ -21,5 +21,5 @@
}
]
},
"trace_id": "b07e6dfde00e7ddfb8fa4b3a2b76ac4c"
"trace_id": "fe909473eeb4bd6ba428b4fda59aab74"
}

View File

@ -142,5 +142,5 @@
}
]
},
"trace_id": "d3dceb28da9a8f8ca2fb2ae170dc186a"
"trace_id": "f8acb37f7d12aeabe646cbd5055ebabf"
}

View File

@ -5,8 +5,8 @@
"brand_id": "3a9930f5-74ef-4e41-a797-04e6b33d3f0f",
"solar_brand": "ANKER",
"solar_model": "A5143",
"solar_sn": "9XK0FGWY2TWW",
"solar_sn": "YTZL4ZEJ4R5B",
"solar_model_name": "MI80 Microinverter(BLE)"
},
"trace_id": "4fddaabaeaabd39fea6d7c474fb97ad2"
"trace_id": "6b2dfaebd1abe931bcec042c1686fc4f"
}

View File

@ -7,7 +7,7 @@
"solarbank_list": [
{
"device_pn": "A17C0",
"device_sn": "DILM86K2GJV2NRAI",
"device_sn": "G55HAP9LVQO2LAPM",
"device_name": "SB E1600",
"device_img": "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",
"battery_power": "",
@ -28,5 +28,5 @@
],
"powerpanel_list": []
},
"trace_id": "3607fa3ff416d93ded855c2ee9b10bea"
"trace_id": "4acd422dafadcb3f2ccabbe1fb92ceda"
}

View File

@ -5,9 +5,9 @@
"wifi_info_list": [
{
"wifi_name": "wifi-network-1",
"wifi_signal": "54"
"wifi_signal": "50"
}
]
},
"trace_id": "38ec9a1a8d29dac96ac9adc12fbb41b8"
"trace_id": "af7beabd3e81cfb62be0f4abf96298fb"
}

View File

@ -50,7 +50,8 @@ def randomize(val, key: str = "") -> str:
if not RANDOMIZE:
return str(val)
randomstr = RANDOMDATA.get(val, "")
if not randomstr and val:
# generate new random string
if not randomstr and val and key not in ["device_name"]:
if "_sn" in key or key in ["sn"]:
randomstr = "".join(
random.choices(string.ascii_uppercase + string.digits, k=len(val))
@ -68,7 +69,7 @@ def randomize(val, key: str = "") -> str:
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])
a + b for a, b in zip(randomstr[::2], randomstr[1::2], strict=False)
)
elif "_id" in key:
for part in val.split("-"):
@ -102,7 +103,7 @@ def randomize(val, key: str = "") -> str:
# default randomize format
randomstr = "".join(random.choices(string.ascii_letters, k=len(val)))
RANDOMDATA.update({val: randomstr})
return randomstr
return randomstr or str(val)
def check_keys(data):
@ -125,6 +126,7 @@ def check_keys(data):
"wifi_name",
"home_load_data",
"param_data",
"device_name"
]
) or k in ["sn"]:
data[k] = randomize(v, k)
@ -133,7 +135,7 @@ def check_keys(data):
def export(
filename: str,
d: dict = None,
d: dict | None = None,
skip_randomize: bool = False,
randomkeys: bool = False,
) -> None:
@ -329,6 +331,22 @@ async def main() -> bool: # noqa: C901 # pylint: disable=too-many-branches,too-
except (ClientError, errors.AnkerSolixError):
if not admin:
CONSOLE.warning("Query requires account of site owner!")
CONSOLE.info("Exporting site upgrade record...")
try:
export(
os.path.join(
folder,
f"get_upgrade_record_{randomize(siteId,'site_id')}.json",
),
await myapi.request(
"post",
api._API_ENDPOINTS["get_upgrade_record"],
json={"site_id": siteId, "type": 2},
),
) # works only for site owners
except (ClientError, errors.AnkerSolixError):
if not admin:
CONSOLE.warning("Query requires account of site owner!")
for sn, device in myapi.devices.items():
CONSOLE.info(
"\nExporting device specific data for device %s SN %s...",
@ -414,6 +432,19 @@ async def main() -> bool: # noqa: C901 # pylint: disable=too-many-branches,too-
except (ClientError, errors.AnkerSolixError):
if not admin:
CONSOLE.warning("Query requires account of site owner!")
CONSOLE.info("Exporting OTA update info...")
try:
export(
os.path.join(folder, f"ota_update_{randomize(sn,'_sn')}.json"),
await myapi.request(
"post",
api._API_ENDPOINTS["get_ota_update"],
json={"device_sn": sn, "insert_sn": ""},
),
) # works only for site owners
except (ClientError, errors.AnkerSolixError):
if not admin:
CONSOLE.warning("Query requires account of site owner!")
CONSOLE.info("\nExporting site rules...")
export(

View File

@ -1,6 +1,6 @@
[project]
name = "Anker-Solix-Api"
version = "1.8.0"
version = "1.8.1"
description = "Python library for Anker Solix Power devices (Solarbank, Inverter etc)"
readme = "README.md"
requires-python = ">=3.11"