This commit is contained in:
Thomas Luther 2024-04-24 12:22:21 +02:00
parent 2afab331db
commit b41590e888
25 changed files with 301 additions and 170 deletions

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(