pykoplenti 1.0.0__py3-none-any.whl → 1.2.1__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.
Potentially problematic release.
This version of pykoplenti might be problematic. Click here for more details.
- pykoplenti/__init__.py +37 -821
- pykoplenti/api.py +729 -0
- pykoplenti/cli.py +20 -14
- pykoplenti/extended.py +239 -0
- pykoplenti/model.py +99 -0
- {pykoplenti-1.0.0.dist-info → pykoplenti-1.2.1.dist-info}/METADATA +39 -29
- pykoplenti-1.2.1.dist-info/RECORD +12 -0
- {pykoplenti-1.0.0.dist-info → pykoplenti-1.2.1.dist-info}/WHEEL +1 -1
- pykoplenti-1.2.1.dist-info/entry_points.txt +2 -0
- kostal/plenticore/__init__.py +0 -659
- kostal/plenticore/cli.py +0 -352
- pykoplenti-1.0.0.dist-info/RECORD +0 -11
- pykoplenti-1.0.0.dist-info/entry_points.txt +0 -3
- /kostal/__init__.py → /pykoplenti/py.typed +0 -0
- {pykoplenti-1.0.0.dist-info → pykoplenti-1.2.1.dist-info}/LICENSE +0 -0
- {pykoplenti-1.0.0.dist-info → pykoplenti-1.2.1.dist-info}/top_level.txt +0 -0
pykoplenti/cli.py
CHANGED
|
@@ -7,13 +7,14 @@ from pprint import pprint
|
|
|
7
7
|
import re
|
|
8
8
|
import tempfile
|
|
9
9
|
import traceback
|
|
10
|
-
from typing import Callable
|
|
10
|
+
from typing import Any, Awaitable, Callable, Dict, Union
|
|
11
11
|
|
|
12
12
|
from aiohttp import ClientSession, ClientTimeout
|
|
13
13
|
import click
|
|
14
14
|
from prompt_toolkit import PromptSession, print_formatted_text
|
|
15
15
|
|
|
16
16
|
from pykoplenti import ApiClient
|
|
17
|
+
from pykoplenti.extended import ExtendedApiClient
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class SessionCache:
|
|
@@ -22,7 +23,7 @@ class SessionCache:
|
|
|
22
23
|
def __init__(self, host):
|
|
23
24
|
self.host = host
|
|
24
25
|
|
|
25
|
-
def read_session_id(self) -> str:
|
|
26
|
+
def read_session_id(self) -> Union[str, None]:
|
|
26
27
|
file = os.path.join(tempfile.gettempdir(), f"pykoplenti-session-{self.host}")
|
|
27
28
|
if os.path.isfile(file):
|
|
28
29
|
with open(file, "rt") as f:
|
|
@@ -77,7 +78,8 @@ class ApiShell:
|
|
|
77
78
|
# Test commands:
|
|
78
79
|
# get_settings
|
|
79
80
|
# get_setting_values 'devices:local' 'Battery:MinSoc'
|
|
80
|
-
# get_setting_values 'devices:local' ['Battery:MinSoc',
|
|
81
|
+
# get_setting_values 'devices:local' ['Battery:MinSoc', \
|
|
82
|
+
# 'Battery:MinHomeComsumption']
|
|
81
83
|
# get_setting_values 'scb:time'
|
|
82
84
|
# set_setting_values 'devices:local' {'Battery:MinSoc':'15'}
|
|
83
85
|
|
|
@@ -93,7 +95,8 @@ class ApiShell:
|
|
|
93
95
|
if text.strip() == "":
|
|
94
96
|
continue
|
|
95
97
|
else:
|
|
96
|
-
# TODO split does not know about lists or dicts or strings
|
|
98
|
+
# TODO split does not know about lists or dicts or strings
|
|
99
|
+
# with spaces
|
|
97
100
|
method_name, *arg_values = text.strip().split()
|
|
98
101
|
|
|
99
102
|
if method_name == "help":
|
|
@@ -152,17 +155,17 @@ class ApiShell:
|
|
|
152
155
|
|
|
153
156
|
async def repl_main(host, port, passwd):
|
|
154
157
|
async with ClientSession(timeout=ClientTimeout(total=10)) as session:
|
|
155
|
-
client =
|
|
158
|
+
client = ExtendedApiClient(session, host=host, port=port)
|
|
156
159
|
|
|
157
160
|
shell = ApiShell(client)
|
|
158
161
|
await shell.run(passwd)
|
|
159
162
|
|
|
160
163
|
|
|
161
164
|
async def command_main(
|
|
162
|
-
host: str, port: int, passwd: str, fn: Callable[[ApiClient],
|
|
165
|
+
host: str, port: int, passwd: str, fn: Callable[[ApiClient], Awaitable[Any]]
|
|
163
166
|
):
|
|
164
167
|
async with ClientSession(timeout=ClientTimeout(total=10)) as session:
|
|
165
|
-
client =
|
|
168
|
+
client = ExtendedApiClient(session, host=host, port=port)
|
|
166
169
|
session_cache = SessionCache(host)
|
|
167
170
|
|
|
168
171
|
# Try to reuse an existing session
|
|
@@ -171,7 +174,8 @@ async def command_main(
|
|
|
171
174
|
if not me.is_authenticated:
|
|
172
175
|
# create a new session
|
|
173
176
|
await client.login(passwd)
|
|
174
|
-
|
|
177
|
+
if client.session_id is not None:
|
|
178
|
+
session_cache.write_session_id(client.session_id)
|
|
175
179
|
|
|
176
180
|
await fn(client)
|
|
177
181
|
|
|
@@ -231,7 +235,8 @@ def read_events(global_args, lang, count):
|
|
|
231
235
|
data = await client.get_events(lang=lang, max_count=count)
|
|
232
236
|
for event in data:
|
|
233
237
|
print(
|
|
234
|
-
f"{event.is_active < 5} {event.start_time} {event.end_time}
|
|
238
|
+
f"{event.is_active < 5} {event.start_time} {event.end_time} "
|
|
239
|
+
"{event.description}"
|
|
235
240
|
)
|
|
236
241
|
|
|
237
242
|
asyncio.run(
|
|
@@ -253,7 +258,7 @@ def download_log(global_args, out, begin, end):
|
|
|
253
258
|
"""Download the log data from the inverter to a file."""
|
|
254
259
|
|
|
255
260
|
async def fn(client: ApiClient):
|
|
256
|
-
|
|
261
|
+
await client.download_logdata(writer=out, begin=begin, end=end)
|
|
257
262
|
|
|
258
263
|
asyncio.run(
|
|
259
264
|
command_main(global_args.host, global_args.port, global_args.passwd, fn)
|
|
@@ -309,7 +314,7 @@ def read_processdata(global_args, ids):
|
|
|
309
314
|
values = await client.get_process_data_values(query)
|
|
310
315
|
|
|
311
316
|
for k, v in values.items():
|
|
312
|
-
for x in v:
|
|
317
|
+
for x in v.values():
|
|
313
318
|
print(f"{k}/{x.id}={x.value}")
|
|
314
319
|
|
|
315
320
|
asyncio.run(
|
|
@@ -348,7 +353,8 @@ def read_settings(global_args, ids):
|
|
|
348
353
|
\b
|
|
349
354
|
Examples:
|
|
350
355
|
read-settings devices:local/Battery:MinSoc
|
|
351
|
-
read-settings devices:local/Battery:MinSoc
|
|
356
|
+
read-settings devices:local/Battery:MinSoc \
|
|
357
|
+
devices:local/Battery:MinHomeComsumption
|
|
352
358
|
"""
|
|
353
359
|
|
|
354
360
|
async def fn(client: ApiClient):
|
|
@@ -388,7 +394,7 @@ def write_settings(global_args, id_values):
|
|
|
388
394
|
"""
|
|
389
395
|
|
|
390
396
|
async def fn(client: ApiClient):
|
|
391
|
-
query = defaultdict(dict)
|
|
397
|
+
query: Dict[str, Dict[str, str]] = defaultdict(dict)
|
|
392
398
|
for id_value in id_values:
|
|
393
399
|
m = re.match(
|
|
394
400
|
r"(?P<module_id>.+)/(?P<setting_id>.+)=(?P<value>.+)", id_value
|
|
@@ -414,4 +420,4 @@ def write_settings(global_args, id_values):
|
|
|
414
420
|
if __name__ == "__main__":
|
|
415
421
|
import sys
|
|
416
422
|
|
|
417
|
-
cli(sys.argv[1:])
|
|
423
|
+
cli(sys.argv[1:], auto_envvar_prefix="PYKOPLENTI")
|
pykoplenti/extended.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Extended ApiClient which provides virtual process data values."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections import ChainMap, defaultdict
|
|
5
|
+
from typing import Final, Iterable, Literal, Mapping, MutableMapping, Union
|
|
6
|
+
|
|
7
|
+
from aiohttp import ClientSession
|
|
8
|
+
|
|
9
|
+
from .api import ApiClient
|
|
10
|
+
from .model import ProcessData, ProcessDataCollection
|
|
11
|
+
|
|
12
|
+
_VIRT_MODUL_ID: Final = "_virt_"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _VirtProcessDataItemBase(ABC):
|
|
16
|
+
"""Base class for all virtual process data items."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, processid: str, process_data: dict[str, set[str]]) -> None:
|
|
19
|
+
self.processid = processid
|
|
20
|
+
self.process_data = process_data
|
|
21
|
+
self.available_process_data: dict[str, set[str]] = {}
|
|
22
|
+
|
|
23
|
+
def update_actual_process_ids(
|
|
24
|
+
self, available_process_ids: Mapping[str, Iterable[str]]
|
|
25
|
+
):
|
|
26
|
+
"""Update which process data for this item are available."""
|
|
27
|
+
self.available_process_data.clear()
|
|
28
|
+
for module_id, process_ids in self.process_data.items():
|
|
29
|
+
if module_id in available_process_ids:
|
|
30
|
+
matching_process_ids = process_ids.intersection(
|
|
31
|
+
available_process_ids[module_id]
|
|
32
|
+
)
|
|
33
|
+
if len(matching_process_ids) > 0:
|
|
34
|
+
self.available_process_data[module_id] = matching_process_ids
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def get_value(
|
|
38
|
+
self, process_values: Mapping[str, ProcessDataCollection]
|
|
39
|
+
) -> ProcessData:
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def is_available(self) -> bool:
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _VirtProcessDataItemSum(_VirtProcessDataItemBase):
|
|
48
|
+
def get_value(
|
|
49
|
+
self, process_values: Mapping[str, ProcessDataCollection]
|
|
50
|
+
) -> ProcessData:
|
|
51
|
+
values: list[float] = []
|
|
52
|
+
for module_id, process_ids in self.available_process_data.items():
|
|
53
|
+
values += (process_values[module_id][pid].value for pid in process_ids)
|
|
54
|
+
|
|
55
|
+
return ProcessData(id=self.processid, unit="W", value=sum(values))
|
|
56
|
+
|
|
57
|
+
def is_available(self) -> bool:
|
|
58
|
+
return len(self.available_process_data) > 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _VirtProcessDataItemEnergyToGrid(_VirtProcessDataItemBase):
|
|
62
|
+
def __init__(
|
|
63
|
+
self, processid: str, scope: Literal["Total", "Year", "Month", "Day"]
|
|
64
|
+
) -> None:
|
|
65
|
+
super().__init__(
|
|
66
|
+
processid,
|
|
67
|
+
{
|
|
68
|
+
"scb:statistic:EnergyFlow": {
|
|
69
|
+
f"Statistic:Yield:{scope}",
|
|
70
|
+
f"Statistic:EnergyHomeBat:{scope}",
|
|
71
|
+
f"Statistic:EnergyHomePv:{scope}",
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
self.scope = scope
|
|
76
|
+
|
|
77
|
+
def get_value(
|
|
78
|
+
self, process_values: Mapping[str, ProcessDataCollection]
|
|
79
|
+
) -> ProcessData:
|
|
80
|
+
statistics = process_values["scb:statistic:EnergyFlow"]
|
|
81
|
+
energy_yield = statistics[f"Statistic:Yield:{self.scope}"].value
|
|
82
|
+
energy_home_bat = statistics[f"Statistic:EnergyHomeBat:{self.scope}"].value
|
|
83
|
+
energy_home_pv = statistics[f"Statistic:EnergyHomePv:{self.scope}"].value
|
|
84
|
+
|
|
85
|
+
return ProcessData(
|
|
86
|
+
id=self.processid,
|
|
87
|
+
unit="Wh",
|
|
88
|
+
value=energy_yield - energy_home_pv - energy_home_bat,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def is_available(self) -> bool:
|
|
92
|
+
return self.available_process_data == self.process_data
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class _VirtProcessDataManager:
|
|
96
|
+
"""Manager for all virtual process data items."""
|
|
97
|
+
|
|
98
|
+
def __init__(self) -> None:
|
|
99
|
+
self._virt_process_data: Iterable[_VirtProcessDataItemBase] = [
|
|
100
|
+
_VirtProcessDataItemSum(
|
|
101
|
+
"pv_P",
|
|
102
|
+
{
|
|
103
|
+
"devices:local:pv1": {"P"},
|
|
104
|
+
"devices:local:pv2": {"P"},
|
|
105
|
+
"devices:local:pv3": {"P"},
|
|
106
|
+
},
|
|
107
|
+
),
|
|
108
|
+
_VirtProcessDataItemEnergyToGrid("Statistic:EnergyGrid:Total", "Total"),
|
|
109
|
+
_VirtProcessDataItemEnergyToGrid("Statistic:EnergyGrid:Year", "Year"),
|
|
110
|
+
_VirtProcessDataItemEnergyToGrid("Statistic:EnergyGrid:Month", "Month"),
|
|
111
|
+
_VirtProcessDataItemEnergyToGrid("Statistic:EnergyGrid:Day", "Day"),
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
def initialize(self, available_process_data: Mapping[str, Iterable[str]]):
|
|
115
|
+
"""Initialize the virtual items with the list of available process ids."""
|
|
116
|
+
for vpd in self._virt_process_data:
|
|
117
|
+
vpd.update_actual_process_ids(available_process_data)
|
|
118
|
+
|
|
119
|
+
def adapt_process_data_response(
|
|
120
|
+
self, process_data: dict[str, list[str]]
|
|
121
|
+
) -> Mapping[str, list[str]]:
|
|
122
|
+
"""Adapt the reponse of reading process data."""
|
|
123
|
+
virt_process_data: dict[str, list[str]] = {_VIRT_MODUL_ID: []}
|
|
124
|
+
|
|
125
|
+
for vpd in self._virt_process_data:
|
|
126
|
+
if vpd.is_available():
|
|
127
|
+
virt_process_data[_VIRT_MODUL_ID].append(vpd.processid)
|
|
128
|
+
|
|
129
|
+
return ChainMap(process_data, virt_process_data)
|
|
130
|
+
|
|
131
|
+
def adapt_process_value_request(
|
|
132
|
+
self, process_data: Mapping[str, Iterable[str]]
|
|
133
|
+
) -> Mapping[str, Iterable[str]]:
|
|
134
|
+
"""Adapt the request for process values."""
|
|
135
|
+
result: MutableMapping[str, set[str]] = defaultdict(set)
|
|
136
|
+
|
|
137
|
+
for mid, pids in process_data.items():
|
|
138
|
+
result[mid].update(pids)
|
|
139
|
+
|
|
140
|
+
for requested_virtual_process_id in result.pop(_VIRT_MODUL_ID):
|
|
141
|
+
for virtual_process_data in self._virt_process_data:
|
|
142
|
+
if virtual_process_data.is_available():
|
|
143
|
+
if requested_virtual_process_id == virtual_process_data.processid:
|
|
144
|
+
# add ids for virtual if they are missing
|
|
145
|
+
for (
|
|
146
|
+
mid,
|
|
147
|
+
pids,
|
|
148
|
+
) in virtual_process_data.available_process_data.items():
|
|
149
|
+
result[mid].update(pids)
|
|
150
|
+
break
|
|
151
|
+
else:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f"No virtual process data '{requested_virtual_process_id}'."
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
def adapt_process_value_response(
|
|
159
|
+
self,
|
|
160
|
+
values: Mapping[str, ProcessDataCollection],
|
|
161
|
+
request_data: Mapping[str, Iterable[str]],
|
|
162
|
+
) -> Mapping[str, ProcessDataCollection]:
|
|
163
|
+
"""Adapt the reponse for process values."""
|
|
164
|
+
result = {}
|
|
165
|
+
|
|
166
|
+
# add virtual items
|
|
167
|
+
virtual_process_data_values = []
|
|
168
|
+
for id in request_data[_VIRT_MODUL_ID]:
|
|
169
|
+
for vpd in self._virt_process_data:
|
|
170
|
+
if vpd.processid == id:
|
|
171
|
+
virtual_process_data_values.append(vpd.get_value(values))
|
|
172
|
+
result["_virt_"] = ProcessDataCollection(virtual_process_data_values)
|
|
173
|
+
|
|
174
|
+
# add all values which was requested but not the extra ids for the virtual ids
|
|
175
|
+
for mid, pdc in values.items():
|
|
176
|
+
if mid in request_data:
|
|
177
|
+
pids = [x for x in pdc.values() if x.id in request_data[mid]]
|
|
178
|
+
if len(pids) > 0:
|
|
179
|
+
result[mid] = ProcessDataCollection(pids)
|
|
180
|
+
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class ExtendedApiClient(ApiClient):
|
|
185
|
+
"""Extend ApiClient with virtual process data."""
|
|
186
|
+
|
|
187
|
+
def __init__(self, websession: ClientSession, host: str, port: int = 80):
|
|
188
|
+
super().__init__(websession, host, port)
|
|
189
|
+
|
|
190
|
+
self._virt_process_data = _VirtProcessDataManager()
|
|
191
|
+
self._virt_process_data_initialized = False
|
|
192
|
+
|
|
193
|
+
async def get_process_data(self) -> Mapping[str, Iterable[str]]:
|
|
194
|
+
process_data = await super().get_process_data()
|
|
195
|
+
|
|
196
|
+
self._virt_process_data.initialize(process_data)
|
|
197
|
+
self._virt_process_data_initialized = True
|
|
198
|
+
return self._virt_process_data.adapt_process_data_response(process_data)
|
|
199
|
+
|
|
200
|
+
async def get_process_data_values(
|
|
201
|
+
self,
|
|
202
|
+
module_id: Union[str, Mapping[str, Iterable[str]]],
|
|
203
|
+
processdata_id: Union[str, Iterable[str], None] = None,
|
|
204
|
+
) -> Mapping[str, ProcessDataCollection]:
|
|
205
|
+
contains_virt_process_data = (
|
|
206
|
+
isinstance(module_id, str) and _VIRT_MODUL_ID == module_id
|
|
207
|
+
) or (isinstance(module_id, dict) and _VIRT_MODUL_ID in module_id)
|
|
208
|
+
|
|
209
|
+
if not contains_virt_process_data:
|
|
210
|
+
# short-cut if no virtual process is requested
|
|
211
|
+
return await super().get_process_data_values(module_id, processdata_id)
|
|
212
|
+
|
|
213
|
+
process_data: dict[str, Iterable[str]] = {}
|
|
214
|
+
if isinstance(module_id, str) and processdata_id is None:
|
|
215
|
+
process_data[module_id] = []
|
|
216
|
+
elif isinstance(module_id, str) and isinstance(processdata_id, str):
|
|
217
|
+
process_data[module_id] = [processdata_id]
|
|
218
|
+
elif (
|
|
219
|
+
isinstance(module_id, str)
|
|
220
|
+
and processdata_id is not None
|
|
221
|
+
and hasattr(processdata_id, "__iter__")
|
|
222
|
+
):
|
|
223
|
+
process_data[module_id] = list(processdata_id)
|
|
224
|
+
elif isinstance(module_id, Mapping) and processdata_id is None:
|
|
225
|
+
process_data.update(module_id)
|
|
226
|
+
else:
|
|
227
|
+
raise TypeError("Invalid combination of module_id and processdata_id.")
|
|
228
|
+
|
|
229
|
+
if not self._virt_process_data_initialized:
|
|
230
|
+
pd = await self.get_process_data()
|
|
231
|
+
self._virt_process_data.initialize(pd)
|
|
232
|
+
self._virt_process_data_initialized = True
|
|
233
|
+
|
|
234
|
+
process_values = await super().get_process_data_values(
|
|
235
|
+
self._virt_process_data.adapt_process_value_request(process_data)
|
|
236
|
+
)
|
|
237
|
+
return self._virt_process_data.adapt_process_value_response(
|
|
238
|
+
process_values, process_data
|
|
239
|
+
)
|
pykoplenti/model.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Iterator, Mapping
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MeData(BaseModel):
|
|
8
|
+
"""Represent the data of the 'me'-request."""
|
|
9
|
+
|
|
10
|
+
is_locked: bool = Field(alias="locked")
|
|
11
|
+
is_active: bool = Field(alias="active")
|
|
12
|
+
is_authenticated: bool = Field(alias="authenticated")
|
|
13
|
+
permissions: list[str] = Field()
|
|
14
|
+
is_anonymous: bool = Field(alias="anonymous")
|
|
15
|
+
role: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VersionData(BaseModel):
|
|
19
|
+
"""Represent the data of the 'version'-request."""
|
|
20
|
+
|
|
21
|
+
api_version: str
|
|
22
|
+
hostname: str
|
|
23
|
+
name: str
|
|
24
|
+
sw_version: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ModuleData(BaseModel):
|
|
28
|
+
"""Represents a single module."""
|
|
29
|
+
|
|
30
|
+
id: str
|
|
31
|
+
type: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ProcessData(BaseModel):
|
|
35
|
+
"""Represents a single process data."""
|
|
36
|
+
|
|
37
|
+
id: str
|
|
38
|
+
unit: str
|
|
39
|
+
value: float
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ProcessDataCollection(Mapping):
|
|
43
|
+
"""Represents a collection of process data value."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, process_data: list[ProcessData]):
|
|
46
|
+
self._process_data = process_data
|
|
47
|
+
|
|
48
|
+
def __len__(self) -> int:
|
|
49
|
+
return len(self._process_data)
|
|
50
|
+
|
|
51
|
+
def __iter__(self) -> Iterator[str]:
|
|
52
|
+
return (x.id for x in self._process_data)
|
|
53
|
+
|
|
54
|
+
def __getitem__(self, item) -> ProcessData:
|
|
55
|
+
try:
|
|
56
|
+
return next(x for x in self._process_data if x.id == item)
|
|
57
|
+
except StopIteration:
|
|
58
|
+
raise KeyError(item)
|
|
59
|
+
|
|
60
|
+
def __eq__(self, __other: object) -> bool:
|
|
61
|
+
if not isinstance(__other, ProcessDataCollection):
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
return self._process_data == __other._process_data
|
|
65
|
+
|
|
66
|
+
def __str__(self):
|
|
67
|
+
return "[" + ",".join(str(x) for x in self._process_data) + "]"
|
|
68
|
+
|
|
69
|
+
def __repr__(self):
|
|
70
|
+
return (
|
|
71
|
+
"ProcessDataCollection(["
|
|
72
|
+
+ ",".join(repr(x) for x in self._process_data)
|
|
73
|
+
+ "])"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SettingsData(BaseModel):
|
|
78
|
+
"""Represents a single settings data."""
|
|
79
|
+
|
|
80
|
+
min: str | None
|
|
81
|
+
max: str | None
|
|
82
|
+
default: str | None
|
|
83
|
+
access: str
|
|
84
|
+
unit: str | None
|
|
85
|
+
id: str
|
|
86
|
+
type: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class EventData(BaseModel):
|
|
90
|
+
"""Represents an event of the inverter."""
|
|
91
|
+
|
|
92
|
+
start_time: datetime
|
|
93
|
+
end_time: datetime
|
|
94
|
+
code: int
|
|
95
|
+
long_description: str
|
|
96
|
+
category: str
|
|
97
|
+
description: str
|
|
98
|
+
group: str
|
|
99
|
+
is_active: bool
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pykoplenti
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: Python REST-Client for Kostal Plenticore Solar Inverters
|
|
5
5
|
Home-page: https://github.com/stegm/pyclient_koplenti
|
|
6
6
|
Author: @stegm
|
|
7
|
-
|
|
7
|
+
Project-URL: repository, https://github.com/stegm/pyclient_koplenti
|
|
8
|
+
Project-URL: changelog, https://github.com/stegm/pykoplenti/blob/master/CHANGELOG.md
|
|
9
|
+
Project-URL: issues, https://github.com/stegm/pykoplenti/issues
|
|
8
10
|
Keywords: rest kostal plenticore solar
|
|
9
|
-
Platform: UNKNOWN
|
|
10
11
|
Classifier: Development Status :: 4 - Beta
|
|
11
12
|
Classifier: Environment :: Console
|
|
12
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -16,11 +17,13 @@ Classifier: Programming Language :: Python :: 3.7
|
|
|
16
17
|
Classifier: Programming Language :: Python :: 3.8
|
|
17
18
|
Classifier: Topic :: Software Development :: Libraries
|
|
18
19
|
Description-Content-Type: text/markdown
|
|
19
|
-
|
|
20
|
-
Requires-Dist:
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: aiohttp ~=3.8.5
|
|
22
|
+
Requires-Dist: pycryptodome ~=3.19
|
|
23
|
+
Requires-Dist: pydantic ~=1.10
|
|
21
24
|
Provides-Extra: cli
|
|
22
|
-
Requires-Dist: prompt-toolkit
|
|
23
|
-
Requires-Dist: click
|
|
25
|
+
Requires-Dist: prompt-toolkit >=3.0 ; extra == 'cli'
|
|
26
|
+
Requires-Dist: click >=7.1 ; extra == 'cli'
|
|
24
27
|
|
|
25
28
|
# Python Library for Accessing Kostal Plenticore Inverters
|
|
26
29
|
|
|
@@ -32,14 +35,15 @@ This library is not affiliated with Kostal and is no offical product. It uses th
|
|
|
32
35
|
|
|
33
36
|
## Features
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
- Authenticate
|
|
39
|
+
- Read/Write settings
|
|
40
|
+
- Read process data
|
|
41
|
+
- Read events
|
|
42
|
+
- Download of log data
|
|
43
|
+
- Full async-Support for reading and writing data
|
|
44
|
+
- [Commandline interface](doc/command_line.md) for shell access
|
|
45
|
+
- Dynamic data model - adapts automatically to new process data or settings
|
|
46
|
+
- [Virtual Process Data](doc/virtual_process_data.md) values
|
|
43
47
|
|
|
44
48
|
## Getting Started
|
|
45
49
|
|
|
@@ -49,12 +53,11 @@ You will need Python >=3.7.
|
|
|
49
53
|
|
|
50
54
|
### Installing the library
|
|
51
55
|
|
|
52
|
-
Packages of this library are released on [PyPI](https://pypi.org/project/kostal-plenticore/) and can be
|
|
53
|
-
installed with `pip`. Alternatively the packages can also be downloaded from
|
|
56
|
+
Packages of this library are released on [PyPI](https://pypi.org/project/kostal-plenticore/) and can be
|
|
57
|
+
installed with `pip`. Alternatively the packages can also be downloaded from
|
|
54
58
|
[GitHub](https://github.com/stegm/pykoplenti/releases/).
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
I recommend to use a [virtual environment](https://docs.python.org/3/library/venv.html) for this,
|
|
60
|
+
I recommend to use a [virtual environment](https://docs.python.org/3/library/venv.html) for this,
|
|
58
61
|
because it installs the dependecies independently from the system. The installed CLI tools can then be called
|
|
59
62
|
without activating the virtual environment it.
|
|
60
63
|
|
|
@@ -68,7 +71,6 @@ $ pip install pykoplenti
|
|
|
68
71
|
|
|
69
72
|
### Using the command line interface
|
|
70
73
|
|
|
71
|
-
|
|
72
74
|
Installing the libray with `CLI` provides a new command.
|
|
73
75
|
|
|
74
76
|
```shell
|
|
@@ -133,16 +135,28 @@ home_p = device_local['Home_P']
|
|
|
133
135
|
|
|
134
136
|
See the full example here: [read_process_data.py](examples/read_process_data.py).
|
|
135
137
|
|
|
138
|
+
If you should need installer access use the master key (printed on a label at the side of the inverter)
|
|
139
|
+
and additionally pass your service code:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
await client.login(my_master_key, service_code=my_service_code)
|
|
143
|
+
```
|
|
136
144
|
|
|
137
145
|
## Documentation
|
|
138
146
|
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
- [Command Line Interface](doc/command_line.md)
|
|
148
|
+
- [Examples](examples/)
|
|
149
|
+
- [Virtual Process Data](doc/virtual_process_data.md)
|
|
141
150
|
|
|
142
151
|
## Built With
|
|
143
152
|
|
|
144
|
-
|
|
145
|
-
|
|
153
|
+
- [AIOHTTPO](https://docs.aiohttp.org/en/stable/) - asyncio for HTTP
|
|
154
|
+
- [click](https://click.palletsprojects.com/) - command line interface framework
|
|
155
|
+
- [black](https://github.com/psf/black) - Python code formatter
|
|
156
|
+
- [ruff](https://github.com/astral-sh/ruff) - Python linter
|
|
157
|
+
- [pytest](https://docs.pytest.org/) - Python test framework
|
|
158
|
+
- [mypy](https://mypy-lang.org/) - Python type checker
|
|
159
|
+
- [setuptools](https://github.com/pypa/setuptools) - Python packager
|
|
146
160
|
|
|
147
161
|
## License
|
|
148
162
|
|
|
@@ -150,8 +164,4 @@ apache-2.0
|
|
|
150
164
|
|
|
151
165
|
## Acknowledgments
|
|
152
166
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
* [kilianknoll](https://github.com/kilianknoll) for the kostal-RESTAPI project
|
|
156
|
-
|
|
157
|
-
|
|
167
|
+
- [kilianknoll](https://github.com/kilianknoll) for the kostal-RESTAPI project
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pykoplenti/__init__.py,sha256=w9ooy6_JaT9lGvMDAaBHyvYu1uTFTkb1g21WRxp3Kzw,756
|
|
2
|
+
pykoplenti/api.py,sha256=gYWWd8yZ9winj7qPt1-rzx4stX_DLPfy5nOduuamtH4,26431
|
|
3
|
+
pykoplenti/cli.py,sha256=LAiQHlSgoJz07kTtFh0bNyahyYz7gCenhRfradex5wE,12972
|
|
4
|
+
pykoplenti/extended.py,sha256=_MCDtP-6BaAiTFqJ44CLK_Ihkh6nCC0vWWv1lsQfEFs,9311
|
|
5
|
+
pykoplenti/model.py,sha256=g-KyYTF1M1p6OAebyA74OAP_-561u6Hylhgy_jnpMto,2266
|
|
6
|
+
pykoplenti/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
pykoplenti-1.2.1.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
8
|
+
pykoplenti-1.2.1.dist-info/METADATA,sha256=ztvYvxRUUOWJYIbUJ4icUf1X14C_55BJMoA6GiN3ABI,5577
|
|
9
|
+
pykoplenti-1.2.1.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
|
|
10
|
+
pykoplenti-1.2.1.dist-info/entry_points.txt,sha256=Ix-p1uzKNyOMTK0TOlAC0nsfWS6ATXVuaNDJ5-TwBJw,56
|
|
11
|
+
pykoplenti-1.2.1.dist-info/top_level.txt,sha256=Bi915FGIFYzCujwn5Kwhu3B-sxElgc7gX3gNaYjl4j8,11
|
|
12
|
+
pykoplenti-1.2.1.dist-info/RECORD,,
|