pax_global_header00006660000000000000000000000064144324634040014516gustar00rootroot0000000000000052 comment=089df15be663cb66b598b1a7ee4eb5d0f045d7be home-assistant-ecosystem-python-mystrom-6297e87/000077500000000000000000000000001443246340400220125ustar00rootroot00000000000000home-assistant-ecosystem-python-mystrom-6297e87/.github/000077500000000000000000000000001443246340400233525ustar00rootroot00000000000000home-assistant-ecosystem-python-mystrom-6297e87/.github/workflows/000077500000000000000000000000001443246340400254075ustar00rootroot00000000000000home-assistant-ecosystem-python-mystrom-6297e87/.github/workflows/pythonpackage.yml000066400000000000000000000023471443246340400307750ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python package on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: [ 3.9, "3.10", "3.11" ] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: Lint with flake8 run: | pip install flake8 # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Black Code Formatter uses: jpetrucciani/black-check@23.3.0 home-assistant-ecosystem-python-mystrom-6297e87/.gitignore000066400000000000000000000012761443246340400240100ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ home-assistant-ecosystem-python-mystrom-6297e87/CHANGELOG.md000066400000000000000000000017221443246340400236250ustar00rootroot00000000000000# Changelog ## next release - None changes ## 2.2.0 (2023-05-21) - Add additional devices (incl. Switch Zero) - Support for Switch Zero (thanks @hguetlin) - Implement get_device_info (thanks @pail23) ## 2.1.0 (2022-11-26) - Add dd consumed energy to switch (thanks @OneCyrus) ## 2.0.0 (2020-11-12) - Update the CLI to work with the bulbs - Add support for Motion/PIR sensors - Add support for device discovery ## 1.1.3 (2020-06-08) - Improve temperature handling (Switch HW v2) ## 1.1.2 (2020-04-12) - Minor changes and fixes ## 1.1.1 (2020-04-12) - Fix typo - Minor fixes ## 1.1.0 (2020-04-10) - Add new features for bulb - Add new features for switch/plug ## 1.0.0 (2020-01-05) - Full asynchronous now - Move to aiohttp - Update file header ## 0.5.0 (2019-02-27) - Add support for temperature provided by Switch v2 ## 0.4.4 (2018-06-07) - Fix install_requires ## 0.4.3 (2018-06-07) - Update README ## 0.4.2 (2018-03-27) - Remove subprocess - Add CLI home-assistant-ecosystem-python-mystrom-6297e87/LICENSE000066400000000000000000000021401443246340400230140ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015-2023 Fabian Affolter Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. home-assistant-ecosystem-python-mystrom-6297e87/MANIFEST.in000066400000000000000000000000551443246340400235500ustar00rootroot00000000000000include LICENSE CHANGELOG.rst graft examples home-assistant-ecosystem-python-mystrom-6297e87/README.rst000066400000000000000000000163271443246340400235120ustar00rootroot00000000000000python-mystrom |License| |PyPI| =================================== Asynchronous Python API client for interacting with `myStrom `_ devices. This module is not official, developed, supported or endorsed by myStrom AG. For questions and other inquiries, use the issue tracker in this repo please. Without the support of myStrom AG it would have taken much longer to create this module which is the base for the integration into `Home Assistant `_. myStrom AG has provided hardware. Their continuous support make further development of this module possible. Requirements ------------ You need to have `Python `_ installed. - `myStrom `_ device (bulb, plug or button) - The ``python-mystrom`` requirements - Network connection - Devices connected to your network Installation ------------ The package is available in the `Python Package Index `_ . .. code:: bash $ pip3 install python-mystrom On a Fedora-based system or on a CentOS/RHEL machine which has EPEL enabled. .. code:: bash $ sudo dnf -y install python3-mystrom For Nix or NixOS users is a package available. Keep in mind that the lastest releases might only be present in the ``unstable`` channel. .. code:: bash $ nix-env -iA nixos.python3Packages.python-mystrom Plug/switch ----------- At the moment the following endpoints are covered according `https://api.mystrom.ch `_: - ``/report``: for getting the current state and the power consumption - ``/relay``: for setting the relay state You will still be able to use your device with the smartphone application, ``curl`` or other tools. The samples below shows how to use the switch with ``httpie`` and ``curl`` along with ``python-mystrom``. .. code:: bash $ http http://IP_ADDRESS_PLUG/report HTTP/1.1 200 OK Content-Length: 39 Content-Type: application/json Date: Mon, 15 Feb 2016 17:52:21 GMT { "power": 51.630947, "relay": true } .. code:: bash $ curl -X GET http://IP_ADDRESS_PLUG/relay?state=1 Bulb ---- If the bulb is on then you should be able to retrieve the current state of the bulb. Browse to http://IP_ADDRESS_BULB/api/v1/device/MAC_ADDRESS_BULB or use a command-line tool. .. code:: bash $ curl -d "color=0;0;100" -d "action=on" http://IP_ADDRESS_BULB/api/v1/device/MAC_ADDRESS_BULB { "5DFF7FAHZ987": { "on": true, "color": "0;0;100", "mode": "hsv", "ramp": 100, "notifyurl": "" } } The bulbs are not able to handle payload formatted as JSON. It's required to use ``application/x-www-form-urlencoded``. Keep that in mind if something is not working, especially around setting the color with HSV. If you are planning to use your bulbs with `Home Assistant `_ set the bulb to a state from `Colors` with the app or use the command below. .. code:: bash $ curl -d "color=0;0;100" IP_ADDRESS_BULB/api/v1/device/MAC_ADDRESS_BULB Set State ````````` You can set the state with a POST request and a payload. - **on**: ``curl -d "action=on" http://IP_ADDRESS_BULB/api/v1/device/MAC_ADDRESS_BULB`` - **off**: ``curl -d "action=off" http://IP_ADDRESS_BULB/api/v1/device/MAC_ADDRESS_BULB`` - **toggle**: ``$ curl -d "action=toggle" http://IP_ADDRESS_BULB/api/v1/device/MAC_ADDRESS_BULB`` Set Color RGB ````````````` One of the supported modes for setting the color is **RBG**. - **white**: ``$ curl -d "color=FF000000" http://IP_ADDRESS_BULB/api/v1/device/MAC_ADDRESS_BULB`` - **red**: ``$ curl -d "color=00FF0000" http://IP_ADDRESS_BULB/api/v1/device/MAC_ADDRESS_BULB`` - **green**: ``$ curl -d "color=0000FF00" http://IP_ADDRESS_BULB/api/v1/device/MAC_ADDRESS_BULB`` - **blue**: ``$ curl -d "color=000000FF" http://IP_ADDRESS_BULB/api/v1/device/MAC_ADDRESS_BULB`` Set Color HSV (Hue, Saturation, Value) `````````````````````````````````````` It's also possible to use **HSV**. .. code:: bash $ curl -d "color=0;0;100" http://IP_ADDRESS_BULB/api/v1/device/MAC_ADDRESS_BULB While "color=" is composed with hue, saturation, and value. Set Mono (white) ```````````````` If you only want to set the "white" color of the bulb, use **mono**. .. code:: bash $ curl -d "color=10;100" http://IP_ADDRESS_BULB/api/v1/device/MAC_ADDRESS_BULB "color=" contains the value for the color temperature (from 1 to 18) and the brightness (from 0 to 100). Dimming (ramp) `````````````` Add **ramp** and an interval to set up the transition time while changing colors. .. code:: bash $ curl -d "action=on&ramp=1000&color=00FF0000" http://IP_ADDRESS_BULB/api/v1/device/MAC_ADDRESS_BULB The unit of measurement for ramp is milliseconds (ms). Button ------ The buttons can be set with the myStrom app or directly via HTTP requests. To set the configuration the payload must contains the relevant details for the actions: ``$ curl -v -d "single=&double=&long=&touch=" http://IP_ADDRESS_BUTTON/api/v1/device/MAC_ADDRESS_BUTTON`` Available actions: - **single**: Short push (approx. 1/2 seconds) - **double**: 2x sequential short pushes (within 2 seconds) - **long**: Long push (approx. 2 seconds) - **touch**: Touch of the button's surface (only affective for the WiFi Button +) The button is set up to extend the life span of the battery as much as possible. This means that only within the first 3 minutes or when connected to an USB port/USB charger and the battery is not full, the button is able to receive configuration information or publish its details. ``mystrom`` helper tool ----------------------- The command-line tool ``mystrom`` can help to set up the buttons and get the details from bulbs and plugs. .. code:: bash $ mystrom Usage: mystrom [OPTIONS] COMMAND [ARGS]... Simple command-line tool to get and set the values of a myStrom devices. This tool can set the targets of a myStrom button for the different available actions single, double, long and touch. Options: --version Show the version and exit. --help Show this message and exit. Commands: bulb Get and set details of a myStrom bulb. button Get and set details of a myStrom button. config Get and set the configuration of a myStrom... The examples shows how to get the details of a given bulb. .. code:: bash $ mystrom config read IP address of the myStrom device: IP_ADDRESS_BULB MAC address of the device: MAC_ADDRESS_BULB Read configuration from IP_ADDRESS_BULB { 'MAC_ADDRESS_BULB':{ 'type':'rgblamp', 'battery':False, 'reachable':True, 'meshroot':False, 'on':True, 'color':'191;90;14', 'mode':'hsv', 'ramp':100, 'power':0.953, 'fw_version':'2.25' } } Example usage of the module --------------------------- Examples for the bulb can be found in the directory ``examples``. License ------- ``python-mystrom`` is licensed under MIT, for more details check LICENSE. .. |License| image:: https://img.shields.io/badge/License-MIT-green.svg :target: https://pypi.python.org/pypi/python-mystrom :alt: License .. |PyPI| image:: https://img.shields.io/pypi/v/python-mystrom.svg :target: https://pypi.python.org/pypi/python-mystrom :alt: PyPI release home-assistant-ecosystem-python-mystrom-6297e87/examples/000077500000000000000000000000001443246340400236305ustar00rootroot00000000000000home-assistant-ecosystem-python-mystrom-6297e87/examples/example-bulb-hsv.py000066400000000000000000000006331443246340400273570ustar00rootroot00000000000000"""Example code for communicating with a myStrom bulb and HSV values.""" import time from pymystrom import bulb bulb = bulb.MyStromBulb("192.168.0.51", "5CCF7FA0AFB0") bulb.set_color_hex("000000FF") # Get the details of the bulb print("Current color details:", bulb.get_color()) # Set color as HSV (Hue, Saturation, Value) bulb.set_color_hsv(50, 100, 100) time.sleep(3) # Shutdown the bulb bulb.set_off() home-assistant-ecosystem-python-mystrom-6297e87/examples/example-bulb.py000066400000000000000000000043151443246340400265620ustar00rootroot00000000000000"""Example code for communicating with a myStrom bulb.""" import asyncio import logging from pymystrom.bulb import MyStromBulb from pymystrom.discovery import discover_devices IP_ADDRESS = "192.168.0.51" MAC_ADDRESS = "5CCF7FA0AFB0" async def main(): """Sample code to work with a myStrom bulb.""" # Discover myStrom bulbs devices devices = await discover_devices() print(f"Found {len(devices)} bulb(s)") for device in devices: print(f" IP address: {device.host}, MAC address: {device.mac}") async with MyStromBulb(IP_ADDRESS, MAC_ADDRESS) as bulb: print("Get the details from the bulb...") await bulb.get_state() print("Power consumption:", bulb.consumption) print("Firmware:", bulb.firmware) print("Current state:", "off" if bulb.state is False else "on") print("Bulb will be switched on with their previous setting") await bulb.set_on() # print("Waiting for a couple of seconds...") await asyncio.sleep(2) print("Bulb will be set to white") await bulb.set_white() # Wait a few seconds to get a reading of the power consumption print("Waiting for a couple of seconds...") await asyncio.sleep(2) # Set transition time to 2 s await bulb.set_transition_time(2000) # Set to blue as HEX await bulb.set_color_hex("000000FF") await asyncio.sleep(3) # Set color as HSV (Hue, Saturation, Value) await bulb.set_color_hsv(0, 0, 100) await asyncio.sleep(3) # Test a fast flashing sequence print("Flash it for 10 seconds...") await bulb.set_flashing(10, [100, 50, 30], [200, 0, 71]) await bulb.set_off() # Show a sunrise within a minute print("Show a sunrise for 60 s") await bulb.set_sunrise(60) # Show a rainbow for 60 seconds print("Show a rainbow") await bulb.set_rainbow(60) # Reset transition time await bulb.set_transition_time(1000) # Shutdown the bulb await bulb.set_off() if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) loop = asyncio.get_event_loop() loop.run_until_complete(main()) home-assistant-ecosystem-python-mystrom-6297e87/examples/example-get-data-bulb.py000066400000000000000000000007061443246340400302460ustar00rootroot00000000000000"""Example code for getting the data from a myStrom bulb.""" from pymystrom import bulb bulb = bulb.MyStromBulb("192.168.0.51", "5CCF7FA0AFB0") # Get the details of the bulb print("Bulb state:", bulb.get_bulb_state()) print("Current color:", bulb.get_color()) print("Brightness:", bulb.get_brightness()) print("Power consumption:", bulb.get_power()) print("Transition time:", bulb.get_transition_time()) print("Firmware version:", bulb.get_firmware()) home-assistant-ecosystem-python-mystrom-6297e87/examples/example-pir.py000066400000000000000000000022141443246340400264240ustar00rootroot00000000000000"""Example code for communicating with a myStrom PIR unit.""" import asyncio from pymystrom.pir import MyStromPir IP_ADDRESS = "192.168.1.225" async def main(): """Sample code to work with a myStrom PIR.""" async with MyStromPir(IP_ADDRESS) as pir: # Get the PIR settings await pir.get_settings() print("Settings:", pir.settings) # Get the PIR settings await pir.get_pir() print("PIR settings:", pir.pir) # Collect the sensors await pir.get_sensors_state() print("Sensors:", pir.sensors) # Get the temperature data await pir.get_temperatures() print("Temperatures:", pir.temperature_raw) print("Temperature measured:", pir.temperature_measured) # Details of the light sensor await pir.get_light() print("Brightness:", pir.intensity) print("Day?:", pir.day) print("Raw light data:", pir.light_raw) # Get the action settings await pir.get_actions() print("Actions:", pir.actions) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) home-assistant-ecosystem-python-mystrom-6297e87/examples/example-switch.py000066400000000000000000000020001443246340400271240ustar00rootroot00000000000000"""Example code for communicating with a myStrom plug/switch.""" import asyncio from pymystrom.switch import MyStromSwitch IP_ADDRESS = "192.168.0.40" async def main(): """Sample code to work with a myStrom switch.""" async with MyStromSwitch(IP_ADDRESS) as switch: # Collect the data of the current state await switch.get_state() print("Power consumption:", switch.consumption) print("Energy consumed:", switch.consumedWs) print("Relay state:", switch.relay) print("Temperature:", switch.temperature) print("Firmware:", switch.firmware) print("MAC address:", switch.mac) print("Turn on the switch") if not switch.relay: await switch.turn_on() # print("Toggle the switch") # await switch.toggle() # Switch relay off if it was off if switch.relay: await switch.turn_off() if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) home-assistant-ecosystem-python-mystrom-6297e87/pymystrom/000077500000000000000000000000001443246340400240755ustar00rootroot00000000000000home-assistant-ecosystem-python-mystrom-6297e87/pymystrom/__init__.py000066400000000000000000000061461443246340400262150ustar00rootroot00000000000000"""Base details for the myStrom Python bindings.""" import asyncio import aiohttp import async_timeout from yarl import URL from typing import Any, Mapping, Optional import socket from .exceptions import MyStromConnectionError import pkg_resources __version__ = pkg_resources.get_distribution("setuptools").version TIMEOUT = 10 USER_AGENT = f"PythonMyStrom/{__version__}" async def _request( self, uri: str, method: str = "GET", data: Optional[Any] = None, json_data: Optional[dict] = None, params: Optional[Mapping[str, str]] = None, ) -> Any: """Handle a request to the myStrom device.""" headers = { "User-Agent": USER_AGENT, "Accept": "application/json, text/plain, */*", } if self._session is None: self._session = aiohttp.ClientSession() self._close_session = True try: with async_timeout.timeout(TIMEOUT): response = await self._session.request( method, uri, data=data, json=json_data, params=params, headers=headers, ) except asyncio.TimeoutError as exception: raise MyStromConnectionError( "Timeout occurred while connecting to myStrom device." ) from exception except (aiohttp.ClientError, socket.gaierror) as exception: raise MyStromConnectionError( "Error occurred while communicating with myStrom device." ) from exception content_type = response.headers.get("Content-Type", "") if (response.status // 100) in [4, 5]: response.close() if "application/json" in content_type: response_json = await response.json() return response_json return response.text class MyStromDevice: """A class for a myStrom device.""" def __init__( self, host, session: aiohttp.client.ClientSession = None, ): """Initialize the device.""" self._close_session = False self._host = host self._session = session self.uri = URL.build(scheme="http", host=self._host) async def get_device_info(self) -> dict: """Get the device info of a myStrom device.""" url = URL(self.uri).join(URL("api/v1/info")) response = await _request(self, uri=url) if not isinstance(response, dict): # Fall back to the old API version if the device runs with old firmware url = URL(self.uri).join(URL("info.json")) response = await _request(self, uri=url) return response async def close(self) -> None: """Close an open client session.""" if self._session and self._close_session: await self._session.close() async def __aenter__(self) -> "MyStromDevice": """Async enter.""" return self async def __aexit__(self, *exc_info) -> None: """Async exit.""" await self.close() async def get_device_info(host: str) -> dict: """Get the device info of a myStrom device.""" async with MyStromDevice(host) as device: return await device.get_device_info() home-assistant-ecosystem-python-mystrom-6297e87/pymystrom/bulb.py000066400000000000000000000131771443246340400254040ustar00rootroot00000000000000"""Support for communicating with myStrom bulbs.""" import asyncio import logging import aiohttp from yarl import URL from typing import Any, Dict, Iterable, List, Optional, Union from . import _request as request _LOGGER = logging.getLogger(__name__) URI_BULB = URL("api/v1/device") class MyStromBulb: """A class for a myStrom bulb.""" def __init__( self, host: str, mac: str, session: aiohttp.client.ClientSession = None, ): """Initialize the bulb.""" self._close_session = False self._host = host self._mac = mac self._session = session self.brightness = 0 self._color = None self._consumption = 0 self.data = None self._firmware = None self._mode = None self._bulb_type = None self._state = None self._transition_time = 0 self.uri = URL.build(scheme="http", host=self._host).join(URI_BULB) / self._mac async def get_state(self) -> object: """Get the state of the bulb.""" response = await request(self, uri=self.uri) self._consumption = response[self._mac]["power"] self._firmware = response[self._mac]["fw_version"] self._color = response[self._mac]["color"] self._mode = response[self._mac]["mode"] self._transition_time = response[self._mac]["ramp"] self._state = bool(response[self._mac]["on"]) self._bulb_type = response[self._mac]["type"] @property def firmware(self) -> Optional[str]: """Return current firmware.""" return self._firmware @property def mac(self) -> float: """Return the MAC address.""" return self._mac @property def consumption(self) -> Optional[float]: """Return current firmware.""" return self._consumption @property def color(self) -> Optional[str]: """Return current color settings.""" return self._color @property def mode(self) -> Optional[str]: """Return current mode.""" return self._mode @property def transition_time(self) -> Optional[int]: """Return current transition time (ramp).""" return self._transition_time @property def bulb_type(self) -> Optional[str]: """Return the type of the bulb.""" return self._bulb_type @property def state(self) -> Optional[str]: """Return the current state of the bulb.""" return self._state async def set_on(self): """Turn the bulb on with the previous settings.""" response = await request( self, uri=self.uri, method="POST", data={"action": "on"} ) return response async def set_color_hex(self, value): """Turn the bulb on with the given color as HEX. white: FF000000 red: 00FF0000 green: 0000FF00 blue: 000000FF """ data = { "action": "on", "color": value, } response = await request(self, uri=self.uri, method="POST", data=data) return response async def set_color_hsv(self, hue, saturation, value): """Turn the bulb on with the given values as HSV.""" # The current situation doesn't allow to send JSON to the bulb as # the firmware wants a string separated by ;. This is was # reported in 2018 to myStrom # data = { # 'action': 'on', # 'color': f"{hue};{saturation};{value}", # } data = "action=on&color={};{};{}".format(hue, saturation, value) response = await request(self, uri=self.uri, method="POST", data=data) return response async def set_white(self): """Turn the bulb on, full white.""" await self.set_color_hsv(0, 0, 100) async def set_rainbow(self, duration): """Turn the bulb on and create a rainbow.""" for i in range(0, 359): await self.set_color_hsv(i, 100, 100) await asyncio.sleep(duration / 359) async def set_sunrise(self, duration): """Turn the bulb on and create a sunrise. The brightness is from 0 till 100. """ max_brightness = 100 await self.set_transition_time((duration / max_brightness)) for i in range(0, duration): data = "action=on&color=3;{}".format(i) await request(self, uri=self.uri, method="POST", data=data) await asyncio.sleep(duration / max_brightness) async def set_flashing(self, duration, hsv1, hsv2): """Turn the bulb on, flashing with two colors.""" await self.set_transition_time(100) for step in range(0, int(duration / 2)): await self.set_color_hsv(hsv1[0], hsv1[1], hsv1[2]) await asyncio.sleep(1) await self.set_color_hsv(hsv2[0], hsv2[1], hsv2[2]) await asyncio.sleep(1) async def set_transition_time(self, value): """Set the transition time in ms.""" response = await request( self, uri=self.uri, method="POST", data={"ramp": int(round(value))} ) return response async def set_off(self): """Turn the bulb off.""" response = await request( self, uri=self.uri, method="POST", data={"action": "off"} ) return response async def close(self) -> None: """Close an open client session.""" if self._session and self._close_session: await self._session.close() async def __aenter__(self) -> "MyStromBulb": """Async enter.""" return self async def __aexit__(self, *exc_info) -> None: """Async exit.""" await self.close() home-assistant-ecosystem-python-mystrom-6297e87/pymystrom/cli.py000066400000000000000000000206771443246340400252320ustar00rootroot00000000000000"""Command-line tool for working with myStrom devices.""" import click import requests import asyncio from functools import wraps from pymystrom.bulb import MyStromBulb URI = "api/v1/device" TIMEOUT = 5 def coro(f): """Allow to use async in click.""" @wraps(f) def wrapper(*args, **kwargs): """Async wrapper.""" return asyncio.run(f(*args, **kwargs)) return wrapper @click.group() @click.version_option() def main(): """Simple command-line tool to get and set the values of myStrom devices. This tool can also set the targets of a myStrom button for the different available actions single, double, long and touch. """ @main.group("config") def config(): """Get and set the configuration of a myStrom device.""" @config.command("read") @click.option( "--ip", prompt="IP address of the device", help="IP address of the device." ) @click.option( "--mac", prompt="MAC address of the device", help="MAC address of the device.", ) def read_config(ip, mac): """Read the current configuration of a myStrom device.""" click.echo("Read configuration from %s" % ip) try: request = requests.get("http://{}/{}/{}/".format(ip, URI, mac), timeout=TIMEOUT) click.echo(request.json()) except requests.exceptions.ConnectionError: click.echo("Communication issue with the device") @main.group("button") def button(): """Get and set details of a myStrom button.""" @button.command("generic") @click.option( "--ip", prompt="IP address of the button", help="IP address of the button." ) @click.option( "--mac", prompt="MAC address of the button", help="MAC address of the button.", ) @click.option( "--single", prompt="URL for a single tap", default="", help="URL for a single tap.", ) @click.option( "--double", prompt="URL for a double tap", default="", help="URL for a double tap.", ) @click.option( "--long", prompt="URL for a long tab", default="", help="URL for a long tab." ) @click.option("--touch", prompt="URL for a touch", default="", help="URL for a touch.") def write_config(ip, mac, single, double, long, touch): """Write the current configuration of a myStrom button.""" click.echo("Write configuration to device %s" % ip) data = { "single": single, "double": double, "long": long, "touch": touch, } try: request = requests.post( "http://{}/{}/{}/".format(ip, URI, mac), data=data, timeout=TIMEOUT ) if request.status_code == 200: click.echo("Configuration of %s set" % mac) except requests.exceptions.ConnectionError: click.echo("Communication issue with the device. No action performed") @button.command("home-assistant") @click.option( "--ip", prompt="IP address of the button", help="IP address of the button." ) @click.option( "--mac", prompt="MAC address of the button", help="MAC address of the button.", ) @click.option( "--hass", prompt="IP address of the Home Assistant instance", help="IP address of Home Assistant instance to use.", ) @click.option( "--port", prompt="Port of Home Assistant instance", default="8123", help="Port where Home Assistant instance is listening.", ) @click.option( "--id", prompt="ID of the button", default="", help="ID of the myStrom button.", ) def write_ha_config(ip, mac, hass, port, id): """Write the configuration for Home Assistant to a myStrom button.""" click.echo("Write configuration for Home Assistant to device %s..." % ip) action = "get://{1}:{2}/api/mystrom?{0}={3}" data = { "single": action.format("single", hass, port, id), "double": action.format("double", hass, port, id), "long": action.format("long", hass, port, id), "touch": action.format("touch", hass, port, id), } try: request = requests.post( "http://{}/{}/{}/".format(ip, URI, mac), data=data, timeout=TIMEOUT ) if request.status_code == 200: click.echo("Configuration for %s set" % ip) click.echo( "After using the push pattern the first time then " "the myStrom WiFi Button will show up as %s" % id ) except requests.exceptions.ConnectionError: click.echo("Communication issue with the device") @button.command("reset") @click.option( "--ip", prompt="IP address of the WiFi Button", help="P address of the WiFi Button.", ) @click.option( "--mac", prompt="MAC address of the button", help="MAC address of the Wifi Button.", ) def reset_config(ip, mac): """Reset the current configuration of a myStrom WiFi Button.""" click.echo("Reset configuration of button %s..." % ip) data = { "single": "", "double": "", "long": "", "touch": "", } try: request = requests.post( "http://{}/{}/{}/".format(ip, URI, mac), data=data, timeout=TIMEOUT ) if request.status_code == 200: click.echo("Reset configuration of %s" % mac) except requests.exceptions.ConnectionError: click.echo("Communication issue with the device. No action performed") @button.command("read") @click.option( "--ip", prompt="IP address of the WiFi Button", help="P address of the WiFi Button.", ) @click.option( "--mac", prompt="MAC address of the button", help="MAC address of the Wifi Button.", ) def read_config(ip, mac): """Read the current configuration of a myStrom WiFi Button.""" click.echo("Read the configuration of button %s..." % ip) try: request = requests.get("http://{}/{}/{}/".format(ip, URI, mac), timeout=TIMEOUT) click.echo(request.json()) except requests.exceptions.ConnectionError: click.echo("Communication issue with the device. No action performed") @main.group("bulb") def bulb(): """Get and set details of a myStrom bulb.""" @bulb.command("on") @coro @click.option("--ip", prompt="IP address of the bulb", help="IP address of the bulb.") @click.option( "--mac", prompt="MAC address of the bulb", help="MAC address of the bulb." ) async def on(ip, mac): """Switch the bulb on.""" async with MyStromBulb(ip, mac) as bulb: await bulb.set_color_hex("00FFFFFF") @bulb.command("color") @coro @click.option("--ip", prompt="IP address of the bulb", help="IP address of the bulb.") @click.option( "--mac", prompt="MAC address of the bulb", help="MAC address of the bulb." ) @click.option( "--hue", prompt="Set the hue of the bulb", help="Set the hue of the bulb." ) @click.option( "--saturation", prompt="Set the saturation of the bulb", help="Set the saturation of the bulb.", ) @click.option( "--value", prompt="Set the value of the bulb", help="Set the value of the bulb.", ) async def color(ip, mac, hue, saturation, value): """Switch the bulb on with the given color.""" async with MyStromBulb(ip, mac) as bulb: await bulb.set_color_hsv(hue, saturation, value) @bulb.command("off") @coro @click.option("--ip", prompt="IP address of the bulb", help="IP address of the bulb.") @click.option( "--mac", prompt="MAC address of the bulb", help="MAC address of the bulb." ) async def off(ip, mac): """Switch the bulb off.""" async with MyStromBulb(ip, mac) as bulb: await bulb.set_off() @bulb.command("flash") @coro @click.option("--ip", prompt="IP address of the bulb", help="IP address of the bulb.") @click.option( "--mac", prompt="MAC address of the bulb", help="MAC address of the bulb." ) @click.option( "--time", prompt="Time to flash", help="Time to flash the bulb in seconds.", default=10, ) async def flash(ip, mac, time): """Flash the bulb off.""" async with MyStromBulb(ip, mac) as bulb: await bulb.set_flashing(time, [100, 50, 30], [200, 0, 71]) @bulb.command("rainbow") @coro @click.option("--ip", prompt="IP address of the bulb", help="IP address of the bulb.") @click.option( "--mac", prompt="MAC address of the bulb", help="MAC address of the bulb." ) @click.option( "--time", prompt="Time for the complete rainbow", help="Time to perform the rainbow in seconds.", default=30, ) async def rainbow(ip, mac, time): """Let the buld change the color and show a rainbow.""" async with MyStromBulb(ip, mac) as bulb: await bulb.set_rainbow(time) await bulb.set_transition_time(1000) if __name__ == "__main__": main() home-assistant-ecosystem-python-mystrom-6297e87/pymystrom/discovery.py000066400000000000000000000072211443246340400264600ustar00rootroot00000000000000"""Support for discovering myStrom devices.""" import asyncio import logging from typing import Optional, List _LOGGER = logging.getLogger(__name__) DEVICE_MAPPING = { "101": "myStrom Switch v1", "102": "myStrom Bulb", "103": "myStrom Button+", "104": "myStrom Button", "105": "myStrom LED strip", "106": "myStzrom Switch v2", "107": "myStrom Switch EU", "110": "myStrom Motion sensor", "120": "myStrom Switch Zero", } class DiscoveredDevice(object): """Representation of discovered device.""" mac: str type: int is_child: bool mystrom_registered: bool mystrom_online: bool restarted: bool @staticmethod def create_from_announce_msg(raw_addr, announce_msg): """Create announce message.""" _LOGGER.debug("Received announce message '%s' from %s ", announce_msg, raw_addr) if len(announce_msg) != 8: raise RuntimeError("Unexpected announcement, '%s'" % announce_msg) device = DiscoveredDevice(host=raw_addr[0], mac=announce_msg[0:6].hex(":")) device.type = announce_msg[6] if device.type == "102": device.hardware = DEVICE_MAPPING[str(announce_msg[6])] else: device.hardware = "non_mystrom" status = announce_msg[7] # Parse status field device.is_child = status & 1 != 0 device.mystrom_registered = status & 2 != 0 device.mystrom_online = status & 4 != 0 device.restarted = status & 8 != 0 return device def __init__(self, host, mac): """Initialize the discovery.""" self.host = host self.mac = mac class DeviceRegistry(object): """Representation of the device registry.""" def __init__(self): """Initialize the device registry.""" self.devices_by_mac = {} def register(self, device): """Register a device.""" self.devices_by_mac[device.mac] = device def devices(self): """Get all present devices.""" return list(self.devices_by_mac.values()) class DiscoveryProtocol(asyncio.DatagramProtocol): """Representation of the discovery protocol.""" def __init__(self, registry: DeviceRegistry): """ "Initialize the discovery protocol.""" super().__init__() self.registry = registry def connection_made(self, transport): """Create an UDP listener.""" _LOGGER.debug("Starting up UDP listener") self.transport = transport def datagram_received(self, data, addr): """Handle a datagram.""" device = DiscoveredDevice.create_from_announce_msg(addr, data) self.registry.register(device) def connection_lost(self, exc: Optional[Exception]) -> None: """Stop if connection is lost.""" _LOGGER.debug("Shutting down UDP listener") super().connection_lost(exc) async def discover_devices(timeout: int = 7) -> List[DiscoveredDevice]: """Discover local myStrom devices. Some myStrom devices report their presence every ~5 seconds in an UDP broadcast to port 7979. """ registry = DeviceRegistry() loop = asyncio.get_event_loop() (transport, protocol) = await loop.create_datagram_endpoint( lambda: DiscoveryProtocol(registry), local_addr=("0.0.0.0", 7979) ) # Server runs in the background, meanwhile wait until timeout expires await asyncio.sleep(timeout) # Shutdown server transport.close() devices = registry.devices() for device in devices: _LOGGER.debug( "Discovered myStrom device %s (%s) (MAC addresse: %s)", device.host, device.type, device.mac, ) return devices home-assistant-ecosystem-python-mystrom-6297e87/pymystrom/exceptions.py000066400000000000000000000005561443246340400266360ustar00rootroot00000000000000"""All exceptions for the myStrom Python bindings.""" class MyStromError(Exception): """General MyStromError exception occurred.""" pass class MyStromConnectionError(MyStromError): """When a connection error is encountered.""" pass class MyStromNotVersionTwoSwitch(MyStromError): """When version 2 function is not supported.""" pass home-assistant-ecosystem-python-mystrom-6297e87/pymystrom/pir.py000066400000000000000000000124051443246340400252430ustar00rootroot00000000000000"""Support for communicating with myStrom PIRs.""" import aiohttp from yarl import URL from typing import Any, Dict, Iterable, List, Optional, Union from . import _request as request URI_PIR = URL("api/v1/") class MyStromPir: """A class for a myStrom PIR.""" def __init__(self, host: str, session: aiohttp.client.ClientSession = None) -> None: """Initialize the switch.""" self._close_session = False self._host = host self._session = session self._intensity = None self._day = None self._light_raw = None self._sensors = None self._temperature_measured = None self._temperature_compensated = None self._temperature_compensation = None self._temperature_raw = None self._motion = None self._settings = None self._pir = None self._actions = None self.uri = URL.build(scheme="http", host=self._host).join(URI_PIR) async def get_settings(self) -> None: """Get the current settings from the PIR.""" url = URL(self.uri).join(URL("settings")) response = await request(self, uri=url) self._settings = response async def get_actions(self) -> None: """Get the current action settings from the PIR.""" url = URL(self.uri).join(URL("action")) response = await request(self, uri=url) self._actions = response async def get_pir(self) -> None: """Get the current PIR settings.""" url = URL(self.uri).join(URL("settings/pir")) response = await request(self, uri=url) self._pir = response async def get_sensors_state(self) -> None: """Get the state of the sensors from the PIR.""" url = URL(self.uri).join(URL("sensors")) response = await request(self, uri=url) # The return data has the be re-written as the temperature is not rounded self._sensors = { "motion": response["motion"], "light": response["light"], "temperature": round(response["temperature"], 2), } async def get_temperatures(self) -> None: """Get the temperatures from the PIR.""" # There is a different URL for the temp endpoint url = URL.build(scheme="http", host=self._host) / "temp" response = await request(self, uri=url) self._temperature_raw = response self._temperature_measured = round(response["measured"], 2) self._temperature_compensated = round(response["compensated"], 2) self._temperature_compensation = round(response["compensation"], 3) async def get_motion(self) -> None: """Get the state of the motion sensor from the PIR.""" url = URL(self.uri).join(URL("motion")) response = await request(self, uri=url) self._motion = response["motion"] async def get_light(self) -> None: """Get the state of the light sensor from the PIR.""" url = URL(self.uri).join(URL("light")) response = await request(self, uri=url) self._intensity = response["intensity"] self._day = response["day"] self._light_raw = response["raw"] @property def settings(self) -> Optional[dict]: """Return current settings.""" return self._settings @property def actions(self) -> Optional[dict]: """Return current action settings.""" return self._actions @property def pir(self) -> Optional[dict]: """Return current PIR settings.""" return self._pir @property def sensors(self) -> Optional[dict]: """Return current sensor values.""" return self._sensors @property def temperature_measured(self) -> Optional[str]: """Return current measured temperature.""" return self._temperature_measured @property def temperature_compensated(self) -> Optional[str]: """Return current compensated temperature.""" return self._temperature_compensated @property def temperature_compensation(self) -> Optional[str]: """Return current temperature compensation.""" return self._temperature_compensation @property def temperature_raw(self) -> Optional[dict]: """Return current raw temperature values.""" return self._temperature_raw @property def motion(self) -> Optional[str]: """Return the state of the motion sensor.""" return self._motion @property def intensity(self) -> Optional[str]: """Return the intensity reported by the light sensor.""" return self._intensity @property def day(self) -> Optional[str]: """Return the information based on the thresholds set.""" return self._day @property def light_raw(self) -> Optional[str]: """Return the raw data from the ADC.""" return { "visible": self._light_raw["adc0"], "infrared": self._light_raw["adc1"], } async def close(self) -> None: """Close an open client session.""" if self._session and self._close_session: await self._session.close() async def __aenter__(self) -> "MyStromPir": """Async enter.""" return self async def __aexit__(self, *exc_info) -> None: """Async exit.""" await self.close() home-assistant-ecosystem-python-mystrom-6297e87/pymystrom/switch.py000066400000000000000000000100201443246340400257410ustar00rootroot00000000000000"""Support for communicating with myStrom plugs/switches.""" import aiohttp from yarl import URL from typing import Any, Dict, Iterable, List, Optional, Union from . import _request as request class MyStromSwitch: """A class for a myStrom switch/plug.""" def __init__(self, host: str, session: aiohttp.client.ClientSession = None) -> None: """Initialize the switch.""" self._close_session = False self._host = host self._session = session self._consumption = 0 self._consumedWs = 0 self._state = None self._temperature = None self._firmware = None self._mac = None self.uri = URL.build(scheme="http", host=self._host) async def turn_on(self) -> None: """Turn the relay on.""" parameters = {"state": "1"} url = URL(self.uri).join(URL("relay")) await request(self, uri=url, params=parameters) await self.get_state() async def turn_off(self) -> None: """Turn the relay off.""" parameters = {"state": "0"} url = URL(self.uri).join(URL("relay")) await request(self, uri=url, params=parameters) await self.get_state() async def toggle(self) -> None: """Toggle the relay.""" url = URL(self.uri).join(URL("toggle")) await request(self, uri=url) await self.get_state() async def get_state(self) -> None: """Get the details from the switch/plug.""" url = URL(self.uri).join(URL("report")) response = await request(self, uri=url) try: self._consumption = response["power"] except KeyError: self._consumption = None try: self._consumedWs = response["Ws"] except KeyError: self._consumedWs = None self._state = response["relay"] try: self._temperature = response["temperature"] except KeyError: self._temperature = None # Try the new API (Devices with newer firmware) url = URL(self.uri).join(URL("api/v1/info")) response = await request(self, uri=url) if not isinstance(response, dict): # Fall back to the old API version if the device runs with old firmware url = URL(self.uri).join(URL("info.json")) response = await request(self, uri=url) self._firmware = response["version"] self._mac = response["mac"] @property def relay(self) -> bool: """Return the relay state.""" return bool(self._state) @property def consumption(self) -> float: """Return the current power consumption in mWh.""" if self._consumption is not None: return round(self._consumption, 1) return self._consumption @property def consumedWs(self) -> float: """The average of energy consumed per second since last report call.""" if self._consumedWs is not None: return round(self._consumedWs, 1) return self._consumedWs @property def firmware(self) -> float: """Return the current firmware.""" return self._firmware @property def mac(self) -> float: """Return the MAC address.""" return self._mac @property def temperature(self) -> float: """Return the current temperature in celsius.""" if self._temperature is not None: return round(self._temperature, 1) return self._temperature async def get_temperature_full(self) -> str: """Get current temperature in celsius.""" url = URL(self.uri).join(URL("temp")) response = await request(self, uri=url) return response async def close(self) -> None: """Close an open client session.""" if self._session and self._close_session: await self._session.close() async def __aenter__(self) -> "MyStromSwitch": """Async enter.""" return self async def __aexit__(self, *exc_info) -> None: """Async exit.""" await self.close() home-assistant-ecosystem-python-mystrom-6297e87/requirements.txt000066400000000000000000000001041443246340400252710ustar00rootroot00000000000000# python-mystrom requirements. Use setup.py for the dependencies. . home-assistant-ecosystem-python-mystrom-6297e87/setup.py000066400000000000000000000025551443246340400235330ustar00rootroot00000000000000"""Set up the Python API for myStrom devices.""" import os import sys from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) with open(os.path.join(here, "README.rst"), encoding="utf-8") as readme: long_description = readme.read() setup( name="python-mystrom", version="2.2.0", description="Asynchronous Python API client for interacting with myStrom devices", long_description=long_description, url="https://github.com/home-assistant-ecosystem/python-mystrom", author="Fabian Affolter", author_email="fabian@affolter-engineering.ch", license="MIT", install_requires=[ "requests", "click", "aiohttp", "setuptools", ], packages=find_packages(), python_requires=">=3.9", zip_safe=True, include_package_data=True, entry_points=""" [console_scripts] mystrom=pymystrom.cli:main """, classifiers=[ "Development Status :: 3 - Alpha", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: Utilities", ], )