python-mystrom 2.1.0__tar.gz → 2.4.0__tar.gz
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.
- {python-mystrom-2.1.0 → python_mystrom-2.4.0}/LICENSE +1 -1
- {python-mystrom-2.1.0/python_mystrom.egg-info → python_mystrom-2.4.0}/PKG-INFO +17 -16
- {python-mystrom-2.1.0 → python_mystrom-2.4.0}/README.rst +1 -1
- python_mystrom-2.4.0/pymystrom/__init__.py +107 -0
- {python-mystrom-2.1.0 → python_mystrom-2.4.0}/pymystrom/bulb.py +6 -7
- {python-mystrom-2.1.0 → python_mystrom-2.4.0}/pymystrom/cli.py +9 -24
- python_mystrom-2.4.0/pymystrom/device_types.py +33 -0
- {python-mystrom-2.1.0 → python_mystrom-2.4.0}/pymystrom/discovery.py +7 -12
- {python-mystrom-2.1.0 → python_mystrom-2.4.0}/pymystrom/pir.py +4 -4
- python_mystrom-2.4.0/pymystrom/switch.py +177 -0
- python_mystrom-2.4.0/pyproject.toml +26 -0
- python-mystrom-2.1.0/CHANGELOG.rst +0 -69
- python-mystrom-2.1.0/MANIFEST.in +0 -2
- python-mystrom-2.1.0/PKG-INFO +0 -274
- python-mystrom-2.1.0/examples/example-bulb-hsv.py +0 -18
- python-mystrom-2.1.0/examples/example-bulb.py +0 -72
- python-mystrom-2.1.0/examples/example-discovery.py +0 -13
- python-mystrom-2.1.0/examples/example-get-data-bulb.py +0 -12
- python-mystrom-2.1.0/examples/example-pir.py +0 -43
- python-mystrom-2.1.0/examples/example-switch.py +0 -37
- python-mystrom-2.1.0/pymystrom/__init__.py +0 -63
- python-mystrom-2.1.0/pymystrom/switch.py +0 -114
- python-mystrom-2.1.0/python_mystrom.egg-info/SOURCES.txt +0 -25
- python-mystrom-2.1.0/python_mystrom.egg-info/dependency_links.txt +0 -1
- python-mystrom-2.1.0/python_mystrom.egg-info/entry_points.txt +0 -2
- python-mystrom-2.1.0/python_mystrom.egg-info/requires.txt +0 -4
- python-mystrom-2.1.0/python_mystrom.egg-info/top_level.txt +0 -1
- python-mystrom-2.1.0/python_mystrom.egg-info/zip-safe +0 -1
- python-mystrom-2.1.0/setup.cfg +0 -4
- python-mystrom-2.1.0/setup.py +0 -48
- {python-mystrom-2.1.0 → python_mystrom-2.4.0}/pymystrom/exceptions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2015-
|
|
3
|
+
Copyright (c) 2015-2025 Fabian Affolter <fabian@affolter-engineering.ch>
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: python-mystrom
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Asynchronous Python API client for interacting with myStrom devices
|
|
5
|
-
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: myStrom,API,client,asynchronous
|
|
6
7
|
Author: Fabian Affolter
|
|
7
8
|
Author-email: fabian@affolter-engineering.ch
|
|
8
|
-
|
|
9
|
-
Classifier: Development Status :: 3 - Alpha
|
|
10
|
-
Classifier: Environment :: Console
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
9
|
+
Requires-Python: >=3.11
|
|
12
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
-
Classifier:
|
|
14
|
-
Classifier:
|
|
15
|
-
Classifier:
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Requires-
|
|
20
|
-
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Dist: aiohttp
|
|
16
|
+
Requires-Dist: click
|
|
17
|
+
Requires-Dist: requests
|
|
18
|
+
Project-URL: Homepage, https://github.com/home-assistant-ecosystem/python-mystrom
|
|
19
|
+
Project-URL: Repository, https://github.com/home-assistant-ecosystem/python-mystrom
|
|
20
|
+
Description-Content-Type: text/x-rst
|
|
21
21
|
|
|
22
22
|
python-mystrom |License| |PyPI|
|
|
23
23
|
===================================
|
|
@@ -63,7 +63,7 @@ be present in the ``unstable`` channel.
|
|
|
63
63
|
|
|
64
64
|
.. code:: bash
|
|
65
65
|
|
|
66
|
-
$ nix-env -iA nixos.
|
|
66
|
+
$ nix-env -iA nixos.python3Packages.python-mystrom
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
Plug/switch
|
|
@@ -272,3 +272,4 @@ License
|
|
|
272
272
|
.. |PyPI| image:: https://img.shields.io/pypi/v/python-mystrom.svg
|
|
273
273
|
:target: https://pypi.python.org/pypi/python-mystrom
|
|
274
274
|
:alt: PyPI release
|
|
275
|
+
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Base details for the myStrom Python bindings."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import socket
|
|
5
|
+
from typing import Any, Mapping, Optional
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
from yarl import URL
|
|
9
|
+
|
|
10
|
+
from .exceptions import MyStromConnectionError
|
|
11
|
+
|
|
12
|
+
TIMEOUT = 10
|
|
13
|
+
USER_AGENT = "PythonMyStrom/1.0"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def _request(
|
|
17
|
+
self,
|
|
18
|
+
uri: str,
|
|
19
|
+
method: str = "GET",
|
|
20
|
+
data: Optional[Any] = None,
|
|
21
|
+
json_data: Optional[dict] = None,
|
|
22
|
+
params: Optional[Mapping[str, str]] = None,
|
|
23
|
+
) -> Any:
|
|
24
|
+
"""Handle a request to the myStrom device."""
|
|
25
|
+
headers = {
|
|
26
|
+
"User-Agent": USER_AGENT,
|
|
27
|
+
"Accept": "application/json, text/plain, */*",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if self._session is None:
|
|
31
|
+
self._session = aiohttp.ClientSession()
|
|
32
|
+
self._close_session = True
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
response = await asyncio.wait_for(
|
|
36
|
+
self._session.request(
|
|
37
|
+
method,
|
|
38
|
+
uri,
|
|
39
|
+
data=data,
|
|
40
|
+
json=json_data,
|
|
41
|
+
params=params,
|
|
42
|
+
headers=headers,
|
|
43
|
+
),
|
|
44
|
+
timeout=TIMEOUT,
|
|
45
|
+
)
|
|
46
|
+
except asyncio.TimeoutError as exception:
|
|
47
|
+
raise MyStromConnectionError(
|
|
48
|
+
"Timeout occurred while connecting to myStrom device."
|
|
49
|
+
) from exception
|
|
50
|
+
except (aiohttp.ClientError, socket.gaierror) as exception:
|
|
51
|
+
raise MyStromConnectionError(
|
|
52
|
+
"Error occurred while communicating with myStrom device."
|
|
53
|
+
) from exception
|
|
54
|
+
|
|
55
|
+
content_type = response.headers.get("Content-Type", "")
|
|
56
|
+
if (response.status // 100) in [4, 5]:
|
|
57
|
+
response.close()
|
|
58
|
+
|
|
59
|
+
if "application/json" in content_type:
|
|
60
|
+
response_json = await response.json()
|
|
61
|
+
return response_json
|
|
62
|
+
|
|
63
|
+
return response.text
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class MyStromDevice:
|
|
67
|
+
"""A class for a myStrom device."""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
host,
|
|
72
|
+
session: aiohttp.client.ClientSession = None,
|
|
73
|
+
):
|
|
74
|
+
"""Initialize the device."""
|
|
75
|
+
self._close_session = False
|
|
76
|
+
self._host = host
|
|
77
|
+
self._session = session
|
|
78
|
+
self.uri = URL.build(scheme="http", host=self._host)
|
|
79
|
+
|
|
80
|
+
async def get_device_info(self) -> dict:
|
|
81
|
+
"""Get the device info of a myStrom device."""
|
|
82
|
+
url = URL(self.uri).join(URL("api/v1/info"))
|
|
83
|
+
response = await _request(self, uri=url)
|
|
84
|
+
if not isinstance(response, dict):
|
|
85
|
+
# Fall back to the old API version if the device runs with old firmware
|
|
86
|
+
url = URL(self.uri).join(URL("info.json"))
|
|
87
|
+
response = await _request(self, uri=url)
|
|
88
|
+
return response
|
|
89
|
+
|
|
90
|
+
async def close(self) -> None:
|
|
91
|
+
"""Close an open client session."""
|
|
92
|
+
if self._session and self._close_session:
|
|
93
|
+
await self._session.close()
|
|
94
|
+
|
|
95
|
+
async def __aenter__(self) -> "MyStromDevice":
|
|
96
|
+
"""Async enter."""
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
async def __aexit__(self, *exc_info) -> None:
|
|
100
|
+
"""Async exit."""
|
|
101
|
+
await self.close()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def get_device_info(host: str) -> dict:
|
|
105
|
+
"""Get the device info of a myStrom device."""
|
|
106
|
+
async with MyStromDevice(host) as device:
|
|
107
|
+
return await device.get_device_info()
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""Support for communicating with myStrom bulbs."""
|
|
2
|
+
|
|
2
3
|
import asyncio
|
|
3
4
|
import logging
|
|
5
|
+
from typing import Optional
|
|
4
6
|
|
|
5
7
|
import aiohttp
|
|
6
8
|
from yarl import URL
|
|
7
|
-
from typing import Any, Dict, Iterable, List, Optional, Union
|
|
8
9
|
|
|
9
10
|
from . import _request as request
|
|
10
11
|
|
|
@@ -21,7 +22,7 @@ class MyStromBulb:
|
|
|
21
22
|
host: str,
|
|
22
23
|
mac: str,
|
|
23
24
|
session: aiohttp.client.ClientSession = None,
|
|
24
|
-
):
|
|
25
|
+
) -> None:
|
|
25
26
|
"""Initialize the bulb."""
|
|
26
27
|
self._close_session = False
|
|
27
28
|
self._host = host
|
|
@@ -36,11 +37,9 @@ class MyStromBulb:
|
|
|
36
37
|
self._bulb_type = None
|
|
37
38
|
self._state = None
|
|
38
39
|
self._transition_time = 0
|
|
39
|
-
self.uri = (
|
|
40
|
-
URL.build(scheme="http", host=self._host).join(URI_BULB) / self._mac
|
|
41
|
-
)
|
|
40
|
+
self.uri = URL.build(scheme="http", host=self._host).join(URI_BULB) / self._mac
|
|
42
41
|
|
|
43
|
-
async def get_state(self) ->
|
|
42
|
+
async def get_state(self) -> None:
|
|
44
43
|
"""Get the state of the bulb."""
|
|
45
44
|
response = await request(self, uri=self.uri)
|
|
46
45
|
self._consumption = response[self._mac]["power"]
|
|
@@ -57,7 +56,7 @@ class MyStromBulb:
|
|
|
57
56
|
return self._firmware
|
|
58
57
|
|
|
59
58
|
@property
|
|
60
|
-
def mac(self) ->
|
|
59
|
+
def mac(self) -> str:
|
|
61
60
|
"""Return the MAC address."""
|
|
62
61
|
return self._mac
|
|
63
62
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Command-line tool for working with myStrom devices."""
|
|
2
|
+
|
|
2
3
|
import click
|
|
3
4
|
import requests
|
|
4
5
|
import asyncio
|
|
@@ -50,9 +51,7 @@ def read_config(ip, mac):
|
|
|
50
51
|
"""Read the current configuration of a myStrom device."""
|
|
51
52
|
click.echo("Read configuration from %s" % ip)
|
|
52
53
|
try:
|
|
53
|
-
request = requests.get(
|
|
54
|
-
"http://{}/{}/{}/".format(ip, URI, mac), timeout=TIMEOUT
|
|
55
|
-
)
|
|
54
|
+
request = requests.get("http://{}/{}/{}/".format(ip, URI, mac), timeout=TIMEOUT)
|
|
56
55
|
click.echo(request.json())
|
|
57
56
|
except requests.exceptions.ConnectionError:
|
|
58
57
|
click.echo("Communication issue with the device")
|
|
@@ -87,9 +86,7 @@ def button():
|
|
|
87
86
|
@click.option(
|
|
88
87
|
"--long", prompt="URL for a long tab", default="", help="URL for a long tab."
|
|
89
88
|
)
|
|
90
|
-
@click.option(
|
|
91
|
-
"--touch", prompt="URL for a touch", default="", help="URL for a touch."
|
|
92
|
-
)
|
|
89
|
+
@click.option("--touch", prompt="URL for a touch", default="", help="URL for a touch.")
|
|
93
90
|
def write_config(ip, mac, single, double, long, touch):
|
|
94
91
|
"""Write the current configuration of a myStrom button."""
|
|
95
92
|
click.echo("Write configuration to device %s" % ip)
|
|
@@ -208,9 +205,7 @@ def read_config(ip, mac):
|
|
|
208
205
|
"""Read the current configuration of a myStrom WiFi Button."""
|
|
209
206
|
click.echo("Read the configuration of button %s..." % ip)
|
|
210
207
|
try:
|
|
211
|
-
request = requests.get(
|
|
212
|
-
"http://{}/{}/{}/".format(ip, URI, mac), timeout=TIMEOUT
|
|
213
|
-
)
|
|
208
|
+
request = requests.get("http://{}/{}/{}/".format(ip, URI, mac), timeout=TIMEOUT)
|
|
214
209
|
click.echo(request.json())
|
|
215
210
|
except requests.exceptions.ConnectionError:
|
|
216
211
|
click.echo("Communication issue with the device. No action performed")
|
|
@@ -223,9 +218,7 @@ def bulb():
|
|
|
223
218
|
|
|
224
219
|
@bulb.command("on")
|
|
225
220
|
@coro
|
|
226
|
-
@click.option(
|
|
227
|
-
"--ip", prompt="IP address of the bulb", help="IP address of the bulb."
|
|
228
|
-
)
|
|
221
|
+
@click.option("--ip", prompt="IP address of the bulb", help="IP address of the bulb.")
|
|
229
222
|
@click.option(
|
|
230
223
|
"--mac", prompt="MAC address of the bulb", help="MAC address of the bulb."
|
|
231
224
|
)
|
|
@@ -237,9 +230,7 @@ async def on(ip, mac):
|
|
|
237
230
|
|
|
238
231
|
@bulb.command("color")
|
|
239
232
|
@coro
|
|
240
|
-
@click.option(
|
|
241
|
-
"--ip", prompt="IP address of the bulb", help="IP address of the bulb."
|
|
242
|
-
)
|
|
233
|
+
@click.option("--ip", prompt="IP address of the bulb", help="IP address of the bulb.")
|
|
243
234
|
@click.option(
|
|
244
235
|
"--mac", prompt="MAC address of the bulb", help="MAC address of the bulb."
|
|
245
236
|
)
|
|
@@ -264,9 +255,7 @@ async def color(ip, mac, hue, saturation, value):
|
|
|
264
255
|
|
|
265
256
|
@bulb.command("off")
|
|
266
257
|
@coro
|
|
267
|
-
@click.option(
|
|
268
|
-
"--ip", prompt="IP address of the bulb", help="IP address of the bulb."
|
|
269
|
-
)
|
|
258
|
+
@click.option("--ip", prompt="IP address of the bulb", help="IP address of the bulb.")
|
|
270
259
|
@click.option(
|
|
271
260
|
"--mac", prompt="MAC address of the bulb", help="MAC address of the bulb."
|
|
272
261
|
)
|
|
@@ -278,9 +267,7 @@ async def off(ip, mac):
|
|
|
278
267
|
|
|
279
268
|
@bulb.command("flash")
|
|
280
269
|
@coro
|
|
281
|
-
@click.option(
|
|
282
|
-
"--ip", prompt="IP address of the bulb", help="IP address of the bulb."
|
|
283
|
-
)
|
|
270
|
+
@click.option("--ip", prompt="IP address of the bulb", help="IP address of the bulb.")
|
|
284
271
|
@click.option(
|
|
285
272
|
"--mac", prompt="MAC address of the bulb", help="MAC address of the bulb."
|
|
286
273
|
)
|
|
@@ -298,9 +285,7 @@ async def flash(ip, mac, time):
|
|
|
298
285
|
|
|
299
286
|
@bulb.command("rainbow")
|
|
300
287
|
@coro
|
|
301
|
-
@click.option(
|
|
302
|
-
"--ip", prompt="IP address of the bulb", help="IP address of the bulb."
|
|
303
|
-
)
|
|
288
|
+
@click.option("--ip", prompt="IP address of the bulb", help="IP address of the bulb.")
|
|
304
289
|
@click.option(
|
|
305
290
|
"--mac", prompt="MAC address of the bulb", help="MAC address of the bulb."
|
|
306
291
|
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Device types.
|
|
3
|
+
|
|
4
|
+
See https://api.mystrom.ch/#f37a4be7-0233-4d93-915e-c6f92656f129
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
DEVICE_MAPPING_NUMERIC = {
|
|
8
|
+
101: "Switch CH v1",
|
|
9
|
+
102: "Bulb",
|
|
10
|
+
103: "Button+",
|
|
11
|
+
104: "Button",
|
|
12
|
+
105: "LED Strip",
|
|
13
|
+
106: "Switch CH v2",
|
|
14
|
+
107: "Switch EU",
|
|
15
|
+
110: "Motion Sensor",
|
|
16
|
+
113: "modulo® STECCO / CUBO",
|
|
17
|
+
118: "Button Plus 2nd",
|
|
18
|
+
120: "Switch Zero",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
DEVICE_MAPPING_LITERAL = {
|
|
22
|
+
"WSW": DEVICE_MAPPING_NUMERIC[101],
|
|
23
|
+
"WRB": DEVICE_MAPPING_NUMERIC[102],
|
|
24
|
+
"WBP": DEVICE_MAPPING_NUMERIC[103],
|
|
25
|
+
"WBS": DEVICE_MAPPING_NUMERIC[104],
|
|
26
|
+
"WRS": DEVICE_MAPPING_NUMERIC[105],
|
|
27
|
+
"WS2": DEVICE_MAPPING_NUMERIC[106],
|
|
28
|
+
"WSE": DEVICE_MAPPING_NUMERIC[107],
|
|
29
|
+
"WMS": DEVICE_MAPPING_NUMERIC[110],
|
|
30
|
+
"WLL": DEVICE_MAPPING_NUMERIC[113],
|
|
31
|
+
"BP2": DEVICE_MAPPING_NUMERIC[118],
|
|
32
|
+
"LCS": DEVICE_MAPPING_NUMERIC[120],
|
|
33
|
+
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"""Support for discovering myStrom devices."""
|
|
2
|
+
|
|
2
3
|
import asyncio
|
|
3
4
|
import logging
|
|
4
|
-
from typing import
|
|
5
|
+
from typing import List, Optional
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
from .device_types import DEVICE_MAPPING_NUMERIC
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
"102": "myStrom Bulb",
|
|
10
|
-
}
|
|
9
|
+
_LOGGER = logging.getLogger(__name__)
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
class DiscoveredDevice(object):
|
|
@@ -23,19 +22,15 @@ class DiscoveredDevice(object):
|
|
|
23
22
|
@staticmethod
|
|
24
23
|
def create_from_announce_msg(raw_addr, announce_msg):
|
|
25
24
|
"""Create announce message."""
|
|
26
|
-
_LOGGER.debug(
|
|
27
|
-
"Received announce message '%s' from %s ", announce_msg, raw_addr
|
|
28
|
-
)
|
|
25
|
+
_LOGGER.debug("Received announce message '%s' from %s ", announce_msg, raw_addr)
|
|
29
26
|
if len(announce_msg) != 8:
|
|
30
27
|
raise RuntimeError("Unexpected announcement, '%s'" % announce_msg)
|
|
31
28
|
|
|
32
|
-
device = DiscoveredDevice(
|
|
33
|
-
host=raw_addr[0], mac=announce_msg[0:6].hex(":")
|
|
34
|
-
)
|
|
29
|
+
device = DiscoveredDevice(host=raw_addr[0], mac=announce_msg[0:6].hex(":"))
|
|
35
30
|
device.type = announce_msg[6]
|
|
36
31
|
|
|
37
32
|
if device.type == "102":
|
|
38
|
-
device.hardware =
|
|
33
|
+
device.hardware = DEVICE_MAPPING_NUMERIC[int(announce_msg[6])]
|
|
39
34
|
else:
|
|
40
35
|
device.hardware = "non_mystrom"
|
|
41
36
|
status = announce_msg[7]
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Support for communicating with myStrom PIRs."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Iterable, List, Optional, Union
|
|
4
|
+
|
|
2
5
|
import aiohttp
|
|
3
6
|
from yarl import URL
|
|
4
|
-
from typing import Any, Dict, Iterable, List, Optional, Union
|
|
5
7
|
|
|
6
8
|
from . import _request as request
|
|
7
9
|
|
|
@@ -11,9 +13,7 @@ URI_PIR = URL("api/v1/")
|
|
|
11
13
|
class MyStromPir:
|
|
12
14
|
"""A class for a myStrom PIR."""
|
|
13
15
|
|
|
14
|
-
def __init__(
|
|
15
|
-
self, host: str, session: aiohttp.client.ClientSession = None
|
|
16
|
-
) -> None:
|
|
16
|
+
def __init__(self, host: str, session: aiohttp.client.ClientSession = None) -> None:
|
|
17
17
|
"""Initialize the switch."""
|
|
18
18
|
self._close_session = False
|
|
19
19
|
self._host = host
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Support for communicating with myStrom plugs/switches."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Union
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
from yarl import URL
|
|
7
|
+
|
|
8
|
+
from . import _request as request
|
|
9
|
+
from .device_types import DEVICE_MAPPING_LITERAL, DEVICE_MAPPING_NUMERIC
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MyStromSwitch:
|
|
13
|
+
"""A class for a myStrom switch/plug."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, host: str, session: aiohttp.client.ClientSession = None) -> None:
|
|
16
|
+
"""Initialize the switch."""
|
|
17
|
+
self._close_session = False
|
|
18
|
+
self._host = host
|
|
19
|
+
self._session = session
|
|
20
|
+
self._consumption = 0
|
|
21
|
+
self._consumedWs = 0
|
|
22
|
+
self._boot_id = None
|
|
23
|
+
self._energy_since_boot = None
|
|
24
|
+
self._time_since_boot = None
|
|
25
|
+
self._state = None
|
|
26
|
+
self._temperature = None
|
|
27
|
+
self._firmware = None
|
|
28
|
+
self._mac = None
|
|
29
|
+
self._device_type: Optional[Union[str, int]] = None
|
|
30
|
+
self.uri = URL.build(scheme="http", host=self._host)
|
|
31
|
+
|
|
32
|
+
async def turn_on(self) -> None:
|
|
33
|
+
"""Turn the relay on."""
|
|
34
|
+
parameters = {"state": "1"}
|
|
35
|
+
url = URL(self.uri).join(URL("relay"))
|
|
36
|
+
await request(self, uri=url, params=parameters)
|
|
37
|
+
await self.get_state()
|
|
38
|
+
|
|
39
|
+
async def turn_off(self) -> None:
|
|
40
|
+
"""Turn the relay off."""
|
|
41
|
+
parameters = {"state": "0"}
|
|
42
|
+
url = URL(self.uri).join(URL("relay"))
|
|
43
|
+
await request(self, uri=url, params=parameters)
|
|
44
|
+
await self.get_state()
|
|
45
|
+
|
|
46
|
+
async def toggle(self) -> None:
|
|
47
|
+
"""Toggle the relay."""
|
|
48
|
+
url = URL(self.uri).join(URL("toggle"))
|
|
49
|
+
await request(self, uri=url)
|
|
50
|
+
await self.get_state()
|
|
51
|
+
|
|
52
|
+
async def get_state(self) -> None:
|
|
53
|
+
"""Get the details from the switch/plug."""
|
|
54
|
+
url = URL(self.uri).join(URL("report"))
|
|
55
|
+
response = await request(self, uri=url)
|
|
56
|
+
try:
|
|
57
|
+
self._consumption = response["power"]
|
|
58
|
+
except KeyError:
|
|
59
|
+
self._consumption = None
|
|
60
|
+
try:
|
|
61
|
+
self._consumedWs = response["Ws"]
|
|
62
|
+
except KeyError:
|
|
63
|
+
self._consumedWs = None
|
|
64
|
+
try:
|
|
65
|
+
self._boot_id = response["boot_id"]
|
|
66
|
+
except KeyError:
|
|
67
|
+
self._boot_id = None
|
|
68
|
+
try:
|
|
69
|
+
self._energy_since_boot = response["energy_since_boot"]
|
|
70
|
+
except KeyError:
|
|
71
|
+
self._energy_since_boot = None
|
|
72
|
+
try:
|
|
73
|
+
self._time_since_boot = response["time_since_boot"]
|
|
74
|
+
except KeyError:
|
|
75
|
+
self._time_since_boot = None
|
|
76
|
+
self._state = response["relay"]
|
|
77
|
+
try:
|
|
78
|
+
self._temperature = response["temperature"]
|
|
79
|
+
except KeyError:
|
|
80
|
+
self._temperature = None
|
|
81
|
+
|
|
82
|
+
# Try the new API (Devices with newer firmware)
|
|
83
|
+
url = URL(self.uri).join(URL("api/v1/info"))
|
|
84
|
+
response = await request(self, uri=url)
|
|
85
|
+
if not isinstance(response, dict):
|
|
86
|
+
# Fall back to the old API version if the device runs with old firmware
|
|
87
|
+
url = URL(self.uri).join(URL("info.json"))
|
|
88
|
+
response = await request(self, uri=url)
|
|
89
|
+
|
|
90
|
+
self._firmware = response["version"]
|
|
91
|
+
self._mac = response["mac"]
|
|
92
|
+
self._device_type = response["type"]
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def device_type(self) -> Optional[str]:
|
|
96
|
+
"""Return the device type as string (e.g. "Switch CH v1" or "Button+")."""
|
|
97
|
+
if isinstance(self._device_type, int):
|
|
98
|
+
return DEVICE_MAPPING_NUMERIC.get(self._device_type)
|
|
99
|
+
elif isinstance(self._device_type, str):
|
|
100
|
+
return DEVICE_MAPPING_LITERAL.get(self._device_type)
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def relay(self) -> bool:
|
|
105
|
+
"""Return the relay state."""
|
|
106
|
+
return bool(self._state)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def consumption(self) -> Optional[float]:
|
|
110
|
+
"""Return the current power consumption in mWh."""
|
|
111
|
+
if self._consumption is not None:
|
|
112
|
+
return round(self._consumption, 1)
|
|
113
|
+
|
|
114
|
+
return self._consumption
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def consumedWs(self) -> Optional[float]:
|
|
118
|
+
"""The average of energy consumed per second since last report call."""
|
|
119
|
+
if self._consumedWs is not None:
|
|
120
|
+
return round(self._consumedWs, 1)
|
|
121
|
+
|
|
122
|
+
return self._consumedWs
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def boot_id(self) -> Optional[str]:
|
|
126
|
+
"""A unique identifier to distinguish whether the energy counter has been reset."""
|
|
127
|
+
return self._boot_id
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def energy_since_boot(self) -> Optional[float]:
|
|
131
|
+
"""The total energy in watt seconds (Ws) that has been measured since the last power-up or restart of the device."""
|
|
132
|
+
if self._energy_since_boot is not None:
|
|
133
|
+
return round(self._energy_since_boot, 2)
|
|
134
|
+
|
|
135
|
+
return self._energy_since_boot
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def time_since_boot(self) -> Optional[int]:
|
|
139
|
+
"""The time in seconds that has elapsed since the last start or restart of the device."""
|
|
140
|
+
return self._time_since_boot
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def firmware(self) -> Optional[str]:
|
|
144
|
+
"""Return the current firmware."""
|
|
145
|
+
return self._firmware
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def mac(self) -> Optional[str]:
|
|
149
|
+
"""Return the MAC address."""
|
|
150
|
+
return self._mac
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def temperature(self) -> Optional[float]:
|
|
154
|
+
"""Return the current temperature in Celsius."""
|
|
155
|
+
if self._temperature is not None:
|
|
156
|
+
return round(self._temperature, 1)
|
|
157
|
+
|
|
158
|
+
return self._temperature
|
|
159
|
+
|
|
160
|
+
async def get_temperature_full(self) -> str:
|
|
161
|
+
"""Get current temperature in celsius."""
|
|
162
|
+
url = URL(self.uri).join(URL("temp"))
|
|
163
|
+
response = await request(self, uri=url)
|
|
164
|
+
return response
|
|
165
|
+
|
|
166
|
+
async def close(self) -> None:
|
|
167
|
+
"""Close an open client session."""
|
|
168
|
+
if self._session and self._close_session:
|
|
169
|
+
await self._session.close()
|
|
170
|
+
|
|
171
|
+
async def __aenter__(self) -> "MyStromSwitch":
|
|
172
|
+
"""Async enter."""
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
async def __aexit__(self, *exc_info) -> None:
|
|
176
|
+
"""Async exit."""
|
|
177
|
+
await self.close()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "python-mystrom"
|
|
3
|
+
version = "2.4.0"
|
|
4
|
+
description = "Asynchronous Python API client for interacting with myStrom devices"
|
|
5
|
+
authors = ["Fabian Affolter <fabian@affolter-engineering.ch>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.rst"
|
|
8
|
+
homepage = "https://github.com/home-assistant-ecosystem/python-mystrom"
|
|
9
|
+
repository = "https://github.com/home-assistant-ecosystem/python-mystrom"
|
|
10
|
+
keywords = ["myStrom", "API", "client", "asynchronous"]
|
|
11
|
+
packages = [
|
|
12
|
+
{ include = "pymystrom" }
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.poetry.dependencies]
|
|
16
|
+
python = ">=3.11"
|
|
17
|
+
aiohttp = "*"
|
|
18
|
+
click = "*"
|
|
19
|
+
requests = "*"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["poetry-core>=1.0.0"]
|
|
23
|
+
build-backend = "poetry.core.masonry.api"
|
|
24
|
+
|
|
25
|
+
[tool.poetry.scripts]
|
|
26
|
+
pymystrom = "pymystrom.cli:main"
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
Changelog
|
|
2
|
-
=========
|
|
3
|
-
|
|
4
|
-
2.1.0 (2022-11-26)
|
|
5
|
-
------------------
|
|
6
|
-
|
|
7
|
-
- Add dd consumed energy to switch (thanks @OneCyrus)
|
|
8
|
-
|
|
9
|
-
2.0.0 (2020-11-12)
|
|
10
|
-
------------------
|
|
11
|
-
|
|
12
|
-
- Update the CLI to work with the bulbs
|
|
13
|
-
- Add support for Motion/PIR sensors
|
|
14
|
-
- Add support for device discovery
|
|
15
|
-
|
|
16
|
-
1.1.3 (2020-06-08)
|
|
17
|
-
------------------
|
|
18
|
-
|
|
19
|
-
- Improve temperature handling (Switch HW v2)
|
|
20
|
-
|
|
21
|
-
1.1.2 (2020-04-12)
|
|
22
|
-
------------------
|
|
23
|
-
|
|
24
|
-
- Minor changes and fixes
|
|
25
|
-
|
|
26
|
-
1.1.1 (2020-04-12)
|
|
27
|
-
------------------
|
|
28
|
-
|
|
29
|
-
- Fix typo
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
1.1.1 (2020-04-11)
|
|
33
|
-
------------------
|
|
34
|
-
|
|
35
|
-
- Minor fixes
|
|
36
|
-
|
|
37
|
-
1.1.0 (2020-04-10)
|
|
38
|
-
------------------
|
|
39
|
-
|
|
40
|
-
- Add new features for bulb
|
|
41
|
-
- Add new features for switch/plug
|
|
42
|
-
|
|
43
|
-
1.0.0 (2020-01-05)
|
|
44
|
-
------------------
|
|
45
|
-
|
|
46
|
-
- Full asynchronous now
|
|
47
|
-
- Move to aiohttp
|
|
48
|
-
- Update file header
|
|
49
|
-
|
|
50
|
-
0.5.0 (2019-02-27)
|
|
51
|
-
------------------
|
|
52
|
-
|
|
53
|
-
- Add support for temperature provided by Switch v2
|
|
54
|
-
|
|
55
|
-
0.4.4 (2018-06-07)
|
|
56
|
-
------------------
|
|
57
|
-
|
|
58
|
-
- Fix install_requires
|
|
59
|
-
|
|
60
|
-
0.4.3 (2018-06-07)
|
|
61
|
-
------------------
|
|
62
|
-
|
|
63
|
-
- Update README
|
|
64
|
-
|
|
65
|
-
0.4.2 (2018-03-27)
|
|
66
|
-
------------------
|
|
67
|
-
|
|
68
|
-
- Remove subprocess
|
|
69
|
-
- Add CLI
|