This commit is contained in:
Thomas Luther 2024-03-07 16:47:52 +01:00
parent 7f9a99b09b
commit 4cf9641ec2
24 changed files with 400 additions and 183 deletions

View File

@ -131,19 +131,19 @@ _API_ENDPOINTS = {
"set_device_parm": "power_service/v1/site/set_site_device_param", # Apply provided settings to a device for the provided site id and param type (e.g. Schedules), NOT IMPLEMENTED YET
"wifi_list": "power_service/v1/site/get_wifi_info_list", # List of available networks for provided site id
"get_site_price": "power_service/v1/site/get_site_price", # List defined power price and CO2 for given site, works only for site owner account
"update_site_price": "power_service/v1/site/update_site_price", # Update power price for given site, REQUIRED PARAMETERS UNKNOWN
"get_auto_upgrade": "power_service/v1/app/get_auto_upgrade", # List of Auto-Upgrade configuration and enabled devices, onyl works for site owner accout
"set_auto_upgrade": "power_service/v1/app/set_auto_upgrade", # Set/Enable Auto-Upgrade configuration, not implemented yet, REQUIRED PARAMETERS UNKNOWN
"update_site_price": "power_service/v1/site/update_site_price", # Update power price and CO2 for given site, works only for site owner account
"get_auto_upgrade": "power_service/v1/app/get_auto_upgrade", # List of Auto-Upgrade configuration and enabled devices, only works for site owner accout
"set_auto_upgrade": "power_service/v1/app/set_auto_upgrade", # Set/Enable Auto-Upgrade configuration, works only for site owner account
"bind_devices": "power_service/v1/app/get_relate_and_bind_devices", # List with details of locally connected/bound devices, includes firmware version, works only for owner account
"get_device_load": "power_service/v1/app/device/get_device_home_load", # Get defined device schedule (same data as provided with device param query)
"set_device_load": "power_service/v1/app/device/set_device_home_load", # Set defined device schedule (Not implemented yet, REQUIRED PARAMETERS UNKNOWN)
"set_device_load": "power_service/v1/app/device/set_device_home_load", # Set defined device schedule, Accepts the new schedule, but does NOT change it? Maybe future use for schedules per device
"get_ota_info": "power_service/v1/app/compatible/get_ota_info", # Get OTA status for solarbank and/or inverter serials
"get_ota_update": "power_service/v1/app/compatible/get_ota_update", # Not clear what this does, shows some OTA settings
"solar_info": "power_service/v1/app/compatible/get_compatible_solar_info", # Solar inverter definition for solarbanks, works only with owner account
"get_cutoff": "power_service/v1/app/compatible/get_power_cutoff", # Get Power Cutoff settings (Min SOC) for provided site id and device sn, works only with owner account
"set_cutoff": "power_service/v1/app/compatible/set_power_cutoff", # Set Min SOC for device, only works for onwer accounts
"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. Solarbank/inverter responses do not contain info
"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)
"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
@ -183,8 +183,8 @@ _API_ENDPOINTS = {
'power_service/v1/get_message_not_disturb',
'power_service/v1/read_message',
'power_service/v1/del_message',
'power_service/v1/product_categories',
'power_service/v1/product_accessories',
'power_service/v1/product_categories', # GET method to list all supported products with details and web picture links
'power_service/v1/product_accessories', # GET method to list all supported products accessories with details and web picture links
Structure of the JSON response for an API Login Request:
@ -249,18 +249,19 @@ class SolixDeviceStatus(Enum):
"""Enumuration for Anker Solix Device status."""
# TODO(3): Add descriptions once status code usage is observed/known
off = "0"
on = "1"
offline = "0"
online = "1"
unknown = "unknown"
class SolarbankStatus(Enum):
"""Enumuration for Anker Solix Solarbank status."""
charging = "1"
discharging = "2"
bypass = "3"
bypass_charging = "35" # pseudo state, the solarbank does not distinguish this
detection = "0"
bypass = "1"
discharge = "2"
charge = "3"
charge_bypass = "35" # pseudo state, the solarbank does not distinguish this
charge_priority = "37" # pseudo state, the solarbank does not distinguish this but reports 3 as seen so far
wakeup = "4" # Not clear what happens during this state, but observed short intervals during night as well
# TODO(3): Add descriptions once status code usage is observed/known
@ -442,6 +443,24 @@ class AnkerSolixApi:
self._logger.error(err)
return False
def _update_site( # noqa: C901
self,
siteId: str,
details: dict,
) -> None:
"""Update the internal sites dictionary with data provided for the nested site details dictionary.
This method is used to consolidate site details from various less frequent requests that are not covered with the update_sites method.
"""
# lookup old site details if any
if siteId in self.sites:
site_details = (self.sites[siteId]).get("site_details") or {}
site_details.update(details)
else:
site_details = details
self.sites[siteId] = {}
self.sites[siteId]["site_details"] = site_details
def _update_dev( # noqa: C901
self,
devData: dict,
@ -466,8 +485,8 @@ class AnkerSolixApi:
elif isAdmin is False and device.get("is_admin") is None:
device.update({"is_admin": False})
calc_capacity = False # Flag whether capacity may need recalculation
try:
for key, value in devData.items():
for key, value in devData.items():
try:
if key in ["product_code", "device_pn"] and value:
device.update({"device_pn": str(value)})
elif key in ["device_name"] and value:
@ -510,7 +529,9 @@ class AnkerSolixApi:
)
# Value for device home load may be empty for single solarbank, use this setting also for device preset in this case
if not device.get("set_output_power"):
device.update({"set_output_power": str(value).replace("W", "")})
device.update(
{"set_output_power": str(value).replace("W", "")}
)
elif key in ["power_unit"]:
device.update({"power_unit": str(value)})
elif key in ["status"]:
@ -530,21 +551,30 @@ class AnkerSolixApi:
if str(value) == status.value:
description = status.name
break
# check if battery charging during bypass and if output during bypass
# check if battery has bypass during charge (if output during charge)
# NOTE: charging power may be updated after initial device details update
# NOTE: If status is 3=Bypass but nothing goes out, the charge priority is active (e.g. 0 Watt switch)
# NOTE: If status is 3=charging and larger than preset but nothing goes out, the charge priority is active (e.g. 0 Watt switch)
# This key can be passed separately, make sure the other values are looked up in passed data first, then in device details
preset = devData.get("set_load_power") or device(
"set_output_power"
)
out = devData.get("output_power") or device.get("output_power")
solar = devData.get("photovoltaic_power") or device.get(
"input_power"
)
if (
description == SolarbankStatus.bypass.name
and (charge := devData.get("charging_power"))
and (out := devData.get("output_power"))
description == SolarbankStatus.charge.name
and preset is not None
and out is not None
and solar is not None
):
with contextlib.suppress(ValueError):
if int(out) == 0:
if int(out) == 0 and int(solar) > int(preset):
# Bypass but 0 W output must be active charge priority
description = SolarbankStatus.charge_priority.name
elif int(charge) > 0:
# Bypass with output and charge must be bypass charging
description = SolarbankStatus.bypass_charging.name
elif int(out) > 0:
# Charge with output must be bypass charging
description = SolarbankStatus.charge_bypass.name
device.update({"charging_status_desc": description})
elif key in ["bws_surplus"]:
device.update({"bws_surplus": str(value)})
@ -552,7 +582,10 @@ class AnkerSolixApi:
device.update({"charge": bool(value)})
elif key in ["auto_upgrade"]:
device.update({"auto_upgrade": bool(value)})
elif key in ["power_cutoff"]:
elif (
key in ["power_cutoff", "output_cutoff_data"]
and str(value).isdigit()
):
device.update({"power_cutoff": int(value)})
elif key in ["power_cutoff_data"] and value:
device.update({"power_cutoff_data": list(value)})
@ -585,9 +618,13 @@ class AnkerSolixApi:
if key in ["battery_power"] or calc_capacity:
# generate battery values when soc updated or device name changed or PN is known
if not (cap := device.get("battery_capacity")):
if hasattr(SolixDeviceCapacity, device.get("device_pn", "")):
if hasattr(
SolixDeviceCapacity, device.get("device_pn", "")
):
# get battery capacity from known PNs
cap = SolixDeviceCapacity[device.get("device_pn", "")].value
cap = SolixDeviceCapacity[
device.get("device_pn", "")
].value
elif device.get("type") == SolixDeviceType.SOLARBANK.value:
# Derive battery capacity in Wh from latest solarbank name or alias if available
cap = (
@ -603,11 +640,19 @@ class AnkerSolixApi:
device.update(
{
"battery_capacity": str(cap),
"battery_energy": str(int(int(cap) * int(soc) / 100)),
"battery_energy": str(
int(int(cap) * int(soc) / 100)
),
}
)
except Exception as err: #pylint: disable=broad-exception-caught
self._logger.error("%s occured when updating device details for key %s with value %s: %s", type(err), key, value, err)
except Exception as err: # pylint: disable=broad-exception-caught
self._logger.error(
"%s occured when updating device details for key %s with value %s: %s",
type(err),
key,
value,
err,
)
self.devices.update({str(sn): device})
return sn
@ -876,7 +921,7 @@ class AnkerSolixApi:
'powerpanel_list': []}}
"""
self._logger.debug("Updating Sites data")
self.sites = {}
new_sites = {}
self._logger.debug("Getting site list")
sites = await self.get_site_list(fromFile=fromFile)
act_devices = []
@ -898,7 +943,7 @@ class AnkerSolixApi:
self._logger.debug("Getting scene info for site")
scene = await self.get_scene_info(myid, fromFile=fromFile)
mysite.update(scene)
self.sites.update({myid: mysite})
new_sites.update({myid: mysite})
# Update device details from scene info
sb_total_charge = (mysite.get("solarbank_info", {})).get(
"total_charging_power", ""
@ -943,10 +988,12 @@ class AnkerSolixApi:
power_out = int(solarbank.get("output_power", ""))
# power_charge = int(solarbank.get("charging_power", "")) # This value seems to reflect the output power, which is corect for status 2, but may be wrong for other states
charge_calc = power_in - power_out
solarbank["charging_power"] = str(charge_calc) # allow negative values
solarbank["charging_power"] = str(
charge_calc
) # allow negative values
sb_total_charge_calc += charge_calc
mysite["solarbank_info"]["solarbank_list"][index] = solarbank
self.sites.update({myid: mysite})
new_sites.update({myid: mysite})
sn = self._update_dev(
solarbank,
devType=SolixDeviceType.SOLARBANK.value,
@ -962,8 +1009,12 @@ class AnkerSolixApi:
if sb_total_charge_calc < 0:
with contextlib.suppress(ValueError):
# discharging, adjust sb total charge value in scene info and allow negativ value to indicate discharge
sb_total_charge = float(sb_total_solar) - float(sb_total_output)
mysite["solarbank_info"]["total_charging_power"] = str(sb_total_charge)
sb_total_charge = float(sb_total_solar) - float(
sb_total_output
)
mysite["solarbank_info"]["total_charging_power"] = str(
sb_total_charge
)
for sn, charge in sb_charges.items():
self.devices[sn]["charging_power"] = str(
0
@ -972,7 +1023,7 @@ class AnkerSolixApi:
)
# Update also the charge status description which may change after charging power correction
charge_status = self.devices[sn].get("charging_status")
if charge_status == SolarbankStatus.bypass:
if charge_status == SolarbankStatus.charge:
self._update_dev(
{
"device_sn": sn,
@ -980,7 +1031,7 @@ class AnkerSolixApi:
}
)
# make sure to write back any changes to the solarbank info in sites dict
self.sites.update({myid: mysite})
new_sites.update({myid: mysite})
for pps in (mysite.get("pps_info", {})).get("pps_list", []):
# work around for device_name which is actually the device_alias in scene info
@ -1024,17 +1075,34 @@ class AnkerSolixApi:
)
if sn:
act_devices.append(sn)
# Write back the updated sites
self.sites = new_sites
# recycle device list and remove devices no longer used in sites
rem_devices = [dev for dev in self.devices if dev not in act_devices]
for dev in rem_devices:
self.devices.pop(dev)
return self.sites
async def update_site_details(self, fromFile: bool = False) -> dict:
"""Get the latest updates for additional site related details updated less frequently.
Most of theses requests return data only when user has admin rights for sites owning the devices.
To limit API requests, this update site details method should be called less frequently than update site method,
and it updates just the nested site_details dictionary in the sites dictionary.
"""
self._logger.debug("Updating Sites Details")
for site_id, data in self.sites.items():
# Fetch site price and CO2 settings
self._logger.debug("Getting price and CO2 settings for site")
await self.get_site_price(siteId=site_id,fromFile=fromFile)
return self.sites
async def update_device_details(self, fromFile: bool = False) -> dict:
"""Get the latest updates for additional device info updated less frequently.
Most of theses requests return data only when user has admin rights for sites owning the devices.
To limit API requests, this update device details method should be called less frequently than update site method, which will also update most device details as found in the site data response.
To limit API requests, this update device details method should be called less frequently than update site method,
which will also update most device details as found in the site data response.
"""
self._logger.debug("Updating Device Details")
# Fetch firmware version of device
@ -1391,6 +1459,73 @@ class AnkerSolixApi:
await self.get_power_cutoff(deviceSn=deviceSn)
return True
async def get_site_price(self, siteId: str, fromFile: bool = False) -> dict:
"""Get the power price set for the site.
Example data:
{"site_id": "efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c","price": 0.4,"site_co2": 0,"site_price_unit": "\u20ac"}
"""
data = {"site_id": siteId}
if fromFile:
resp = self._loadFromFile(
os.path.join(self._testdir, f"price_{siteId}.json")
)
else:
resp = await self.request(
"post", _API_ENDPOINTS["get_site_price"], json=data
)
data = resp.get("data", {})
# update site details in sites dict
details = data.copy()
details.pop("site_id")
self._update_site(siteId, details)
return data
async def set_site_price(
self, siteId: str, price: float = None, unit: str = None, co2: float = None
) -> bool:
"""Set the power price, the unit and/or CO2 for a site.
Example input:
{"site_id": 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c', "price": 0.325, "site_price_unit": "\u20ac", "site_co2": 0}
The id must be one of the ids listed with the get_power_cutoff endpoint
"""
# First get the old settings if only single setting should be updated
details = {}
if siteId in self.sites:
details = (self.sites.get(siteId) or {}).get("site_details") or {}
new_price = details.get("price") if price is None else price
new_unit = details.get("site_price_unit") if unit is None else unit
new_co2 = details.get("site_co2") if co2 is None else co2
data = {}
# Need to query old setting to avoid changing them if parameter not provided
if new_price is None or new_unit is None or new_co2 is None:
data = await self.get_site_price(siteId=siteId)
if new_price is not None:
data["price"] = new_price
if new_unit is not None:
data["site_price_unit"] = new_unit
if new_co2 is not None:
data["site_co2"] = new_co2
else:
data.update(
{
"site_id": siteId,
"price": new_price,
"site_price_unit": new_unit,
"site_co2": new_co2,
}
)
# Make the Api call and check for return code
code = (
await self.request("post", _API_ENDPOINTS["update_site_price"], json=data)
).get("code")
if not isinstance(code, int) or int(code) != 0:
return False
# update the data in api dict
await self.get_site_price(siteId=siteId)
return True
async def get_device_load(
self, siteId: str, deviceSn: str, fromFile: bool = False
) -> dict:
@ -1414,7 +1549,8 @@ class AnkerSolixApi:
resp = await self.request(
"post", _API_ENDPOINTS["get_device_load"], json=data
)
# API Bug? home_load_data provided as string instead of object...Convert into object for proper handling
# The home_load_data is provided as string instead of object...Convert into object for proper handling
# It must be converted back to a string when passing this as input to set home load
string_data = (resp.get("data") or {}).get("home_load_data") or {}
if isinstance(string_data, str):
resp["data"].update({"home_load_data": json.loads(string_data)})
@ -1430,10 +1566,47 @@ class AnkerSolixApi:
)
return data
async def set_device_load(
self,
siteId: str,
deviceSn: str,
loadData: dict,
) -> dict:
"""Set device home load (e.g. solarbank schedule).
Example input for system with single solarbank:
{'site_id': 'efaca6b5-f4a0-e82e-3b2e-6b9cf90ded8c', 'device_sn': '9JVB42LJK8J0P5RY',
'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":0,"power_setting_mode":1,"device_power_loads":[{"device_sn":"9JVB42LJK8J0P5RY","power":150}]},'
'{"id":0,"start_time":"07:30","end_time":"18:00","turn_on":false,"appliance_loads":[{"id":0,"name":"Benutzerdefiniert","power":100,"number":1}],'
'"charge_priority":80,"power_setting_mode":1,"device_power_loads":[{"device_sn":"9JVB42LJK8J0P5RY","power":50}]},'
'{"id":0,"start_time":"18:00","end_time":"24:00","turn_on":true,"appliance_loads":[{"id":0,"name":"Benutzerdefiniert","power":300,"number":1}],'
'"charge_priority":0,"power_setting_mode":1,"device_power_loads":[{"device_sn":"9JVB42LJK8J0P5RY","power":150}]}],'
'"min_load":100,"max_load":800,"step":0,"is_charge_priority":0,"default_charge_priority":0,"is_zero_output_tips":1,"display_advanced_mode":0,"advanced_mode_min_load":0}'
}
Attention: This method and endpoint actually accepts the inputs, but does not change anything. The set_device_parm endpoint may have to be used
"""
data = {
"site_id": siteId,
"device_sn": deviceSn,
"home_load_data": json.dumps(loadData),
}
# Make the Api call and check for return code
code = (
await self.request("post", _API_ENDPOINTS["set_device_load"], json=data)
).get("code")
if not isinstance(code, int) or int(code) != 0:
return False
# update the data in api dict
await self.get_device_load(siteId=siteId, deviceSn=deviceSn)
return True
async def get_device_parm(
self,
siteId: str,
paramType: str = SolixParmType.SOLARBANK_SCHEDULE.value,
deviceSn: str = 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.
@ -1454,10 +1627,24 @@ class AnkerSolixApi:
resp = await self.request(
"post", _API_ENDPOINTS["get_device_parm"], json=data
)
# API Bug? param_data provided as string instead of object...Convert into object for proper handling
# The home_load_data is provided as string instead of object...Convert into object for proper handling
# It must be converted back to a string when passing this as input to set home load
string_data = (resp.get("data", {})).get("param_data", {})
if isinstance(string_data, str):
resp["data"].update({"param_data": json.loads(string_data)})
# update api device dict with latest data if optional device SN was provided, e.g. when called by set_device_parm for device details update
if deviceSn:
data = resp.get("data") or {}
if schedule := data.get("param_data") or {}:
self._update_dev(
{
"device_sn": deviceSn,
"schedule": schedule,
"current_home_load": data.get("current_home_load") or "",
"parallel_home_load": data.get("parallel_home_load") or "",
}
)
return resp.get("data", {})
async def set_device_parm(
@ -1466,34 +1653,34 @@ class AnkerSolixApi:
paramData: dict,
paramType: str = SolixParmType.SOLARBANK_SCHEDULE.value,
command: int = 17,
toFile: bool = False,
deviceSn: str = None,
) -> dict:
"""Set device parameters (e.g. solarbank schedule).
command: Must be 17 for solarbank schedule.
paramType: was always string "4"
Example paramData:
{"param_data":{"ranges":[
{"id":0,"start_time":"00:00","end_time":"08:30","turn_on":true,"appliance_loads":[{"id":0,"name":"Benutzerdefiniert","power":300,"number":1}],"charge_priority":80},
{"id":0,"start_time":"08:30","end_time":"17:00","turn_on":false,"appliance_loads":[{"id":0,"name":"Benutzerdefiniert","power":100,"number":1}],"charge_priority":80},
{"id":0,"start_time":"17:00","end_time":"24:00","turn_on":true,"appliance_loads":[{"id":0,"name":"Benutzerdefiniert","power":300,"number":1}],"charge_priority":0}],
"min_load":100,"max_load":800,"step":0,"is_charge_priority":0,default_charge_priority":0}}
{"param_data": '{"ranges":['
'{"id":0,"start_time":"00:00","end_time":"08:30","turn_on":true,"appliance_loads":[{"id":0,"name":"Benutzerdefiniert","power":300,"number":1}],"charge_priority":80},'
'{"id":0,"start_time":"08:30","end_time":"17:00","turn_on":false,"appliance_loads":[{"id":0,"name":"Benutzerdefiniert","power":100,"number":1}],"charge_priority":80},'
'{"id":0,"start_time":"17:00","end_time":"24:00","turn_on":true,"appliance_loads":[{"id":0,"name":"Benutzerdefiniert","power":300,"number":1}],"charge_priority":0}],'
'"min_load":100,"max_load":800,"step":0,"is_charge_priority":0,default_charge_priority":0}}'
"""
data = {
"site_id": siteId,
"param_type": paramType,
"cmd": command,
"param_data": json.dumps(paramData),
"param_data": json.dumps(paramData), # Must be string type
}
if toFile:
resp = self._saveToFile(
os.path.join(self._testdir, f"set_device_parm_{siteId}.json"), data=data
)
else:
resp = await self.request(
"post", _API_ENDPOINTS["set_device_parm"], json=data
)
return resp.get("data", {})
code = (
await self.request("post", _API_ENDPOINTS["set_device_parm"], json=data)
).get("code")
if not isinstance(code, int) or int(code) != 0:
return False
# update the data in api dict
if deviceSn:
await self.get_device_parm(siteId=siteId, deviceSn=deviceSn)
return True
async def get_device_fittings(
self, siteId: str, deviceSn: str, fromFile: bool = False

View File

@ -1,33 +1,51 @@
{
"S8S4GS0KZKLP0BH8": {
"device_sn": "S8S4GS0KZKLP0BH8",
"AN7ZOO7INFLCIMD9": {
"device_sn": "AN7ZOO7INFLCIMD9",
"type": "solarbank",
"site_id": "c6a3216c-b3bd-fe9a-bd00-e46bfe61ee42",
"site_id": "a253fbdd-ab6b-048f-1887-d0bcf58ff2c5",
"is_admin": true,
"device_pn": "A17C0",
"battery_soc": "75",
"battery_soc": "65",
"battery_capacity": "1600",
"battery_energy": "1040",
"charging_power": "0",
"power_unit": "W",
"charging_status": "2",
"charging_status_desc": "discharging",
"status": "0",
"status_desc": "off",
"charging_status": "0",
"charging_status_desc": "detection",
"status": "1",
"status_desc": "online",
"wireless_type": "1",
"input_power": "0",
"output_power": "0",
"alias": "Solarbank E1600",
"bt_ble_mac": "FD49FD9BFEC1",
"set_output_power": "0",
"power_cutoff": 10,
"alias": "SB E1600",
"set_system_output_power": "0",
"bt_ble_mac": "F24FAA9C72CF",
"name": "Solarbank E1600",
"battery_capacity": "1600",
"battery_energy": "1200",
"wifi_online": false,
"wifi_online": true,
"charge": false,
"bws_surplus": "0",
"sw_version": "v1.4.4",
"sw_version": "v1.5.6",
"auto_upgrade": false,
"wifi_name": "wifi-network-1",
"wifi_signal": "100",
"power_cutoff": 10,
"wifi_signal": "48",
"power_cutoff_data": [
{
"id": 1,
"is_selected": 1,
"output_cutoff_data": 10,
"lowpower_input_data": 5,
"input_cutoff_data": 10
},
{
"id": 2,
"is_selected": 0,
"output_cutoff_data": 5,
"lowpower_input_data": 4,
"input_cutoff_data": 5
}
],
"solar_info": {
"solar_brand": "ANKER",
"solar_model": "A5140",
@ -39,7 +57,7 @@
{
"id": 0,
"start_time": "00:00",
"end_time": "08:00",
"end_time": "06:30",
"turn_on": true,
"appliance_loads": [
{
@ -49,19 +67,19 @@
"number": 1
}
],
"charge_priority": 80,
"charge_priority": 0,
"power_setting_mode": 1,
"device_power_loads": [
{
"device_sn": "S8S4GS0KZKLP0BH8",
"device_sn": "AN7ZOO7INFLCIMD9",
"power": 150
}
]
},
{
"id": 0,
"start_time": "08:00",
"end_time": "17:00",
"start_time": "06:30",
"end_time": "17:30",
"turn_on": false,
"appliance_loads": [
{
@ -75,14 +93,14 @@
"power_setting_mode": 1,
"device_power_loads": [
{
"device_sn": "S8S4GS0KZKLP0BH8",
"device_sn": "AN7ZOO7INFLCIMD9",
"power": 50
}
]
},
{
"id": 0,
"start_time": "17:00",
"start_time": "17:30",
"end_time": "24:00",
"turn_on": true,
"appliance_loads": [
@ -97,7 +115,7 @@
"power_setting_mode": 1,
"device_power_loads": [
{
"device_sn": "S8S4GS0KZKLP0BH8",
"device_sn": "AN7ZOO7INFLCIMD9",
"power": 150
}
]
@ -112,8 +130,6 @@
"display_advanced_mode": 0,
"advanced_mode_min_load": 0
},
"set_system_output_power": "0",
"set_output_power": "0",
"fittings": {}
}
}

View File

@ -1,8 +1,8 @@
{
"c6a3216c-b3bd-fe9a-bd00-e46bfe61ee42": {
"a253fbdd-ab6b-048f-1887-d0bcf58ff2c5": {
"type": "system",
"site_info": {
"site_id": "c6a3216c-b3bd-fe9a-bd00-e46bfe61ee42",
"site_id": "a253fbdd-ab6b-048f-1887-d0bcf58ff2c5",
"site_name": "BKW",
"site_img": "",
"device_type_list": [
@ -36,17 +36,17 @@
"statistics": [
{
"type": "1",
"total": "89.75",
"total": "100.99",
"unit": "kwh"
},
{
"type": "2",
"total": "89.48",
"total": "100.69",
"unit": "kg"
},
{
"type": "3",
"total": "35.90",
"total": "33.33",
"unit": "\u20ac"
}
],
@ -55,37 +55,45 @@
"solarbank_list": [
{
"device_pn": "A17C0",
"device_sn": "S8S4GS0KZKLP0BH8",
"device_name": "Solarbank E1600",
"device_sn": "AN7ZOO7INFLCIMD9",
"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": "75",
"battery_power": "65",
"bind_site_status": "",
"charging_power": "0",
"power_unit": "W",
"charging_status": "2",
"status": "0",
"charging_status": "0",
"status": "1",
"wireless_type": "1",
"main_version": "",
"photovoltaic_power": "0",
"output_power": "0",
"create_time": 1695392386,
"set_load_power": "",
"output_cutoff_data": 10
"set_load_power": "0",
"output_cutoff_data": 10,
"is_display": true,
"alias_name": "SB E1600",
"current_home_load": "0"
}
],
"total_charging_power": "0",
"power_unit": "W",
"charging_status": "0",
"total_battery_power": "0.00",
"updated_time": "2024-02-22 01:16:32",
"total_battery_power": "0.65",
"updated_time": "2024-03-07 16:10:47",
"total_photovoltaic_power": "0",
"total_output_power": "0.00",
"display_set_power": false
"display_set_power": false,
"is_display_data": true
},
"retain_load": "0W",
"updated_time": "01-01-0001 00:00:00",
"power_site_type": 2,
"site_id": "c6a3216c-b3bd-fe9a-bd00-e46bfe61ee42",
"powerpanel_list": []
"site_id": "a253fbdd-ab6b-048f-1887-d0bcf58ff2c5",
"powerpanel_list": [],
"site_details": {
"price": 0.33,
"site_co2": 0,
"site_price_unit": "\u20ac"
}
}
}

View File

@ -5,13 +5,13 @@
"main_switch": true,
"device_list": [
{
"device_sn": "S8S4GS0KZKLP0BH8",
"device_sn": "AN7ZOO7INFLCIMD9",
"device_name": "Solarbank E1600",
"auto_upgrade": false,
"alias_name": "Solarbank E1600",
"alias_name": "SB E1600",
"icon": "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"
}
]
},
"trace_id": "466cd3eff9469c2c7deaf25f6f26ab0a"
"trace_id": "18a4cce292c43f44034a6bd375fbeece"
}

View File

@ -4,15 +4,15 @@
"data": {
"data": [
{
"device_sn": "S8S4GS0KZKLP0BH8",
"device_sn": "AN7ZOO7INFLCIMD9",
"product_code": "A17C0",
"bt_ble_id": "FD:49:FD:9B:FE:C1",
"bt_ble_mac": "FD49FD9BFEC1",
"bt_ble_id": "F2:4F:AA:9C:72:CF",
"bt_ble_mac": "F24FAA9C72CF",
"device_name": "Solarbank E1600",
"alias_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",
"link_time": 1695392302068,
"wifi_online": false,
"link_time": 1709743368,
"wifi_online": true,
"wifi_name": "",
"relate_type": [
"ble",
@ -20,11 +20,11 @@
],
"charge": false,
"bws_surplus": 0,
"device_sw_version": "v1.4.4",
"device_sw_version": "v1.5.6",
"has_manual": false,
"hes_data": null
}
]
},
"trace_id": "731c3d838addc0fe0368cdb0c3c0c9d7"
"trace_id": "e16b11e6cdbdef4560bbba2e513c7ccb"
}

View File

@ -5,5 +5,5 @@
"device_list": null,
"guide_txt": ""
},
"trace_id": "7be5820ec6c6f8d7e2fa3415e213b50b"
"trace_id": "0ba9c2e2ecc7adbba9b4ea6adb9bfda6"
}

View File

@ -15,5 +15,5 @@
"solar_model_name": "MI60 Microinverter"
}
},
"trace_id": "aa0e32b025caf38eab6545a84fccb5c4"
"trace_id": "5adcdcd9c5d644bcd1ed5ada6ccb0ccd"
}

View File

@ -4,5 +4,5 @@
"data": {
"data": []
},
"trace_id": "4dabbd807c0e98fe8e58ebae70910f0e"
"trace_id": "aae331bd1ceb0982fdede5fc72eff7c4"
}

View File

@ -0,0 +1,12 @@
{
"code": 0,
"msg": "success!",
"data": {
"site_id": "a253fbdd-ab6b-048f-1887-d0bcf58ff2c5",
"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\":0,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"AN7ZOO7INFLCIMD9\",\"power\":150}]},{\"id\":0,\"start_time\":\"06:30\",\"end_time\":\"17:30\",\"turn_on\":false,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":100,\"number\":1}],\"charge_priority\":80,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"AN7ZOO7INFLCIMD9\",\"power\":50}]},{\"id\":0,\"start_time\":\"17:30\",\"end_time\":\"24:00\",\"turn_on\":true,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":300,\"number\":1}],\"charge_priority\":0,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"AN7ZOO7INFLCIMD9\",\"power\":150}]}],\"min_load\":100,\"max_load\":800,\"step\":0,\"is_charge_priority\":0,\"default_charge_priority\":0,\"is_zero_output_tips\":1,\"display_advanced_mode\":0,\"advanced_mode_min_load\":0}",
"current_home_load": "0W",
"parallel_home_load": "",
"parallel_display": false
},
"trace_id": "b2ec26d818215fdec4abdd74efefcadd"
}

View File

@ -1,12 +0,0 @@
{
"code": 0,
"msg": "success!",
"data": {
"site_id": "c6a3216c-b3bd-fe9a-bd00-e46bfe61ee42",
"home_load_data": "{\"ranges\":[{\"id\":0,\"start_time\":\"00:00\",\"end_time\":\"08:00\",\"turn_on\":true,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":300,\"number\":1}],\"charge_priority\":80,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"S8S4GS0KZKLP0BH8\",\"power\":150}]},{\"id\":0,\"start_time\":\"08:00\",\"end_time\":\"17:00\",\"turn_on\":false,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":100,\"number\":1}],\"charge_priority\":80,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"S8S4GS0KZKLP0BH8\",\"power\":50}]},{\"id\":0,\"start_time\":\"17:00\",\"end_time\":\"24:00\",\"turn_on\":true,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":300,\"number\":1}],\"charge_priority\":0,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"S8S4GS0KZKLP0BH8\",\"power\":150}]}],\"min_load\":100,\"max_load\":800,\"step\":0,\"is_charge_priority\":0,\"default_charge_priority\":0,\"is_zero_output_tips\":1,\"display_advanced_mode\":0,\"advanced_mode_min_load\":0}",
"current_home_load": "0W",
"parallel_home_load": "",
"parallel_display": false
},
"trace_id": "edc0aace41b8f1dbbdd0ddb7dcbcbbc0"
}

View File

@ -0,0 +1,8 @@
{
"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\":0,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"AN7ZOO7INFLCIMD9\",\"power\":150}]},{\"id\":0,\"start_time\":\"06:30\",\"end_time\":\"17:30\",\"turn_on\":false,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":100,\"number\":1}],\"charge_priority\":80,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"AN7ZOO7INFLCIMD9\",\"power\":50}]},{\"id\":0,\"start_time\":\"17:30\",\"end_time\":\"24:00\",\"turn_on\":true,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":300,\"number\":1}],\"charge_priority\":0,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"AN7ZOO7INFLCIMD9\",\"power\":150}]}],\"min_load\":100,\"max_load\":800,\"step\":0,\"is_charge_priority\":0,\"default_charge_priority\":0,\"is_zero_output_tips\":1,\"display_advanced_mode\":0,\"advanced_mode_min_load\":0}"
},
"trace_id": "7d9b5fb6eca77392752c95891ef22767"
}

View File

@ -1,8 +0,0 @@
{
"code": 0,
"msg": "success!",
"data": {
"param_data": "{\"ranges\":[{\"id\":0,\"start_time\":\"00:00\",\"end_time\":\"08:00\",\"turn_on\":true,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":300,\"number\":1}],\"charge_priority\":80,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"S8S4GS0KZKLP0BH8\",\"power\":150}]},{\"id\":0,\"start_time\":\"08:00\",\"end_time\":\"17:00\",\"turn_on\":false,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":100,\"number\":1}],\"charge_priority\":80,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"S8S4GS0KZKLP0BH8\",\"power\":50}]},{\"id\":0,\"start_time\":\"17:00\",\"end_time\":\"24:00\",\"turn_on\":true,\"appliance_loads\":[{\"id\":0,\"name\":\"Benutzerdefiniert\",\"power\":300,\"number\":1}],\"charge_priority\":0,\"power_setting_mode\":1,\"device_power_loads\":[{\"device_sn\":\"S8S4GS0KZKLP0BH8\",\"power\":150}]}],\"min_load\":100,\"max_load\":800,\"step\":0,\"is_charge_priority\":0,\"default_charge_priority\":0,\"is_zero_output_tips\":1,\"display_advanced_mode\":0,\"advanced_mode_min_load\":0}"
},
"trace_id": "abe0cd159ab3d5fd1e5ed06f7bd79cbe"
}

View File

@ -4,7 +4,7 @@
"data": {
"site_list": [
{
"site_id": "c6a3216c-b3bd-fe9a-bd00-e46bfe61ee42",
"site_id": "a253fbdd-ab6b-048f-1887-d0bcf58ff2c5",
"site_name": "BKW",
"site_img": "",
"device_type_list": [
@ -21,10 +21,10 @@
"solarbank_list": [
{
"device_pn": "",
"device_sn": "S8S4GS0KZKLP0BH8",
"device_name": "Solarbank E1600",
"device_sn": "AN7ZOO7INFLCIMD9",
"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": "75",
"battery_power": "65",
"bind_site_status": "1",
"charging_power": "",
"power_unit": "",
@ -36,10 +36,11 @@
"output_power": "",
"create_time": 0,
"set_load_power": "",
"output_cutoff_data": 0
"output_cutoff_data": 0,
"is_display": false
}
],
"powerpanel_list": []
},
"trace_id": "9a39fafe7f5c72ab4a9746a92793bf6d"
"trace_id": "f5b4f0bd0d3dde4a93dcc131e20c4e1b"
}

View File

@ -19,5 +19,5 @@
}
]
},
"trace_id": "810134a03be0200fc75bf5ce15cacc3b"
"trace_id": "13ec0de9fbc13cbe339fc2e8d0fabeda"
}

View File

@ -0,0 +1,11 @@
{
"code": 0,
"msg": "success!",
"data": {
"site_id": "a253fbdd-ab6b-048f-1887-d0bcf58ff2c5",
"price": 0.33,
"site_co2": 0,
"site_price_unit": "\u20ac"
},
"trace_id": "b3dacca456d8bd0cac185da99ac5cd75"
}

View File

@ -1,11 +0,0 @@
{
"code": 0,
"msg": "success!",
"data": {
"site_id": "c6a3216c-b3bd-fe9a-bd00-e46bfe61ee42",
"price": 0.4,
"site_co2": 0,
"site_price_unit": "\u20ac"
},
"trace_id": "3c10c236f183f13be3233f802227ffd0"
}

View File

@ -20,17 +20,17 @@
"statistics": [
{
"type": "1",
"total": "89.75",
"total": "100.99",
"unit": "kwh"
},
{
"type": "2",
"total": "89.48",
"total": "100.69",
"unit": "kg"
},
{
"type": "3",
"total": "35.90",
"total": "33.33",
"unit": "\u20ac"
}
],
@ -39,38 +39,40 @@
"solarbank_list": [
{
"device_pn": "A17C0",
"device_sn": "S8S4GS0KZKLP0BH8",
"device_name": "Solarbank E1600",
"device_sn": "AN7ZOO7INFLCIMD9",
"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": "75",
"battery_power": "65",
"bind_site_status": "",
"charging_power": "0",
"power_unit": "W",
"charging_status": "2",
"status": "0",
"charging_status": "0",
"status": "1",
"wireless_type": "1",
"main_version": "",
"photovoltaic_power": "0",
"output_power": "0",
"create_time": 1695392386,
"set_load_power": "",
"output_cutoff_data": 10
"output_cutoff_data": 10,
"is_display": true
}
],
"total_charging_power": "0",
"power_unit": "W",
"charging_status": "0",
"total_battery_power": "0.00",
"updated_time": "2024-02-22 01:16:32",
"total_battery_power": "0.65",
"updated_time": "2024-03-07 16:10:47",
"total_photovoltaic_power": "0",
"total_output_power": "0.00",
"display_set_power": false
"display_set_power": false,
"is_display_data": true
},
"retain_load": "0W",
"updated_time": "01-01-0001 00:00:00",
"power_site_type": 2,
"site_id": "c6a3216c-b3bd-fe9a-bd00-e46bfe61ee42",
"site_id": "a253fbdd-ab6b-048f-1887-d0bcf58ff2c5",
"powerpanel_list": []
},
"trace_id": "be165dce6edbfda3eef203e9f8cd3cfb"
"trace_id": "f1db89088b5ef100b876e2dabf387b9d"
}

View File

@ -3,7 +3,7 @@
"msg": "success!",
"data": {
"site_info": {
"site_id": "c6a3216c-b3bd-fe9a-bd00-e46bfe61ee42",
"site_id": "a253fbdd-ab6b-048f-1887-d0bcf58ff2c5",
"site_name": "BKW",
"site_img": "",
"device_type_list": [
@ -23,8 +23,8 @@
"solarbank_list": [
{
"device_pn": "A17C0",
"device_sn": "S8S4GS0KZKLP0BH8",
"device_name": "Solarbank E1600",
"device_sn": "AN7ZOO7INFLCIMD9",
"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": "",
"bind_site_status": "",
@ -38,10 +38,11 @@
"output_power": "",
"create_time": 1695392386,
"set_load_power": "",
"output_cutoff_data": 0
"output_cutoff_data": 0,
"is_display": false
}
],
"powerpanel_list": []
},
"trace_id": "42fca6e31c7597b30ccb8bafdb5c409f"
"trace_id": "cf2dda7f4a1db4dcbafd2314deee10c8"
}

View File

@ -4,7 +4,7 @@
"data": {
"site_list": [
{
"site_id": "c6a3216c-b3bd-fe9a-bd00-e46bfe61ee42",
"site_id": "a253fbdd-ab6b-048f-1887-d0bcf58ff2c5",
"site_name": "BKW",
"site_img": "",
"device_type_list": [
@ -21,5 +21,5 @@
}
]
},
"trace_id": "f4a07b0fc7eda9aac8a047bfe3ce0881"
"trace_id": "5de5a4ec2ebc9f2cff1fa10f1ae98ac3"
}

View File

@ -8,5 +8,5 @@
"solar_sn": "",
"solar_model_name": "MI60 Microinverter"
},
"trace_id": "a982c1bc38ab3f9da5afef26e496f013"
"trace_id": "45452d1ecaf757adaa5eaeac7e1c2abb"
}

View File

@ -7,25 +7,26 @@
"solarbank_list": [
{
"device_pn": "A17C0",
"device_sn": "S8S4GS0KZKLP0BH8",
"device_name": "Solarbank E1600",
"device_sn": "AN7ZOO7INFLCIMD9",
"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": "",
"bind_site_status": "1",
"charging_power": "",
"power_unit": "",
"charging_status": "",
"status": "0",
"status": "1",
"wireless_type": "1",
"main_version": "",
"photovoltaic_power": "",
"output_power": "",
"create_time": 0,
"set_load_power": "",
"output_cutoff_data": 0
"output_cutoff_data": 0,
"is_display": false
}
],
"powerpanel_list": []
},
"trace_id": "e9eeb54a20b422df82fafdaedc75f21f"
"trace_id": "6e7ce08a0a87cf45c6cba57efb68abcb"
}

View File

@ -5,9 +5,9 @@
"wifi_info_list": [
{
"wifi_name": "wifi-network-1",
"wifi_signal": "100"
"wifi_signal": "48"
}
]
},
"trace_id": "0a0d59c1b266de112d9028dfcfcf5ffe"
"trace_id": "a12c7ddfdc319ffa7ca7ae9187ecfa8d"
}

View File

@ -403,6 +403,7 @@ async def main() -> bool: # noqa: C901 # pylint: disable=too-many-branches,too-
# this is more efficient and allows validation of randomized data in export files
myapi.testDir(folder)
await myapi.update_sites(fromFile=True)
await myapi.update_site_details(fromFile=True)
await myapi.update_device_details(fromFile=True)
# avoid randomizing dictionary export twice when imported from randomized files already
CONSOLE.info("\nExporting Api Sites overview...")

View File

@ -177,7 +177,7 @@ async def main() -> ( # noqa: C901 # pylint: disable=too-many-locals,too-many-b
f"{'SW Version':<{col1}}: {dev.get('sw_version','Unknown'):<{col2}} {'Auto-Upgrade':<{col3}}: {'Unknown' if upgrade is None else 'Enabled' if upgrade else 'Disabled'}"
)
CONSOLE.info(
f"{'Status':<{col1}}: {dev.get('status_desc','Unknown'):<{col2}} {'Status code':<{col3}}: {str(dev.get('status','-'))}"
f"{'Cloud Status':<{col1}}: {dev.get('status_desc','Unknown'):<{col2}} {'Status code':<{col3}}: {str(dev.get('status','-'))}"
)
CONSOLE.info(
f"{'Charge Status':<{col1}}: {dev.get('charging_status_desc','Unknown'):<{col2}} {'Status code':<{col3}}: {str(dev.get('charging_status','-'))}"