violet-poolController-api 0.0.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.
@@ -0,0 +1,488 @@
1
+
2
+ """Input Sanitization Utilities für User-Inputs und API-Parameter."""
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ from html import escape
8
+ from typing import Any
9
+
10
+ _LOGGER = logging.getLogger(__name__)
11
+
12
+
13
+ class InputSanitizer:
14
+ """
15
+ Input Sanitization für Sicherheit und Datenintegrität.
16
+
17
+ Schützt vor:
18
+ - XSS (Cross-Site Scripting)
19
+ - SQL Injection (nicht relevant bei HTTP API, aber defensiv)
20
+ - Command Injection
21
+ - Path Traversal
22
+ - Unerwarteten Zeichen
23
+ """
24
+
25
+ # Erlaubte Zeichen-Patterns
26
+ ALPHANUMERIC = re.compile(r"^[a-zA-Z0-9]+$")
27
+ ALPHANUMERIC_UNDERSCORE = re.compile(r"^[a-zA-Z0-9_]+$")
28
+ ALPHANUMERIC_DASH_UNDERSCORE = re.compile(r"^[a-zA-Z0-9_-]+$")
29
+ NUMERIC = re.compile(r"^-?[0-9]+(\.[0-9]+)?$")
30
+ INTEGER = re.compile(r"^-?[0-9]+$")
31
+ FLOAT = re.compile(r"^-?[0-9]+\.[0-9]+$")
32
+
33
+ # Gefährliche Patterns
34
+ DANGEROUS_CHARS = re.compile(r'[<>&"\';\\]')
35
+ PATH_TRAVERSAL = re.compile(r"\.\.|/|\\")
36
+ COMMAND_INJECTION = re.compile(r"[;&|`$(){}[\]]")
37
+
38
+ @staticmethod
39
+ def sanitize_string(
40
+ value: Any,
41
+ max_length: int = 255,
42
+ allow_special_chars: bool = False,
43
+ escape_html: bool = True,
44
+ ) -> str:
45
+ """
46
+ Sanitize einen String-Wert.
47
+
48
+ Args:
49
+ value: Zu sanitisierender Wert
50
+ max_length: Maximale Länge
51
+ allow_special_chars: Erlaube Sonderzeichen (sonst nur alphanumerisch)
52
+ escape_html: HTML-Escape durchführen
53
+
54
+ Returns:
55
+ Sanitisierter String
56
+
57
+ Raises:
58
+ ValueError: Bei ungültigen Eingaben
59
+ """
60
+ if value is None:
61
+ return ""
62
+
63
+ # Konvertiere zu String
64
+ str_value = str(value).strip()
65
+
66
+ # Längen-Validierung
67
+ if len(str_value) > max_length:
68
+ _LOGGER.warning(
69
+ "String zu lang (%d > %d), wird gekürzt: %s...",
70
+ len(str_value),
71
+ max_length,
72
+ str_value[:50],
73
+ )
74
+ str_value = str_value[:max_length]
75
+
76
+ # Wenn keine Sonderzeichen erlaubt sind, entfernen wir sie ZUERST.
77
+ if not allow_special_chars:
78
+ # Entferne gefährliche Zeichen
79
+ original = str_value
80
+ str_value = re.sub(r"[^a-zA-Z0-9_-]", "", str_value)
81
+ if str_value != original:
82
+ _LOGGER.warning(
83
+ "Gefährliche Zeichen entfernt: '%s' → '%s'",
84
+ original,
85
+ str_value,
86
+ )
87
+ # HTML-Escape nur wenn Sonderzeichen erlaubt sind
88
+ # (sonst sind < und > bereits durch Regex entfernt)
89
+ elif escape_html:
90
+ str_value = escape(str_value)
91
+
92
+ return str_value
93
+
94
+ @staticmethod
95
+ def sanitize_numeric(value: Any) -> float:
96
+ """
97
+ Sanitize a numeric value.
98
+
99
+ Args:
100
+ value: Zu sanitisierender numerischer Wert
101
+
102
+ Returns:
103
+ Sanierte Fließkommazahl
104
+ """
105
+ try:
106
+ if isinstance(value, (int, float)):
107
+ return float(value)
108
+
109
+ # Konvertiere String zu Zahl
110
+ str_value = str(value).strip()
111
+ # Entferne alle Zeichen außer Zahlen, Minus und Punkt
112
+ cleaned = re.sub(r"[^0-9.-]", "", str_value)
113
+
114
+ if not cleaned:
115
+ return 0.0
116
+
117
+ return float(cleaned)
118
+ except (ValueError, TypeError):
119
+ _LOGGER.warning("Ungültiger numerischer Wert: %s", value)
120
+ return 0.0
121
+
122
+ @staticmethod
123
+ def sanitize_integer(
124
+ value: Any,
125
+ min_value: int | None = None,
126
+ max_value: int | None = None,
127
+ default: int = 0,
128
+ ) -> int:
129
+ """
130
+ Sanitize einen Integer-Wert.
131
+
132
+ Args:
133
+ value: Zu sanitisierender Wert
134
+ min_value: Minimaler erlaubter Wert
135
+ max_value: Maximaler erlaubter Wert
136
+ default: Default-Wert bei Fehler
137
+
138
+ Returns:
139
+ Sanitisierter Integer
140
+ """
141
+ try:
142
+ int_value = int(float(value))
143
+
144
+ # Range-Validierung
145
+ if min_value is not None and int_value < min_value:
146
+ _LOGGER.warning(
147
+ "Integer-Wert %d < min %d, verwende min",
148
+ int_value,
149
+ min_value,
150
+ )
151
+ return min_value
152
+
153
+ if max_value is not None and int_value > max_value:
154
+ _LOGGER.warning(
155
+ "Integer-Wert %d > max %d, verwende max",
156
+ int_value,
157
+ max_value,
158
+ )
159
+ return max_value
160
+
161
+ return int_value
162
+
163
+ except (ValueError, TypeError) as err:
164
+ _LOGGER.warning(
165
+ "Ungültiger Integer-Wert '%s', verwende default %d: %s",
166
+ value,
167
+ default,
168
+ err,
169
+ )
170
+ return default
171
+
172
+ @staticmethod
173
+ def sanitize_float(
174
+ value: Any,
175
+ min_value: float | None = None,
176
+ max_value: float | None = None,
177
+ precision: int = 2,
178
+ default: float = 0.0,
179
+ ) -> float:
180
+ """
181
+ Sanitize einen Float-Wert.
182
+
183
+ Args:
184
+ value: Zu sanitisierender Wert
185
+ min_value: Minimaler erlaubter Wert
186
+ max_value: Maximaler erlaubter Wert
187
+ precision: Dezimalstellen-Präzision
188
+ default: Default-Wert bei Fehler
189
+
190
+ Returns:
191
+ Sanitisierter Float
192
+ """
193
+ try:
194
+ float_value = float(value)
195
+
196
+ # Range-Validierung
197
+ if min_value is not None and float_value < min_value:
198
+ _LOGGER.warning(
199
+ "Float-Wert %.2f < min %.2f, verwende min",
200
+ float_value,
201
+ min_value,
202
+ )
203
+ return min_value
204
+
205
+ if max_value is not None and float_value > max_value:
206
+ _LOGGER.warning(
207
+ "Float-Wert %.2f > max %.2f, verwende max",
208
+ float_value,
209
+ max_value,
210
+ )
211
+ return max_value
212
+
213
+ # Präzision
214
+ return round(float_value, precision)
215
+
216
+ except (ValueError, TypeError) as err:
217
+ _LOGGER.warning(
218
+ "Ungültiger Float-Wert '%s', verwende default %.2f: %s",
219
+ value,
220
+ default,
221
+ err,
222
+ )
223
+ return default
224
+
225
+ @staticmethod
226
+ def sanitize_boolean(value: Any, default: bool = False) -> bool:
227
+ """
228
+ Sanitize einen Boolean-Wert.
229
+
230
+ Args:
231
+ value: Zu sanitisierender Wert
232
+ default: Default-Wert bei Fehler
233
+
234
+ Returns:
235
+ Sanitisierter Boolean
236
+ """
237
+ if isinstance(value, bool):
238
+ return value
239
+
240
+ if isinstance(value, str):
241
+ lower_value = value.lower().strip()
242
+ if lower_value in ("true", "1", "yes", "on", "enabled"):
243
+ return True
244
+ if lower_value in ("false", "0", "no", "off", "disabled"):
245
+ return False
246
+
247
+ if isinstance(value, (int, float)):
248
+ return bool(value)
249
+
250
+ _LOGGER.warning(
251
+ "Ungültiger Boolean-Wert '%s', verwende default %s",
252
+ value,
253
+ default,
254
+ )
255
+ return default
256
+
257
+ @staticmethod
258
+ def validate_device_key(key: str) -> str:
259
+ """
260
+ Validiere einen Device-Key (z.B. PUMP, HEATER, etc.).
261
+
262
+ Args:
263
+ key: Device-Key
264
+
265
+ Returns:
266
+ Validierter Key
267
+
268
+ Raises:
269
+ ValueError: Bei ungültigem Key
270
+ """
271
+ if not key:
272
+ raise ValueError("Device-Key darf nicht leer sein")
273
+
274
+ # Nur Großbuchstaben, Zahlen und Underscore erlaubt
275
+ # Wir erlauben auch Bindestriche und konvertieren sie zu Underscores
276
+ key = key.upper().replace("-", "_")
277
+ sanitized = re.sub(r"[^A-Z0-9_]", "", key)
278
+
279
+ if sanitized != key:
280
+ _LOGGER.warning(
281
+ "Device-Key enthielt ungültige Zeichen: '%s' → '%s'",
282
+ key,
283
+ sanitized,
284
+ )
285
+
286
+ if len(sanitized) > 50:
287
+ raise ValueError(f"Device-Key zu lang: {len(sanitized)} > 50")
288
+
289
+ return sanitized
290
+
291
+ @staticmethod
292
+ def validate_api_parameter(param: str) -> str:
293
+ """
294
+ Validiere einen API-Parameter-Namen.
295
+
296
+ Args:
297
+ param: Parameter-Name
298
+
299
+ Returns:
300
+ Validierter Parameter
301
+
302
+ Raises:
303
+ ValueError: Bei ungültigem Parameter oder Path Traversal
304
+ """
305
+ if not param:
306
+ raise ValueError("API-Parameter darf nicht leer sein")
307
+
308
+ # Prüfe auf Path Traversal VOR der Bereinigung
309
+ if InputSanitizer.PATH_TRAVERSAL.search(param):
310
+ raise ValueError(f"Path Traversal erkannt in Parameter: {param}")
311
+
312
+ # Entferne gefährliche Zeichen
313
+ sanitized = re.sub(r"[^a-zA-Z0-9_-]", "", param)
314
+
315
+ if sanitized != param:
316
+ _LOGGER.warning(
317
+ "API-Parameter enthielt ungültige Zeichen: '%s' → '%s'",
318
+ param,
319
+ sanitized,
320
+ )
321
+
322
+ if len(sanitized) > 100:
323
+ raise ValueError(f"API-Parameter zu lang: {len(sanitized)} > 100")
324
+
325
+ return sanitized
326
+
327
+ @staticmethod
328
+ def validate_duration(duration: Any, min_sec: int = 0, max_sec: int = 86400) -> int:
329
+ """
330
+ Validiere eine Duration in Sekunden.
331
+
332
+ Args:
333
+ duration: Duration-Wert
334
+ min_sec: Minimale Duration (Standard: 0)
335
+ max_sec: Maximale Duration (Standard: 24h)
336
+
337
+ Returns:
338
+ Validierte Duration in Sekunden
339
+ """
340
+ duration_int = InputSanitizer.sanitize_integer(
341
+ duration,
342
+ min_value=min_sec,
343
+ max_value=max_sec,
344
+ default=0,
345
+ )
346
+
347
+ if duration_int < 0:
348
+ _LOGGER.warning("Negative Duration %d, setze auf 0", duration_int)
349
+ return 0
350
+
351
+ return duration_int
352
+
353
+ @staticmethod
354
+ def validate_speed(
355
+ speed: Any, min_speed: int = 1, max_speed: int = 4, default: int = 2
356
+ ) -> int:
357
+ """
358
+ Validiere einen Speed-Wert (z.B. Pumpengeschwindigkeit).
359
+
360
+ Args:
361
+ speed: Speed-Wert
362
+ min_speed: Minimale Speed (Standard: 1)
363
+ max_speed: Maximale Speed (Standard: 4)
364
+ default: Default Speed (Standard: 2)
365
+
366
+ Returns:
367
+ Validierte Speed
368
+ """
369
+ return InputSanitizer.sanitize_integer(
370
+ speed,
371
+ min_value=min_speed,
372
+ max_value=max_speed,
373
+ default=default,
374
+ )
375
+
376
+ @staticmethod
377
+ def validate_temperature(
378
+ temp: Any,
379
+ min_temp: float = -50.0,
380
+ max_temp: float = 100.0,
381
+ ) -> float:
382
+ """
383
+ Validiere einen Temperatur-Wert.
384
+
385
+ Args:
386
+ temp: Temperatur-Wert
387
+ min_temp: Minimale Temperatur
388
+ max_temp: Maximale Temperatur
389
+
390
+ Returns:
391
+ Validierte Temperatur
392
+ """
393
+ return InputSanitizer.sanitize_float(
394
+ temp,
395
+ min_value=min_temp,
396
+ max_value=max_temp,
397
+ precision=1,
398
+ default=20.0,
399
+ )
400
+
401
+ @staticmethod
402
+ def validate_ph_value(ph: Any) -> float:
403
+ """
404
+ Validiere einen pH-Wert.
405
+
406
+ Args:
407
+ ph: pH-Wert
408
+
409
+ Returns:
410
+ Validierter pH-Wert (6.0-9.0)
411
+ """
412
+ return InputSanitizer.sanitize_float(
413
+ ph,
414
+ min_value=6.0,
415
+ max_value=9.0,
416
+ precision=1,
417
+ default=7.2,
418
+ )
419
+
420
+ @staticmethod
421
+ def validate_orp_value(orp: Any) -> int:
422
+ """
423
+ Validiere einen ORP-Wert (Redoxpotential).
424
+
425
+ Args:
426
+ orp: ORP-Wert in mV
427
+
428
+ Returns:
429
+ Validierter ORP-Wert (400-900 mV)
430
+ """
431
+ return InputSanitizer.sanitize_integer(
432
+ orp,
433
+ min_value=400,
434
+ max_value=900,
435
+ default=700,
436
+ )
437
+
438
+ @staticmethod
439
+ def validate_chlorine_level(chlorine: Any) -> float:
440
+ """
441
+ Validiere einen Chlor-Wert.
442
+
443
+ Args:
444
+ chlorine: Chlor-Wert in mg/l
445
+
446
+ Returns:
447
+ Validierter Chlor-Wert (0.0-5.0 mg/l)
448
+ """
449
+ return InputSanitizer.sanitize_float(
450
+ chlorine,
451
+ min_value=0.0,
452
+ max_value=5.0,
453
+ precision=1,
454
+ default=0.6,
455
+ )
456
+
457
+
458
+ # Singleton-Instanz für einfachen Zugriff
459
+ _sanitizer = InputSanitizer()
460
+
461
+
462
+ def sanitize_string(*args, **kwargs) -> str:
463
+ """Shortcut für InputSanitizer.sanitize_string()."""
464
+ return _sanitizer.sanitize_string(*args, **kwargs)
465
+
466
+
467
+ def sanitize_integer(*args, **kwargs) -> int:
468
+ """Shortcut für InputSanitizer.sanitize_integer()."""
469
+ return _sanitizer.sanitize_integer(*args, **kwargs)
470
+
471
+
472
+ def sanitize_float(*args, **kwargs) -> float:
473
+ """Shortcut für InputSanitizer.sanitize_float()."""
474
+ return _sanitizer.sanitize_float(*args, **kwargs)
475
+
476
+
477
+ def sanitize_boolean(*args, **kwargs) -> bool:
478
+ """Shortcut für InputSanitizer.sanitize_boolean()."""
479
+ return _sanitizer.sanitize_boolean(*args, **kwargs)
480
+
481
+
482
+ __all__ = [
483
+ "InputSanitizer",
484
+ "sanitize_string",
485
+ "sanitize_integer",
486
+ "sanitize_float",
487
+ "sanitize_boolean",
488
+ ]
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: violet-poolController-api
3
+ Version: 0.0.1
4
+ Summary: Asynchronous Python client for the Violet Pool Controller.
5
+ Home-page: https://github.com/Xerolux/violet-poolController-api
6
+ Author: Basti (Xerolux)
7
+ Author-email: "Basti (Xerolux)" <git@xerolux.de>
8
+ Project-URL: Homepage, https://github.com/Xerolux/violet-poolController-api
9
+ Project-URL: Bug Tracker, https://github.com/Xerolux/violet-poolController-api/issues
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Home Automation
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: aiohttp>=3.9.0
19
+ Dynamic: author
20
+ Dynamic: home-page
21
+ Dynamic: license-file
22
+ Dynamic: requires-python
23
+
24
+ # Violet Pool Controller API
25
+
26
+ [![PyPI version](https://img.shields.io/pypi/v/violet-poolController-api.svg?style=for-the-badge)](https://pypi.org/project/violet-poolController-api/)
27
+ [![PyPI downloads](https://img.shields.io/pypi/dm/violet-poolController-api.svg?style=for-the-badge)](https://pypistats.org/packages/violet-poolcontroller-api)
28
+ [![Python versions](https://img.shields.io/pypi/pyversions/violet-poolController-api.svg?style=for-the-badge)](https://pypi.org/project/violet-poolController-api/)
29
+ [![License](https://img.shields.io/github/license/Xerolux/violet-poolController-api.svg?style=for-the-badge)](LICENSE)
30
+
31
+ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-xerolux-yellow?logo=buy-me-a-coffee&style=for-the-badge)](https://www.buymeacoffee.com/xerolux)
32
+ [![Tesla](https://img.shields.io/badge/Tesla-Referral-red?style=for-the-badge&logo=tesla)](https://ts.la/sebastian564489)
33
+
34
+ An asynchronous Python client for interacting with the **Violet Pool Controller**.
35
+
36
+ This library is primarily designed to power the official [Violet Pool Controller Home Assistant Integration](https://github.com/Xerolux/violet-hass), but it can be used independently for any Python project that needs to fetch readings or control a Violet Pool system.
37
+
38
+ ## Features
39
+ * **Asynchronous:** Fully async operations using `aiohttp`.
40
+ * **Resilient:** Built-in Circuit Breaker and Rate Limiter to protect both the client and the controller from overload.
41
+ * **Sanitization:** Strict payload input sanitization to prevent injection and invalid settings.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install violet-poolController-api
47
+ ```
48
+
49
+ ## Basic Usage
50
+
51
+ ```python
52
+ import asyncio
53
+ import aiohttp
54
+ from violet_poolcontroller_api.api import VioletPoolAPI, VioletPoolAPIError
55
+
56
+ async def main():
57
+ # Create an aiohttp ClientSession
58
+ async with aiohttp.ClientSession() as session:
59
+ # Initialize the API
60
+ api = VioletPoolAPI(
61
+ host="192.168.1.100",
62
+ username="admin",
63
+ password="your_password",
64
+ session=session
65
+ )
66
+
67
+ try:
68
+ # --- 1. Fetch current sensor readings ---
69
+ readings = await api.get_readings()
70
+ print("Current Pool Readings:")
71
+ print(readings)
72
+
73
+ # --- 2. Control the Filter Pump ---
74
+ # Set pump speed to 2 (Normal) permanently (duration=0)
75
+ await api.set_pump_speed(speed=2, duration=0)
76
+ print("\nPump speed set to 2.")
77
+
78
+ # --- 3. Set Target Temperature ---
79
+ # Set the target temperature for the heater to 28.5 degrees
80
+ await api.set_device_temperature("HEATER", 28.5)
81
+ print("\nHeater target temperature set to 28.5°C.")
82
+
83
+ # --- 4. Control Pool Lights ---
84
+ # Trigger the color pulse animation for the pool light
85
+ await api.set_light_color_pulse()
86
+ print("\nLight color pulse triggered.")
87
+
88
+ except VioletPoolAPIError as e:
89
+ print(f"An error occurred while communicating with the Violet controller: {e}")
90
+
91
+ if __name__ == "__main__":
92
+ asyncio.run(main())
93
+ ```
94
+
95
+ ## Advanced Operations
96
+
97
+ The API client includes many more functions tailored to the Violet Controller:
98
+ - `get_config(["PUMP_SPEED_1", "PUMP_SPEED_2"])`: Fetch specific configuration values.
99
+ - `set_ph_target(7.2)`: Change the pH target value.
100
+ - `set_orp_target(750)`: Change the ORP (Redox) target value.
101
+ - `set_pv_surplus(active=True)`: Enable the PV-Surplus mode.
102
+ - `manual_dosing(dosing_type="Chlor", duration=120)`: Trigger manual chemical dosing.
103
+
104
+ For a full list of available commands, please refer to the source code in `api.py`.
105
+
106
+ ## License
107
+ MIT License
108
+
109
+ ---
110
+
111
+ ## About the Violet Pool Controller
112
+
113
+ Der **VIOLET Pool Controller** von [PoolDigital GmbH & Co. KG](https://www.pooldigital.de/) ist ein Premium Smart Pool Automation System aus deutscher Entwicklung – mit JSON API für nahtlose Home Assistant Integration.
114
+
115
+ - **Offizieller Shop:** [pooldigital.de](https://www.pooldigital.de/)
116
+ - **Community:** [PoolDigital Forum](http://forum.pooldigital.de/)
117
+
118
+ **Disclaimer:**
119
+ *This is an unofficial, community-driven project. It is not affiliated with, endorsed by, or associated with PoolDigital GmbH & Co. KG in any way. "VIOLET" and any related trademarks are the property of their respective owners.*
120
+
121
+ ⚠️ **WARNING - USE AT YOUR OWN RISK:**
122
+ *This software interacts with physical hardware and automation systems that control water chemistry (pH, Chlorine/ORP) and electrical equipment (pumps, heaters). A bug, network issue, or incorrect configuration could result in hardware damage, unsafe water conditions, or other hazards. By using this software, you acknowledge and agree that you are solely responsible for any damage, injury, or loss of property that may occur. Please always monitor your pool's chemistry and hardware independently.*
@@ -0,0 +1,12 @@
1
+ violet_poolcontroller_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ violet_poolcontroller_api/api.py,sha256=LwzvBE3nfkJcmJfU3GZO_qJCufy3ReV2R1PKfpCznkA,31423
3
+ violet_poolcontroller_api/circuit_breaker.py,sha256=FZWzDMHJSS_uLibXnPxyLo2bTkGrWezwfJkE1tzzlEc,5994
4
+ violet_poolcontroller_api/const_api.py,sha256=GEVXbxD41-8gHkx1LGzUbnd-huKYJX0m1ZqfH3sfF08,4209
5
+ violet_poolcontroller_api/const_devices.py,sha256=J-a8Y9td9NGKac-xxkzhcQrOoeU7idXjXE60G0W7_tM,10162
6
+ violet_poolcontroller_api/utils_rate_limiter.py,sha256=GcPG1fVXBiU0GUZ3sUjFEzFSwuJEXlH_vAMx65uFJqU,7805
7
+ violet_poolcontroller_api/utils_sanitizer.py,sha256=-_PsBAgeJUgs6NqS9usr2VrePYlDcSz34c2cQqyKtdw,13431
8
+ violet_poolcontroller_api-0.0.1.dist-info/licenses/LICENSE,sha256=4TCuq5DDi4BfoOjXzxmnrAt4udsH5qeX5-jpLcsJ3jw,1072
9
+ violet_poolcontroller_api-0.0.1.dist-info/METADATA,sha256=KT6XzXcLqk6UujW4q5dYqQyP2CxU4W2n4vmKgvLMfa0,5739
10
+ violet_poolcontroller_api-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ violet_poolcontroller_api-0.0.1.dist-info/top_level.txt,sha256=CJIVuLVLzANlKYdPaogpTbmEkaJ9IGqgk-WHWEkOlIg,26
12
+ violet_poolcontroller_api-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Basti (Xerolux)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ violet_poolcontroller_api