Skip to content
Merged
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
77 changes: 55 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,68 @@
# soliscloud-api
Python implementation for the SolisCloud API

Supports all endpoints specified in SolisCloud API v1.2 for reading Solis PV monitoring data from the SolisCloud service.
Supports all endpoints specified in SolisCloud API spec 2.0 for reading Solis PV monitoring data from the SolisCloud service.

# Prerequisites
Usage of the API requires an active account on https://www.soliscloud.com and also requires an API key and secret,
to be obtained via SolisCloud.

# Supported endpoints

* /V1/API/STATIONYEAR (PLANT YEARLY GRAPH)
* /V1/API/STATIONALL (PLANT CUMULATIVE GRAPH)
* /V1/API/INVERTERDAY (INVERTER DAILY GRAPH)
* /V1/API/INVERTERMONTH (INVERTER MONTHLY GRAPH)
* /V1/API/INVERTERYEAR (INVERTER YEARLY GRAPH)
* /V1/API/INVERTERALL (INVERTER CUMULATIVE GRAPH)
* /V1/API/ALARMLIST (ALARM INFO CHECK)
* /V1/API/STATIONDETAILLIST (BATCH ACQUIRE PLANT DETAILS)
* /V1/API/INVERTERDETAILLIST (BATCH ACQUIRE INVERTER DETAILS)
* /V1/API/STATIONDAYENERGYLIST (BATCH ACQUIRE PLANT DAILY GENERATION)
* /V1/API/STATIONMONTHENERGYLIST (BATCH ACQUIRE PLANT MONTHLY GENERATION)
* /V1/API/STATIONYEARENERGYLIST (BATCH ACQUIRE PLANT YEARLY GENERATION)
* /V1/API/EPMLIST (EPM LIST)
* /V1/API/EPMDETAIL (EPM DETAILS)
* /V1/API/EPM/DAY (EPM DAILY GRAPH)
* /V1/API/EPM/MONTH (EPM MONTHLY GRAPH)
* /V1/API/EPM/YEAR (EPM YEARLY GRAPH)
* /V1/API/EPM/ALL (EPM CUMULATIVE GRAPH)
* Submit a [service ticket](https://solis-service.solisinverters.com/support/solutions/articles/44002212561-api-access-soliscloud) and wait till it is resolved.
* Go to https://www.soliscloud.com/#/apiManage.
* Activate API management and agree with the usage conditions.
* After activation, click on view key tot get a pop-up window asking for the verification code.
* First click on "Verification code" after which you get an image with 2 puzzle pieces, which you need to overlap each other using the slider below.
* After that, you will receive an email with the verification code you need to enter (within 60 seconds).
* Once confirmed, you get the API ID, secret and API URL


# Supported API calls
NOTE: The spec uses the terms Plant, Station and Power Station interchangeably. In this document we will use plant.

| Call | Description |
|----------------------------------|---------------------------------------------------------------|
| /v1/api/inverterList | Obtain list of all inverters under account or a specific plant. |
| /v1/api/inverterDetail | Obtain details for a single inverter. |
| /v1/api/inverterDetailList | Obtain list of details of all inverters under account. |
| /v1/api/inverterDay | Obtain real-time data of a single inverter on the specified day. |
| /v1/api/inverterMonth | Obtain daily data of a single inverter for the specified month. |
| /v1/api/inverterYear | Obtain monthly data of a single inverter for the specified year. |
| /v1/api/inverterAll | Obtain cumulative data of a single inverter for the current year. |
| /v1/api/inverter/shelfTime :new: | Warranty data of a single inverter. |
| /v1/api/alarmList | Obtain device alarm list of all or specific inverter under account. |
| /v1/api/collectorList | Obtain list of all collectors under account or for a specific plant. |
| /v1/api/collectorDetail | Obtain details for a single collector. |
| /v1/api/collector/day :new: | Obtain daily data for a single collector. |
| /v1/api/epmList | Obtain list of all EPM's under account or for a specific plant. |
| /v1/api/epmDetail | Obtain details for a single EPM. |
| /v1/api/epm/day | Obtain real-time data of a single EPM on the specified day. |
| /v1/api/epm/month | Obtain daily data of a single EPM for the specified month. |
| /v1/api/epm/year | Obtain monthly data of a single EPM for the specified year. |
| /v1/api/epm/all | Obtain cumulative data of a single EPM for the current year. |
| /v1/api/weatherList :new: | Obtain list of all meteorological instruments under account or for a specific plant. |
| /v1/api/weatherDetail :new: | Obtain details for a single meteorological instrument. |
| /v1/api/userStationList | Obtain list of all plants under account. |
| /v1/api/stationDetail | Obtain details for a single plant. |
| /v1/api/stationDetailList | Obtain details for a all plants under account. |
| /v1/api/stationDayEnergyList | Obtain real-time data of a single or all plants on the specified day. |
| /v1/api/stationMonthEnergyList | Obtain daily data of a single or all plants for the specified month. |
| /v1/api/stationYearEnergyList | Obtain monthly data of a single or all plants for the specified year. |
| /v1/api/stationDay | Obtain real-time data of a single plant on the specified day. |
| /v1/api/stationMonth | Obtain daily data of a single plant for the specified month. |
| /v1/api/stationYear | Obtain monthly data of a single plant for the specified year. |
| /v1/api/stationAll | Obtain cumulative data of a single plant for the current year. |

# Currently unsupported API calls
| Call | Description |
|----------------------------------|---------------------------------------------------------------|
| /v1/api/addStation | Add a plant to the account. |
| /v1/api/stationUpdate | Update plant information. |
| /v1/api/addStationBindCollector | Binding a new collector to the plant. |
| /v1/api/delCollector | Unbind a collector from the plant. |
| /v1/api/addDevice | Binding a new inverter to the plant. |

# Known issues

1. If the local time deviates more than 15 minutes from SolisCloud server time then the server will respond with HTTP 408.
2. When calls to the API return with error message "数据异常 请联系管理员" (English: abnormal data, please contact administrator), then SolisCloud helpdesk needs to fix your account, raise a ticket via soliscloud.com
2. When calls to the API return with error message "数据异常 请联系管理员" (English: abnormal data, please contact administrator.), then SolisCloud helpdesk needs to fix your account, raise a ticket via soliscloud.com
3. I could not test the use of the NMI parameter for AUS use cases, please create a ticket or pull request if you experience issues
Binary file removed doc/SolisCloud API 1.2.pdf
Binary file not shown.
21 changes: 21 additions & 0 deletions example.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ async def main():
# Use helper class as alternative
inverter_ids = await Helpers.get_inverter_ids(
soliscloud, api_key, api_secret)

inverter_detail = await soliscloud.inverter_detail(
api_key, api_secret, inverter_id=inverter_ids[0])
inverter_detail_json = json.dumps(inverter_detail, indent=2)

# Get data collectors for all stations
collector_list = await soliscloud.collector_list(
api_key, api_secret, page_no=1, page_size=100)
# Australian accounts require nmi, uncomment if required.
# (NOT TESTED!)
# collector_list = await soliscloud.collector_list(
# api_key, api_secret, page_no=1,
# page_size=100, nmi_code=api_nmi)
collector_list_json = json.dumps(collector_list, indent=2)

except (
SoliscloudAPI.SolisCloudError,
SoliscloudAPI.HttpError,
Expand All @@ -72,9 +87,15 @@ async def main():
print("InverterList call success:")
print(f"{inverter_list_json}")

print("InverterDetails call success:")
print(f"{inverter_detail_json}")

print("Helper call success:")
print(f"{inverter_ids}")

print("CollectorList call success:")
print(f"{collector_list_json}")

loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()
88 changes: 86 additions & 2 deletions soliscloud_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
STATION_DETAIL = RESOURCE_PREFIX + 'stationDetail'
COLLECTOR_LIST = RESOURCE_PREFIX + 'collectorList'
COLLECTOR_DETAIL = RESOURCE_PREFIX + 'collectorDetail'
COLLECTOR_DAY = RESOURCE_PREFIX + 'collector/day'
INVERTER_LIST = RESOURCE_PREFIX + 'inverterList'
INVERTER_DETAIL = RESOURCE_PREFIX + 'inverterDetail'
STATION_DAY = RESOURCE_PREFIX + 'stationDay'
Expand All @@ -42,7 +43,8 @@
INVERTER_MONTH = RESOURCE_PREFIX + 'inverterMonth'
INVERTER_YEAR = RESOURCE_PREFIX + 'inverterYear'
INVERTER_ALL = RESOURCE_PREFIX + 'inverterAll'
ALARM_LIST = RESOURCE_PREFIX + 'inverterAlarmList'
INVERTER_SHELF_TIME = RESOURCE_PREFIX + 'inverter/shelfTime'
ALARM_LIST = RESOURCE_PREFIX + 'alarmList'
STATION_DETAIL_LIST = RESOURCE_PREFIX + 'stationDetailList'
INVERTER_DETAIL_LIST = RESOURCE_PREFIX + 'inverterDetailList'
STATION_DAY_ENERGY_LIST = RESOURCE_PREFIX + 'stationDayEnergyList'
Expand All @@ -54,14 +56,21 @@
EPM_MONTH = RESOURCE_PREFIX + 'epm/month'
EPM_YEAR = RESOURCE_PREFIX + 'epm/year'
EPM_ALL = RESOURCE_PREFIX + 'epm/all'
WEATHER_LIST = RESOURCE_PREFIX + 'weatherList'
WEATHER_DETAIL = RESOURCE_PREFIX + 'weatherDetail'


ONLY_INV_ID_OR_SN_ERR = \
"Only pass one of inverter_id or inverter_sn as identifier"
INV_SN_ERR = "Pass inverter_sn as identifier"
ONLY_COL_ID_OR_SN_ERR = \
"Only pass one of collector_id or collector_sn as identifier"
COL_SN_ERR = "Pass collector_sn as identifier"
ONLY_STN_ID_OR_SN_ERR = \
"Only pass one of station_id or nmi_code as identifier"
PAGE_SIZE_ERR = "page_size must be <= 100"
WEATHER_SN_ERR = "Pass instrument_sn as identifier, \
containing weather instrument serial"


class SoliscloudAPI():
Expand Down Expand Up @@ -211,6 +220,26 @@ async def collector_detail(
raise SoliscloudAPI.SolisCloudError(ONLY_COL_ID_OR_SN_ERR)
return await self._get_data(COLLECTOR_DETAIL, key_id, secret, params)

async def collector_day(
self, key_id: str, secret: bytes, /, *,
collector_sn: int = None,
time: str,
time_zone: int,
) -> dict[str, str]:
"""Datalogger day statistics"""

SoliscloudAPI._verify_date(SoliscloudAPI.DateFormat.DAY, time)
params: dict[str, Any] = {
'time': time,
'timeZone': time_zone
}

if (collector_sn is None):
raise SoliscloudAPI.SolisCloudError(COL_SN_ERR)
params['sn'] = collector_sn

return await self._get_data(COLLECTOR_DAY, key_id, secret, params)

async def inverter_list(
self, key_id: str, secret: bytes, /, *,
page_no: int = 1,
Expand Down Expand Up @@ -420,7 +449,28 @@ async def inverter_all(

return await self._get_data(INVERTER_ALL, key_id, secret, params)

async def inverter_alarm_list(
async def inverter_shelf_time(
self, key_id: str, secret: bytes, /, *,
page_no: int = 1,
page_size: int = 20,
inverter_sn: str = None
) -> dict[str, str]:
"""Inverter warranty information"""

if page_size > 100:
raise SoliscloudAPI.SolisCloudError(PAGE_SIZE_ERR)
if inverter_sn is None:
raise SoliscloudAPI.SolisCloudError(INV_SN_ERR)

params: dict[str, Any] = {
'pageNo': page_no,
'pageSize': page_size,
'sn': inverter_sn}

return await self._get_records(
INVERTER_SHELF_TIME, key_id, secret, params)

async def alarm_list(
self, key_id: str, secret: bytes, /, *,
page_no: int = 1,
page_size: int = 20,
Expand Down Expand Up @@ -617,6 +667,40 @@ async def epm_all(

return await self._get_records(EPM_ALL, key_id, secret, params)

async def weather_list(
self, key_id: str, secret: bytes, /, *,
page_no: int = 1,
page_size: int = 20,
station_id: str = None,
nmi_code: str = None
) -> dict[str, str]:
"""Weather list"""

if page_size > 100:
raise SoliscloudAPI.SolisCloudError(PAGE_SIZE_ERR)

params: dict[str, Any] = {'pageNo': page_no, 'pageSize': page_size}
if station_id is not None:
# If not specified all inverters for all stations for key_id are
# returned
params['stationId'] = station_id
if nmi_code is not None:
params['nmiCode'] = nmi_code
return await self._get_records(WEATHER_LIST, key_id, secret, params)

async def weather_detail(
self, key_id: str, secret: bytes, /, *,
instrument_sn: str = None
) -> dict[str, str]:
"""Inverter details"""

params: dict[str, Any] = {}
if instrument_sn is None:
raise SoliscloudAPI.SolisCloudError(WEATHER_SN_ERR)
params['sn'] = instrument_sn

return await self._get_data(WEATHER_DETAIL, key_id, secret, params)

async def _get_records(
self, canonicalized_resource: str, key_id: str, secret: bytes,
params: dict[str, Any]
Expand Down
12 changes: 8 additions & 4 deletions test/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,21 @@ def test_soliscloud_error(mocker):

def test_api_error(mocker):
err = SoliscloudAPI.ApiError()
assert f"{err}" == 'API returned an error: Undefined API error occurred, error code: Unknown, response: None'
assert f"{err}" == 'API returned an error: \
Undefined API error occurred, error code: Unknown, response: None'
err = SoliscloudAPI.ApiError("TEST")
assert f"{err}" == 'API returned an error: TEST, error code: Unknown, response: None'
assert f"{err}" == 'API returned an error: \
TEST, error code: Unknown, response: None'
err = SoliscloudAPI.ApiError("TEST", 3, 1)
assert f"{err}" == 'API returned an error: TEST, error code: 3, response: 1'
assert f"{err}" == 'API returned an error: \
TEST, error code: 3, response: 1'


def test_http_error(mocker):
err = SoliscloudAPI.HttpError(408)
now = datetime.now().strftime("%d-%m-%Y %H:%M GMT")
assert f"{err}" == f'Your system time is different from server time, your time is {now}'
assert f"{err}" == f'Your system time is different from server time, \
your time is {now}'
err = SoliscloudAPI.HttpError(502)
assert f"{err}" == 'Http status code: 502'
err = SoliscloudAPI.HttpError(502, "TEST")
Expand Down
Loading
Loading