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 CHANGED
@@ -1,821 +1,37 @@
1
- from base64 import b64decode, b64encode
2
- from collections.abc import Mapping
3
- import contextlib
4
- from datetime import datetime
5
- import functools
6
- import hashlib
7
- import hmac
8
- from json import dumps
9
- import locale
10
- import logging
11
- from os import urandom
12
- from typing import IO, Dict, Iterable, Union
13
-
14
- from Crypto.Cipher import AES
15
- from aiohttp import ClientResponse, ClientSession, ClientTimeout
16
- from yarl import URL
17
-
18
- logger = logging.getLogger(__name__)
19
-
20
-
21
- class MeData:
22
- """Represent the data of the 'me'-request."""
23
-
24
- def __init__(self, raw):
25
- self._raw = raw
26
-
27
- @property
28
- def is_locked(self) -> bool:
29
- return self._raw["locked"]
30
-
31
- @property
32
- def is_active(self) -> bool:
33
- return self._raw["active"]
34
-
35
- @property
36
- def is_authenticated(self) -> bool:
37
- return self._raw["authenticated"]
38
-
39
- @property
40
- def permissions(self) -> Iterable[str]:
41
- return self._raw["permissions"]
42
-
43
- @property
44
- def is_anonymous(self) -> bool:
45
- return self._raw["anonymous"]
46
-
47
- @property
48
- def role(self) -> str:
49
- return self._raw["role"]
50
-
51
- def __str__(self):
52
- return (
53
- f"Me(locked={self.is_locked}, "
54
- f"active={self.is_active}, "
55
- f"authenticated={self.is_authenticated}, "
56
- f"permissions={str(self.permissions)},"
57
- f" anonymous={self.is_anonymous}, "
58
- f"role={self.role})"
59
- )
60
-
61
- def __repr__(self):
62
- return dumps(self._raw)
63
-
64
-
65
- class VersionData:
66
- """Represent the data of the 'version'-request."""
67
-
68
- def __init__(self, raw):
69
- self._raw = raw
70
-
71
- @property
72
- def api_version(self) -> str:
73
- return self._raw["api_version"]
74
-
75
- @property
76
- def hostname(self) -> bool:
77
- return self._raw["hostname"]
78
-
79
- @property
80
- def name(self) -> bool:
81
- return self._raw["name"]
82
-
83
- @property
84
- def sw_version(self) -> Iterable[str]:
85
- return self._raw["sw_version"]
86
-
87
- def __str__(self):
88
- return (
89
- f"Version(api_version={self.api_version}, "
90
- f"hostname={self.hostname}, "
91
- f"name={self.name}, "
92
- f"sw_version={str(self.sw_version)})"
93
- )
94
-
95
- def __repr__(self):
96
- return dumps(self._raw)
97
-
98
-
99
- class ModuleData:
100
- """Represents a single module."""
101
-
102
- def __init__(self, raw):
103
- self._raw = raw
104
-
105
- @property
106
- def id(self) -> str:
107
- return self._raw["id"]
108
-
109
- @property
110
- def type(self) -> str:
111
- return self._raw["type"]
112
-
113
- def __str__(self):
114
- return f"Module(id={self.id}, type={self.type})"
115
-
116
- def __repr__(self):
117
- return dumps(self._raw)
118
-
119
-
120
- class ProcessData:
121
- """Represents a single process data."""
122
-
123
- def __init__(self, raw):
124
- self._raw = raw
125
-
126
- @property
127
- def id(self) -> str:
128
- return self._raw["id"]
129
-
130
- @property
131
- def unit(self) -> str:
132
- return self._raw["unit"]
133
-
134
- @property
135
- def value(self) -> float:
136
- return self._raw["value"]
137
-
138
- def __str__(self):
139
- return (
140
- f"ProcessData(id={self.id}, " f"unit={self.unit}, " f"value={self.value})"
141
- )
142
-
143
- def __repr__(self):
144
- return dumps(self._raw)
145
-
146
-
147
- class ProcessDataCollection(Mapping):
148
- """Represents a collection of process data value."""
149
-
150
- def __init__(self, raw):
151
- self._process_data = list([ProcessData(x) for x in raw])
152
- self._raw = raw
153
-
154
- def __len__(self):
155
- return len(self._process_data)
156
-
157
- def __iter__(self):
158
- return iter(self._process_data)
159
-
160
- def __getitem__(self, item):
161
- try:
162
- return next(x for x in self._process_data if x.id == item)
163
- except StopIteration:
164
- raise KeyError(item)
165
-
166
- def __repr__(self):
167
- return self._raw
168
-
169
-
170
- class SettingsData:
171
- """Represents a single settings data."""
172
-
173
- def __init__(self, raw):
174
- self._raw = raw
175
-
176
- @property
177
- def unit(self) -> str:
178
- return self._raw["unit"]
179
-
180
- @property
181
- def default(self) -> str:
182
- return self._raw["default"]
183
-
184
- @property
185
- def id(self) -> str:
186
- return self._raw["id"]
187
-
188
- @property
189
- def max(self) -> str:
190
- return self._raw["max"]
191
-
192
- @property
193
- def min(self) -> str:
194
- return self._raw["min"]
195
-
196
- @property
197
- def type(self) -> str:
198
- return self._raw["type"]
199
-
200
- @property
201
- def access(self) -> str:
202
- return self._raw["access"]
203
-
204
- def __str__(self):
205
- return (
206
- f"SettingsData(id={self.id}, unit={self.unit}, "
207
- f"default={self.default}, "
208
- f"min={self.min}, max={self.max},"
209
- f"type={self.type}, access={self.access})"
210
- )
211
-
212
- def __repr__(self):
213
- return dumps(self._raw)
214
-
215
-
216
- class EventData:
217
- """Represents an event of the inverter."""
218
-
219
- def __init__(self, raw):
220
- self._raw = raw
221
-
222
- @property
223
- def start_time(self) -> datetime:
224
- ts = self._raw["start_time"]
225
- # "2020-12-26T10:18:35.854Z"
226
- return datetime.fromisoformat(ts)
227
-
228
- @property
229
- def end_time(self) -> datetime:
230
- ts = self._raw["start_time"]
231
- # "2020-12-26T10:18:35.854Z"
232
- return datetime.fromisoformat(ts)
233
-
234
- @property
235
- def is_active(self) -> bool:
236
- return self._raw["is_active"]
237
-
238
- @property
239
- def code(self) -> int:
240
- return self._raw["code"]
241
-
242
- @property
243
- def long_description(self) -> str:
244
- return self._raw["long_description"]
245
-
246
- @property
247
- def long_description(self) -> str:
248
- return self._raw["long_description"]
249
-
250
- @property
251
- def category(self) -> str:
252
- return self._raw["category"]
253
-
254
- @property
255
- def description(self) -> str:
256
- return self._raw["description"]
257
-
258
- @property
259
- def group(self) -> str:
260
- return self._raw["group"]
261
-
262
- def __str__(self):
263
- return (
264
- f"EventData(start={self.start_time}, end={self.end_time}, "
265
- f"code={self.code}, "
266
- f"category={self.category()}, "
267
- f"description={self.description}, "
268
- f"group={self.group})"
269
- )
270
-
271
- def __repr__(self):
272
- return dumps(self._raw)
273
-
274
-
275
- class ApiException(Exception):
276
- """Base exception for API calls."""
277
-
278
- def __init__(self, msg):
279
- self.msg = msg
280
-
281
- def __str__(self):
282
- return f"API Error: {self.msg}"
283
-
284
-
285
- class InternalCommunicationException(ApiException):
286
- """Exception for internal communication error response."""
287
-
288
- def __init__(self, status_code: int, error: str):
289
- super().__init__(f"Internal communication error ([{status_code}] - {error})")
290
- self.status_code = status_code
291
- self.error = error
292
-
293
-
294
- class AuthenticationException(ApiException):
295
- """Exception for authentication or user error response."""
296
-
297
- def __init__(self, status_code: int, error: str):
298
- super().__init__(
299
- f"Invalid user/Authentication failed ([{status_code}] - {error})"
300
- )
301
- self.status_code = status_code
302
- self.error = error
303
-
304
-
305
- class UserLockedException(ApiException):
306
- """Exception for user locked error response."""
307
-
308
- def __init__(self, status_code: int, error: str):
309
- super().__init__(f"User is locked ([{status_code}] - {error})")
310
- self.status_code = status_code
311
- self.error = error
312
-
313
-
314
- class ModuleNotFoundException(ApiException):
315
- """Exception for module or setting not found response."""
316
-
317
- def __init__(self, status_code: int, error: str):
318
- super().__init__(f"Module or setting not found ([{status_code}] - {error})")
319
- self.status_code = status_code
320
- self.error = error
321
-
322
-
323
- class ApiClient(contextlib.AbstractAsyncContextManager):
324
- """Client for the REST-API of Kostal Plenticore inverters.
325
-
326
- The RESP-API provides several scopes of information. Each scope provides a
327
- dynamic set of data which can be retrieved using this interface. The scopes
328
- are:
329
-
330
- - process data (readonly, dynamic values of the operation)
331
- - settings (some are writable, static values for configuration)
332
-
333
- The data are grouped into modules. For example the module `devices:local`
334
- provides a process data `Dc_P` which contains the value of the current
335
- DC power.
336
-
337
- To get all process data or settings the methods `get_process_data` or
338
- `get_settings` can be used. Depending of the current logged in user the
339
- returned data can vary.
340
-
341
- The methods `get_process_data_values` and `get_setting_values` can be used
342
- to read process data or setting values from the inverter. You can use
343
- `set_setting_values` to write new setting values to the inverter if the
344
- setting is writable.
345
-
346
- The authorization system of the inverter comprises three states:
347
- * not logged in (is_active=False, authenticated=False)
348
- * logged in and active (is_active=True, authenticated=True)
349
- * logged in and inactive (is_active=False, authenticated=False)
350
-
351
- The current state can be queried with the `get_me` method. Depending of
352
- this state some operation might not be available.
353
- """
354
-
355
- BASE_URL = "/api/v1/"
356
- SUPPORTED_LANGUAGES = {
357
- "de": ["de"],
358
- "en": ["gb"],
359
- "es": ["es"],
360
- "fr": ["fr"],
361
- "hu": ["hu"],
362
- "it": ["it"],
363
- "nl": ["nl"],
364
- "pl": ["pl"],
365
- "pt": ["pt"],
366
- "cs": ["cz"],
367
- "el": ["gr"],
368
- "zh": ["cn"],
369
- }
370
-
371
- def __init__(self, websession: ClientSession, host: str, port: int = 80):
372
- """Create a new client.
373
-
374
- :param websession: A aiohttp ClientSession for all requests
375
- :param host: The hostname or ip of the inverter
376
- :param port: The port of the API interface (default 80)
377
- """
378
- self.websession = websession
379
- self.host = host
380
- self.port = port
381
- self.session_id = None
382
- self._password = None
383
- self._user = None
384
-
385
- async def __aexit__(self, exc_type, exc_value, traceback):
386
- """Logout support for context manager."""
387
- if self.session_id is not None:
388
- await self.logout()
389
-
390
- def _create_url(self, path: str) -> URL:
391
- """Creates a REST-API URL with the given path as suffix.
392
-
393
- :param path: path suffix, must not start with '/'
394
- :return: a URL instance
395
- """
396
- base = URL.build(
397
- scheme="http",
398
- host=self.host,
399
- port=self.port,
400
- path=ApiClient.BASE_URL,
401
- )
402
- return base.join(URL(path))
403
-
404
- async def login(self, password: str, user: str = "user"):
405
- """Login the given user (default is 'user') with the given
406
- password.
407
-
408
- :raises AuthenticationException: if authentication failed
409
- :raises aiohttp.client_exceptions.ClientConnectorError: if host is not reachable
410
- :raises asyncio.exceptions.TimeoutError: if a timeout occurs
411
- """
412
-
413
- self._password = password
414
- self._user = user
415
- try:
416
- await self._login()
417
- except Exception:
418
- self._password = None
419
- self._user = None
420
- raise
421
-
422
- async def _login(self):
423
- # Step 1 start authentication
424
- client_nonce = urandom(12)
425
-
426
- start_request = {
427
- "username": self._user,
428
- "nonce": b64encode(client_nonce).decode("utf-8"),
429
- }
430
-
431
- async with self.websession.request(
432
- "POST", self._create_url("auth/start"), json=start_request
433
- ) as resp:
434
- await self._check_response(resp)
435
- start_response = await resp.json()
436
- server_nonce = b64decode(start_response["nonce"])
437
- transaction_id = b64decode(start_response["transactionId"])
438
- salt = b64decode(start_response["salt"])
439
- rounds = start_response["rounds"]
440
-
441
- # Step 2 finish authentication (RFC5802)
442
- salted_passwd = hashlib.pbkdf2_hmac(
443
- "sha256", self._password.encode("utf-8"), salt, rounds
444
- )
445
- client_key = hmac.new(
446
- salted_passwd, "Client Key".encode("utf-8"), hashlib.sha256
447
- ).digest()
448
- stored_key = hashlib.sha256(client_key).digest()
449
-
450
- auth_msg = "n={user},r={client_nonce},r={server_nonce},s={salt},i={rounds},c=biws,r={server_nonce}".format(
451
- user=self._user,
452
- client_nonce=b64encode(client_nonce).decode("utf-8"),
453
- server_nonce=b64encode(server_nonce).decode("utf-8"),
454
- salt=b64encode(salt).decode("utf-8"),
455
- rounds=rounds,
456
- )
457
- client_signature = hmac.new(
458
- stored_key, auth_msg.encode("utf-8"), hashlib.sha256
459
- ).digest()
460
- client_proof = bytes([a ^ b for a, b in zip(client_key, client_signature)])
461
-
462
- server_key = hmac.new(
463
- salted_passwd, "Server Key".encode("utf-8"), hashlib.sha256
464
- ).digest()
465
- server_signature = hmac.new(
466
- server_key, auth_msg.encode("utf-8"), hashlib.sha256
467
- ).digest()
468
-
469
- finish_request = {
470
- "transactionId": b64encode(transaction_id).decode("utf-8"),
471
- "proof": b64encode(client_proof).decode("utf-8"),
472
- }
473
-
474
- async with self.websession.request(
475
- "POST", self._create_url("auth/finish"), json=finish_request
476
- ) as resp:
477
- await self._check_response(resp)
478
- finish_response = await resp.json()
479
- token = finish_response["token"]
480
- signature = b64decode(finish_response["signature"])
481
- if signature != server_signature:
482
- raise Exception("Server signature mismatch.")
483
-
484
- # Step 3 create session
485
- session_key_hmac = hmac.new(
486
- stored_key, "Session Key".encode("utf-8"), hashlib.sha256
487
- )
488
- session_key_hmac.update(auth_msg.encode("utf-8"))
489
- session_key_hmac.update(client_key)
490
- protocol_key = session_key_hmac.digest()
491
- session_nonce = urandom(16)
492
- cipher = AES.new(protocol_key, AES.MODE_GCM, nonce=session_nonce)
493
- cipher_text, auth_tag = cipher.encrypt_and_digest(token.encode("utf-8"))
494
-
495
- session_request = {
496
- # AES initialization vector
497
- "iv": b64encode(session_nonce).decode("utf-8"),
498
- # AES GCM tag
499
- "tag": b64encode(auth_tag).decode("utf-8"),
500
- # ID of authentication transaction
501
- "transactionId": b64encode(transaction_id).decode("utf-8"),
502
- # Only the token or token and service code (separated by colon). Encrypted with
503
- # AES using the protocol key
504
- "payload": b64encode(cipher_text).decode("utf-8"),
505
- }
506
-
507
- async with self.websession.request(
508
- "POST", self._create_url("auth/create_session"), json=session_request
509
- ) as resp:
510
- await self._check_response(resp)
511
- session_response = await resp.json()
512
- self.session_id = session_response["sessionId"]
513
-
514
- def _session_request(self, path: str, method="GET", **kwargs):
515
- """Make an request on the current active session.
516
-
517
- :param path: the URL suffix
518
- :param method: the request method, defaults to 'GET'
519
- :param **kwargs: all other args are forwarded to the request
520
- """
521
-
522
- headers = {}
523
- if self.session_id is not None:
524
- headers["authorization"] = f"Session {self.session_id}"
525
-
526
- return self.websession.request(
527
- method, self._create_url(path), headers=headers, **kwargs
528
- )
529
-
530
- async def _check_response(self, resp: ClientResponse):
531
- """Check if the given response contains an error and throws
532
- the appropriate exception."""
533
-
534
- if resp.status != 200:
535
- try:
536
- response = await resp.json()
537
- error = response["message"]
538
- except Exception:
539
- error = None
540
-
541
- if resp.status == 400:
542
- raise AuthenticationException(resp.status, error)
543
-
544
- if resp.status == 403:
545
- raise UserLockedException(resp.status, error)
546
-
547
- if resp.status == 404:
548
- raise ModuleNotFoundException(resp.status, error)
549
-
550
- if resp.status == 503:
551
- raise InternalCommunicationException(resp.status, error)
552
-
553
- # we got an undocumented status code
554
- raise ApiException(
555
- f"Unknown API response [{resp.status}] - {error}"
556
- )
557
-
558
- def _relogin(fn):
559
- """Decorator for automatic re-login if session was expired."""
560
-
561
- @functools.wraps(fn)
562
- async def _wrapper(self, *args, **kwargs):
563
- try:
564
- return await fn(self, *args, **kwargs)
565
- except AuthenticationException:
566
- pass
567
-
568
- logger.debug("Request failed - try to re-login")
569
- await self._login()
570
- return await fn(self, *args, **kwargs)
571
-
572
- return _wrapper
573
-
574
- async def logout(self):
575
- """Logs the current user out."""
576
- self._password = None
577
- async with self._session_request("auth/logout", method="POST") as resp:
578
- await self._check_response(resp)
579
-
580
- async def get_me(self) -> MeData:
581
- """Returns information about the user.
582
-
583
- No login is required.
584
- """
585
- async with self._session_request("auth/me") as resp:
586
- await self._check_response(resp)
587
- me_response = await resp.json()
588
- return MeData(me_response)
589
-
590
- async def get_version(self) -> VersionData:
591
- """Returns information about the API of the inverter.
592
-
593
- No login is required.
594
- """
595
- async with self._session_request("info/version") as resp:
596
- await self._check_response(resp)
597
- response = await resp.json()
598
- return VersionData(response)
599
-
600
- @_relogin
601
- async def get_events(self, max_count=10, lang=None) -> Iterable[EventData]:
602
- """Returns a list with the latest localized events.
603
-
604
- :param max_count: the max number of events to read
605
- :param lang: the RFC1766 based language code, for example 'de_CH' or 'en'
606
- """
607
- if lang is None:
608
- lang = locale.getlocale()[0]
609
-
610
- language = lang[0:2].lower()
611
- variant = lang[3:5].lower()
612
- if language not in ApiClient.SUPPORTED_LANGUAGES.keys():
613
- # Fallback to default
614
- language = "en"
615
- variant = "gb"
616
- else:
617
- variants = ApiClient.SUPPORTED_LANGUAGES[language]
618
- if variant not in variants:
619
- variant = variants[0]
620
-
621
- request = {"language": f"{language}-{variant}", "max": max_count}
622
-
623
- async with self._session_request(
624
- "events/latest", method="POST", json=request
625
- ) as resp:
626
- await self._check_response(resp)
627
- event_response = await resp.json()
628
- return [EventData(x) for x in event_response]
629
-
630
- async def get_modules(self) -> Iterable[ModuleData]:
631
- """Returns list of all available modules (providing process data or settings)."""
632
- async with self._session_request("modules") as resp:
633
- await self._check_response(resp)
634
- modules_response = await resp.json()
635
- return [ModuleData(x) for x in modules_response]
636
-
637
- @_relogin
638
- async def get_process_data(self) -> Dict[str, Iterable[str]]:
639
- """Returns a dictionary of all processdata ids and its module ids.
640
-
641
- :return: a dictionary with the module id as key and a list of process data ids
642
- as value
643
- """
644
- async with self._session_request("processdata") as resp:
645
- await self._check_response(resp)
646
- data_response = await resp.json()
647
- return {x["moduleid"]: x["processdataids"] for x in data_response}
648
-
649
- @_relogin
650
- async def get_process_data_values(
651
- self,
652
- module_id: Union[str, Dict[str, Iterable[str]]],
653
- processdata_id: Union[str, Iterable[str]] = None,
654
- ) -> Dict[str, ProcessDataCollection]:
655
- """Returns a dictionary of process data of one or more modules.
656
-
657
- :param module_id: required, must be a module id or a dictionary with the
658
- module id as key and the process data ids as values.
659
- :param processdata_id: optional, if given `module_id` must be string. Can
660
- be either a string or a list of string. If missing
661
- all process data ids are returned.
662
- :return: a dictionary with the module id as key and a instance of :py:class:`ProcessDataCollection`
663
- as value
664
- """
665
- if isinstance(module_id, str) and processdata_id is None:
666
- # get all process data of a module
667
- async with self._session_request(f"processdata/{module_id}") as resp:
668
- await self._check_response(resp)
669
- data_response = await resp.json()
670
- return {
671
- data_response[0]["moduleid"]: ProcessDataCollection(
672
- data_response[0]["processdata"]
673
- )
674
- }
675
-
676
- if isinstance(module_id, str) and isinstance(processdata_id, str):
677
- # get a single process data of a module
678
- async with self._session_request(
679
- f"processdata/{module_id}/{processdata_id}"
680
- ) as resp:
681
- await self._check_response(resp)
682
- data_response = await resp.json()
683
- return {
684
- data_response[0]["moduleid"]: ProcessDataCollection(
685
- data_response[0]["processdata"]
686
- )
687
- }
688
-
689
- if isinstance(module_id, str) and hasattr(processdata_id, "__iter__"):
690
- # get multiple process data of a module
691
- ids = ",".join(processdata_id)
692
- async with self._session_request(f"processdata/{module_id}/{ids}") as resp:
693
- await self._check_response(resp)
694
- data_response = await resp.json()
695
- return {
696
- data_response[0]["moduleid"]: ProcessDataCollection(
697
- data_response[0]["processdata"]
698
- )
699
- }
700
-
701
- if isinstance(module_id, dict) and processdata_id is None:
702
- # get multiple process data of multiple modules
703
- request = []
704
- for mid, pids in module_id.items():
705
- request.append(dict(moduleid=mid, processdataids=pids))
706
-
707
- async with self._session_request(
708
- "processdata", method="POST", json=request
709
- ) as resp:
710
- await self._check_response(resp)
711
- data_response = await resp.json()
712
- return {
713
- x["moduleid"]: ProcessDataCollection(x["processdata"])
714
- for x in data_response
715
- }
716
-
717
- raise TypeError("Invalid combination of module_id and processdata_id.")
718
-
719
- async def get_settings(self) -> Dict[str, Iterable[SettingsData]]:
720
- """Returns list of all modules with a list of available settings identifiers."""
721
- async with self._session_request("settings") as resp:
722
- await self._check_response(resp)
723
- response = await resp.json()
724
- result = {}
725
- for module in response:
726
- id = module["moduleid"]
727
- data = list([SettingsData(x) for x in module["settings"]])
728
- result[id] = data
729
-
730
- return result
731
-
732
- @_relogin
733
- async def get_setting_values(
734
- self,
735
- module_id: Union[str, Dict[str, Iterable[str]]],
736
- setting_id: Union[str, Iterable[str]] = None,
737
- ) -> Dict[str, Dict[str, str]]:
738
- """Returns a dictionary of setting values of one or more modules.
739
-
740
- :param module_id: required, must be a module id or a dictionary with the
741
- module id as key and the setting ids as values.
742
- :param setting_id: optional, if given `module_id` must be string. Can
743
- be either a string or a list of string. If missing
744
- all setting ids are returned.
745
- """
746
- if isinstance(module_id, str) and setting_id is None:
747
- # get all setting data of a module
748
- async with self._session_request(f"settings/{module_id}") as resp:
749
- await self._check_response(resp)
750
- data_response = await resp.json()
751
- return {module_id: {data_response[0]["id"]: data_response[0]["value"]}}
752
-
753
- if isinstance(module_id, str) and isinstance(setting_id, str):
754
- # get a single setting of a module
755
- async with self._session_request(
756
- f"settings/{module_id}/{setting_id}"
757
- ) as resp:
758
- await self._check_response(resp)
759
- data_response = await resp.json()
760
- return {module_id: {data_response[0]["id"]: data_response[0]["value"]}}
761
-
762
- if isinstance(module_id, str) and hasattr(setting_id, "__iter__"):
763
- # get multiple settings of a module
764
- ids = ",".join(setting_id)
765
- async with self._session_request(f"settings/{module_id}/{ids}") as resp:
766
- await self._check_response(resp)
767
- data_response = await resp.json()
768
- return {module_id: {x["id"]: x["value"] for x in data_response}}
769
-
770
- if isinstance(module_id, dict) and setting_id is None:
771
- # get multiple process data of multiple modules
772
- request = []
773
- for mid, pids in module_id.items():
774
- request.append(dict(moduleid=mid, settingids=pids))
775
-
776
- async with self._session_request(
777
- "settings", method="POST", json=request
778
- ) as resp:
779
- await self._check_response(resp)
780
- data_response = await resp.json()
781
- return {
782
- x["moduleid"]: {y["id"]: y["value"] for y in x["settings"]}
783
- for x in data_response
784
- }
785
-
786
- raise TypeError("Invalid combination of module_id and setting_id.")
787
-
788
- @_relogin
789
- async def set_setting_values(self, module_id: str, values: Dict[str, str]):
790
- """Writes a list of settings for one modules."""
791
- request = [
792
- {
793
- "moduleid": module_id,
794
- "settings": list([dict(value=v, id=k) for k, v in values.items()]),
795
- }
796
- ]
797
- async with self._session_request(
798
- "settings", method="PUT", json=request
799
- ) as resp:
800
- await self._check_response(resp)
801
-
802
- @_relogin
803
- async def download_logdata(
804
- self, writer: IO, begin: datetime = None, end: datetime = None
805
- ):
806
- """Download logdata as tab-separated file."""
807
- request = {}
808
- if begin is not None:
809
- request["begin"] = begin.strftime("%Y-%m-%d")
810
- if end is not None:
811
- request["end"] = end.strftime("%Y-%m-%d")
812
-
813
- async with self._session_request(
814
- "logdata/download",
815
- method="POST",
816
- json=request,
817
- timeout=ClientTimeout(total=360),
818
- ) as resp:
819
- await self._check_response(resp)
820
- async for data in resp.content.iter_any():
821
- writer.write(data.decode("UTF-8"))
1
+ from .api import (
2
+ ApiClient,
3
+ ApiException,
4
+ AuthenticationException,
5
+ InternalCommunicationException,
6
+ ModuleNotFoundException,
7
+ NotAuthorizedException,
8
+ UserLockedException,
9
+ )
10
+ from .extended import ExtendedApiClient
11
+ from .model import (
12
+ EventData,
13
+ MeData,
14
+ ModuleData,
15
+ ProcessData,
16
+ ProcessDataCollection,
17
+ SettingsData,
18
+ VersionData,
19
+ )
20
+
21
+ __all__ = [
22
+ "MeData",
23
+ "VersionData",
24
+ "ModuleData",
25
+ "ProcessData",
26
+ "ProcessDataCollection",
27
+ "SettingsData",
28
+ "EventData",
29
+ "ApiException",
30
+ "InternalCommunicationException",
31
+ "AuthenticationException",
32
+ "NotAuthorizedException",
33
+ "UserLockedException",
34
+ "ModuleNotFoundException",
35
+ "ApiClient",
36
+ "ExtendedApiClient",
37
+ ]