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.
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
@@ -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)
@@ -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)
@@ -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)
@@ -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, [])