python-siseli 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.
- python_siseli-0.1.0.dist-info/METADATA +57 -0
- python_siseli-0.1.0.dist-info/RECORD +29 -0
- python_siseli-0.1.0.dist-info/WHEEL +5 -0
- python_siseli-0.1.0.dist-info/top_level.txt +1 -0
- siseli/__init__.py +51 -0
- siseli/_utils.py +29 -0
- siseli/alarms.py +159 -0
- siseli/auth.py +96 -0
- siseli/client.py +474 -0
- siseli/config.py +112 -0
- siseli/const.py +5 -0
- siseli/dashboard.py +100 -0
- siseli/device.py +89 -0
- siseli/dictionary.py +37 -0
- siseli/exceptions.py +28 -0
- siseli/history.py +123 -0
- siseli/models/__init__.py +50 -0
- siseli/models/alarm.py +41 -0
- siseli/models/auth.py +19 -0
- siseli/models/common.py +51 -0
- siseli/models/config.py +28 -0
- siseli/models/dashboard.py +45 -0
- siseli/models/device.py +29 -0
- siseli/models/dictionary.py +18 -0
- siseli/models/history.py +34 -0
- siseli/models/state.py +106 -0
- siseli/models/station.py +82 -0
- siseli/state.py +184 -0
- siseli/station.py +181 -0
siseli/client.py
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""SiseliClient — the main public entry point for the SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Iterable
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .alarms import (
|
|
11
|
+
fetch_alarm_list,
|
|
12
|
+
fetch_alarm_report_details,
|
|
13
|
+
fetch_alarm_report_headers,
|
|
14
|
+
fetch_alarm_reports,
|
|
15
|
+
fetch_latest_alarm,
|
|
16
|
+
)
|
|
17
|
+
from .auth import Auth
|
|
18
|
+
from .config import (
|
|
19
|
+
fetch_cached_device_configs,
|
|
20
|
+
fetch_device_config,
|
|
21
|
+
fetch_device_config_batch_details,
|
|
22
|
+
fetch_device_configs,
|
|
23
|
+
write_device_config,
|
|
24
|
+
)
|
|
25
|
+
from .const import BASE_URL, DEFAULT_DATA_SOURCE, DEFAULT_PAGE_SIZE, DEFAULT_TIMEOUT
|
|
26
|
+
from .dashboard import (
|
|
27
|
+
fetch_dashboard_daily_generation_time_rank,
|
|
28
|
+
fetch_dashboard_monthly_generated_energy,
|
|
29
|
+
fetch_dashboard_station_distribution,
|
|
30
|
+
fetch_dashboard_summary,
|
|
31
|
+
)
|
|
32
|
+
from .device import fetch_device_details, fetch_device_list
|
|
33
|
+
from .dictionary import fetch_dictionary
|
|
34
|
+
from .exceptions import ApiError, NetworkError
|
|
35
|
+
from .history import fetch_attribute_history, fetch_state_history
|
|
36
|
+
from .models.alarm import Alarm, AlarmReport
|
|
37
|
+
from .models.common import PagedResult, TimePoint
|
|
38
|
+
from .models.config import ConfigBatchRead
|
|
39
|
+
from .models.dashboard import DashboardSummary, LocationDistribution, StationRankEntry
|
|
40
|
+
from .models.device import Device
|
|
41
|
+
from .models.dictionary import DictionaryData
|
|
42
|
+
from .models.history import HistorySeries
|
|
43
|
+
from .models.state import AttributeGroupSet, AttributeMetadata, DeviceState, EnergyFlow
|
|
44
|
+
from .models.station import Station, StationEnergyFlow, StationSummary
|
|
45
|
+
from .state import (
|
|
46
|
+
fetch_device_attribute_groups,
|
|
47
|
+
fetch_device_attributes,
|
|
48
|
+
fetch_device_state,
|
|
49
|
+
fetch_energy_flow,
|
|
50
|
+
)
|
|
51
|
+
from .station import (
|
|
52
|
+
fetch_station_details,
|
|
53
|
+
fetch_station_energy_flow,
|
|
54
|
+
fetch_station_income,
|
|
55
|
+
fetch_station_list,
|
|
56
|
+
fetch_station_state_summary,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SiseliClient:
|
|
61
|
+
"""Async client for the Siseli Cloud API."""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
account: str,
|
|
66
|
+
password: str,
|
|
67
|
+
*,
|
|
68
|
+
base_url: str = BASE_URL,
|
|
69
|
+
timezone: str = "UTC",
|
|
70
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
71
|
+
) -> None:
|
|
72
|
+
self._auth = Auth(account, password)
|
|
73
|
+
self._timezone = timezone
|
|
74
|
+
self._http = httpx.AsyncClient(
|
|
75
|
+
base_url=base_url,
|
|
76
|
+
timeout=timeout,
|
|
77
|
+
headers={"Accept": "application/json"},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
async def authenticate(self) -> None:
|
|
81
|
+
await self._auth.login(self._http)
|
|
82
|
+
|
|
83
|
+
async def close(self) -> None:
|
|
84
|
+
await self._http.aclose()
|
|
85
|
+
|
|
86
|
+
async def __aenter__(self) -> SiseliClient:
|
|
87
|
+
await self.authenticate()
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
91
|
+
await self.close()
|
|
92
|
+
|
|
93
|
+
async def _ensure_authenticated(self) -> None:
|
|
94
|
+
if not self._auth.is_authenticated():
|
|
95
|
+
await self._auth.login(self._http)
|
|
96
|
+
|
|
97
|
+
async def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
98
|
+
await self._ensure_authenticated()
|
|
99
|
+
auth_headers = {
|
|
100
|
+
"IOT-Token": self._auth.access_token,
|
|
101
|
+
"IOT-Time-Zone": self._timezone,
|
|
102
|
+
}
|
|
103
|
+
try:
|
|
104
|
+
response = await self._http.request(
|
|
105
|
+
method, path, headers=auth_headers, **kwargs
|
|
106
|
+
)
|
|
107
|
+
response.raise_for_status()
|
|
108
|
+
except httpx.HTTPStatusError as exc:
|
|
109
|
+
raise NetworkError(
|
|
110
|
+
f"HTTP {exc.response.status_code} for {method} {path}"
|
|
111
|
+
) from exc
|
|
112
|
+
except httpx.HTTPError as exc:
|
|
113
|
+
raise NetworkError(f"Request failed for {method} {path}: {exc}") from exc
|
|
114
|
+
|
|
115
|
+
body = response.json()
|
|
116
|
+
if body.get("code") != 0:
|
|
117
|
+
raise ApiError(body.get("code", -1), body.get("message", ""))
|
|
118
|
+
return body.get("data")
|
|
119
|
+
|
|
120
|
+
async def get_devices(
|
|
121
|
+
self,
|
|
122
|
+
*,
|
|
123
|
+
page: int = 1,
|
|
124
|
+
count: int = DEFAULT_PAGE_SIZE,
|
|
125
|
+
name: str = "",
|
|
126
|
+
serial_number: str = "",
|
|
127
|
+
station_id: str = "",
|
|
128
|
+
state: str = "",
|
|
129
|
+
) -> list[Device]:
|
|
130
|
+
devices, _ = await fetch_device_list(
|
|
131
|
+
self._request,
|
|
132
|
+
page=page,
|
|
133
|
+
count=count,
|
|
134
|
+
name=name,
|
|
135
|
+
serial_number=serial_number,
|
|
136
|
+
station_id=station_id,
|
|
137
|
+
state=state,
|
|
138
|
+
)
|
|
139
|
+
return devices
|
|
140
|
+
|
|
141
|
+
async def get_all_devices(self) -> list[Device]:
|
|
142
|
+
all_devices: list[Device] = []
|
|
143
|
+
page = 1
|
|
144
|
+
while True:
|
|
145
|
+
devices, total = await fetch_device_list(
|
|
146
|
+
self._request,
|
|
147
|
+
page=page,
|
|
148
|
+
count=DEFAULT_PAGE_SIZE,
|
|
149
|
+
)
|
|
150
|
+
all_devices.extend(devices)
|
|
151
|
+
if len(all_devices) >= total:
|
|
152
|
+
break
|
|
153
|
+
page += 1
|
|
154
|
+
return all_devices
|
|
155
|
+
|
|
156
|
+
async def get_device(self, device_id: str) -> Device:
|
|
157
|
+
return await fetch_device_details(self._request, device_id)
|
|
158
|
+
|
|
159
|
+
async def get_device_state(
|
|
160
|
+
self,
|
|
161
|
+
device_id: str,
|
|
162
|
+
*,
|
|
163
|
+
data_source: int = DEFAULT_DATA_SOURCE,
|
|
164
|
+
) -> DeviceState:
|
|
165
|
+
return await fetch_device_state(
|
|
166
|
+
self._request, device_id, data_source=data_source
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
async def get_device_attributes(
|
|
170
|
+
self,
|
|
171
|
+
device_id: str,
|
|
172
|
+
*,
|
|
173
|
+
category: str = "",
|
|
174
|
+
render_in: str = "",
|
|
175
|
+
) -> list[AttributeMetadata]:
|
|
176
|
+
return await fetch_device_attributes(
|
|
177
|
+
self._request,
|
|
178
|
+
device_id,
|
|
179
|
+
category=category,
|
|
180
|
+
render_in=render_in,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
async def get_device_attribute_groups(
|
|
184
|
+
self,
|
|
185
|
+
device_id: str,
|
|
186
|
+
*,
|
|
187
|
+
category: str = "",
|
|
188
|
+
render_in: str = "",
|
|
189
|
+
) -> AttributeGroupSet:
|
|
190
|
+
return await fetch_device_attribute_groups(
|
|
191
|
+
self._request,
|
|
192
|
+
device_id,
|
|
193
|
+
category=category,
|
|
194
|
+
render_in=render_in,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
async def get_energy_flow(
|
|
198
|
+
self,
|
|
199
|
+
device_id: str,
|
|
200
|
+
*,
|
|
201
|
+
data_source: int = DEFAULT_DATA_SOURCE,
|
|
202
|
+
) -> EnergyFlow:
|
|
203
|
+
return await fetch_energy_flow(
|
|
204
|
+
self._request, device_id, data_source=data_source
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
async def get_attribute_history(
|
|
208
|
+
self,
|
|
209
|
+
device_id: str,
|
|
210
|
+
keys: Iterable[str],
|
|
211
|
+
*,
|
|
212
|
+
from_time: datetime | str | None = None,
|
|
213
|
+
to_time: datetime | str | None = None,
|
|
214
|
+
page: int = 1,
|
|
215
|
+
count: int = DEFAULT_PAGE_SIZE,
|
|
216
|
+
order_by_time_asc: bool = True,
|
|
217
|
+
) -> HistorySeries:
|
|
218
|
+
return await fetch_attribute_history(
|
|
219
|
+
self._request,
|
|
220
|
+
device_id,
|
|
221
|
+
keys,
|
|
222
|
+
from_time=from_time,
|
|
223
|
+
to_time=to_time,
|
|
224
|
+
page=page,
|
|
225
|
+
count=count,
|
|
226
|
+
order_by_time_asc=order_by_time_asc,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
async def get_state_history(
|
|
230
|
+
self,
|
|
231
|
+
device_id: str,
|
|
232
|
+
*,
|
|
233
|
+
from_time: datetime | str | None = None,
|
|
234
|
+
to_time: datetime | str | None = None,
|
|
235
|
+
page: int = 1,
|
|
236
|
+
count: int = DEFAULT_PAGE_SIZE,
|
|
237
|
+
order_by_time_asc: bool = False,
|
|
238
|
+
) -> HistorySeries:
|
|
239
|
+
return await fetch_state_history(
|
|
240
|
+
self._request,
|
|
241
|
+
device_id,
|
|
242
|
+
from_time=from_time,
|
|
243
|
+
to_time=to_time,
|
|
244
|
+
page=page,
|
|
245
|
+
count=count,
|
|
246
|
+
order_by_time_asc=order_by_time_asc,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
async def get_device_config(
|
|
250
|
+
self,
|
|
251
|
+
device_id: str,
|
|
252
|
+
*,
|
|
253
|
+
key: str = "",
|
|
254
|
+
config_id: str = "",
|
|
255
|
+
) -> AttributeMetadata:
|
|
256
|
+
return await fetch_device_config(
|
|
257
|
+
self._request,
|
|
258
|
+
device_id,
|
|
259
|
+
key=key,
|
|
260
|
+
config_id=config_id,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
async def get_cached_device_configs(
|
|
264
|
+
self,
|
|
265
|
+
device_id: str,
|
|
266
|
+
) -> dict[str, AttributeMetadata]:
|
|
267
|
+
return await fetch_cached_device_configs(self._request, device_id)
|
|
268
|
+
|
|
269
|
+
async def read_device_configs(self, device_id: str) -> ConfigBatchRead:
|
|
270
|
+
return await fetch_device_configs(self._request, device_id)
|
|
271
|
+
|
|
272
|
+
async def get_device_config_batch(self, batch_read_id: str) -> ConfigBatchRead:
|
|
273
|
+
return await fetch_device_config_batch_details(self._request, batch_read_id)
|
|
274
|
+
|
|
275
|
+
async def set_device_config(
|
|
276
|
+
self,
|
|
277
|
+
device_id: str,
|
|
278
|
+
*,
|
|
279
|
+
key: str,
|
|
280
|
+
value: Any,
|
|
281
|
+
config_id: str = "",
|
|
282
|
+
) -> AttributeMetadata:
|
|
283
|
+
return await write_device_config(
|
|
284
|
+
self._request,
|
|
285
|
+
device_id,
|
|
286
|
+
key=key,
|
|
287
|
+
value=value,
|
|
288
|
+
config_id=config_id,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
async def get_stations(
|
|
292
|
+
self,
|
|
293
|
+
*,
|
|
294
|
+
page: int = 1,
|
|
295
|
+
count: int = DEFAULT_PAGE_SIZE,
|
|
296
|
+
name: str = "",
|
|
297
|
+
connected_grid_type: str = "",
|
|
298
|
+
state: str = "",
|
|
299
|
+
station_type: str = "",
|
|
300
|
+
) -> list[Station]:
|
|
301
|
+
result = await fetch_station_list(
|
|
302
|
+
self._request,
|
|
303
|
+
page=page,
|
|
304
|
+
count=count,
|
|
305
|
+
name=name,
|
|
306
|
+
connected_grid_type=connected_grid_type,
|
|
307
|
+
state=state,
|
|
308
|
+
station_type=station_type,
|
|
309
|
+
)
|
|
310
|
+
return result.items
|
|
311
|
+
|
|
312
|
+
async def get_all_stations(self) -> list[Station]:
|
|
313
|
+
all_stations: list[Station] = []
|
|
314
|
+
page = 1
|
|
315
|
+
while True:
|
|
316
|
+
result = await fetch_station_list(
|
|
317
|
+
self._request,
|
|
318
|
+
page=page,
|
|
319
|
+
count=DEFAULT_PAGE_SIZE,
|
|
320
|
+
)
|
|
321
|
+
all_stations.extend(result.items)
|
|
322
|
+
if len(all_stations) >= result.total:
|
|
323
|
+
break
|
|
324
|
+
page += 1
|
|
325
|
+
return all_stations
|
|
326
|
+
|
|
327
|
+
async def get_station(self, station_id: str) -> Station:
|
|
328
|
+
return await fetch_station_details(self._request, station_id)
|
|
329
|
+
|
|
330
|
+
async def get_station_energy_flow(
|
|
331
|
+
self,
|
|
332
|
+
station_id: str,
|
|
333
|
+
*,
|
|
334
|
+
is_manual_refresh: bool = False,
|
|
335
|
+
) -> StationEnergyFlow:
|
|
336
|
+
return await fetch_station_energy_flow(
|
|
337
|
+
self._request,
|
|
338
|
+
station_id,
|
|
339
|
+
is_manual_refresh=is_manual_refresh,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
async def get_station_income(
|
|
343
|
+
self,
|
|
344
|
+
station_id: str,
|
|
345
|
+
aggregation: str,
|
|
346
|
+
*,
|
|
347
|
+
time: datetime | str | None = None,
|
|
348
|
+
) -> list[TimePoint]:
|
|
349
|
+
return await fetch_station_income(
|
|
350
|
+
self._request,
|
|
351
|
+
station_id,
|
|
352
|
+
aggregation,
|
|
353
|
+
time=time,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
async def get_station_summary(
|
|
357
|
+
self,
|
|
358
|
+
station_id: str,
|
|
359
|
+
summary_category_key: str,
|
|
360
|
+
aggregation: str,
|
|
361
|
+
*,
|
|
362
|
+
time: datetime | str | None = None,
|
|
363
|
+
) -> StationSummary:
|
|
364
|
+
return await fetch_station_state_summary(
|
|
365
|
+
self._request,
|
|
366
|
+
station_id,
|
|
367
|
+
summary_category_key,
|
|
368
|
+
aggregation,
|
|
369
|
+
time=time,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
async def get_latest_alarm(
|
|
373
|
+
self,
|
|
374
|
+
*,
|
|
375
|
+
certificate_dtu_id: str = "",
|
|
376
|
+
device_serial_number: str = "",
|
|
377
|
+
page: int = 1,
|
|
378
|
+
count: int = 1,
|
|
379
|
+
) -> Alarm | None:
|
|
380
|
+
return await fetch_latest_alarm(
|
|
381
|
+
self._request,
|
|
382
|
+
certificate_dtu_id=certificate_dtu_id,
|
|
383
|
+
device_serial_number=device_serial_number,
|
|
384
|
+
page=page,
|
|
385
|
+
count=count,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
async def get_alarms(
|
|
389
|
+
self,
|
|
390
|
+
*,
|
|
391
|
+
page: int = 1,
|
|
392
|
+
count: int = DEFAULT_PAGE_SIZE,
|
|
393
|
+
certificate_dtu_id: str = "",
|
|
394
|
+
device_serial_number: str = "",
|
|
395
|
+
from_time: datetime | str | None = None,
|
|
396
|
+
to_time: datetime | str | None = None,
|
|
397
|
+
is_processed: int | None = None,
|
|
398
|
+
level: int | None = None,
|
|
399
|
+
order_by_created_time_desc: bool = True,
|
|
400
|
+
) -> PagedResult[Alarm]:
|
|
401
|
+
return await fetch_alarm_list(
|
|
402
|
+
self._request,
|
|
403
|
+
page=page,
|
|
404
|
+
count=count,
|
|
405
|
+
certificate_dtu_id=certificate_dtu_id,
|
|
406
|
+
device_serial_number=device_serial_number,
|
|
407
|
+
from_time=from_time,
|
|
408
|
+
to_time=to_time,
|
|
409
|
+
is_processed=is_processed,
|
|
410
|
+
level=level,
|
|
411
|
+
order_by_created_time_desc=order_by_created_time_desc,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
async def get_alarm_report_headers(self) -> list[dict[str, Any]]:
|
|
415
|
+
return await fetch_alarm_report_headers(self._request)
|
|
416
|
+
|
|
417
|
+
async def get_alarm_report(self, record_id: str) -> dict[str, Any]:
|
|
418
|
+
return await fetch_alarm_report_details(self._request, record_id)
|
|
419
|
+
|
|
420
|
+
async def get_alarm_reports(
|
|
421
|
+
self,
|
|
422
|
+
*,
|
|
423
|
+
page: int = 1,
|
|
424
|
+
count: int = DEFAULT_PAGE_SIZE,
|
|
425
|
+
dtu_id: str = "",
|
|
426
|
+
state: int | None = None,
|
|
427
|
+
created_from_time: datetime | str | None = None,
|
|
428
|
+
created_to_time: datetime | str | None = None,
|
|
429
|
+
order_by_created_at_asc: bool = False,
|
|
430
|
+
) -> PagedResult[AlarmReport]:
|
|
431
|
+
return await fetch_alarm_reports(
|
|
432
|
+
self._request,
|
|
433
|
+
page=page,
|
|
434
|
+
count=count,
|
|
435
|
+
dtu_id=dtu_id,
|
|
436
|
+
state=state,
|
|
437
|
+
created_from_time=created_from_time,
|
|
438
|
+
created_to_time=created_to_time,
|
|
439
|
+
order_by_created_at_asc=order_by_created_at_asc,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
async def get_dashboard_summary(self) -> DashboardSummary:
|
|
443
|
+
return await fetch_dashboard_summary(self._request)
|
|
444
|
+
|
|
445
|
+
async def get_dashboard_daily_generation_time_rank(
|
|
446
|
+
self,
|
|
447
|
+
*,
|
|
448
|
+
asc: bool = False,
|
|
449
|
+
) -> list[StationRankEntry]:
|
|
450
|
+
return await fetch_dashboard_daily_generation_time_rank(self._request, asc=asc)
|
|
451
|
+
|
|
452
|
+
async def get_dashboard_station_distribution(
|
|
453
|
+
self,
|
|
454
|
+
*,
|
|
455
|
+
east_longitude: float | None = None,
|
|
456
|
+
west_longitude: float | None = None,
|
|
457
|
+
north_latitude: float | None = None,
|
|
458
|
+
south_latitude: float | None = None,
|
|
459
|
+
level: int | None = None,
|
|
460
|
+
) -> list[LocationDistribution]:
|
|
461
|
+
return await fetch_dashboard_station_distribution(
|
|
462
|
+
self._request,
|
|
463
|
+
east_longitude=east_longitude,
|
|
464
|
+
west_longitude=west_longitude,
|
|
465
|
+
north_latitude=north_latitude,
|
|
466
|
+
south_latitude=south_latitude,
|
|
467
|
+
level=level,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
async def get_dashboard_monthly_generated_energy(self) -> list[TimePoint]:
|
|
471
|
+
return await fetch_dashboard_monthly_generated_energy(self._request)
|
|
472
|
+
|
|
473
|
+
async def get_dictionary(self, name: str) -> DictionaryData:
|
|
474
|
+
return await fetch_dictionary(self._request, name)
|
siseli/config.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Remote device configuration retrieval."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Awaitable, Callable
|
|
6
|
+
|
|
7
|
+
from ._utils import parse_datetime
|
|
8
|
+
from .models.config import ConfigBatchRead
|
|
9
|
+
from .models.state import AttributeMetadata
|
|
10
|
+
from .state import _parse_attribute_metadata
|
|
11
|
+
|
|
12
|
+
RequestFunc = Callable[..., Awaitable[Any]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parse_config_map(raw: dict[str, dict] | None) -> dict[str, AttributeMetadata]:
|
|
16
|
+
if not raw:
|
|
17
|
+
return {}
|
|
18
|
+
return {key: _parse_attribute_metadata(item, key) for key, item in raw.items()}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_batch_read(raw: dict) -> ConfigBatchRead:
|
|
23
|
+
target_config = raw.get("targetConfig")
|
|
24
|
+
return ConfigBatchRead(
|
|
25
|
+
id=raw.get("id", ""),
|
|
26
|
+
device_id=raw.get("deviceId", ""),
|
|
27
|
+
scene=raw.get("scene"),
|
|
28
|
+
request_keys=raw.get("requestKeys", []) or [],
|
|
29
|
+
target_config=_parse_config_map(target_config if isinstance(target_config, dict) else None),
|
|
30
|
+
gather_protocol_version_id=raw.get("gatherProtocolVersionId"),
|
|
31
|
+
gather_protocol_version_code=raw.get("gatherProtocolVersionCode"),
|
|
32
|
+
start_at=parse_datetime(raw.get("startAt")),
|
|
33
|
+
end_at=parse_datetime(raw.get("endAt")),
|
|
34
|
+
error=raw.get("error"),
|
|
35
|
+
is_finished=raw.get("isFinished", False),
|
|
36
|
+
created_at=parse_datetime(raw.get("createdAt")),
|
|
37
|
+
raw=raw,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def fetch_device_config(
|
|
42
|
+
request: RequestFunc,
|
|
43
|
+
device_id: str,
|
|
44
|
+
*,
|
|
45
|
+
key: str = "",
|
|
46
|
+
config_id: str = "",
|
|
47
|
+
) -> AttributeMetadata:
|
|
48
|
+
"""Read one remote device configuration value."""
|
|
49
|
+
data = await request(
|
|
50
|
+
"POST",
|
|
51
|
+
"/apis/remote/device/config/read",
|
|
52
|
+
params={"deviceId": device_id},
|
|
53
|
+
json={"id": config_id, "key": key},
|
|
54
|
+
)
|
|
55
|
+
return _parse_attribute_metadata(data, key)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def write_device_config(
|
|
59
|
+
request: RequestFunc,
|
|
60
|
+
device_id: str,
|
|
61
|
+
*,
|
|
62
|
+
key: str,
|
|
63
|
+
value: Any,
|
|
64
|
+
config_id: str = "",
|
|
65
|
+
) -> AttributeMetadata:
|
|
66
|
+
"""Write one remote device configuration value."""
|
|
67
|
+
data = await request(
|
|
68
|
+
"POST",
|
|
69
|
+
"/apis/remote/device/config/write",
|
|
70
|
+
params={"deviceId": device_id},
|
|
71
|
+
json={"id": config_id, "key": key, "value": value},
|
|
72
|
+
)
|
|
73
|
+
return _parse_attribute_metadata(data, key)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def fetch_cached_device_configs(
|
|
77
|
+
request: RequestFunc,
|
|
78
|
+
device_id: str,
|
|
79
|
+
) -> dict[str, AttributeMetadata]:
|
|
80
|
+
"""Return cached remote configurations for a device."""
|
|
81
|
+
data = await request(
|
|
82
|
+
"POST",
|
|
83
|
+
"/apis/remote/device/configs/cache/get",
|
|
84
|
+
params={"deviceId": device_id},
|
|
85
|
+
)
|
|
86
|
+
return _parse_config_map(data)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def fetch_device_configs(
|
|
90
|
+
request: RequestFunc,
|
|
91
|
+
device_id: str,
|
|
92
|
+
) -> ConfigBatchRead:
|
|
93
|
+
"""Start a batch remote configuration read."""
|
|
94
|
+
data = await request(
|
|
95
|
+
"POST",
|
|
96
|
+
"/apis/remote/device/configs/read",
|
|
97
|
+
params={"deviceId": device_id},
|
|
98
|
+
)
|
|
99
|
+
return _parse_batch_read(data)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def fetch_device_config_batch_details(
|
|
103
|
+
request: RequestFunc,
|
|
104
|
+
batch_read_id: str,
|
|
105
|
+
) -> ConfigBatchRead:
|
|
106
|
+
"""Return status/details for a batch remote configuration read."""
|
|
107
|
+
data = await request(
|
|
108
|
+
"GET",
|
|
109
|
+
"/apis/remote/device/configs/read/details",
|
|
110
|
+
params={"batchReadId": batch_read_id},
|
|
111
|
+
)
|
|
112
|
+
return _parse_batch_read(data)
|
siseli/const.py
ADDED
siseli/dashboard.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Dashboard summary helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Awaitable, Callable
|
|
6
|
+
|
|
7
|
+
from .models.common import CountByValue, TimePoint
|
|
8
|
+
from .models.dashboard import DashboardSummary, LocationDistribution, StationRankEntry
|
|
9
|
+
from .station import _parse_time_point
|
|
10
|
+
|
|
11
|
+
RequestFunc = Callable[..., Awaitable[Any]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def fetch_dashboard_summary(request: RequestFunc) -> DashboardSummary:
|
|
15
|
+
"""Return top-level dashboard summary metrics."""
|
|
16
|
+
data = await request("POST", "/apis/dashboard/summary/commons", json={})
|
|
17
|
+
return DashboardSummary(
|
|
18
|
+
daily_produced_quantity=data.get("dailyProducedQuantity"),
|
|
19
|
+
total_produced_quantity=data.get("totalProducedQuantity"),
|
|
20
|
+
total_power=data.get("totalPower"),
|
|
21
|
+
saving_standard_carbon=data.get("savingStandardCarbon"),
|
|
22
|
+
co2_emission_reduction=data.get("co2EmissionReduction"),
|
|
23
|
+
so2_emission_reduction=data.get("so2EmissionReduction"),
|
|
24
|
+
nox_emission_reduction=data.get("noxEmissionReduction"),
|
|
25
|
+
devices_number=data.get("devicesNumber"),
|
|
26
|
+
all_installed_capacity=data.get("allInstalledCapacity"),
|
|
27
|
+
station_total_number=data.get("stationTotalNumber"),
|
|
28
|
+
station_state_summary=[
|
|
29
|
+
CountByValue(
|
|
30
|
+
value=item.get("state"),
|
|
31
|
+
count=item.get("count", 0),
|
|
32
|
+
label=item.get("stateDict"),
|
|
33
|
+
raw=item,
|
|
34
|
+
)
|
|
35
|
+
for item in data.get("stationStateSummary", [])
|
|
36
|
+
],
|
|
37
|
+
raw=data,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def fetch_dashboard_daily_generation_time_rank(
|
|
42
|
+
request: RequestFunc,
|
|
43
|
+
*,
|
|
44
|
+
asc: bool = False,
|
|
45
|
+
) -> list[StationRankEntry]:
|
|
46
|
+
"""Return dashboard station ranking by daily generation time."""
|
|
47
|
+
data = await request(
|
|
48
|
+
"POST",
|
|
49
|
+
"/apis/dashboard/summary/station/dailyGenerationTimeRank",
|
|
50
|
+
params={"asc": asc},
|
|
51
|
+
json={},
|
|
52
|
+
)
|
|
53
|
+
return [
|
|
54
|
+
StationRankEntry(name=item.get("name", ""), full_time=item.get("fullTime"), raw=item)
|
|
55
|
+
for item in data
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def fetch_dashboard_station_distribution(
|
|
60
|
+
request: RequestFunc,
|
|
61
|
+
*,
|
|
62
|
+
east_longitude: float | None = None,
|
|
63
|
+
west_longitude: float | None = None,
|
|
64
|
+
north_latitude: float | None = None,
|
|
65
|
+
south_latitude: float | None = None,
|
|
66
|
+
level: int | None = None,
|
|
67
|
+
) -> list[LocationDistribution]:
|
|
68
|
+
"""Return dashboard station distribution by location."""
|
|
69
|
+
data = await request(
|
|
70
|
+
"POST",
|
|
71
|
+
"/apis/dashboard/summary/station/distribution/location",
|
|
72
|
+
json={
|
|
73
|
+
"eastLongitude": east_longitude,
|
|
74
|
+
"westLongitude": west_longitude,
|
|
75
|
+
"northLatitude": north_latitude,
|
|
76
|
+
"southLatitude": south_latitude,
|
|
77
|
+
"level": level,
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
return [
|
|
81
|
+
LocationDistribution(
|
|
82
|
+
name=item.get("name"),
|
|
83
|
+
longitude=item.get("longitude"),
|
|
84
|
+
latitude=item.get("latitude"),
|
|
85
|
+
raw=item,
|
|
86
|
+
)
|
|
87
|
+
for item in data
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def fetch_dashboard_monthly_generated_energy(
|
|
92
|
+
request: RequestFunc,
|
|
93
|
+
) -> list[TimePoint]:
|
|
94
|
+
"""Return monthly generated energy points for the dashboard."""
|
|
95
|
+
data = await request(
|
|
96
|
+
"POST",
|
|
97
|
+
"/apis/dashboard/summary/station/generatedEnergy/monthly",
|
|
98
|
+
json={},
|
|
99
|
+
)
|
|
100
|
+
return [_parse_time_point(item) for item in data]
|