Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 59 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,52 @@ Once registered, add your battery/inverter to the developer account via the web

To be good internet citizens, it is advised that your polling frequency for any AlphaCloud endpoints are 10 seconds at a minimum.

# API documentation

The developer portal requires registration to read, so the documentation is mirrored here —
transcribed from the portal and verified endpoint by endpoint against the live API:

+ **[docs/API.md](docs/API.md)** — all 19 endpoints. For each one: what you send, what comes
back (with every field, its type and its unit), a real captured response, and the matching
library method.
+ **[docs/RETURN_CODES.md](docs/RETURN_CODES.md)** — the complete return code table (both pages
of the portal's paginated list), grouped by cause, plus codes the portal does not publish.

Four things the official documentation gets wrong are corrected in
[docs/API.md](docs/API.md#corrections-to-the-official-documentation).

# Methods

There are public methods in this module that duplicate the AlphaESS OpenAPI and provide wrappers for
all 19 documented endpoints:

+ https://openapi.alphaess.com/api/getEssList
+ https://openapi.alphaess.com/api/getLastPowerData
+ https://openapi.alphaess.com/api/getOneDayPowerBySn
+ https://openapi.alphaess.com/api/getOneDateEnergyBySn
+ https://openapi.alphaess.com/api/getChargeConfigInfo
+ https://openapi.alphaess.com/api/updateChargeConfigInfo
+ https://openapi.alphaess.com/api/getDisChargeConfigInfo
+ https://openapi.alphaess.com/api/updateDisChargeConfigInfo
| Endpoint | Method |
| --- | --- |
| https://openapi.alphaess.com/api/getEssList | `getESSList()` |
| https://openapi.alphaess.com/api/getLastPowerData | `getLastPowerData(sysSn)` |
| https://openapi.alphaess.com/api/getOneDayPowerBySn | `getOneDayPowerBySn(sysSn, queryDate=None)` |
| https://openapi.alphaess.com/api/getOneDateEnergyBySn | `getOneDateEnergyBySn(sysSn, queryDate=None)` |
| https://openapi.alphaess.com/api/getSumDataForCustomer | `getSumDataForCustomer(sysSn)` |
| https://openapi.alphaess.com/api/getChargeConfigInfo | `getChargeConfigInfo(sysSn)` |
| https://openapi.alphaess.com/api/updateChargeConfigInfo | `updateChargeConfigInfo(...)` |
| https://openapi.alphaess.com/api/getDisChargeConfigInfo | `getDisChargeConfigInfo(sysSn)` |
| https://openapi.alphaess.com/api/updateDisChargeConfigInfo | `updateDisChargeConfigInfo(...)` |
| https://openapi.alphaess.com/api/getTimeChargeBySn | `getTimeChargeBySn(sysSn)` |
| https://openapi.alphaess.com/api/setTimeChargeBySn | `setTimeChargeBySn(...)` |
| https://openapi.alphaess.com/api/getVerificationCode | `getVerificationCode(sysSn, checkCode)` |
| https://openapi.alphaess.com/api/bindSn | `bindSn(sysSn, code)` |
| https://openapi.alphaess.com/api/unBindSn | `unBindSn(sysSn)` |
| https://openapi.alphaess.com/api/getEvChargerConfigList | `getEvChargerConfigList(sysSn)` |
| https://openapi.alphaess.com/api/getEvChargerCurrentsBySn | `getEvChargerCurrentsBySn(sysSn)` |
| https://openapi.alphaess.com/api/setEvChargerCurrentsBySn | `setEvChargerCurrentsBySn(sysSn, currentsetting)` |
| https://openapi.alphaess.com/api/getEvChargerStatusBySn | `getEvChargerStatusBySn(sysSn, evchargerSn)` |
| https://openapi.alphaess.com/api/remoteControlEvCharger | `remoteControlEvCharger(sysSn, evchargerSn, controlMode)` |

All of the above are documented at https://open.alphaess.com/developmentManagement/apiList (Registration required)

+ getdata() - Attempts to get statistical energy data for use in Home Assistant for all registered Alpha ESS systems - will return None if there are issues retrieving data from the Alpha ESS API.
## Convenience methods

+ getdata(get_power=False, get_ev=False, self_delay=0, get_timecharge=False) - Attempts to get statistical energy data for use in Home Assistant for all registered Alpha ESS systems - will return None if there are issues retrieving data from the Alpha ESS API.
+ authenticate - Attempts to use https://openapi.alphaess.com/api/getEssList to validate authentication to the ALpha ESS API - will return True or False.
+ setbatterycharge (serial, enabled, dp1start, dp1end, dp2start, dp2end, chargecutoffsoc)
**Parameters:**
Expand All @@ -39,6 +69,26 @@ All of the above are documented at https://open.alphaess.com/developmentManageme
- `dp2end` (`datetime.time`) The end time of charging period 2 (the minutes must be one of :00, :15, :30, :45)
- `serial` (str) The serial number of the battery/inverter.

+ setTimeChargeBySn (sysSn, executeCycleType, chargeTimeList, dischargeTimeList, gridChargeCycle=None, ctrDisCycle=None)

The periodic (weekly) scheduling API. Unlike `setbatterycharge`/`setbatterydischarge` it supports up to six periods per day, per-weekday selection, and a power setpoint per period. Not every system is entitled to it — systems without the feature return code `6017` (`No operation permissions`).

**Parameters:**
- `sysSn` (str) The serial number of the battery/inverter.
- `executeCycleType` (int) 0 - daily, 1 - weekly
- `chargeTimeList` / `dischargeTimeList` (list of dict) Each period is `{"beginTime": "HH:mm", "endTime": "HH:mm", "chargeLimit": 10-100}`, plus optional `weeks` (a list of 1-7 for Monday-Sunday, required when weekly) and `chargePower`. Maximum 6 periods per day / 28 per week; charge and discharge periods must not overlap.
- `gridChargeCycle` (int) 0 - periodic charging disabled, 1 - enabled
- `ctrDisCycle` (int) 0 - periodic discharging disabled, 1 - enabled

```python
await client.setTimeChargeBySn(
serial, 1,
chargeTimeList=[{"beginTime": "01:00", "endTime": "05:00", "weeks": [1, 2, 3, 4, 5], "chargeLimit": 90}],
dischargeTimeList=[{"beginTime": "17:00", "endTime": "21:00", "weeks": [1, 2, 3, 4, 5], "chargeLimit": 20}],
gridChargeCycle=1, ctrDisCycle=1,
)
```

+ setbatterydischarge (serial, enabled, dp1start, dp1end, dp2start, dp2end, dischargecutoffsoc)
**Parameters:**
- `dischargecutoffsoc` (float) % to stop discharging from the battery at
Expand Down
150 changes: 133 additions & 17 deletions alphaess/alphaess.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,35 @@

BASEURL = "https://openapi.alphaess.com/api"

# Return codes as published on the developer portal, see docs/RETURN_CODES.md
RETURN_CODES = {
6001: "Parameter error",
6002: "The SN is not bound to the user",
6003: "You have bound this SN",
6004: "CheckCode error",
6005: "This appId is not bound to the SN",
6006: "Timestamp error",
6007: "Sign verification error",
6008: "Set failed",
6009: "Whitelist verification failed",
6010: "Sign is empty",
6011: "timestamp is empty",
6012: "AppId is empty",
6016: "Data does not exist or has been deleted",
6026: "internal error",
6029: "operation failed",
6038: "system sn does not exist",
6042: "system offline",
6046: "Verification code error",
6053: "The request was too fast, please try again later",
}

# Codes the API returns but the portal does not publish. Kept separate so
# RETURN_CODES stays a faithful copy of the documented table.
UNDOCUMENTED_RETURN_CODES = {
6017: "No operation permissions",
}


class alphaess:
"""Class for Alpha ESS."""
Expand Down Expand Up @@ -55,6 +84,26 @@ def __headers(self):
"timeStamp": timestamp
}

@staticmethod
def __is_success(json_response) -> bool:
"""Check whether a json response indicates success.

Most endpoints report the status as "msg", the periodic charge/discharge
endpoints are documented as reporting it as "info".
"""
return (
json_response.get("code") == 200
or json_response.get("msg") == "Success"
or json_response.get("info") == "Success"
)

@staticmethod
def __return_code_description(json_response) -> str:
"""Return a formatted description for the response code, if known"""
code = json_response.get("code")
description = RETURN_CODES.get(code) or UNDOCUMENTED_RETURN_CODES.get(code)
return f" ({description})" if description else ""

async def getESSList(self) -> Optional(list):
"""According to SN to get system list data"""
try:
Expand All @@ -66,6 +115,7 @@ async def getESSList(self) -> Optional(list):

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def getLastPowerData(self, sysSn) -> Optional(list):
"""According SN to get real-time power data"""
Expand All @@ -78,6 +128,7 @@ async def getLastPowerData(self, sysSn) -> Optional(list):

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def getOneDayPowerBySn(self, sysSn, queryDate=None) -> Optional(list):
"""According SN to get system power data"""
Expand All @@ -92,6 +143,7 @@ async def getOneDayPowerBySn(self, sysSn, queryDate=None) -> Optional(list):

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def getSumDataForCustomer(self, sysSn) -> Optional(list):
"""According SN to get System Summary data"""
Expand All @@ -104,6 +156,7 @@ async def getSumDataForCustomer(self, sysSn) -> Optional(list):

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def getOneDateEnergyBySn(self, sysSn, queryDate=None) -> Optional(list):
"""According SN to get System Energy Data"""
Expand All @@ -118,6 +171,7 @@ async def getOneDateEnergyBySn(self, sysSn, queryDate=None) -> Optional(list):

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def getChargeConfigInfo(self, sysSn) -> Optional(list):
"""According SN to get charging setting information"""
Expand All @@ -130,6 +184,7 @@ async def getChargeConfigInfo(self, sysSn) -> Optional(list):

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def getDisChargeConfigInfo(self, sysSn) -> Optional(list):
"""According to SN discharge setting information"""
Expand All @@ -142,6 +197,7 @@ async def getDisChargeConfigInfo(self, sysSn) -> Optional(list):

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def getEvChargerConfigList(self, sysSn) -> Optional(list):
"""According to SN get Ev Charger Config List"""
Expand All @@ -154,6 +210,7 @@ async def getEvChargerConfigList(self, sysSn) -> Optional(list):

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def setEvChargerCurrentsBySn(self, sysSn, currentsetting) -> Optional(list):
"""According to SN set Ev Charger Currents"""
Expand All @@ -171,6 +228,7 @@ async def setEvChargerCurrentsBySn(self, sysSn, currentsetting) -> Optional(list

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def getEvChargerCurrentsBySn(self, sysSn) -> Optional(list):
"""According to SN get Ev Charger Currents"""
Expand All @@ -183,6 +241,7 @@ async def getEvChargerCurrentsBySn(self, sysSn) -> Optional(list):

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def getEvChargerStatusBySn(self, sysSn, evchargerSn) -> Optional(list):
"""According to SN get Ev Charger Status"""
Expand All @@ -195,6 +254,7 @@ async def getEvChargerStatusBySn(self, sysSn, evchargerSn) -> Optional(list):

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def remoteControlEvCharger(self, sysSn, evchargerSn, controlMode) -> Optional(dict):
"""According SN to Remote Control Ev Charger"""
Expand All @@ -213,6 +273,7 @@ async def remoteControlEvCharger(self, sysSn, evchargerSn, controlMode) -> Optio

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def bindSn(self, sysSn, code) -> Optional(dict):
"""According to SN to Bind SN"""
Expand All @@ -230,23 +291,20 @@ async def bindSn(self, sysSn, code) -> Optional(dict):

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def getVerificationCode(self, sysSn, checkCode) -> Optional(dict):
"""According SN to Get Verification Code"""
try:
resource = f"{BASEURL}/getVerificationCode"

settings = {
"sysSn": sysSn,
"checkCode": checkCode
}
resource = f"{BASEURL}/getVerificationCode?sysSn={sysSn}&checkCode={checkCode}"

logger.debug(f"Trying to call {resource} with settings {settings}")
logger.debug(f"Trying to call {resource}")

return await self.api_post(resource, settings)
return await self.api_get(resource)

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def unBindSn(self, sysSn) -> Optional(dict):
"""According SN to UnBind SN"""
Expand All @@ -263,6 +321,7 @@ async def unBindSn(self, sysSn) -> Optional(dict):

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def updateChargeConfigInfo(self, sysSn, batHighCap, gridCharge, timeChae1, timeChae2, timeChaf1,
timeChaf2) -> Optional(dict):
Expand All @@ -286,6 +345,7 @@ async def updateChargeConfigInfo(self, sysSn, batHighCap, gridCharge, timeChae1,

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def updateDisChargeConfigInfo(self, sysSn, batUseCap, ctrDis, timeDise1, timeDise2, timeDisf1,
timeDisf2) -> Optional(dict):
Expand All @@ -309,6 +369,55 @@ async def updateDisChargeConfigInfo(self, sysSn, batUseCap, ctrDis, timeDise1, t

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def getTimeChargeBySn(self, sysSn) -> Optional(dict):
"""According SN to get periodic charge/discharge settings"""
try:
resource = f"{BASEURL}/getTimeChargeBySn?sysSn={sysSn}"

logger.debug(f"Trying to call {resource}")

return await self.api_get(resource)

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def setTimeChargeBySn(self, sysSn, executeCycleType, chargeTimeList, dischargeTimeList,
gridChargeCycle=None, ctrDisCycle=None) -> Optional(dict):
"""According SN to set periodic charge/discharge settings

executeCycleType: 0 - daily, 1 - weekly
chargeTimeList / dischargeTimeList: lists of periods, each a dict of
beginTime (HH:mm), endTime (HH:mm), chargeLimit (cutoff SOC, 10-100) and
optionally weeks ([1..7], Monday to Sunday, required when weekly) and chargePower.
Maximum 6 periods per day / 28 per week, charge and discharge must not overlap.
gridChargeCycle / ctrDisCycle: 0 - disabled, 1 - enabled
"""
try:
resource = f"{BASEURL}/setTimeChargeBySn"

settings = {
"sysSn": sysSn,
"executeCycleType": executeCycleType,
"chargeTimeList": chargeTimeList,
"dischargeTimeList": dischargeTimeList
}

if gridChargeCycle is not None:
settings["gridChargeCycle"] = int(gridChargeCycle)

if ctrDisCycle is not None:
settings["ctrDisCycle"] = int(ctrDisCycle)

logger.debug(f"Trying to call {resource} with settings {settings}")

return await self.api_post(resource, settings)

except Exception as e:
logger.error(f"Error: {e} when calling {resource}")
raise

async def getIPData(self) -> Optional(dict):
ENDPOINTS = {
Expand Down Expand Up @@ -355,8 +464,10 @@ async def api_get(self, path, json=None) -> Optional(list):
else:
logger.error(f"Unexpected response received: {response.status} when calling {path}")

if ("msg" in json_response and json_response["msg"] != "Success") or ("msg" not in json_response):
logger.error(f"Unexpected json_response : {json_response} when calling {path}")
if not self.__is_success(json_response):
logger.error(
f"Unexpected json_response : {json_response}"
f"{self.__return_code_description(json_response)} when calling {path}")
return None
else:
if json_response["data"] is not None:
Expand Down Expand Up @@ -389,18 +500,19 @@ async def api_post(self, path, json) -> Optional(dict):
else:
logger.error(f"Unexpected response received: {response.status} when calling {path}")

if "msg" in json_response and json_response["msg"] == "Success":
if json_response["data"] is None:
return json_response["data"]
else:
logger.error(f"Unexpected json_response : {json_response} when calling {path}")
return json_response["data"]
if self.__is_success(json_response):
return json_response["data"]

logger.error(
f"Unexpected json_response : {json_response}"
f"{self.__return_code_description(json_response)} when calling {path}")
return None

except Exception as e:
logger.error(e)
raise

async def getdata(self, get_power=False, get_ev=False, self_delay=0) -> Optional(list):
async def getdata(self, get_power=False, get_ev=False, self_delay=0, get_timecharge=False) -> Optional(list):
"""Get All Data For All serial numbers from Alpha ESS"""
try:
alldata = []
Expand Down Expand Up @@ -447,6 +559,10 @@ async def getdata(self, get_power=False, get_ev=False, self_delay=0) -> Optional
await asyncio.sleep(self_delay)
unit['OneDayPower'] = await self.getOneDayPowerBySn(serial, time.strftime("%Y-%m-%d"))

if get_timecharge:
await asyncio.sleep(self_delay)
unit['TimeCharge'] = await self.getTimeChargeBySn(serial)

if get_ev:
await asyncio.sleep(self_delay)
unit['EVData'] = await self.getEvChargerConfigList(serial)
Expand Down
Loading