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/device.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Device discovery and details."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Awaitable, Callable
|
|
7
|
+
|
|
8
|
+
from .models.device import Device
|
|
9
|
+
|
|
10
|
+
RequestFunc = Callable[..., Awaitable[Any]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _dt(value: str | None) -> datetime | None:
|
|
14
|
+
if not value:
|
|
15
|
+
return None
|
|
16
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_device(raw: dict) -> Device:
|
|
20
|
+
return Device(
|
|
21
|
+
id=raw["id"],
|
|
22
|
+
name=raw.get("name", ""),
|
|
23
|
+
serial_number=raw.get("serialNumber", ""),
|
|
24
|
+
state=raw.get("state", 0),
|
|
25
|
+
is_online=raw.get("isOnline", False),
|
|
26
|
+
device_sort_key=raw.get("deviceSortKey", ""),
|
|
27
|
+
station_id=raw.get("stationId"),
|
|
28
|
+
station_name=raw.get("stationName"),
|
|
29
|
+
station_timezone=raw.get("stationTimezone"),
|
|
30
|
+
rated_power=raw.get("ratedPower", 0.0),
|
|
31
|
+
producing_power=raw.get("producingPower", 0.0),
|
|
32
|
+
model=raw.get("model"),
|
|
33
|
+
software_version=raw.get("softwareVersion"),
|
|
34
|
+
last_data_at=_dt(raw.get("lastDataAt")),
|
|
35
|
+
last_online_at=_dt(raw.get("lastOnlineAt")),
|
|
36
|
+
created_at=_dt(raw.get("createdAt")),
|
|
37
|
+
raw=raw,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def fetch_device_list(
|
|
42
|
+
request: RequestFunc,
|
|
43
|
+
*,
|
|
44
|
+
page: int = 1,
|
|
45
|
+
count: int = 20,
|
|
46
|
+
name: str = "",
|
|
47
|
+
serial_number: str = "",
|
|
48
|
+
station_id: str = "",
|
|
49
|
+
state: str = "",
|
|
50
|
+
) -> tuple[list[Device], int]:
|
|
51
|
+
"""Return a page of devices and the total count.
|
|
52
|
+
|
|
53
|
+
:param request: Authenticated request callable from :class:`SiseliClient`.
|
|
54
|
+
:param page: 1-based page number.
|
|
55
|
+
:param count: Page size.
|
|
56
|
+
:param name: Optional name filter (substring match).
|
|
57
|
+
:param serial_number: Optional serial number filter.
|
|
58
|
+
:param station_id: Optional station filter.
|
|
59
|
+
:param state: Optional state filter.
|
|
60
|
+
:returns: ``(devices, total)`` tuple.
|
|
61
|
+
"""
|
|
62
|
+
data = await request(
|
|
63
|
+
"POST",
|
|
64
|
+
"/apis/device/list",
|
|
65
|
+
json={
|
|
66
|
+
"page": page,
|
|
67
|
+
"count": count,
|
|
68
|
+
"name": name,
|
|
69
|
+
"serialNumber": serial_number,
|
|
70
|
+
"stationId": station_id,
|
|
71
|
+
"state": state,
|
|
72
|
+
"fieldNames": [],
|
|
73
|
+
"exportType": 0,
|
|
74
|
+
"applyModeCategory": 1,
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
devices = [_parse_device(d) for d in data.get("list", [])]
|
|
78
|
+
total: int = data.get("total", len(devices))
|
|
79
|
+
return devices, total
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def fetch_device_details(request: RequestFunc, device_id: str) -> Device:
|
|
83
|
+
"""Return full details for a single device.
|
|
84
|
+
|
|
85
|
+
:param request: Authenticated request callable from :class:`SiseliClient`.
|
|
86
|
+
:param device_id: The device ID.
|
|
87
|
+
"""
|
|
88
|
+
data = await request("GET", "/apis/device/details", params={"deviceId": device_id})
|
|
89
|
+
return _parse_device(data)
|
siseli/dictionary.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Lookup dictionary helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Awaitable, Callable
|
|
6
|
+
|
|
7
|
+
from .models.common import LookupValue
|
|
8
|
+
from .models.dictionary import DictionaryData
|
|
9
|
+
|
|
10
|
+
RequestFunc = Callable[..., Awaitable[Any]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _parse_lookup_values(values: list[dict] | None) -> list[LookupValue]:
|
|
15
|
+
if not values:
|
|
16
|
+
return []
|
|
17
|
+
return [
|
|
18
|
+
LookupValue(
|
|
19
|
+
value=item.get("value"),
|
|
20
|
+
name=item.get("name", item.get("text", item.get("localeText", ""))),
|
|
21
|
+
locale_text=item.get("localeText"),
|
|
22
|
+
raw=item,
|
|
23
|
+
)
|
|
24
|
+
for item in values
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def fetch_dictionary(request: RequestFunc, name: str) -> DictionaryData:
|
|
29
|
+
"""Return one dictionary dataset by name."""
|
|
30
|
+
data = await request("GET", f"/apis/dictionary/data/{name}")
|
|
31
|
+
metadata = {key: item for key, item in data.items() if not isinstance(item, list)}
|
|
32
|
+
values = {
|
|
33
|
+
key: _parse_lookup_values(item)
|
|
34
|
+
for key, item in data.items()
|
|
35
|
+
if isinstance(item, list)
|
|
36
|
+
}
|
|
37
|
+
return DictionaryData(name=name, values=values, raw=data, metadata=metadata)
|
siseli/exceptions.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Exception hierarchy for the Siseli SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SiseliError(Exception):
|
|
7
|
+
"""Base exception for all Siseli SDK errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthenticationError(SiseliError):
|
|
11
|
+
"""Raised when authentication fails."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TokenExpiredError(AuthenticationError):
|
|
15
|
+
"""Raised when the access token has expired and cannot be refreshed."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ApiError(SiseliError):
|
|
19
|
+
"""Raised when the API returns a non-zero response code."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, code: int, message: str) -> None:
|
|
22
|
+
self.code = code
|
|
23
|
+
self.message = message
|
|
24
|
+
super().__init__(f"API error {code}: {message}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NetworkError(SiseliError):
|
|
28
|
+
"""Raised on network-level failures (connection errors, timeouts, etc.)."""
|
siseli/history.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Historical telemetry retrieval."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Awaitable, Callable, Iterable
|
|
6
|
+
|
|
7
|
+
from ._utils import extract_value, parse_datetime, serialize_datetime
|
|
8
|
+
from .const import DEFAULT_PAGE_SIZE
|
|
9
|
+
from .models.history import HistoryRecord, HistorySeries
|
|
10
|
+
|
|
11
|
+
RequestFunc = Callable[..., Awaitable[Any]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _build_history_records(
|
|
15
|
+
time_series: list[str],
|
|
16
|
+
fields: dict[str, list[Any]],
|
|
17
|
+
) -> list[HistoryRecord]:
|
|
18
|
+
records: list[HistoryRecord] = []
|
|
19
|
+
for index, timestamp in enumerate(time_series):
|
|
20
|
+
values = {
|
|
21
|
+
key: extract_value(series[index]) if index < len(series) else None
|
|
22
|
+
for key, series in fields.items()
|
|
23
|
+
}
|
|
24
|
+
records.append(
|
|
25
|
+
HistoryRecord(
|
|
26
|
+
time=parse_datetime(timestamp),
|
|
27
|
+
values=values,
|
|
28
|
+
raw={
|
|
29
|
+
"time": timestamp,
|
|
30
|
+
"values": {
|
|
31
|
+
key: series[index] if index < len(series) else None
|
|
32
|
+
for key, series in fields.items()
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
return records
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def fetch_attribute_history(
|
|
41
|
+
request: RequestFunc,
|
|
42
|
+
device_id: str,
|
|
43
|
+
keys: Iterable[str],
|
|
44
|
+
*,
|
|
45
|
+
from_time: Any = None,
|
|
46
|
+
to_time: Any = None,
|
|
47
|
+
page: int = 1,
|
|
48
|
+
count: int = DEFAULT_PAGE_SIZE,
|
|
49
|
+
order_by_time_asc: bool = True,
|
|
50
|
+
) -> HistorySeries:
|
|
51
|
+
"""Return history for selected attribute keys."""
|
|
52
|
+
data = await request(
|
|
53
|
+
"POST",
|
|
54
|
+
"/apis/deviceState/simple/attribute/keys/history/v1",
|
|
55
|
+
json={
|
|
56
|
+
"deviceId": device_id,
|
|
57
|
+
"keys": list(keys),
|
|
58
|
+
"fromTime": serialize_datetime(from_time),
|
|
59
|
+
"toTime": serialize_datetime(to_time),
|
|
60
|
+
"page": page,
|
|
61
|
+
"count": count,
|
|
62
|
+
"orderByTimeAsc": order_by_time_asc,
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
payload = data.get("payload", {})
|
|
66
|
+
raw_fields = payload.get("fields", {})
|
|
67
|
+
time_series = payload.get("timeSeries", [])
|
|
68
|
+
normalized_fields = {
|
|
69
|
+
key: [extract_value(value) for value in values]
|
|
70
|
+
for key, values in raw_fields.items()
|
|
71
|
+
}
|
|
72
|
+
return HistorySeries(
|
|
73
|
+
page=data.get("page", page),
|
|
74
|
+
count=data.get("count", count),
|
|
75
|
+
total=data.get("total", len(time_series)),
|
|
76
|
+
time_series=[parse_datetime(value) for value in time_series],
|
|
77
|
+
fields=normalized_fields,
|
|
78
|
+
formatters=payload.get("formatters", {}),
|
|
79
|
+
records=_build_history_records(time_series, raw_fields),
|
|
80
|
+
raw=data,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def fetch_state_history(
|
|
85
|
+
request: RequestFunc,
|
|
86
|
+
device_id: str,
|
|
87
|
+
*,
|
|
88
|
+
from_time: Any = None,
|
|
89
|
+
to_time: Any = None,
|
|
90
|
+
page: int = 1,
|
|
91
|
+
count: int = DEFAULT_PAGE_SIZE,
|
|
92
|
+
order_by_time_asc: bool = False,
|
|
93
|
+
) -> HistorySeries:
|
|
94
|
+
"""Return paginated historical device state records."""
|
|
95
|
+
data = await request(
|
|
96
|
+
"POST",
|
|
97
|
+
"/apis/deviceState/simple/attribute/record/list/v1",
|
|
98
|
+
json={
|
|
99
|
+
"deviceId": device_id,
|
|
100
|
+
"fromTime": serialize_datetime(from_time),
|
|
101
|
+
"toTime": serialize_datetime(to_time),
|
|
102
|
+
"page": page,
|
|
103
|
+
"count": count,
|
|
104
|
+
"orderByTimeAsc": order_by_time_asc,
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
payload = data.get("payload", {})
|
|
108
|
+
raw_fields = payload.get("fields", {})
|
|
109
|
+
time_series = payload.get("timeSeries", [])
|
|
110
|
+
normalized_fields = {
|
|
111
|
+
key: [extract_value(value) for value in values]
|
|
112
|
+
for key, values in raw_fields.items()
|
|
113
|
+
}
|
|
114
|
+
return HistorySeries(
|
|
115
|
+
page=data.get("page", page),
|
|
116
|
+
count=data.get("count", count),
|
|
117
|
+
total=data.get("total", len(time_series)),
|
|
118
|
+
time_series=[parse_datetime(value) for value in time_series],
|
|
119
|
+
fields=normalized_fields,
|
|
120
|
+
formatters={},
|
|
121
|
+
records=_build_history_records(time_series, raw_fields),
|
|
122
|
+
raw=data,
|
|
123
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Public re-exports from all model modules."""
|
|
2
|
+
|
|
3
|
+
from .alarm import Alarm, AlarmReport
|
|
4
|
+
from .auth import TokenInfo
|
|
5
|
+
from .common import CountByValue, LookupValue, PagedResult, TimePoint
|
|
6
|
+
from .config import ConfigBatchRead
|
|
7
|
+
from .dashboard import DashboardSummary, LocationDistribution, StationRankEntry
|
|
8
|
+
from .device import Device
|
|
9
|
+
from .dictionary import DictionaryData
|
|
10
|
+
from .history import HistoryRecord, HistorySeries
|
|
11
|
+
from .state import (
|
|
12
|
+
AttributeGroup,
|
|
13
|
+
AttributeGroupSet,
|
|
14
|
+
AttributeMetadata,
|
|
15
|
+
DeviceState,
|
|
16
|
+
EnergyFlow,
|
|
17
|
+
FlowNode,
|
|
18
|
+
StateAttribute,
|
|
19
|
+
)
|
|
20
|
+
from .station import Station, StationEnergyFlow, StationSummary, SummaryCategory, SummaryProperty
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Alarm",
|
|
24
|
+
"AlarmReport",
|
|
25
|
+
"AttributeGroup",
|
|
26
|
+
"AttributeGroupSet",
|
|
27
|
+
"AttributeMetadata",
|
|
28
|
+
"ConfigBatchRead",
|
|
29
|
+
"CountByValue",
|
|
30
|
+
"DashboardSummary",
|
|
31
|
+
"Device",
|
|
32
|
+
"DeviceState",
|
|
33
|
+
"DictionaryData",
|
|
34
|
+
"EnergyFlow",
|
|
35
|
+
"FlowNode",
|
|
36
|
+
"HistoryRecord",
|
|
37
|
+
"HistorySeries",
|
|
38
|
+
"LocationDistribution",
|
|
39
|
+
"LookupValue",
|
|
40
|
+
"PagedResult",
|
|
41
|
+
"StateAttribute",
|
|
42
|
+
"Station",
|
|
43
|
+
"StationEnergyFlow",
|
|
44
|
+
"StationRankEntry",
|
|
45
|
+
"StationSummary",
|
|
46
|
+
"SummaryCategory",
|
|
47
|
+
"SummaryProperty",
|
|
48
|
+
"TimePoint",
|
|
49
|
+
"TokenInfo",
|
|
50
|
+
]
|
siseli/models/alarm.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Alarm-related models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Alarm:
|
|
11
|
+
"""One alarm entry."""
|
|
12
|
+
|
|
13
|
+
id: str
|
|
14
|
+
device_id: str | None
|
|
15
|
+
device_serial_number: str | None
|
|
16
|
+
device_name: str | None
|
|
17
|
+
station_id: str | None
|
|
18
|
+
station_name: str | None
|
|
19
|
+
level: int | None
|
|
20
|
+
state: int | None
|
|
21
|
+
category: int | None
|
|
22
|
+
title: str | None
|
|
23
|
+
content: str | None
|
|
24
|
+
created_at: datetime | None
|
|
25
|
+
processed_at: datetime | None
|
|
26
|
+
raw: dict = field(repr=False)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class AlarmReport:
|
|
31
|
+
"""Alarm report/export entry."""
|
|
32
|
+
|
|
33
|
+
id: str
|
|
34
|
+
remark: str | None
|
|
35
|
+
report_from_time: datetime | None
|
|
36
|
+
report_to_time: datetime | None
|
|
37
|
+
progress: int | None
|
|
38
|
+
state: int | None
|
|
39
|
+
download_resid: str | None
|
|
40
|
+
created_at: datetime | None
|
|
41
|
+
raw: dict = field(repr=False)
|
siseli/models/auth.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Auth-related models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class TokenInfo:
|
|
11
|
+
"""Tokens returned by a successful login."""
|
|
12
|
+
|
|
13
|
+
access_token: str
|
|
14
|
+
refresh_token: str
|
|
15
|
+
access_token_expires_at: datetime
|
|
16
|
+
refresh_token_expires_at: datetime
|
|
17
|
+
auth_id: str
|
|
18
|
+
account: str
|
|
19
|
+
user_id: str
|
siseli/models/common.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Common models shared by multiple Siseli API areas."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any, Generic, TypeVar
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class LookupValue:
|
|
14
|
+
"""One value from a lookup table or enum."""
|
|
15
|
+
|
|
16
|
+
value: Any
|
|
17
|
+
name: str
|
|
18
|
+
locale_text: str | None
|
|
19
|
+
raw: dict = field(repr=False)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class CountByValue:
|
|
24
|
+
"""Count grouped by a dictionary value."""
|
|
25
|
+
|
|
26
|
+
value: Any
|
|
27
|
+
count: int
|
|
28
|
+
label: str | None
|
|
29
|
+
raw: dict = field(repr=False)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class TimePoint:
|
|
34
|
+
"""One aggregated point in a time series."""
|
|
35
|
+
|
|
36
|
+
time: datetime | None
|
|
37
|
+
time_display: str
|
|
38
|
+
value: Any
|
|
39
|
+
is_real_value: bool | None
|
|
40
|
+
raw: dict = field(repr=False)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class PagedResult(Generic[T]):
|
|
45
|
+
"""Generic paginated response wrapper."""
|
|
46
|
+
|
|
47
|
+
page: int
|
|
48
|
+
count: int
|
|
49
|
+
total: int
|
|
50
|
+
items: list[T]
|
|
51
|
+
raw: dict = field(repr=False)
|
siseli/models/config.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Remote configuration models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .state import AttributeMetadata
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ConfigBatchRead:
|
|
14
|
+
"""Batch configuration read request and its progress."""
|
|
15
|
+
|
|
16
|
+
id: str
|
|
17
|
+
device_id: str
|
|
18
|
+
scene: int | None
|
|
19
|
+
request_keys: list[str]
|
|
20
|
+
target_config: dict[str, AttributeMetadata]
|
|
21
|
+
gather_protocol_version_id: str | None
|
|
22
|
+
gather_protocol_version_code: str | None
|
|
23
|
+
start_at: datetime | None
|
|
24
|
+
end_at: datetime | None
|
|
25
|
+
error: Any
|
|
26
|
+
is_finished: bool
|
|
27
|
+
created_at: datetime | None
|
|
28
|
+
raw: dict = field(repr=False)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Dashboard models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .common import CountByValue, TimePoint
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class StationRankEntry:
|
|
13
|
+
"""Dashboard station ranking entry."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
full_time: float | int | None
|
|
17
|
+
raw: dict = field(repr=False)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class DashboardSummary:
|
|
22
|
+
"""Dashboard summary counters."""
|
|
23
|
+
|
|
24
|
+
daily_produced_quantity: float | int | None
|
|
25
|
+
total_produced_quantity: float | int | None
|
|
26
|
+
total_power: float | int | None
|
|
27
|
+
saving_standard_carbon: float | int | None
|
|
28
|
+
co2_emission_reduction: float | int | None
|
|
29
|
+
so2_emission_reduction: float | int | None
|
|
30
|
+
nox_emission_reduction: float | int | None
|
|
31
|
+
devices_number: int | None
|
|
32
|
+
all_installed_capacity: float | int | None
|
|
33
|
+
station_total_number: int | None
|
|
34
|
+
station_state_summary: list[CountByValue]
|
|
35
|
+
raw: dict = field(repr=False)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class LocationDistribution:
|
|
40
|
+
"""Dashboard location distribution entry."""
|
|
41
|
+
|
|
42
|
+
name: str | None
|
|
43
|
+
longitude: float | None
|
|
44
|
+
latitude: float | None
|
|
45
|
+
raw: dict = field(repr=False)
|
siseli/models/device.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Device-related models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Device:
|
|
11
|
+
"""A Siseli device (inverter, battery, etc.)."""
|
|
12
|
+
|
|
13
|
+
id: str
|
|
14
|
+
name: str
|
|
15
|
+
serial_number: str
|
|
16
|
+
state: int
|
|
17
|
+
is_online: bool
|
|
18
|
+
device_sort_key: str
|
|
19
|
+
station_id: str | None
|
|
20
|
+
station_name: str | None
|
|
21
|
+
station_timezone: str | None
|
|
22
|
+
rated_power: float
|
|
23
|
+
producing_power: float
|
|
24
|
+
model: str | None
|
|
25
|
+
software_version: str | None
|
|
26
|
+
last_data_at: datetime | None
|
|
27
|
+
last_online_at: datetime | None
|
|
28
|
+
created_at: datetime | None
|
|
29
|
+
raw: dict = field(repr=False)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Dictionary and metadata models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .common import LookupValue
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class DictionaryData:
|
|
13
|
+
"""One dictionary dataset returned by the API."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
values: dict[str, list[LookupValue]]
|
|
17
|
+
raw: dict = field(repr=False)
|
|
18
|
+
metadata: dict[str, Any] = field(default_factory=dict, repr=False)
|
siseli/models/history.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Historical telemetry models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class HistoryRecord:
|
|
12
|
+
"""One timestamped historical record."""
|
|
13
|
+
|
|
14
|
+
time: datetime | None
|
|
15
|
+
values: dict[str, Any]
|
|
16
|
+
raw: dict = field(repr=False)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class HistorySeries:
|
|
21
|
+
"""Historical attribute values for a device."""
|
|
22
|
+
|
|
23
|
+
page: int
|
|
24
|
+
count: int
|
|
25
|
+
total: int
|
|
26
|
+
time_series: list[datetime | None]
|
|
27
|
+
fields: dict[str, list[Any]]
|
|
28
|
+
formatters: dict[str, Any]
|
|
29
|
+
records: list[HistoryRecord]
|
|
30
|
+
raw: dict = field(repr=False)
|
|
31
|
+
|
|
32
|
+
def get_series(self, key: str) -> list[Any]:
|
|
33
|
+
"""Return all values for one key, preserving API order."""
|
|
34
|
+
return self.fields.get(key, [])
|