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/models/state.py ADDED
@@ -0,0 +1,106 @@
1
+ """State, attribute, and energy-flow 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 .common import LookupValue
10
+
11
+
12
+ @dataclass
13
+ class StateAttribute:
14
+ """A single telemetry attribute."""
15
+
16
+ key: str
17
+ unit: str
18
+ value: Any
19
+ value_display: str
20
+ name_display: str
21
+ is_hidden: bool | None
22
+
23
+
24
+ @dataclass
25
+ class AttributeMetadata:
26
+ """Metadata describing a state or configuration attribute."""
27
+
28
+ key: str
29
+ name: str
30
+ name_display: str
31
+ unit: str
32
+ value_type: int | None
33
+ category: int | None
34
+ operation_mode: int | None
35
+ is_hidden: bool | None
36
+ is_config_attribute: bool | None
37
+ is_writable_config_attribute: bool | None
38
+ is_readable_config_attribute: bool | None
39
+ is_state_attribute: bool | None
40
+ is_event_attribute: bool | None
41
+ enum_values: list[LookupValue]
42
+ raw: dict = field(repr=False)
43
+
44
+
45
+ @dataclass
46
+ class AttributeGroup:
47
+ """A group of related device attributes."""
48
+
49
+ id: str
50
+ key: str
51
+ category: int | None
52
+ name: str
53
+ description: str
54
+ attributes: list[AttributeMetadata]
55
+ raw: dict = field(repr=False)
56
+
57
+
58
+ @dataclass
59
+ class AttributeGroupSet:
60
+ """Grouped attribute metadata for a device."""
61
+
62
+ gather_protocol_version_id: str
63
+ groups: list[AttributeGroup]
64
+ raw: dict = field(repr=False)
65
+
66
+
67
+ @dataclass
68
+ class DeviceState:
69
+ """Latest telemetry snapshot for a device."""
70
+
71
+ time: datetime | None
72
+ fields: dict[str, StateAttribute]
73
+ raw: dict = field(repr=False)
74
+
75
+ def get(self, key: str) -> StateAttribute | None:
76
+ """Return an attribute by its key, or *None* if not present."""
77
+ return self.fields.get(key)
78
+
79
+
80
+ @dataclass
81
+ class FlowNode:
82
+ """One node of the energy-flow diagram (PV panel, grid, battery, load…)."""
83
+
84
+ key: str
85
+ locale_title: str
86
+ value: StateAttribute | None
87
+ flow_direction: int | None
88
+ is_light: bool | None
89
+ is_enabled: bool
90
+ extra_values: list[StateAttribute]
91
+ raw: dict = field(repr=False)
92
+
93
+
94
+ @dataclass
95
+ class EnergyFlow:
96
+ """Energy-flow snapshot for a device."""
97
+
98
+ device_state: DeviceState
99
+ pv_panel: FlowNode | None
100
+ grid: FlowNode | None
101
+ battery: FlowNode | None
102
+ load: FlowNode | None
103
+ generator: FlowNode | None
104
+ ups: FlowNode | None
105
+ ct: FlowNode | None
106
+ raw: dict = field(repr=False)
@@ -0,0 +1,82 @@
1
+ """Station-related 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 .common import TimePoint
10
+ from .state import FlowNode
11
+
12
+
13
+ @dataclass
14
+ class Station:
15
+ """A Siseli station."""
16
+
17
+ id: str
18
+ name: str
19
+ timezone: str | None
20
+ country: str | None
21
+ province: str | None
22
+ city: str | None
23
+ area: str | None
24
+ address: str | None
25
+ longitude: float | None
26
+ latitude: float | None
27
+ station_type: int | None
28
+ connected_grid_type: int | None
29
+ state: int | None
30
+ is_online: bool | None
31
+ installed_capacity: float | None
32
+ total_power: float | None
33
+ daily_produced_quantity: float | None
34
+ total_produced_quantity: float | None
35
+ created_at: datetime | None
36
+ raw: dict = field(repr=False)
37
+
38
+
39
+ @dataclass
40
+ class StationEnergyFlow:
41
+ """Energy-flow snapshot for a station."""
42
+
43
+ is_support_flow: bool
44
+ time: datetime | None
45
+ pv_panel: FlowNode | None
46
+ grid: FlowNode | None
47
+ battery: FlowNode | None
48
+ load: FlowNode | None
49
+ generator: FlowNode | None
50
+ ups: FlowNode | None
51
+ ct: FlowNode | None
52
+ raw: dict = field(repr=False)
53
+
54
+
55
+ @dataclass
56
+ class SummaryCategory:
57
+ """Summary category metadata."""
58
+
59
+ id: str
60
+ key: str
61
+ name: str
62
+ raw: dict = field(repr=False)
63
+
64
+
65
+ @dataclass
66
+ class SummaryProperty:
67
+ """One property within an aggregated summary."""
68
+
69
+ property: dict[str, Any]
70
+ time_points: list[TimePoint]
71
+ has_real_time_points: bool | None
72
+ raw: dict = field(repr=False)
73
+
74
+
75
+ @dataclass
76
+ class StationSummary:
77
+ """Aggregated station summary series."""
78
+
79
+ category: SummaryCategory | None
80
+ properties: list[SummaryProperty]
81
+ has_real_time_points: bool
82
+ raw: dict = field(repr=False)
siseli/state.py ADDED
@@ -0,0 +1,184 @@
1
+ """Device state, metadata, and energy-flow retrieval."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Awaitable, Callable
6
+
7
+ from ._utils import parse_datetime
8
+ from .const import DEFAULT_DATA_SOURCE
9
+ from .models.common import LookupValue
10
+ from .models.state import (
11
+ AttributeGroup,
12
+ AttributeGroupSet,
13
+ AttributeMetadata,
14
+ DeviceState,
15
+ EnergyFlow,
16
+ FlowNode,
17
+ StateAttribute,
18
+ )
19
+
20
+ RequestFunc = Callable[..., Awaitable[Any]]
21
+
22
+
23
+
24
+ def _parse_lookup_value(raw: dict) -> LookupValue:
25
+ return LookupValue(
26
+ value=raw.get("value"),
27
+ name=raw.get("text", raw.get("name", raw.get("localeText", ""))),
28
+ locale_text=raw.get("localeText"),
29
+ raw=raw,
30
+ )
31
+
32
+
33
+
34
+ def _parse_attribute(key: str, raw: dict) -> StateAttribute:
35
+ return StateAttribute(
36
+ key=raw.get("key", key),
37
+ unit=raw.get("unit", ""),
38
+ value=raw.get("value"),
39
+ value_display=raw.get("valueDisplay", str(raw.get("value", ""))),
40
+ name_display=raw.get("nameDisplay", raw.get("name", "")),
41
+ is_hidden=raw.get("isHidden"),
42
+ )
43
+
44
+
45
+
46
+ def _parse_attribute_metadata(raw: dict, key: str = "") -> AttributeMetadata:
47
+ enum_values = [_parse_lookup_value(item) for item in raw.get("enumValues", [])]
48
+ return AttributeMetadata(
49
+ key=raw.get("key", key),
50
+ name=raw.get("name", raw.get("nameDisplay", "")),
51
+ name_display=raw.get("nameDisplay", raw.get("name", "")),
52
+ unit=raw.get("unit", ""),
53
+ value_type=raw.get("valueType"),
54
+ category=raw.get("category"),
55
+ operation_mode=raw.get("operationMode"),
56
+ is_hidden=raw.get("isHidden"),
57
+ is_config_attribute=raw.get("isConfigAttribute"),
58
+ is_writable_config_attribute=raw.get("isWritableConfigAttribute"),
59
+ is_readable_config_attribute=raw.get("isReadableConfigAttribute"),
60
+ is_state_attribute=raw.get("isStateAttribute"),
61
+ is_event_attribute=raw.get("isEventAttribute"),
62
+ enum_values=enum_values,
63
+ raw=raw,
64
+ )
65
+
66
+
67
+
68
+ def _parse_attribute_group(raw: dict) -> AttributeGroup:
69
+ return AttributeGroup(
70
+ id=raw.get("id", ""),
71
+ key=raw.get("key", ""),
72
+ category=raw.get("category"),
73
+ name=raw.get("name", ""),
74
+ description=raw.get("description", ""),
75
+ attributes=[_parse_attribute_metadata(item) for item in raw.get("attributes", [])],
76
+ raw=raw,
77
+ )
78
+
79
+
80
+
81
+ def _parse_device_state(raw: dict) -> DeviceState:
82
+ fields_raw: dict = raw.get("fields", {})
83
+ fields = {key: _parse_attribute(key, attr) for key, attr in fields_raw.items()}
84
+ return DeviceState(
85
+ time=parse_datetime(raw.get("time")),
86
+ fields=fields,
87
+ raw=raw,
88
+ )
89
+
90
+
91
+
92
+ def _parse_flow_node(raw: dict | None) -> FlowNode | None:
93
+ if raw is None:
94
+ return None
95
+ value_raw = raw.get("value")
96
+ value = _parse_attribute(raw.get("key", ""), value_raw) if value_raw else None
97
+ extra_values = [_parse_attribute(item.get("key", ""), item) for item in raw.get("extraValues", [])]
98
+ return FlowNode(
99
+ key=raw.get("key", ""),
100
+ locale_title=raw.get("localeTitle", ""),
101
+ value=value,
102
+ flow_direction=raw.get("flowDirection"),
103
+ is_light=raw.get("isLight"),
104
+ is_enabled=raw.get("isEnabled", raw.get("enabled", True)),
105
+ extra_values=extra_values,
106
+ raw=raw,
107
+ )
108
+
109
+
110
+ async def fetch_device_state(
111
+ request: RequestFunc,
112
+ device_id: str,
113
+ *,
114
+ data_source: int = DEFAULT_DATA_SOURCE,
115
+ ) -> DeviceState:
116
+ """Return the latest telemetry snapshot for *device_id*."""
117
+ data = await request(
118
+ "GET",
119
+ "/apis/deviceState/simple/state/latest/v1",
120
+ params={"deviceId": device_id, "dataSource": data_source},
121
+ )
122
+ return _parse_device_state(data)
123
+
124
+
125
+ async def fetch_device_attributes(
126
+ request: RequestFunc,
127
+ device_id: str,
128
+ *,
129
+ category: str = "",
130
+ render_in: str = "",
131
+ ) -> list[AttributeMetadata]:
132
+ """Return attribute metadata for the device state screen."""
133
+ data = await request(
134
+ "GET",
135
+ "/apis/deviceState/simple/gatherAttributes/v1",
136
+ params={"deviceId": device_id, "category": category, "renderIn": render_in},
137
+ )
138
+ return [_parse_attribute_metadata(item) for item in data]
139
+
140
+
141
+ async def fetch_device_attribute_groups(
142
+ request: RequestFunc,
143
+ device_id: str,
144
+ *,
145
+ category: str = "",
146
+ render_in: str = "",
147
+ ) -> AttributeGroupSet:
148
+ """Return grouped device attribute metadata."""
149
+ data = await request(
150
+ "GET",
151
+ "/apis/device/query/attribute/group",
152
+ params={"deviceId": device_id, "category": category, "renderIn": render_in},
153
+ )
154
+ return AttributeGroupSet(
155
+ gather_protocol_version_id=data.get("gatherProtocolVersionId", ""),
156
+ groups=[_parse_attribute_group(item) for item in data.get("attributesGroups", [])],
157
+ raw=data,
158
+ )
159
+
160
+
161
+ async def fetch_energy_flow(
162
+ request: RequestFunc,
163
+ device_id: str,
164
+ *,
165
+ data_source: int = DEFAULT_DATA_SOURCE,
166
+ ) -> EnergyFlow:
167
+ """Return the current energy-flow diagram data for *device_id*."""
168
+ data = await request(
169
+ "GET",
170
+ "/apis/deviceState/simple/energy/flow/v1",
171
+ params={"deviceId": device_id, "dataSource": data_source},
172
+ )
173
+ device_state = _parse_device_state(data.get("deviceAttributeState", {}))
174
+ return EnergyFlow(
175
+ device_state=device_state,
176
+ pv_panel=_parse_flow_node(data.get("pvPanelFlow")),
177
+ grid=_parse_flow_node(data.get("gridFlow")),
178
+ battery=_parse_flow_node(data.get("batteryFlow")),
179
+ load=_parse_flow_node(data.get("loadFlow")),
180
+ generator=_parse_flow_node(data.get("generatorFlow")),
181
+ ups=_parse_flow_node(data.get("upsFlow")),
182
+ ct=_parse_flow_node(data.get("ctFlow")),
183
+ raw=data,
184
+ )
siseli/station.py ADDED
@@ -0,0 +1,181 @@
1
+ """Station retrieval and aggregation helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Awaitable, Callable
6
+
7
+ from ._utils import parse_datetime, serialize_datetime
8
+ from .const import DEFAULT_PAGE_SIZE
9
+ from .models.common import PagedResult, TimePoint
10
+ from .models.station import Station, StationEnergyFlow, StationSummary, SummaryCategory, SummaryProperty
11
+ from .state import _parse_flow_node
12
+
13
+ RequestFunc = Callable[..., Awaitable[Any]]
14
+
15
+
16
+
17
+ def _parse_station(raw: dict) -> Station:
18
+ return Station(
19
+ id=raw.get("id", ""),
20
+ name=raw.get("name", ""),
21
+ timezone=raw.get("timezone"),
22
+ country=raw.get("country"),
23
+ province=raw.get("province"),
24
+ city=raw.get("city"),
25
+ area=raw.get("area"),
26
+ address=raw.get("address"),
27
+ longitude=raw.get("longitude"),
28
+ latitude=raw.get("latitude"),
29
+ station_type=raw.get("stationType"),
30
+ connected_grid_type=raw.get("connectedGridType"),
31
+ state=raw.get("state"),
32
+ is_online=raw.get("isOnline"),
33
+ installed_capacity=raw.get("installedCapacity"),
34
+ total_power=raw.get("totalPower"),
35
+ daily_produced_quantity=raw.get("dailyProducedQuantity"),
36
+ total_produced_quantity=raw.get("totalProducedQuantity"),
37
+ created_at=parse_datetime(raw.get("createdAt")),
38
+ raw=raw,
39
+ )
40
+
41
+
42
+
43
+ def _parse_time_point(raw: dict) -> TimePoint:
44
+ return TimePoint(
45
+ time=parse_datetime(raw.get("time")),
46
+ time_display=raw.get("timeDisplay", ""),
47
+ value=raw.get("value", raw.get("generatedEnergy")),
48
+ is_real_value=raw.get("isRealValue"),
49
+ raw=raw,
50
+ )
51
+
52
+
53
+
54
+ def _parse_summary_category(raw: dict | None) -> SummaryCategory | None:
55
+ if raw is None:
56
+ return None
57
+ return SummaryCategory(
58
+ id=raw.get("id", ""),
59
+ key=raw.get("key", ""),
60
+ name=raw.get("name", ""),
61
+ raw=raw,
62
+ )
63
+
64
+
65
+
66
+ def _parse_station_summary(raw: dict) -> StationSummary:
67
+ properties = [
68
+ SummaryProperty(
69
+ property=item.get("property", {}),
70
+ time_points=[_parse_time_point(point) for point in item.get("timePoints", [])],
71
+ has_real_time_points=item.get("hasRealTimePoints"),
72
+ raw=item,
73
+ )
74
+ for item in raw.get("properties", [])
75
+ ]
76
+ return StationSummary(
77
+ category=_parse_summary_category(raw.get("category")),
78
+ properties=properties,
79
+ has_real_time_points=raw.get("hasRealTimePoints", False),
80
+ raw=raw,
81
+ )
82
+
83
+
84
+ async def fetch_station_list(
85
+ request: RequestFunc,
86
+ *,
87
+ page: int = 1,
88
+ count: int = DEFAULT_PAGE_SIZE,
89
+ name: str = "",
90
+ connected_grid_type: str = "",
91
+ state: str = "",
92
+ station_type: str = "",
93
+ ) -> PagedResult[Station]:
94
+ """Return a page of stations."""
95
+ data = await request(
96
+ "POST",
97
+ "/apis/station/list",
98
+ json={
99
+ "page": page,
100
+ "count": count,
101
+ "name": name,
102
+ "connectedGridType": connected_grid_type,
103
+ "state": state,
104
+ "stationType": station_type,
105
+ },
106
+ )
107
+ items = [_parse_station(item) for item in data.get("list", [])]
108
+ return PagedResult(
109
+ page=data.get("page", page),
110
+ count=data.get("count", count),
111
+ total=data.get("total", len(items)),
112
+ items=items,
113
+ raw=data,
114
+ )
115
+
116
+
117
+ async def fetch_station_details(request: RequestFunc, station_id: str) -> Station:
118
+ """Return station details."""
119
+ data = await request("GET", "/apis/station/details", params={"stationId": station_id})
120
+ return _parse_station(data)
121
+
122
+
123
+ async def fetch_station_energy_flow(
124
+ request: RequestFunc,
125
+ station_id: str,
126
+ *,
127
+ is_manual_refresh: bool = False,
128
+ ) -> StationEnergyFlow:
129
+ """Return current station energy-flow data."""
130
+ data = await request(
131
+ "GET",
132
+ "/apis/station/energy/flow",
133
+ params={"stationId": station_id, "isManualRefresh": is_manual_refresh},
134
+ )
135
+ return StationEnergyFlow(
136
+ is_support_flow=data.get("isSupportFlow", False),
137
+ time=parse_datetime(data.get("time")),
138
+ pv_panel=_parse_flow_node(data.get("pvPanelFlow")),
139
+ grid=_parse_flow_node(data.get("gridFlow")),
140
+ battery=_parse_flow_node(data.get("batteryFlow")),
141
+ load=_parse_flow_node(data.get("loadFlow")),
142
+ generator=_parse_flow_node(data.get("generatorFlow")),
143
+ ups=_parse_flow_node(data.get("upsFlow")),
144
+ ct=_parse_flow_node(data.get("ctFlow")),
145
+ raw=data,
146
+ )
147
+
148
+
149
+ async def fetch_station_income(
150
+ request: RequestFunc,
151
+ station_id: str,
152
+ aggregation: str,
153
+ *,
154
+ time: Any = None,
155
+ ) -> list[TimePoint]:
156
+ """Return aggregated station income points."""
157
+ data = await request(
158
+ "POST",
159
+ f"/apis/stationOverView/income/{aggregation}",
160
+ params={"stationId": station_id},
161
+ json={"time": serialize_datetime(time)},
162
+ )
163
+ return [_parse_time_point(item) for item in data]
164
+
165
+
166
+ async def fetch_station_state_summary(
167
+ request: RequestFunc,
168
+ station_id: str,
169
+ summary_category_key: str,
170
+ aggregation: str,
171
+ *,
172
+ time: Any = None,
173
+ ) -> StationSummary:
174
+ """Return aggregated station summary metrics."""
175
+ data = await request(
176
+ "POST",
177
+ f"/apis/stationOverView/stateAttributeSummary/category/{aggregation}",
178
+ params={"stationId": station_id, "summaryCategoryKey": summary_category_key},
179
+ json={"time": serialize_datetime(time)},
180
+ )
181
+ return _parse_station_summary(data)