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
kostal/plenticore/cli.py
DELETED
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import os
|
|
3
|
-
from pprint import pprint
|
|
4
|
-
import re
|
|
5
|
-
import tempfile
|
|
6
|
-
import traceback
|
|
7
|
-
from ast import literal_eval
|
|
8
|
-
from collections import defaultdict
|
|
9
|
-
from inspect import iscoroutinefunction
|
|
10
|
-
from typing import Callable
|
|
11
|
-
|
|
12
|
-
import click
|
|
13
|
-
from aiohttp import ClientSession, ClientTimeout
|
|
14
|
-
from prompt_toolkit import PromptSession, print_formatted_text
|
|
15
|
-
|
|
16
|
-
from kostal import PlenticoreApiClient
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class SessionCache:
|
|
20
|
-
"""Persistent the session in a temporary file."""
|
|
21
|
-
def __init__(self, host):
|
|
22
|
-
self.host = host
|
|
23
|
-
|
|
24
|
-
def read_session_id(self) -> str:
|
|
25
|
-
file = os.path.join(tempfile.gettempdir(),
|
|
26
|
-
f'plenticore-session-{self.host}')
|
|
27
|
-
if os.path.isfile(file):
|
|
28
|
-
with open(file, 'rt') as f:
|
|
29
|
-
return f.readline(256)
|
|
30
|
-
else:
|
|
31
|
-
return None
|
|
32
|
-
|
|
33
|
-
def write_session_id(self, id: str):
|
|
34
|
-
file = os.path.join(tempfile.gettempdir(),
|
|
35
|
-
f'plenticore-session-{self.host}')
|
|
36
|
-
f = os.open(file, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode=0o600)
|
|
37
|
-
try:
|
|
38
|
-
os.write(f, id.encode('ascii'))
|
|
39
|
-
finally:
|
|
40
|
-
os.close(f)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class PlenticoreShell:
|
|
44
|
-
"""Provides a shell-like access to the plenticore client."""
|
|
45
|
-
def __init__(self, client: PlenticoreApiClient):
|
|
46
|
-
super().__init__()
|
|
47
|
-
self.client = client
|
|
48
|
-
self._session_cache = SessionCache(self.client.host)
|
|
49
|
-
|
|
50
|
-
async def prepare_client(self, passwd):
|
|
51
|
-
# first try to reuse existing session
|
|
52
|
-
session_id = self._session_cache.read_session_id()
|
|
53
|
-
if session_id is not None:
|
|
54
|
-
self.client.session_id = session_id
|
|
55
|
-
print_formatted_text('Trying to reuse existing session... ',
|
|
56
|
-
end=None)
|
|
57
|
-
me = await self.client.get_me()
|
|
58
|
-
if me.is_authenticated:
|
|
59
|
-
print_formatted_text('Success')
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
print_formatted_text('Failed')
|
|
63
|
-
|
|
64
|
-
if passwd is not None:
|
|
65
|
-
print_formatted_text('Logging in... ', end=None)
|
|
66
|
-
await self.client.login(passwd)
|
|
67
|
-
self._session_cache.write_session_id(self.client.session_id)
|
|
68
|
-
print_formatted_text('Success')
|
|
69
|
-
|
|
70
|
-
def print_exception(self):
|
|
71
|
-
"""Prints an excpetion from executing a method."""
|
|
72
|
-
print_formatted_text(traceback.format_exc())
|
|
73
|
-
|
|
74
|
-
async def run(self, passwd):
|
|
75
|
-
session = PromptSession()
|
|
76
|
-
print_formatted_text(flush=True) # Initialize output
|
|
77
|
-
|
|
78
|
-
# Test commands:
|
|
79
|
-
# get_settings
|
|
80
|
-
# get_setting_values 'devices:local' 'Battery:MinSoc'
|
|
81
|
-
# get_setting_values 'devices:local' ['Battery:MinSoc','Battery:MinHomeComsumption']
|
|
82
|
-
# get_setting_values 'scb:time'
|
|
83
|
-
# set_setting_values 'devices:local' {'Battery:MinSoc':'15'}
|
|
84
|
-
|
|
85
|
-
await self.prepare_client(passwd)
|
|
86
|
-
|
|
87
|
-
while True:
|
|
88
|
-
try:
|
|
89
|
-
text = await session.prompt_async('(plenticore)> ')
|
|
90
|
-
|
|
91
|
-
if text.strip().lower() == 'exit':
|
|
92
|
-
raise EOFError()
|
|
93
|
-
|
|
94
|
-
if text.strip() == '':
|
|
95
|
-
continue
|
|
96
|
-
else:
|
|
97
|
-
# TODO split does not know about lists or dicts or strings with spaces
|
|
98
|
-
method_name, *arg_values = text.strip().split()
|
|
99
|
-
|
|
100
|
-
if method_name == 'help':
|
|
101
|
-
if len(arg_values) == 0:
|
|
102
|
-
print_formatted_text("Try: help <command>")
|
|
103
|
-
else:
|
|
104
|
-
method = getattr(self.client, arg_values[0])
|
|
105
|
-
print_formatted_text(method.__doc__)
|
|
106
|
-
continue
|
|
107
|
-
|
|
108
|
-
try:
|
|
109
|
-
method = getattr(self.client, method_name)
|
|
110
|
-
except AttributeError:
|
|
111
|
-
print_formatted_text(f'Unknown method: {method_name}')
|
|
112
|
-
continue
|
|
113
|
-
|
|
114
|
-
try:
|
|
115
|
-
args = list([literal_eval(x) for x in arg_values])
|
|
116
|
-
except:
|
|
117
|
-
print_formatted_text('Error parsing arguments')
|
|
118
|
-
self.print_exception()
|
|
119
|
-
continue
|
|
120
|
-
|
|
121
|
-
try:
|
|
122
|
-
if iscoroutinefunction(method):
|
|
123
|
-
result = await method(*args)
|
|
124
|
-
else:
|
|
125
|
-
result = method(*args)
|
|
126
|
-
except:
|
|
127
|
-
print_formatted_text('Error executing method')
|
|
128
|
-
self.print_exception()
|
|
129
|
-
continue
|
|
130
|
-
|
|
131
|
-
pprint(result)
|
|
132
|
-
|
|
133
|
-
except KeyboardInterrupt:
|
|
134
|
-
continue
|
|
135
|
-
except EOFError:
|
|
136
|
-
break
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
async def repl_main(host, port, passwd):
|
|
140
|
-
async with ClientSession(timeout=ClientTimeout(total=10)) as session:
|
|
141
|
-
client = PlenticoreApiClient(session, host=host, port=port)
|
|
142
|
-
|
|
143
|
-
shell = PlenticoreShell(client)
|
|
144
|
-
await shell.run(passwd)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
async def command_main(host: str, port: int, passwd: str,
|
|
148
|
-
fn: Callable[[PlenticoreApiClient], None]):
|
|
149
|
-
async with ClientSession(timeout=ClientTimeout(total=10)) as session:
|
|
150
|
-
client = PlenticoreApiClient(session, host=host, port=port)
|
|
151
|
-
session_cache = SessionCache(host)
|
|
152
|
-
|
|
153
|
-
# Try to reuse an existing session
|
|
154
|
-
client.session_id = session_cache.read_session_id()
|
|
155
|
-
me = await client.get_me()
|
|
156
|
-
if not me.is_authenticated:
|
|
157
|
-
# create a new session
|
|
158
|
-
await client.login(passwd)
|
|
159
|
-
session_cache.write_session_id(client.session_id)
|
|
160
|
-
|
|
161
|
-
await fn(client)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
class GlobalArgs:
|
|
165
|
-
"""Global arguments over all sub commands."""
|
|
166
|
-
def __init__(self):
|
|
167
|
-
self.host = None
|
|
168
|
-
self.port = None
|
|
169
|
-
self.password = None
|
|
170
|
-
self.password_file = None
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
pass_global_args = click.make_pass_decorator(GlobalArgs, ensure=True)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
@click.group()
|
|
177
|
-
@click.option('--host', help='hostname or ip of plenticore inverter')
|
|
178
|
-
@click.option('--port', default=80, help='port of plenticore (default 80)')
|
|
179
|
-
@click.option('--password', default=None, help='the password')
|
|
180
|
-
@click.option(
|
|
181
|
-
'--password-file',
|
|
182
|
-
default='secrets',
|
|
183
|
-
help='password file (default "secrets" in the current working directory)')
|
|
184
|
-
@pass_global_args
|
|
185
|
-
def cli(global_args, host, port, password, password_file):
|
|
186
|
-
"""Handling of global arguments with click"""
|
|
187
|
-
if password is not None:
|
|
188
|
-
global_args.passwd = password
|
|
189
|
-
elif os.path.isfile(password_file):
|
|
190
|
-
with open(password_file, 'rt') as f:
|
|
191
|
-
global_args.passwd = f.readline()
|
|
192
|
-
else:
|
|
193
|
-
global_args.passwd = None
|
|
194
|
-
|
|
195
|
-
global_args.host = host
|
|
196
|
-
global_args.port = port
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
@cli.command()
|
|
200
|
-
@pass_global_args
|
|
201
|
-
def repl(global_args):
|
|
202
|
-
"""Provides a simple REPL for executing API requests to plenticore inverters."""
|
|
203
|
-
asyncio.run(
|
|
204
|
-
repl_main(global_args.host, global_args.port, global_args.passwd))
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
@cli.command()
|
|
208
|
-
@pass_global_args
|
|
209
|
-
def all_processdata(global_args):
|
|
210
|
-
"""Returns a list of all available process data."""
|
|
211
|
-
async def fn(client: PlenticoreApiClient):
|
|
212
|
-
data = await client.get_process_data()
|
|
213
|
-
for k, v in data.items():
|
|
214
|
-
for x in v:
|
|
215
|
-
print(f'{k}/{x}')
|
|
216
|
-
|
|
217
|
-
asyncio.run(
|
|
218
|
-
command_main(global_args.host, global_args.port, global_args.passwd,
|
|
219
|
-
fn))
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
@cli.command()
|
|
223
|
-
@click.argument('ids', required=True, nargs=-1)
|
|
224
|
-
@pass_global_args
|
|
225
|
-
def read_processdata(global_args, ids):
|
|
226
|
-
"""Returns the values of the given process data.
|
|
227
|
-
|
|
228
|
-
IDS is the identifier (<module_id>/<processdata_id>) of one or more processdata
|
|
229
|
-
to read.
|
|
230
|
-
|
|
231
|
-
\b
|
|
232
|
-
Examples:
|
|
233
|
-
read-processdata devices:local/Inverter:State
|
|
234
|
-
"""
|
|
235
|
-
async def fn(client: PlenticoreApiClient):
|
|
236
|
-
if len(ids) == 1 and '/' not in ids[0]:
|
|
237
|
-
# all process data ids of a moudle
|
|
238
|
-
values = await client.get_process_data_values(ids[0])
|
|
239
|
-
else:
|
|
240
|
-
query = defaultdict(list)
|
|
241
|
-
for id in ids:
|
|
242
|
-
m = re.match(r'(?P<module_id>.+)/(?P<processdata_id>.+)', id)
|
|
243
|
-
if not m:
|
|
244
|
-
raise Exception(f'Invalid format of {id}')
|
|
245
|
-
|
|
246
|
-
module_id = m.group('module_id')
|
|
247
|
-
setting_id = m.group('processdata_id')
|
|
248
|
-
|
|
249
|
-
query[module_id].append(setting_id)
|
|
250
|
-
|
|
251
|
-
values = await client.get_process_data_values(query)
|
|
252
|
-
|
|
253
|
-
for k, v in values.items():
|
|
254
|
-
for x in v:
|
|
255
|
-
print(f'{k}/{x.id}={x.value}')
|
|
256
|
-
|
|
257
|
-
asyncio.run(
|
|
258
|
-
command_main(global_args.host, global_args.port, global_args.passwd,
|
|
259
|
-
fn))
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
@cli.command()
|
|
263
|
-
@click.option('--rw', is_flag=True, default=False, help='display only writable settings')
|
|
264
|
-
@pass_global_args
|
|
265
|
-
def all_settings(global_args, rw):
|
|
266
|
-
"""Returns the ids of all settings."""
|
|
267
|
-
async def fn(client: PlenticoreApiClient):
|
|
268
|
-
settings = await client.get_settings()
|
|
269
|
-
for k, v in settings.items():
|
|
270
|
-
for x in v:
|
|
271
|
-
if not rw or x.access == 'readwrite':
|
|
272
|
-
print(f'{k}/{x.id}')
|
|
273
|
-
|
|
274
|
-
asyncio.run(
|
|
275
|
-
command_main(global_args.host, global_args.port, global_args.passwd,
|
|
276
|
-
fn))
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
@cli.command()
|
|
280
|
-
@click.argument('ids', required=True, nargs=-1)
|
|
281
|
-
@pass_global_args
|
|
282
|
-
def read_settings(global_args, ids):
|
|
283
|
-
"""Read the value of the given settings.
|
|
284
|
-
|
|
285
|
-
IDS is the identifier (<module_id>/<setting_id>) of one or more settings to read
|
|
286
|
-
|
|
287
|
-
\b
|
|
288
|
-
Examples:
|
|
289
|
-
read-settings devices:local/Battery:MinSoc
|
|
290
|
-
read-settings devices:local/Battery:MinSoc devices:local/Battery:MinHomeComsumption
|
|
291
|
-
"""
|
|
292
|
-
async def fn(client: PlenticoreApiClient):
|
|
293
|
-
query = defaultdict(list)
|
|
294
|
-
for id in ids:
|
|
295
|
-
m = re.match(r'(?P<module_id>.+)/(?P<setting_id>.+)', id)
|
|
296
|
-
if not m:
|
|
297
|
-
raise Exception(f'Invalid format of {id}')
|
|
298
|
-
|
|
299
|
-
module_id = m.group('module_id')
|
|
300
|
-
setting_id = m.group('setting_id')
|
|
301
|
-
|
|
302
|
-
query[module_id].append(setting_id)
|
|
303
|
-
|
|
304
|
-
values = await client.get_setting_values(query)
|
|
305
|
-
|
|
306
|
-
for k, x in values.items():
|
|
307
|
-
for i, v in x.items():
|
|
308
|
-
print(f'{k}/{i}={v}')
|
|
309
|
-
|
|
310
|
-
asyncio.run(
|
|
311
|
-
command_main(global_args.host, global_args.port, global_args.passwd,
|
|
312
|
-
fn))
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
@cli.command()
|
|
316
|
-
@click.argument('id_values', required=True, nargs=-1)
|
|
317
|
-
@pass_global_args
|
|
318
|
-
def write_settings(global_args, id_values):
|
|
319
|
-
"""Write the values of the given settings.
|
|
320
|
-
|
|
321
|
-
ID_VALUES is the identifier plus the the value to write
|
|
322
|
-
|
|
323
|
-
\b
|
|
324
|
-
Examples:
|
|
325
|
-
write-settings devices:local/Battery:MinSoc=15
|
|
326
|
-
"""
|
|
327
|
-
async def fn(client: PlenticoreApiClient):
|
|
328
|
-
query = defaultdict(dict)
|
|
329
|
-
for id_value in id_values:
|
|
330
|
-
m = re.match(r'(?P<module_id>.+)/(?P<setting_id>.+)=(?P<value>.+)',
|
|
331
|
-
id_value)
|
|
332
|
-
if not m:
|
|
333
|
-
raise Exception(f'Invalid format of {id_value}')
|
|
334
|
-
|
|
335
|
-
module_id = m.group('module_id')
|
|
336
|
-
setting_id = m.group('setting_id')
|
|
337
|
-
value = m.group('value')
|
|
338
|
-
|
|
339
|
-
query[module_id][setting_id] = value
|
|
340
|
-
|
|
341
|
-
for module_id, setting_values in query.items():
|
|
342
|
-
await client.set_setting_values(module_id, setting_values)
|
|
343
|
-
|
|
344
|
-
asyncio.run(
|
|
345
|
-
command_main(global_args.host, global_args.port, global_args.passwd,
|
|
346
|
-
fn))
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
# entry point for pycharm; should not be used for commandline usage
|
|
350
|
-
if __name__ == '__main__':
|
|
351
|
-
import sys
|
|
352
|
-
cli(sys.argv[1:])
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
kostal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
kostal/plenticore/__init__.py,sha256=SIjZp--uGq0VIQbrGeei4ZeLQTVaNewoFMbgYMnOZTQ,24186
|
|
3
|
-
kostal/plenticore/cli.py,sha256=woo0izeFltTv5TZ9YdPMMUV2jBnV5YN_antSTbjK4iI,11544
|
|
4
|
-
pykoplenti/__init__.py,sha256=HjjB5yAnA2GnX9xAI2dD4b9MswoA3vYda5QpJ6AXTpU,27205
|
|
5
|
-
pykoplenti/cli.py,sha256=lKAWqUJqu9-R_2wdcZKAk3WzrBio_7rkgj9r5C-un-U,12674
|
|
6
|
-
pykoplenti-1.0.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
7
|
-
pykoplenti-1.0.0.dist-info/METADATA,sha256=OgnpHqubJ6bVqcxtSS7NCrRMpYZTVsGXqrTl8VJH_aY,4633
|
|
8
|
-
pykoplenti-1.0.0.dist-info/WHEEL,sha256=OqRkF0eY5GHssMorFjlbTIq072vpHpF60fIQA6lS9xA,92
|
|
9
|
-
pykoplenti-1.0.0.dist-info/entry_points.txt,sha256=wEHiNCeOALOIPnjw9l0OSvplSDWqYyMqO7QcOU8bRAo,57
|
|
10
|
-
pykoplenti-1.0.0.dist-info/top_level.txt,sha256=Bi915FGIFYzCujwn5Kwhu3B-sxElgc7gX3gNaYjl4j8,11
|
|
11
|
-
pykoplenti-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|