fronius-solarapi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fronius_solarapi/__init__.py +125 -0
- fronius_solarapi/client.py +411 -0
- fronius_solarapi/const.py +338 -0
- fronius_solarapi/device_types.py +227 -0
- fronius_solarapi/exceptions.py +68 -0
- fronius_solarapi/models.py +561 -0
- fronius_solarapi/parsers.py +705 -0
- fronius_solarapi/py.typed +0 -0
- fronius_solarapi-0.1.0.dist-info/METADATA +182 -0
- fronius_solarapi-0.1.0.dist-info/RECORD +12 -0
- fronius_solarapi-0.1.0.dist-info/WHEEL +4 -0
- fronius_solarapi-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Async client for the Fronius Solar API."""
|
|
2
|
+
|
|
3
|
+
from .client import FroniusSolarApi
|
|
4
|
+
from .const import (
|
|
5
|
+
ApiVersion,
|
|
6
|
+
BatteryMode,
|
|
7
|
+
BydBatteryState,
|
|
8
|
+
DeviceClass,
|
|
9
|
+
InverterState,
|
|
10
|
+
LedColor,
|
|
11
|
+
LedState,
|
|
12
|
+
LgChemBatteryState,
|
|
13
|
+
MeterCategory,
|
|
14
|
+
MeterLocation,
|
|
15
|
+
OhmpilotState,
|
|
16
|
+
PowerFlowMeterLocation,
|
|
17
|
+
PowerFlowMode,
|
|
18
|
+
PowerFlowOhmpilotEcoState,
|
|
19
|
+
PowerFlowOhmpilotState,
|
|
20
|
+
SolarBatteryCellState,
|
|
21
|
+
StatusCode,
|
|
22
|
+
battery_controller_state,
|
|
23
|
+
inverter_state,
|
|
24
|
+
meter_location,
|
|
25
|
+
solar_battery_cell_states,
|
|
26
|
+
)
|
|
27
|
+
from .device_types import INVERTER_DEVICE_TYPES, InverterModel, inverter_model
|
|
28
|
+
from .exceptions import (
|
|
29
|
+
EndpointNotFoundError,
|
|
30
|
+
FroniusConnectionError,
|
|
31
|
+
FroniusError,
|
|
32
|
+
InvalidResponseError,
|
|
33
|
+
NotSupportedError,
|
|
34
|
+
RequestNotSupportedError,
|
|
35
|
+
StatusError,
|
|
36
|
+
)
|
|
37
|
+
from .models import (
|
|
38
|
+
ActiveDevice,
|
|
39
|
+
ActiveDeviceInfo,
|
|
40
|
+
ApiVersionInfo,
|
|
41
|
+
CommonInverterData,
|
|
42
|
+
CumulationInverterData,
|
|
43
|
+
DeviceDetails,
|
|
44
|
+
DeviceStatus,
|
|
45
|
+
InverterInfo,
|
|
46
|
+
LedStatus,
|
|
47
|
+
LoggerInfo,
|
|
48
|
+
LoggerLedInfo,
|
|
49
|
+
MeterData,
|
|
50
|
+
OhmpilotData,
|
|
51
|
+
PowerFlow,
|
|
52
|
+
PowerFlowInverter,
|
|
53
|
+
PowerFlowOhmpilot,
|
|
54
|
+
PowerFlowOhmpilotEco,
|
|
55
|
+
PowerFlowSecondaryMeter,
|
|
56
|
+
PowerFlowSite,
|
|
57
|
+
Response,
|
|
58
|
+
ResponseHead,
|
|
59
|
+
ResponseStatus,
|
|
60
|
+
StorageController,
|
|
61
|
+
StorageData,
|
|
62
|
+
StorageModule,
|
|
63
|
+
ThreePhaseInverterData,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"INVERTER_DEVICE_TYPES",
|
|
68
|
+
"ActiveDevice",
|
|
69
|
+
"ActiveDeviceInfo",
|
|
70
|
+
"ApiVersion",
|
|
71
|
+
"ApiVersionInfo",
|
|
72
|
+
"BatteryMode",
|
|
73
|
+
"BydBatteryState",
|
|
74
|
+
"CommonInverterData",
|
|
75
|
+
"CumulationInverterData",
|
|
76
|
+
"DeviceClass",
|
|
77
|
+
"DeviceDetails",
|
|
78
|
+
"DeviceStatus",
|
|
79
|
+
"EndpointNotFoundError",
|
|
80
|
+
"FroniusConnectionError",
|
|
81
|
+
"FroniusError",
|
|
82
|
+
"FroniusSolarApi",
|
|
83
|
+
"InvalidResponseError",
|
|
84
|
+
"InverterInfo",
|
|
85
|
+
"InverterModel",
|
|
86
|
+
"InverterState",
|
|
87
|
+
"LedColor",
|
|
88
|
+
"LedState",
|
|
89
|
+
"LedStatus",
|
|
90
|
+
"LgChemBatteryState",
|
|
91
|
+
"LoggerInfo",
|
|
92
|
+
"LoggerLedInfo",
|
|
93
|
+
"MeterCategory",
|
|
94
|
+
"MeterData",
|
|
95
|
+
"MeterLocation",
|
|
96
|
+
"NotSupportedError",
|
|
97
|
+
"OhmpilotData",
|
|
98
|
+
"OhmpilotState",
|
|
99
|
+
"PowerFlow",
|
|
100
|
+
"PowerFlowInverter",
|
|
101
|
+
"PowerFlowMeterLocation",
|
|
102
|
+
"PowerFlowMode",
|
|
103
|
+
"PowerFlowOhmpilot",
|
|
104
|
+
"PowerFlowOhmpilotEco",
|
|
105
|
+
"PowerFlowOhmpilotEcoState",
|
|
106
|
+
"PowerFlowOhmpilotState",
|
|
107
|
+
"PowerFlowSecondaryMeter",
|
|
108
|
+
"PowerFlowSite",
|
|
109
|
+
"RequestNotSupportedError",
|
|
110
|
+
"Response",
|
|
111
|
+
"ResponseHead",
|
|
112
|
+
"ResponseStatus",
|
|
113
|
+
"SolarBatteryCellState",
|
|
114
|
+
"StatusCode",
|
|
115
|
+
"StatusError",
|
|
116
|
+
"StorageController",
|
|
117
|
+
"StorageData",
|
|
118
|
+
"StorageModule",
|
|
119
|
+
"ThreePhaseInverterData",
|
|
120
|
+
"battery_controller_state",
|
|
121
|
+
"inverter_model",
|
|
122
|
+
"inverter_state",
|
|
123
|
+
"meter_location",
|
|
124
|
+
"solar_battery_cell_states",
|
|
125
|
+
]
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""The Solar API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import Any, Final
|
|
10
|
+
|
|
11
|
+
import aiohttp
|
|
12
|
+
|
|
13
|
+
from .const import (
|
|
14
|
+
API_BASE_PATHS,
|
|
15
|
+
API_VERSION_PATH,
|
|
16
|
+
ApiVersion,
|
|
17
|
+
DeviceClass,
|
|
18
|
+
StatusCode,
|
|
19
|
+
)
|
|
20
|
+
from .exceptions import (
|
|
21
|
+
EndpointNotFoundError,
|
|
22
|
+
FroniusConnectionError,
|
|
23
|
+
InvalidResponseError,
|
|
24
|
+
NotSupportedError,
|
|
25
|
+
RequestNotSupportedError,
|
|
26
|
+
StatusError,
|
|
27
|
+
)
|
|
28
|
+
from .models import (
|
|
29
|
+
ActiveDeviceInfo,
|
|
30
|
+
ApiVersionInfo,
|
|
31
|
+
CommonInverterData,
|
|
32
|
+
CumulationInverterData,
|
|
33
|
+
InverterInfo,
|
|
34
|
+
LoggerInfo,
|
|
35
|
+
LoggerLedInfo,
|
|
36
|
+
MeterData,
|
|
37
|
+
OhmpilotData,
|
|
38
|
+
PowerFlow,
|
|
39
|
+
Response,
|
|
40
|
+
StorageData,
|
|
41
|
+
ThreePhaseInverterData,
|
|
42
|
+
)
|
|
43
|
+
from .parsers import (
|
|
44
|
+
JsonObject,
|
|
45
|
+
body_data,
|
|
46
|
+
parse_3p_inverter_data,
|
|
47
|
+
parse_active_device_info,
|
|
48
|
+
parse_active_devices,
|
|
49
|
+
parse_api_version,
|
|
50
|
+
parse_common_inverter_data,
|
|
51
|
+
parse_cumulation_inverter_data,
|
|
52
|
+
parse_head,
|
|
53
|
+
parse_inverter_info,
|
|
54
|
+
parse_logger_info,
|
|
55
|
+
parse_logger_led_info,
|
|
56
|
+
parse_meter_data,
|
|
57
|
+
parse_ohmpilot_data,
|
|
58
|
+
parse_power_flow,
|
|
59
|
+
parse_storage_data,
|
|
60
|
+
parse_system_inverter_data,
|
|
61
|
+
parse_system_meter_data,
|
|
62
|
+
parse_system_ohmpilot_data,
|
|
63
|
+
parse_system_storage_data,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
_LOGGER = logging.getLogger(__name__)
|
|
67
|
+
|
|
68
|
+
ALL_VERSIONS: Final = frozenset(ApiVersion)
|
|
69
|
+
V1_ONLY: Final = frozenset({ApiVersion.V1})
|
|
70
|
+
|
|
71
|
+
# the spec allows two realtime requests in parallel (section 2.6)
|
|
72
|
+
MAX_CONCURRENT_REQUESTS: Final = 2
|
|
73
|
+
DEFAULT_TIMEOUT: Final = 10.0
|
|
74
|
+
|
|
75
|
+
# Datamanagers reach inverters, meters and storages over Solar Net, whose
|
|
76
|
+
# requests fail now and then and succeed when repeated (about 1 % of inverter
|
|
77
|
+
# requests on Symo devices end in LNRequestTimeout).
|
|
78
|
+
RETRY_STATUS_CODES: Final = frozenset(
|
|
79
|
+
{
|
|
80
|
+
StatusCode.LN_REQUEST_ERROR,
|
|
81
|
+
StatusCode.LN_REQUEST_TIMEOUT,
|
|
82
|
+
StatusCode.LN_PARSE_ERROR,
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
DEFAULT_STATUS_RETRIES: Final = 2
|
|
86
|
+
DEFAULT_RETRY_DELAY: Final = 0.5
|
|
87
|
+
|
|
88
|
+
_DEVICE_ID_PARAM: Final = {ApiVersion.V0: "DeviceIndex", ApiVersion.V1: "DeviceId"}
|
|
89
|
+
_V0_ACTIVE_DEVICE_CLASSES: Final = (
|
|
90
|
+
(DeviceClass.INVERTER, "inverters"),
|
|
91
|
+
(DeviceClass.SENSOR_CARD, "sensor_cards"),
|
|
92
|
+
(DeviceClass.STRING_CONTROL, "string_controls"),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class FroniusSolarApi:
|
|
97
|
+
"""Client for the Solar API of one Fronius device.
|
|
98
|
+
|
|
99
|
+
The connection lifecycle stays with the caller: pass an
|
|
100
|
+
`aiohttp.ClientSession`. GEN24 firmware 1.35 and later redirects requests
|
|
101
|
+
from non-private IP addresses to HTTPS with a self-signed certificate, so a
|
|
102
|
+
session reaching such a device needs to be created with SSL verification
|
|
103
|
+
disabled.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
session: aiohttp.ClientSession,
|
|
109
|
+
host: str,
|
|
110
|
+
*,
|
|
111
|
+
api_version: ApiVersion | None = None,
|
|
112
|
+
timeout: float | aiohttp.ClientTimeout = DEFAULT_TIMEOUT,
|
|
113
|
+
status_retries: int = DEFAULT_STATUS_RETRIES,
|
|
114
|
+
retry_delay: float = DEFAULT_RETRY_DELAY,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Initialize the client.
|
|
117
|
+
|
|
118
|
+
`host` is a host name, an IP address or a full URL. Without a scheme
|
|
119
|
+
plain HTTP is used, which is all most devices support. Without
|
|
120
|
+
`api_version` the highest version the device supports is used; it is
|
|
121
|
+
queried once on the first request.
|
|
122
|
+
|
|
123
|
+
A request answered with a transient Solar Net status code (see
|
|
124
|
+
`RETRY_STATUS_CODES`) is repeated up to `status_retries` times, after
|
|
125
|
+
waiting `retry_delay` seconds. Connection errors and timeouts are not
|
|
126
|
+
retried.
|
|
127
|
+
"""
|
|
128
|
+
if status_retries < 0:
|
|
129
|
+
raise ValueError("status_retries must not be negative")
|
|
130
|
+
if retry_delay < 0:
|
|
131
|
+
raise ValueError("retry_delay must not be negative")
|
|
132
|
+
self._session = session
|
|
133
|
+
url = host.rstrip("/")
|
|
134
|
+
if "://" not in url:
|
|
135
|
+
url = f"http://{url}"
|
|
136
|
+
self._url = url
|
|
137
|
+
self._api_version = api_version
|
|
138
|
+
self._timeout = (
|
|
139
|
+
timeout
|
|
140
|
+
if isinstance(timeout, aiohttp.ClientTimeout)
|
|
141
|
+
else aiohttp.ClientTimeout(total=timeout)
|
|
142
|
+
)
|
|
143
|
+
self._semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS)
|
|
144
|
+
self._status_retries = status_retries
|
|
145
|
+
self._retry_delay = retry_delay
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def url(self) -> str:
|
|
149
|
+
"""Return the base URL of the device, e.g. "http://192.168.0.10"."""
|
|
150
|
+
return self._url
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def api_version(self) -> ApiVersion | None:
|
|
154
|
+
"""Return the API version in use, `None` before it has been resolved."""
|
|
155
|
+
return self._api_version
|
|
156
|
+
|
|
157
|
+
async def _get_json(self, path: str, params: dict[str, str] | None = None) -> Any:
|
|
158
|
+
url = f"{self._url}{path}"
|
|
159
|
+
_LOGGER.debug("Requesting %s %s", url, params or "")
|
|
160
|
+
try:
|
|
161
|
+
async with (
|
|
162
|
+
self._semaphore,
|
|
163
|
+
self._session.get(url, params=params, timeout=self._timeout) as res,
|
|
164
|
+
):
|
|
165
|
+
if res.status == 404:
|
|
166
|
+
raise EndpointNotFoundError(f"{url} not found (HTTP 404)")
|
|
167
|
+
if res.status >= 400:
|
|
168
|
+
raise InvalidResponseError(f"{url} answered HTTP {res.status}")
|
|
169
|
+
text = await res.text()
|
|
170
|
+
except TimeoutError as err:
|
|
171
|
+
raise FroniusConnectionError(f"Request to {url} timed out") from err
|
|
172
|
+
except aiohttp.ClientError as err:
|
|
173
|
+
raise FroniusConnectionError(f"Request to {url} failed: {err}") from err
|
|
174
|
+
# the content type is text/javascript on some devices, so it is not checked
|
|
175
|
+
try:
|
|
176
|
+
return json.loads(text)
|
|
177
|
+
except json.JSONDecodeError as err:
|
|
178
|
+
raise InvalidResponseError(
|
|
179
|
+
f"{url} answered non-JSON: {text[:80]!r}"
|
|
180
|
+
) from err
|
|
181
|
+
|
|
182
|
+
async def fetch_api_version(self) -> ApiVersionInfo:
|
|
183
|
+
"""Query the highest API version the device supports.
|
|
184
|
+
|
|
185
|
+
Devices only serving API v0 answer with HTTP 404 (spec v0 section
|
|
186
|
+
2.3.1), which is reported as version 0.
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
raw = await self._get_json(API_VERSION_PATH)
|
|
190
|
+
except EndpointNotFoundError:
|
|
191
|
+
return ApiVersionInfo(
|
|
192
|
+
api_version=ApiVersion.V0,
|
|
193
|
+
base_url=API_BASE_PATHS[ApiVersion.V0],
|
|
194
|
+
compatibility_range=None,
|
|
195
|
+
)
|
|
196
|
+
return parse_api_version(raw)
|
|
197
|
+
|
|
198
|
+
async def _resolve_api_version(self) -> ApiVersion:
|
|
199
|
+
if self._api_version is None:
|
|
200
|
+
info = await self.fetch_api_version()
|
|
201
|
+
_LOGGER.debug(
|
|
202
|
+
"%s serves Solar API v%s (%s)",
|
|
203
|
+
self._url,
|
|
204
|
+
info.api_version,
|
|
205
|
+
info.compatibility_range,
|
|
206
|
+
)
|
|
207
|
+
self._api_version = info.api_version
|
|
208
|
+
return self._api_version
|
|
209
|
+
|
|
210
|
+
async def _request[T](
|
|
211
|
+
self,
|
|
212
|
+
endpoint: str,
|
|
213
|
+
parse: Callable[[JsonObject], T],
|
|
214
|
+
params: dict[str, str] | None = None,
|
|
215
|
+
*,
|
|
216
|
+
device_id: str | int | None = None,
|
|
217
|
+
versions: frozenset[ApiVersion] = ALL_VERSIONS,
|
|
218
|
+
) -> Response[T]:
|
|
219
|
+
api_version = await self._resolve_api_version()
|
|
220
|
+
if api_version not in versions:
|
|
221
|
+
raise NotSupportedError(
|
|
222
|
+
f"{endpoint} is not available on Solar API v{api_version}"
|
|
223
|
+
)
|
|
224
|
+
query = dict(params or {})
|
|
225
|
+
if device_id is not None:
|
|
226
|
+
# the device id has to follow the scope parameter
|
|
227
|
+
query[_DEVICE_ID_PARAM[api_version]] = str(device_id)
|
|
228
|
+
if "DataCollection" in query:
|
|
229
|
+
query["DataCollection"] = query.pop("DataCollection")
|
|
230
|
+
path = f"{API_BASE_PATHS[api_version]}{endpoint}"
|
|
231
|
+
for retry in range(self._status_retries + 1):
|
|
232
|
+
if retry:
|
|
233
|
+
# the delay is spent outside the concurrency limit
|
|
234
|
+
await asyncio.sleep(self._retry_delay)
|
|
235
|
+
raw = await self._get_json(path, query)
|
|
236
|
+
head = parse_head(raw)
|
|
237
|
+
if head.status.code not in RETRY_STATUS_CODES:
|
|
238
|
+
break
|
|
239
|
+
_LOGGER.debug(
|
|
240
|
+
"%s answered status %s (%s), attempt %s of %s",
|
|
241
|
+
endpoint,
|
|
242
|
+
head.status.code,
|
|
243
|
+
head.status.reason,
|
|
244
|
+
retry + 1,
|
|
245
|
+
self._status_retries + 1,
|
|
246
|
+
)
|
|
247
|
+
if head.status.code != StatusCode.OKAY:
|
|
248
|
+
if head.status.code == StatusCode.NOT_SUPPORTED or (
|
|
249
|
+
head.status.code == StatusCode.UNKNOWN_ERROR
|
|
250
|
+
and "not supported" in head.status.reason.lower()
|
|
251
|
+
):
|
|
252
|
+
raise RequestNotSupportedError(endpoint, head)
|
|
253
|
+
raise StatusError(endpoint, head)
|
|
254
|
+
return Response(head=head, data=parse(body_data(raw)))
|
|
255
|
+
|
|
256
|
+
async def inverter_info(self) -> Response[dict[str, InverterInfo]]:
|
|
257
|
+
"""Return all inverters seen by the logging device, keyed by device id.
|
|
258
|
+
|
|
259
|
+
Inverters that have been offline for less than 24 hours are included.
|
|
260
|
+
"""
|
|
261
|
+
return await self._request("GetInverterInfo.cgi", parse_inverter_info)
|
|
262
|
+
|
|
263
|
+
async def logger_info(self) -> Response[LoggerInfo]:
|
|
264
|
+
"""Return information about the logging device.
|
|
265
|
+
|
|
266
|
+
GEN24/Tauro/Verto devices report the request as not supported.
|
|
267
|
+
"""
|
|
268
|
+
return await self._request("GetLoggerInfo.cgi", parse_logger_info)
|
|
269
|
+
|
|
270
|
+
async def logger_led_info(self) -> Response[LoggerLedInfo]:
|
|
271
|
+
"""Return the states of the LEDs of the logging device.
|
|
272
|
+
|
|
273
|
+
GEN24/Tauro/Verto devices report the request as not supported.
|
|
274
|
+
"""
|
|
275
|
+
return await self._request(
|
|
276
|
+
"GetLoggerLEDInfo.cgi", parse_logger_led_info, versions=V1_ONLY
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
async def active_device_info(self) -> Response[ActiveDeviceInfo]:
|
|
280
|
+
"""Return the devices that are currently online.
|
|
281
|
+
|
|
282
|
+
API v0 has no system scope, so one request per device class it knows
|
|
283
|
+
is made and the header of the first one is returned.
|
|
284
|
+
"""
|
|
285
|
+
if await self._resolve_api_version() is ApiVersion.V1:
|
|
286
|
+
return await self._request(
|
|
287
|
+
"GetActiveDeviceInfo.cgi",
|
|
288
|
+
parse_active_device_info,
|
|
289
|
+
{"DeviceClass": DeviceClass.SYSTEM},
|
|
290
|
+
)
|
|
291
|
+
classes: dict[str, Any] = {
|
|
292
|
+
"meters": {},
|
|
293
|
+
"ohmpilots": {},
|
|
294
|
+
"storages": {},
|
|
295
|
+
}
|
|
296
|
+
head = None
|
|
297
|
+
for device_class, attribute in _V0_ACTIVE_DEVICE_CLASSES:
|
|
298
|
+
response = await self._request(
|
|
299
|
+
"GetActiveDeviceInfo.cgi",
|
|
300
|
+
parse_active_devices,
|
|
301
|
+
{"DeviceClass": device_class},
|
|
302
|
+
)
|
|
303
|
+
head = head or response.head
|
|
304
|
+
classes[attribute] = response.data
|
|
305
|
+
if head is None: # pragma: no cover - the loop always runs
|
|
306
|
+
raise InvalidResponseError("No active device info received")
|
|
307
|
+
return Response(head=head, data=ActiveDeviceInfo(**classes))
|
|
308
|
+
|
|
309
|
+
async def inverter_common_data(
|
|
310
|
+
self, device_id: str | int
|
|
311
|
+
) -> Response[CommonInverterData]:
|
|
312
|
+
"""Return the `CommonInverterData` collection of one inverter."""
|
|
313
|
+
return await self._request(
|
|
314
|
+
"GetInverterRealtimeData.cgi",
|
|
315
|
+
parse_common_inverter_data,
|
|
316
|
+
{"Scope": "Device", "DataCollection": "CommonInverterData"},
|
|
317
|
+
device_id=device_id,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
async def inverter_3p_data(
|
|
321
|
+
self, device_id: str | int
|
|
322
|
+
) -> Response[ThreePhaseInverterData]:
|
|
323
|
+
"""Return the `3PInverterData` collection of one three-phase inverter."""
|
|
324
|
+
return await self._request(
|
|
325
|
+
"GetInverterRealtimeData.cgi",
|
|
326
|
+
parse_3p_inverter_data,
|
|
327
|
+
{"Scope": "Device", "DataCollection": "3PInverterData"},
|
|
328
|
+
device_id=device_id,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
async def inverter_cumulation_data(
|
|
332
|
+
self, device_id: str | int
|
|
333
|
+
) -> Response[CumulationInverterData]:
|
|
334
|
+
"""Return the `CumulationInverterData` collection of one inverter."""
|
|
335
|
+
return await self._request(
|
|
336
|
+
"GetInverterRealtimeData.cgi",
|
|
337
|
+
parse_cumulation_inverter_data,
|
|
338
|
+
{"Scope": "Device", "DataCollection": "CumulationInverterData"},
|
|
339
|
+
device_id=device_id,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
async def system_inverter_data(self) -> Response[dict[str, CumulationInverterData]]:
|
|
343
|
+
"""Return the cumulation values of all inverters, keyed by device id."""
|
|
344
|
+
return await self._request(
|
|
345
|
+
"GetInverterRealtimeData.cgi",
|
|
346
|
+
parse_system_inverter_data,
|
|
347
|
+
{"Scope": "System"},
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
async def meter_data(self, device_id: str | int) -> Response[MeterData]:
|
|
351
|
+
"""Return the data of one meter."""
|
|
352
|
+
return await self._request(
|
|
353
|
+
"GetMeterRealtimeData.cgi",
|
|
354
|
+
parse_meter_data,
|
|
355
|
+
{"Scope": "Device"},
|
|
356
|
+
device_id=device_id,
|
|
357
|
+
versions=V1_ONLY,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
async def system_meter_data(self) -> Response[dict[str, MeterData]]:
|
|
361
|
+
"""Return the data of all meters, keyed by device id."""
|
|
362
|
+
return await self._request(
|
|
363
|
+
"GetMeterRealtimeData.cgi",
|
|
364
|
+
parse_system_meter_data,
|
|
365
|
+
{"Scope": "System"},
|
|
366
|
+
versions=V1_ONLY,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
async def storage_data(self, device_id: str | int) -> Response[StorageData]:
|
|
370
|
+
"""Return the data of one storage."""
|
|
371
|
+
return await self._request(
|
|
372
|
+
"GetStorageRealtimeData.cgi",
|
|
373
|
+
parse_storage_data,
|
|
374
|
+
{"Scope": "Device"},
|
|
375
|
+
device_id=device_id,
|
|
376
|
+
versions=V1_ONLY,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
async def system_storage_data(self) -> Response[dict[str, StorageData]]:
|
|
380
|
+
"""Return the data of all storages, keyed by device id."""
|
|
381
|
+
return await self._request(
|
|
382
|
+
"GetStorageRealtimeData.cgi",
|
|
383
|
+
parse_system_storage_data,
|
|
384
|
+
{"Scope": "System"},
|
|
385
|
+
versions=V1_ONLY,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
async def ohmpilot_data(self, device_id: str | int) -> Response[OhmpilotData]:
|
|
389
|
+
"""Return the data of one Ohmpilot."""
|
|
390
|
+
return await self._request(
|
|
391
|
+
"GetOhmPilotRealtimeData.cgi",
|
|
392
|
+
parse_ohmpilot_data,
|
|
393
|
+
{"Scope": "Device"},
|
|
394
|
+
device_id=device_id,
|
|
395
|
+
versions=V1_ONLY,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
async def system_ohmpilot_data(self) -> Response[dict[str, OhmpilotData]]:
|
|
399
|
+
"""Return the data of all Ohmpilots, keyed by device id."""
|
|
400
|
+
return await self._request(
|
|
401
|
+
"GetOhmPilotRealtimeData.cgi",
|
|
402
|
+
parse_system_ohmpilot_data,
|
|
403
|
+
{"Scope": "System"},
|
|
404
|
+
versions=V1_ONLY,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
async def power_flow(self) -> Response[PowerFlow]:
|
|
408
|
+
"""Return the current power flow of the site."""
|
|
409
|
+
return await self._request(
|
|
410
|
+
"GetPowerFlowRealtimeData.fcgi", parse_power_flow, versions=V1_ONLY
|
|
411
|
+
)
|