diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..511dd5ed --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,15 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.145.1/containers/python-3/.devcontainer/base.Dockerfile + +# [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6 +ARG VARIANT="3.8" +FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} + +# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. +RUN python -m pip install --upgrade pip && pip install flake8-cognitive-complexity isort + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..f3e2c3bb --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,55 @@ +{ + "name": "devolo Home Control API", + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + // Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9 + "VARIANT": "3.8" + } + }, + // Set *default* container specific settings.json values on container create. + "settings": { + "terminal.integrated.shell.linux": "/bin/bash", + "python.pythonPath": "/usr/local/bin/python", + "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", + "python.formatting.provider": "yapf", + "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", + "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", + "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", + "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", + "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", + "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint", + "python.linting.enabled": true, + "python.linting.flake8Enabled": true, + "python.linting.mypyEnabled": true, + "python.linting.pylintEnabled": true, + "python.linting.pylintArgs": [ + "--disable=C,R", + "--enable=useless-suppression", + ], + "python.sortImports.path": "/usr/local/bin/isort", + "python.sortImports.args": [ + "--settings-path=/workspaces/devolo_home_control_api/setup.cfg", + ], + "python.testing.nosetestsEnabled": false, + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "editor.formatOnSave": true, + "files.trimTrailingWhitespace": true, + "files.trimFinalNewlines": true, + "files.insertFinalNewline": true, + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.vscode-pylance", + "sourcery.sourcery", + "visualstudioexptteam.vscodeintellicode", + ], + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "pip install -e .[test]", +} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7a160007..f3733f01 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,8 +8,7 @@ -- [ ] Version number in \_\_init\_\_.py was properly set. - [ ] Changelog is updated. diff --git a/.github/workflows/convert_todos_to_issues.yml b/.github/workflows/convert_todos_to_issues.yml index f3296db6..ddf9ce9f 100644 --- a/.github/workflows/convert_todos_to_issues.yml +++ b/.github/workflows/convert_todos_to_issues.yml @@ -17,4 +17,4 @@ jobs: TOKEN: ${{ secrets.GITHUB_TOKEN }} LABEL: "# TODO:" COMMENT_MARKER: "#" - id: "todo" \ No newline at end of file + id: "todo" diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 9d751e46..9c3fe069 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -3,36 +3,95 @@ name: Python package on: [push] jobs: - build: + format: + name: Check formatting + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Check formatting + uses: pre-commit/action@v2.0.0 + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Lint with flake8 + run: | + python -m pip install --upgrade pip + pip install flake8 + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --ignore=E303,W503 --statistics + - name: Lint with pylint + run: | + pip install pylint + pip install -e . + pylint --errors-only --score=n devolo_home_control_api + pylint --exit-zero --score=n --disable=C,E,R --enable=useless-suppression devolo_home_control_api + - name: Lint with mypy + run: | + pip install mypy + mypy --ignore-missing-imports devolo_home_control_api + + test: + name: Test with Python ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8] - + python-version: [3.6, 3.7, 3.8, 3.9] steps: - - uses: actions/checkout@v2 + - name: Checkout sources + uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - python setup.py install - - 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. - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --ignore=E303,W503 --statistics + pip install -e .[test] - name: Test with pytest run: | - python setup.py test --addopts --cov=devolo_home_control_api + pytest --cov=devolo_home_control_api + - name: Preserve coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: .coverage + + coverage: + name: Upload coverage + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout sources + uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Download coverage + uses: actions/download-artifact@v2 + with: + name: coverage - name: Coveralls run: | - pip install coveralls==1.10.0 - export COVERALLS_REPO_TOKEN=${{ secrets.COVERALLS_TOKEN }} + python -m pip install --upgrade pip + pip install wheel coveralls==1.10.0 + export COVERALLS_REPO_TOKEN=${{ secrets.COVERALLS_TOKEN }} coveralls - + - name: Clean up coverage + uses: geekyeggo/delete-artifact@v1 + with: + name: coverage diff --git a/.gitignore b/.gitignore index 4a539285..74f7c0c4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ # Created by https://www.gitignore.io/api/linux,python,pycharm,visualstudiocode # Edit at https://www.gitignore.io/?templates=linux,python,pycharm,visualstudiocode -###testfile +###testfile test.py ### Linux ### @@ -224,4 +224,3 @@ dmypy.json ### venv venv/ # End of https://www.gitignore.io/api/linux,python,pycharm,visualstudiocode - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..1654d18a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: +- repo: https://github.com/pre-commit/mirrors-yapf + rev: 'v0.30.0' + hooks: + - id: yapf +- repo: https://github.com/pycqa/isort + rev: '5.7.0' + hooks: + - id: isort +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: 'v3.4.0' + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace diff --git a/README.md b/README.md index 5be604c6..ad8584ce 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Defining the system requirements with exact versions typically is difficult. But * Python 3.6.9 * pip 18.1 * requests 2.22.0 -* websocket_client 0.56.0 +* websocket_client 0.58.0 * zeroconf 0.24.4 Other versions and even other operating systems might work. Feel free to tell us about your experience. If you want to run our unit tests, you also need: @@ -50,10 +50,11 @@ cd devolo_home_control_api python setup.py install ``` -If you want to run out tests, change to the tests directory and start pytest via setup.py. +If you want to run out tests, install the extra requirements and start pytest. ```bash -python setup.py test +pip install -e .[test] +pytest ``` ## Quick start diff --git a/devolo_home_control_api/__init__.py b/devolo_home_control_api/__init__.py index 5a313cc7..9bb4050e 100644 --- a/devolo_home_control_api/__init__.py +++ b/devolo_home_control_api/__init__.py @@ -1 +1,10 @@ -__version__ = "0.16.0" +try: + from importlib.metadata import PackageNotFoundError, version +except ImportError: + from importlib_metadata import PackageNotFoundError, version # type: ignore[no-redef] + +try: + __version__ = version("package-name") +except PackageNotFoundError: + # package is not installed - e.g. pulled and run locally + __version__ = "0.0.0" diff --git a/devolo_home_control_api/backend/__init__.py b/devolo_home_control_api/backend/__init__.py index e69de29b..762b613a 100644 --- a/devolo_home_control_api/backend/__init__.py +++ b/devolo_home_control_api/backend/__init__.py @@ -0,0 +1,36 @@ +MESSAGE_TYPES = { + "devolo.BinarySensor": "_binary_sensor", + "devolo.BinarySwitch": "_binary_switch", + "devolo.Blinds": "_multi_level_switch", + "devolo.DevicesPage": "_inspect_devices", + "devolo.DewpointSensor": "_multi_level_sensor", + "devolo.Dimmer": "_multi_level_switch", + "devolo.Grouping": "_grouping", + "devolo.HumidityBarValue": "_humidity_bar", + "devolo.HumidityBarZone": "_humidity_bar", + "devolo.mprm.gw.GatewayAccessibilityFI": "_gateway_accessible", + "devolo.Meter": "_meter", + "devolo.MildewSensor": "_binary_sensor", + "devolo.MultiLevelSensor": "_multi_level_sensor", + "devolo.MultiLevelSwitch": "_multi_level_switch", + "devolo.RemoteControl": "_remote_control", + "devolo.SirenMultiLevelSwitch": "_multi_level_switch", + "devolo.ShutterMovementFI": "_binary_sensor", + "devolo.ValveTemperatureSensor": "_multi_level_sensor", + "devolo.VoltageMultiLevelSensor": "_multi_level_sensor", + "devolo.WarningBinaryFI": "_binary_sensor", + "hdm": "_device_state", + "acs.hdm": "_automatic_calibration", + "bas.hdm": "_binary_async", + "bss.hdm": "_binary_sync", + "cps.hdm": "_parameter", + "gds.hdm": "_general_device", + "lis.hdm": "_led", + "mas.hdm": "_multilevel_async", + "mss.hdm": "_multilevel_sync", + "ps.hdm": "_protection", + "stmss.hdm": "_multilevel_sync", + "sts.hdm": "_switch_type", + "trs.hdm": "_temperature_report", + "vfs.hdm": "_led" +} diff --git a/devolo_home_control_api/backend/mprm.py b/devolo_home_control_api/backend/mprm.py index a9b1c2f6..64888e0d 100644 --- a/devolo_home_control_api/backend/mprm.py +++ b/devolo_home_control_api/backend/mprm.py @@ -1,6 +1,7 @@ import socket import sys import time +from abc import ABC from json import JSONDecodeError from threading import Thread from typing import Optional @@ -10,25 +11,22 @@ from zeroconf import ServiceBrowser, ServiceStateChange, Zeroconf from ..exceptions.gateway import GatewayOfflineError -from ..mydevolo import Mydevolo from .mprm_websocket import MprmWebsocket -class Mprm(MprmWebsocket): +class Mprm(MprmWebsocket, ABC): """ The abstract Mprm object handles the connection to the devolo Cloud (remote) or the gateway in your LAN (local). Either way is chosen, depending on detecting the gateway via mDNS. - - :param mydevolo_instance: Mydevolo instance for talking to the devolo Cloud - :param zeroconf_instance: Zeroconf instance to be potentially reused """ - def __init__(self, mydevolo_instance: Mydevolo, zeroconf_instance: Optional[Zeroconf]): - super().__init__(mydevolo_instance) + def __init__(self): + self._zeroconf: Optional[Zeroconf] - self.detect_gateway_in_lan(zeroconf_instance) - self.create_connection() + super().__init__() + self.detect_gateway_in_lan() + self.create_connection() def create_connection(self): """ @@ -44,69 +42,72 @@ def create_connection(self): self._logger.error("Cannot connect to gateway. No gateway found in LAN and external access is not possible.") raise ConnectionError("Cannot connect to gateway.") - def detect_gateway_in_lan(self, zeroconf_instance: Optional[Zeroconf]): + def detect_gateway_in_lan(self) -> str: """ Detect a gateway in local network via mDNS and check if it is the desired one. Unfortunately, the only way to tell is to try a connection with the known credentials. If the gateway is not found within 3 seconds, it is assumed that a remote connection is needed. - :param zeroconf_instance: Zeroconf instance to be potentially reused + :return: Local IP of the gateway, if found """ - zeroconf = zeroconf_instance or Zeroconf() + zeroconf = self._zeroconf or Zeroconf() browser = ServiceBrowser(zeroconf, "_http._tcp.local.", handlers=[self._on_service_state_change]) self._logger.info("Searching for gateway in LAN.") start_time = time.time() while not time.time() > start_time + 3 and self._local_ip == "": time.sleep(0.05) - Thread(target=browser.cancel, name=f"{__class__.__name__}.browser_cancel").start() - if not zeroconf_instance: - Thread(target=zeroconf.close, name=f"{__class__.__name__}.zeroconf_close").start() + Thread(target=browser.cancel, name=f"{self.__class__.__name__}.browser_cancel").start() + if not self._zeroconf: + Thread(target=zeroconf.close, name=f"{self.__class__.__name__}.zeroconf_close").start() return self._local_ip - def get_local_session(self): + def get_local_session(self) -> bool: """ Connect to the gateway locally. Calling a special portal URL on the gateway returns a second URL with a token. Calling that URL establishes the connection. """ self._logger.info("Connecting to gateway locally.") - self._session.url = "http://" + self._local_ip - self._logger.debug(f"Session URL set to '{self._session.url}'") + self._url = "http://" + self._local_ip + self._logger.debug("Session URL set to '%s'", self._url) try: - token_url = self._session.get(self._session.url + "/dhlp/portal/full", - auth=(self.gateway.local_user, self.gateway.local_passkey), timeout=5).json() - self._logger.debug(f"Got a token URL: {token_url}") + token_url = self._session.get(self._url + "/dhlp/portal/full", + auth=(self.gateway.local_user, + self.gateway.local_passkey), + timeout=5).json() + self._logger.debug("Got a token URL: %s", token_url) except JSONDecodeError: self._logger.error("Could not connect to the gateway locally.") self._logger.debug(sys.exc_info()) raise GatewayOfflineError("Gateway is offline.") from None - except requests.exceptions.ConnectTimeout: + except (requests.exceptions.ConnectTimeout, requests.exceptions.ConnectionError): self._logger.error("Timeout during connecting to the gateway.") self._logger.debug(sys.exc_info()) raise self._session.get(token_url['link']) + return True - def get_remote_session(self): + def get_remote_session(self) -> bool: """ Connect to the gateway remotely. Calling the known portal URL is enough in this case. """ self._logger.info("Connecting to gateway via cloud.") try: url = urlsplit(self._session.get(self.gateway.full_url, timeout=15).url) - self._session.url = f"{url.scheme}://{url.netloc}" - self._logger.debug(f"Session URL set to '{self._session.url}'") + self._url = f"{url.scheme}://{url.netloc}" + self._logger.debug("Session URL set to '%s'", self._url) except JSONDecodeError: self._logger.error("Could not connect to the gateway remotely.") self._logger.debug(sys.exc_info()) raise GatewayOfflineError("Gateway is offline.") from None - + return True def _on_service_state_change(self, zeroconf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange): """ Service handler for Zeroconf state changes. """ if state_change is ServiceStateChange.Added: service_info = zeroconf.get_service_info(service_type, name) - if service_info.server.startswith("devolo-homecontrol"): + if service_info and service_info.server.startswith("devolo-homecontrol"): self._try_local_connection(service_info.addresses) def _try_local_connection(self, addresses: list): @@ -114,7 +115,8 @@ def _try_local_connection(self, addresses: list): for address in addresses: ip = socket.inet_ntoa(address) if requests.get("http://" + ip + "/dhlp/port/full", - auth=(self.gateway.local_user, self.gateway.local_passkey), - timeout=0.5).status_code == requests.codes.ok: - self._logger.debug(f"Got successful answer from ip {ip}. Setting this as local gateway") + auth=(self.gateway.local_user, + self.gateway.local_passkey), + timeout=0.5).status_code == requests.codes.ok: # pylint: disable=no-member + self._logger.debug("Got successful answer from ip %s. Setting this as local gateway", ip) self._local_ip = ip diff --git a/devolo_home_control_api/backend/mprm_rest.py b/devolo_home_control_api/backend/mprm_rest.py index e032dae9..b28a501b 100644 --- a/devolo_home_control_api/backend/mprm_rest.py +++ b/devolo_home_control_api/backend/mprm_rest.py @@ -1,27 +1,32 @@ import json import logging import sys +from abc import ABC +from requests import Session from requests.exceptions import ReadTimeout +from ..devices.gateway import Gateway from ..exceptions.gateway import GatewayOfflineError from ..mydevolo import Mydevolo -class MprmRest: +class MprmRest(ABC): """ The abstract MprmRest object handles calls to the so called mPRM. It does not cover all API calls, just those requested up to now. All calls are done in a gateway context, so you have to create a derived class, that provides a Gateway object and a Session object. - - :param mydevolo_instance: Mydevolo instance for talking to the devolo Cloud """ - def __init__(self, mydevolo_instance: Mydevolo): + def __init__(self): self._logger = logging.getLogger(self.__class__.__name__) - self._mydevolo = mydevolo_instance self._data_id = 0 self._local_ip = "" + self._url = "" + + self._mydevolo: Mydevolo + self._session: Session + self.gateway: Gateway def get_all_devices(self) -> list: """ @@ -30,11 +35,14 @@ def get_all_devices(self) -> list: :return: All devices and their properties. """ self._logger.info("Inspecting devices") - data = {"method": "FIM/getFunctionalItems", - "params": [['devolo.DevicesPage'], 0]} - response = self.post(data) - self._logger.debug(f"Response of 'get_all_devices':\n{response}") - return response["result"]["items"][0]["properties"]["deviceUIDs"] + data = { + "method": "FIM/getFunctionalItems", + "params": [['devolo.DevicesPage'], + 0] + } + response = self._post(data) + self._logger.debug("Response of 'get_all_devices':\n%s", response) + return response['result']['items'][0]['properties']['deviceUIDs'] def get_all_zones(self) -> dict: """ @@ -43,11 +51,15 @@ def get_all_zones(self) -> dict: :return: All zone IDs and their name. """ self._logger.debug("Inspecting zones") - data = {"method": "FIM/getFunctionalItems", - "params": [["devolo.Grouping"], 0]} - response = self.post(data)['result']['items'][0]['properties']['zones'] - self._logger.debug(f"Response of 'get_all_zones':\n{response}") - return dict(zip([key["id"] for key in response], [key["name"] for key in response])) + data = { + "method": "FIM/getFunctionalItems", + "params": [["devolo.Grouping"], + 0] + } + response = self._post(data)['result']['items'][0]['properties']['zones'] + self._logger.debug("Response of 'get_all_zones':\n%s", response) + return {key['id']: key['name'] + for key in response} def get_data_from_uid_list(self, uids: list) -> list: """ @@ -57,11 +69,14 @@ def get_data_from_uid_list(self, uids: list) -> list: devolo.MultiLevelSensor:hdm:ZWave:CBC56091/24#1] :return: Data connected to the element UIDs, payload so to say """ - data = {"method": "FIM/getFunctionalItems", - "params": [uids, 0]} - response = self.post(data) - self._logger.debug(f"Response of 'get_data_from_uid_list':\n{response}") - return response["result"]["items"] + data = { + "method": "FIM/getFunctionalItems", + "params": [uids, + 0] + } + response = self._post(data) + self._logger.debug("Response of 'get_data_from_uid_list':\n%s", response) + return response['result']['items'] def get_name_and_element_uids(self, uid: str): """ @@ -69,13 +84,108 @@ def get_name_and_element_uids(self, uid: str): :param uid: Element UID, something like devolo.MultiLevelSensor:hdm:ZWave:CBC56091/24#2 """ - data = {"method": "FIM/getFunctionalItems", - "params": [[uid], 0]} - response = self.post(data) - self._logger.debug(f"Response of 'get_name_and_element_uids':\n{response}") - return response["result"]["items"][0]["properties"] + data = { + "method": "FIM/getFunctionalItems", + "params": [[uid], + 0] + } + response = self._post(data) + self._logger.debug("Response of 'get_name_and_element_uids':\n%s", response) + return response['result']['items'][0]['properties'] - def post(self, data: dict) -> dict: + def refresh_session(self): + """ + Refresh currently running session. Without this call from time to time especially websockets will terminate. + """ + self._logger.debug("Refreshing session.") + data = { + "method": "FIM/invokeOperation", + "params": [f"devolo.UserPrefs.{self._mydevolo.uuid()}", + "resetSessionTimeout", + []] + } + self._post(data) + + def set_binary_switch(self, uid: str, state: bool) -> bool: + """ + Set a binary switch state of a device. + + :param uid: Element UID, something like devolo.BinarySwitch:hdm:ZWave:CBC56091/24 + :param state: True if switching on, False if switching off + :return: True if successfully switched, false otherwise + """ + data = { + "method": "FIM/invokeOperation", + "params": [uid, + "turnOn" if state else "turnOff", + []] + } + response = self._post(data) + return self._evaluate_response(uid=uid, value=state, response=response) + + def set_multi_level_switch(self, uid: str, value: float) -> bool: + """ + Set a multi level switch value of a device. + + :param uid: Element UID, something like devolo.Dimmer:hdm:ZWave:CBC56091/24 + :param value: Value the multi level switch shall have + :return: True if successfully switched, false otherwise + """ + data = { + "method": "FIM/invokeOperation", + "params": [uid, + "sendValue", + [value]] + } + response = self._post(data) + return self._evaluate_response(uid=uid, value=value, response=response) + + def set_remote_control(self, uid: str, key_pressed: int) -> bool: + """ + Press the button of a remote control virtually. + + :param uid: Element UID, something like devolo.RemoteControl:hdm:ZWave:CBC56091/24 + :param key_pressed: Number of the button pressed + :return: True if successfully switched, false otherwise + """ + data = { + "method": "FIM/invokeOperation", + "params": [uid, + "pressKey", + [key_pressed]] + } + response = self._post(data) + return self._evaluate_response(uid=uid, value=key_pressed, response=response) + + def set_setting(self, uid: str, setting: list) -> bool: + """ + Set a setting of a device. + + :param uid: Element UID, something like acs.hdm:ZWave:CBC56091/24 + :param setting: Settings to set + :return: True if successfully switched, false otherwise + """ + data = { + "method": "FIM/invokeOperation", + "params": [uid, + "save", + setting] + } + response = self._post(data) + return self._evaluate_response(uid=uid, value=setting, response=response) + + def _evaluate_response(self, uid, value, response): + """ Evaluate the response of setting a device to a value. """ + if response['result'].get("status") == 1: + return True + if response['result'].get("status") == 2: + self._logger.debug("Value of %s is already %s.", uid, value) + else: + self._logger.error("Something went wrong setting %s.", uid) + self._logger.debug("Response to set command:\n%s", response) + return False + + def _post(self, data: dict) -> dict: """ Communicate with the RPC interface. If the call times out, it is assumed that the gateway is offline and the state is changed accordingly. @@ -87,9 +197,11 @@ def post(self, data: dict) -> dict: data['jsonrpc'] = "2.0" data['id'] = self._data_id try: - response = self._session.post(self._session.url + "/remote/json-rpc", + response = self._session.post(self._url + "/remote/json-rpc", data=json.dumps(data), - headers={"content-type": "application/json"}, + headers={ + "content-type": "application/json" + }, timeout=30).json() except ReadTimeout: self._logger.error("Gateway is offline.") @@ -98,15 +210,6 @@ def post(self, data: dict) -> dict: raise GatewayOfflineError("Gateway is offline.") from None if response['id'] != data['id']: self._logger.error("Got an unexpected response after posting data.") - self._logger.debug(f"Message had ID {data['id']}, response had ID {response['id']}.") + self._logger.debug("Message had ID %s, response had ID %s.", data['id'], response['id']) raise ValueError("Got an unexpected response after posting data.") return response - - def refresh_session(self): - """ - Refresh currently running session. Without this call from time to time especially websockets will terminate. - """ - self._logger.debug("Refreshing session.") - data = {"method": "FIM/invokeOperation", - "params": [f"devolo.UserPrefs.{self._mydevolo.uuid()}", "resetSessionTimeout", []]} - self.post(data) diff --git a/devolo_home_control_api/backend/mprm_websocket.py b/devolo_home_control_api/backend/mprm_websocket.py index ade7f572..b658de4c 100644 --- a/devolo_home_control_api/backend/mprm_websocket.py +++ b/devolo_home_control_api/backend/mprm_websocket.py @@ -1,17 +1,17 @@ import json import threading import time +from abc import ABC, abstractmethod +import requests import websocket -from requests import ConnectionError from urllib3.connection import ConnectTimeoutError from ..exceptions.gateway import GatewayOfflineError -from ..mydevolo import Mydevolo from .mprm_rest import MprmRest -class MprmWebsocket(MprmRest): +class MprmWebsocket(MprmRest, ABC): """ The abstract MprmWebsocket object handles calls to the mPRM via websockets. It does not cover all API calls, just those requested up to now. All calls are done in a gateway context, so you have to create a derived class, that provides a @@ -20,15 +20,13 @@ class MprmWebsocket(MprmRest): The websocket connection itself runs in a thread, that might not terminate as expected. Using a with-statement is recommended. - - :param mydevolo_instance: Mydevolo instance for talking to the devolo Cloud """ - def __init__(self, mydevolo_instance: Mydevolo): - super().__init__(mydevolo_instance) + def __init__(self): + super().__init__() self._ws: websocket.WebSocketApp = None - self._connected = False # This attribute saves, if the websocket is fully established - self._reachable = True # This attribute saves, if the a new session can be established + self._connected = False # This attribute saves, if the websocket is fully established + self._reachable = True # This attribute saves, if the a new session can be established self._event_sequence = 0 def __enter__(self): @@ -37,15 +35,21 @@ def __enter__(self): def __exit__(self, exception_type, exception_value, traceback): self.websocket_disconnect() + @abstractmethod + def detect_gateway_in_lan(self): + pass + @abstractmethod def get_local_session(self): - raise NotImplementedError(f"{self.__class__.__name__} needs a method to connect locally to a gateway.") + pass + @abstractmethod def get_remote_session(self): - raise NotImplementedError(f"{self.__class__.__name__} needs a method to connect remotely to a gateway.") + pass + @abstractmethod def on_update(self, message): - raise NotImplementedError(f"{self.__class__.__name__} needs a method to process messages from the websocket.") + pass def wait_for_websocket_establishment(self): """ @@ -65,20 +69,20 @@ def websocket_connect(self): used or not. After establishing the websocket, a ping is sent every 30 seconds to keep the connection alive. If there is no response within 5 seconds, the connection is terminated with error state. """ - ws_url = self._session.url.replace("https://", "wss://").replace("http://", "ws://") - cookie = "; ".join([str(name) + "=" + str(value) for name, value in self._session.cookies.items()]) + ws_url = self._url.replace("https://", "wss://").replace("http://", "ws://") + cookie = "; ".join(str(name) + "=" + str(value) for name, value in self._session.cookies.items()) + ws_url = f"{ws_url}/remote/events/?topics=com/prosyst/mbs/services/fim/FunctionalItemEvent/PROPERTY_CHANGED," \ f"com/prosyst/mbs/services/fim/FunctionalItemEvent/UNREGISTERED" \ f"&filter=(|(GW_ID={self.gateway.id})(!(GW_ID=*)))" - self._logger.debug(f"Connecting to {ws_url}") + self._logger.debug("Connecting to %s", ws_url) self._ws = websocket.WebSocketApp(ws_url, cookie=cookie, on_open=self._on_open, on_message=self._on_message, on_error=self._on_error, on_close=self._on_close, - on_pong=self._on_pong, - header={"Connection": "Upgrade"}) + on_pong=self._on_pong) self._ws.run_forever(ping_interval=30, ping_timeout=5) def websocket_disconnect(self, event: str = ""): @@ -87,20 +91,19 @@ def websocket_disconnect(self, event: str = ""): """ self._logger.info("Closing web socket connection.") if event: - self._logger.info(f"Reason: {event}") + self._logger.info("Reason: %s", event) self._ws.close() - - def _on_close(self): + def _on_close(self, ws: websocket.WebSocketApp): """ Callback method to react on closing the websocket. """ self._logger.info("Closed web socket connection.") - def _on_error(self, error: Exception): + def _on_error(self, ws: websocket.WebSocketApp, error: Exception): """ Callback method to react on errors. We will try reconnecting with prolonging intervals. """ self._logger.exception(error) self._connected = False self._reachable = False - self._ws.close() + ws.close() self._event_sequence = 0 sleep_interval = 16 @@ -110,31 +113,36 @@ def _on_error(self, error: Exception): self.websocket_connect() - def _on_message(self, message: str): + def _on_message(self, ws: websocket.WebSocketApp, message: str): """ Callback method to react on a message. """ msg = json.loads(message) - self._logger.debug(f"Got message from websocket:\n{msg}") + self._logger.debug("Got message from websocket:\n%s", msg) event_sequence = msg["properties"]["com.prosyst.mbs.services.remote.event.sequence.number"] if event_sequence == self._event_sequence: self._event_sequence += 1 else: - self._logger.warning(f"We missed a websocket message. Internal event_sequence is at {self._event_sequence}. " - f"Event sequence by websocket is at {event_sequence}") + self._logger.warning( + "We missed a websocket message. Internal event_sequence is at %s. " + "Event sequence by websocket is at %s", + self._event_sequence, + event_sequence) self._event_sequence = event_sequence + 1 - self._logger.debug(f"self._event_sequence is set to {self._event_sequence}") + self._logger.debug("self._event_sequence is set to %s", self._event_sequence) self.on_update(msg) - def _on_open(self): + def _on_open(self, ws: websocket.WebSocketApp): """ Callback method to keep the websocket open. """ + def run(): self._logger.info("Starting web socket connection.") - while self._ws.sock is not None and self._ws.sock.connected: + while ws.sock is not None and ws.sock.connected: time.sleep(1) - threading.Thread(target=run, name=f"{__class__.__name__}.websocket_run").start() + + threading.Thread(target=run, name=f"{self.__class__.__name__}.websocket_run").start() self._connected = True - def _on_pong(self, *args): + def _on_pong(self, ws: websocket.WebSocketApp, *args): # pylint: disable=unused-argument """ Callback method to keep the session valid. """ self.refresh_session() @@ -142,9 +150,11 @@ def _try_reconnect(self, sleep_interval: int): """ Try to reconnect to the websocket. """ try: self._logger.info("Trying to reconnect to the websocket.") - # TODO: Check if local_ip is still correct after lost connection - self.get_local_session() if self._local_ip else self.get_remote_session() - self._reachable = True - except (json.JSONDecodeError, ConnectTimeoutError, ConnectionError, GatewayOfflineError): - self._logger.info(f"Sleeping for {sleep_interval} seconds.") + self._reachable = self.get_local_session() if self._local_ip else self.get_remote_session() + except (ConnectTimeoutError, GatewayOfflineError): + self._logger.info("Sleeping for %s seconds.", sleep_interval) time.sleep(sleep_interval) + except (requests.exceptions.ConnectTimeout, requests.exceptions.ConnectionError): + self._logger.info("Sleeping for %s seconds.", sleep_interval) + time.sleep(sleep_interval - 3) # mDNS browsing will take up tp 3 seconds by itself + self.detect_gateway_in_lan() diff --git a/devolo_home_control_api/devices/gateway.py b/devolo_home_control_api/devices/gateway.py index 76a569a2..bc14d95c 100644 --- a/devolo_home_control_api/devices/gateway.py +++ b/devolo_home_control_api/devices/gateway.py @@ -38,8 +38,7 @@ def __init__(self, gateway_id: str, mydevolo_instance: Mydevolo): self.zones: Dict = {} self.home_id = "" - self._update_state(status=details.get("status"), state=details.get("state")) - + self._update_state(status=details.get("status", ""), state=details.get("state", "")) def update_state(self, online: bool = None): """ @@ -49,12 +48,11 @@ def update_state(self, online: bool = None): """ if online is None: details = self._mydevolo.get_gateway(self.id) - self._update_state(status=details.get("status"), state=details.get("state")) + self._update_state(status=details.get("status", ""), state=details.get("state", "")) else: self.online = online self.sync = online - def _update_state(self, status: str, state: str): """ Helper to update the state. """ self.online = status == "devolo.hc_gateway.status.online" diff --git a/devolo_home_control_api/devices/zwave.py b/devolo_home_control_api/devices/zwave.py index dd85e6cb..f96b6735 100644 --- a/devolo_home_control_api/devices/zwave.py +++ b/devolo_home_control_api/devices/zwave.py @@ -12,23 +12,63 @@ class Zwave: reading them. Nevertheless, a few unwanted attributes are filtered. :param mydevolo_instance: Mydevolo instance for talking to the devolo Cloud + :key batteryLevel: Battery Level of the device in percent, -1 if mains powered + :type batteryLevel: int + :key elementUIDs: All element UIDs the device has + :type elementUIDs: list + :key manID: Manufacturer ID as registered at the Z-Wave alliance + :type manID: str + :key prodID: Product ID as registered at the Z-Wave alliance + :type prodID: str + :key prodTypeID: Product type ID as registered at the Z-Wave alliance + :type prodTypeID: str + :key status: Online status of the device + :type status: int """ def __init__(self, mydevolo_instance: Mydevolo, **kwargs): self._logger = logging.getLogger(self.__class__.__name__) self._mydevolo = mydevolo_instance - unwanted_value = ["icon", "itemName", "operationStatus", "zone", "zoneId"] + # Get important values + self.battery_level = kwargs.pop("batteryLevel", -1) + self.element_uids = kwargs.pop("elementUIDs") + self.man_id = kwargs.pop("manID") + self.prod_id = kwargs.pop("prodID") + self.prod_type_id = kwargs.pop("prodTypeID") + self.status = kwargs.pop("status", 1) + + # Remove unwanted values + unwanted_value = [ + "icon", + "itemName", + "operationStatus", + "zone", + "zoneId", + ] for value in unwanted_value: del kwargs[value] + # Add all other values for key, value in kwargs.items(): setattr(self, camel_case_to_snake_case(key), value) self.uid = get_device_uid_from_element_uid(self.element_uids[0]) # Initialize additional Z-Wave information. Will be filled by Zwave.get_zwave_info, if available. - z_wave_info_list = ["href", "manufacturer_id", "product_type_id", "product_id", "name", "brand", "identifier", - "is_zwave_plus", "device_type", "zwave_version", "specific_device_class", "generic_device_class"] + z_wave_info_list = [ + "href", + "manufacturer_id", + "product_type_id", + "product_id", + "name", + "brand", + "identifier", + "is_zwave_plus", + "device_type", + "zwave_version", + "specific_device_class", + "generic_device_class", + ] for key in z_wave_info_list: setattr(self, key, None) @@ -37,7 +77,6 @@ def __init__(self, mydevolo_instance: Mydevolo, **kwargs): delattr(self, "battery_level") delattr(self, "battery_low") - def get_property(self, name: str) -> list: """ Get element UIDs to a specified property. @@ -53,7 +92,7 @@ def get_zwave_info(self): Get publicly available information like manufacturer or model from my devolo. For a complete list, please look at Zwave.__init__. """ - self._logger.debug(f"Getting Z-Wave information for {self.uid}") + self._logger.debug("Getting Z-Wave information for %s", self.uid) zwave_product = self._mydevolo.get_zwave_products(manufacturer=self.man_id, product_type=self.prod_type_id, product=self.prod_id) @@ -61,7 +100,13 @@ def get_zwave_info(self): setattr(self, camel_case_to_snake_case(key), value) # Clean up attributes which are now unwanted. - clean_up_list = ["man_id", "prod_id", "prod_type_id", "statistics_uid", "wrong_device_paired"] + clean_up_list = [ + "man_id", + "prod_id", + "prod_type_id", + "statistics_uid", + "wrong_device_paired", + ] for attribute in clean_up_list: if hasattr(self, attribute): delattr(self, attribute) diff --git a/devolo_home_control_api/helper/uid.py b/devolo_home_control_api/helper/uid.py index 1729bcef..e4bc441f 100644 --- a/devolo_home_control_api/helper/uid.py +++ b/devolo_home_control_api/helper/uid.py @@ -15,7 +15,10 @@ def get_device_uid_from_element_uid(element_uid: str) -> str: :param element_uid: Element UID, something like devolo.MultiLevelSensor:hdm:ZWave:CBC56091/24#2 :return: Device UID, something like hdm:ZWave:CBC56091/24 """ - return re.search(r".*?:(.*/\d{1,3})", element_uid).group(1) + parts = re.search(r".*?:(.*/\d{1,3})", element_uid) + if parts: + return parts.group(1) + raise ValueError("Element UID has a wrong format.") def get_device_uid_from_setting_uid(setting_uid: str) -> str: @@ -25,7 +28,10 @@ def get_device_uid_from_setting_uid(setting_uid: str) -> str: :param setting_uid: Setting UID, something like lis.hdm:ZWave:EB5A9F6C/2 :return: Device UID, something like hdm:ZWave:EB5A9F6C/2 """ - return re.search(r".*\.(.*/\d{1,3})", setting_uid).group(1) + parts = re.search(r".*\.(.*/\d{1,3})", setting_uid) + if parts: + return parts.group(1) + raise ValueError("Settings UID has a wrong format.") def get_sub_device_uid_from_element_uid(element_uid: str) -> Optional[int]: @@ -54,4 +60,7 @@ def get_home_id_from_device_uid(device_uid: str) -> str: :param device_uid: Device UID, something like hdm:ZWave:EB5A9F6C/4 :return: Home ID, something like EB5A9F6C """ - return re.search(r":.*?(:)(.*)(/)", device_uid).group(2) + parts = re.search(r":.*?(:)(.*)(/)", device_uid) + if parts: + return parts.group(2) + raise ValueError("Device UID has a wrong format.") diff --git a/devolo_home_control_api/homecontrol.py b/devolo_home_control_api/homecontrol.py index a5a43f69..6967ea9e 100644 --- a/devolo_home_control_api/homecontrol.py +++ b/devolo_home_control_api/homecontrol.py @@ -5,6 +5,7 @@ from zeroconf import Zeroconf from . import __version__ +from .backend import MESSAGE_TYPES from .backend.mprm import Mprm from .devices.gateway import Gateway from .devices.zwave import Zwave @@ -34,36 +35,36 @@ class HomeControl(Mprm): :param gateway_id: Gateway ID (aka serial number), typically found on the label of the device :param mydevolo_instance: Mydevolo instance for talking to the devolo Cloud :param zeroconf_instance: Zeroconf instance to be potentially reused - :param url: URL of the mPRM (typically leave it at default) """ def __init__(self, gateway_id: str, mydevolo_instance: Mydevolo, zeroconf_instance: Optional[Zeroconf] = None): self._mydevolo = mydevolo_instance self._session = requests.Session() self._session.headers.update({"User-Agent": f"devolo_home_control_api/{__version__}"}) - + self._zeroconf = zeroconf_instance self.gateway = Gateway(gateway_id, mydevolo_instance) - super().__init__(mydevolo_instance, zeroconf_instance) - self.gateway.zones = self.get_all_zones() + super().__init__() + self._grouping() # Create the initial device dict self.devices: Dict = {} self._inspect_devices(self.get_all_devices()) - self.device_names = dict(zip([(self.devices[device].settings_property['general_device_settings'].name + "/" - + self.devices[device].settings_property['general_device_settings'].zone) - for device in self.devices], - [self.devices[device].uid for device in self.devices])) + self.device_names = { + f"{self.devices[device].settings_property['general_device_settings'].name}/\ + {self.devices[device].settings_property['general_device_settings'].zone}": self.devices[device].uid + for device in self.devices + } self.gateway.home_id = get_home_id_from_device_uid(next(iter(self.device_names.values()))) - self.publisher = Publisher([device for device in self.devices]) + self.publisher = Publisher(self.devices.keys()) self.updater = Updater(devices=self.devices, gateway=self.gateway, publisher=self.publisher) self.updater.on_device_change = self.device_change - threading.Thread(target=self.websocket_connect, name=f"{__class__.__name__}.websocket_connect").start() + threading.Thread(target=self.websocket_connect, name=f"{self.__class__.__name__}.websocket_connect").start() self.wait_for_websocket_establishment() @property @@ -81,9 +82,11 @@ def blinds_devices(self) -> list: """ Get all blinds devices. """ blinds_devices = [] for device in self.multi_level_switch_devices: - blinds_devices.extend([self.devices[device.uid] for multi_level_switch_property - in device.multi_level_switch_property - if multi_level_switch_property.startswith("devolo.Blinds")]) + blinds_devices.extend([ + self.devices[device.uid] + for multi_level_switch_property in device.multi_level_switch_property + if multi_level_switch_property.startswith("devolo.Blinds") + ]) return blinds_devices @property @@ -113,12 +116,12 @@ def device_change(self, device_uids: list): devices = [device for device in device_uids if device not in self.devices] mode = "add" self._inspect_devices([devices[0]]) - self._logger.debug(f"Device {devices[0]} added.") + self._logger.debug("Device %s added.", devices[0]) else: devices = [device for device in self.devices if device not in device_uids] mode = "del" self.devices.pop(devices[0]) - self._logger.debug(f"Device {devices[0]} removed.") + self._logger.debug("Device %s removed.", devices[0]) self.updater.devices = self.devices return (devices[0], mode) @@ -135,44 +138,41 @@ def _binary_sensor(self, uid_info: dict): device_uid = get_device_uid_from_element_uid(uid_info['UID']) if not hasattr(self.devices[device_uid], "binary_sensor_property"): self.devices[device_uid].binary_sensor_property = {} - self._logger.debug(f"Adding binary sensor property to {device_uid}.") - self.devices[device_uid].binary_sensor_property[uid_info['UID']] = \ - BinarySensorProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - state=bool(uid_info['properties']['state']), - sensor_type=uid_info['properties']['sensorType'], - sub_type=uid_info['properties']['subType']) + self._logger.debug("Adding binary sensor property to %s.", device_uid) + self.devices[device_uid].binary_sensor_property[uid_info['UID']] = BinarySensorProperty( + element_uid=uid_info['UID'], + state=bool(uid_info['properties']['state']), + sensor_type=uid_info['properties']['sensorType'], + sub_type=uid_info['properties']['subType']) def _binary_switch(self, uid_info: dict): """ Process BinarySwitch properties. """ device_uid = get_device_uid_from_element_uid(uid_info['UID']) if not hasattr(self.devices[device_uid], "binary_switch_property"): self.devices[device_uid].binary_switch_property = {} - self._logger.debug(f"Adding binary switch property to {device_uid}.") - self.devices[device_uid].binary_switch_property[uid_info['UID']] = \ - BinarySwitchProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - state=bool(uid_info['properties']['state']), - enabled=uid_info['properties']['guiEnabled']) + self._logger.debug("Adding binary switch property to %s.", device_uid) + self.devices[device_uid].binary_switch_property[uid_info['UID']] = BinarySwitchProperty( + element_uid=uid_info['UID'], + setter=self.set_binary_switch, + state=bool(uid_info['properties']['state']), + enabled=uid_info['properties']['guiEnabled']) def _general_device(self, uid_info: dict): """ Process general device setting (gds) properties. """ device_uid = get_device_uid_from_setting_uid(uid_info['UID']) - self._logger.debug(f"Adding general device settings to {device_uid}.") - self.devices[device_uid]. \ - settings_property['general_device_settings'] = \ - SettingsProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - events_enabled=uid_info['properties']['settings']['eventsEnabled'], - name=uid_info['properties']['settings']['name'], - zone_id=uid_info['properties']['settings']['zoneID'], - icon=uid_info['properties']['settings']['icon']) + self._logger.debug("Adding general device settings to %s.", device_uid) + self.devices[device_uid].settings_property['general_device_settings'] = SettingsProperty( + element_uid=uid_info['UID'], + setter=self.set_setting, + events_enabled=uid_info['properties']['settings']['eventsEnabled'], + name=uid_info['properties']['settings']['name'], + zone_id=uid_info['properties']['settings']['zoneID'], + icon=uid_info['properties']['settings']['icon'], + zones=self.gateway.zones) + + def _grouping(self): + """ Get all zones (also called rooms). """ + self.gateway.zones = self.get_all_zones() def _humidity_bar(self, uid_info: dict): """ @@ -186,17 +186,14 @@ def _humidity_bar(self, uid_info: dict): if not hasattr(self.devices[device_uid], "humidity_bar_property"): self.devices[device_uid].humidity_bar_property = {} if self.devices[device_uid].humidity_bar_property.get(fake_element_uid) is None: - self.devices[device_uid].humidity_bar_property[fake_element_uid] = \ - HumidityBarProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=fake_element_uid, - sensorType="humidityBar") + self.devices[device_uid].humidity_bar_property[fake_element_uid] = HumidityBarProperty( + element_uid=fake_element_uid, + sensorType="humidityBar") if uid_info['properties']['sensorType'] == "humidityBarZone": - self._logger.debug(f"Adding humidity bar zone property to {device_uid}.") + self._logger.debug("Adding humidity bar zone property to %s.", device_uid) self.devices[device_uid].humidity_bar_property[fake_element_uid].zone = uid_info['properties']['value'] elif uid_info['properties']['sensorType'] == "humidityBarPos": - self._logger.debug(f"Adding humidity bar position property to {device_uid}.") + self._logger.debug("Adding humidity bar position property to %s.", device_uid) self.devices[device_uid].humidity_bar_property[fake_element_uid].value = uid_info['properties']['value'] def _inspect_devices(self, devices: list): @@ -207,44 +204,10 @@ def _inspect_devices(self, devices: list): self.devices[device_properties['UID']] = Zwave(mydevolo_instance=self._mydevolo, **properties) self.devices[device_properties['UID']].settings_property = {} threading.Thread(target=self.devices[device_properties['UID']].get_zwave_info, - name=f"{__class__.__name__}.{self.devices[device_properties['UID']].uid}").start() - - elements = {"devolo.BinarySensor": self._binary_sensor, - "devolo.BinarySwitch": self._binary_switch, - "devolo.Blinds": self._multi_level_switch, - "devolo.DewpointSensor": self._multi_level_sensor, - "devolo.Dimmer": self._multi_level_switch, - "devolo.HumidityBarValue": self._humidity_bar, - "devolo.HumidityBarZone": self._humidity_bar, - "devolo.LastActivity": self._last_activity, - "devolo.Meter": self._meter, - "devolo.MildewSensor": self._binary_sensor, - "devolo.MultiLevelSensor": self._multi_level_sensor, - "devolo.MultiLevelSwitch": self._multi_level_switch, - "devolo.RemoteControl": self._remote_control, - "devolo.SirenMultiLevelSwitch": self._multi_level_switch, - "devolo.ShutterMovementFI": self._binary_sensor, - "devolo.ValveTemperatureSensor": self._multi_level_sensor, - "devolo.VoltageMultiLevelSensor": self._multi_level_sensor, - "devolo.WarningBinaryFI": self._binary_sensor, - "acs.hdm": self._automatic_calibration, - "bas.hdm": self._binary_async, - "bss.hdm": self._binary_sync, - "lis.hdm": self._led, - "gds.hdm": self._general_device, - "cps.hdm": self._parameter, - "mas.hdm": self._multilevel_async, - "mss.hdm": self._multilevel_sync, - "ps.hdm": self._protection, - "sts.hdm": self._switch_type, - "stmss.hdm": self._multilevel_sync, - "trs.hdm": self._temperature_report, - "vfs.hdm": self._led - } + name=f"{self.__class__.__name__}.{self.devices[device_properties['UID']].uid}").start() # List comprehension gets the list of uids from every device - nested_uids_lists = [(uid['properties'].get("settingUIDs") - + uid['properties']['elementUIDs']) + nested_uids_lists = [(uid['properties'].get("settingUIDs") + uid['properties']['elementUIDs']) for uid in devices_properties] # List comprehension gets all uids into one list to make one big call against the mPRM @@ -253,7 +216,8 @@ def _inspect_devices(self, devices: list): device_properties_list = self.get_data_from_uid_list(uid_list) for uid_info in device_properties_list: - elements.get(get_device_type_from_element_uid(uid_info['UID']), self._unknown)(uid_info) + message_type = MESSAGE_TYPES.get(get_device_type_from_element_uid(uid_info['UID']), "_unknown") + getattr(self, message_type)(uid_info) try: uid = self.devices[get_device_uid_from_element_uid(uid_info['UID'])] except KeyError: @@ -261,19 +225,20 @@ def _inspect_devices(self, devices: list): uid.pending_operations = uid.pending_operations or bool(uid_info['properties'].get("pendingOperations")) # Last activity messages sometimes arrive before a device was initialized and therefore need to be handled afterwards. - [self._last_activity(uid_info) for uid_info in device_properties_list - if uid_info['UID'].startswith("devolo.LastActivity")] + [ # pylint: disable=expression-not-assigned + self._last_activity(uid_info) + for uid_info in device_properties_list + if uid_info['UID'].startswith("devolo.LastActivity") + ] def _automatic_calibration(self, uid_info: dict): """ Process automatic calibration (acs) properties. """ device_uid = get_device_uid_from_setting_uid(uid_info['UID']) - self._logger.debug(f"Adding automatic calibration setting to {device_uid}") - self.devices[device_uid].settings_property['automatic_calibration'] = \ - SettingsProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - calibration_status=bool(uid_info['properties']['calibrationStatus'])) + self._logger.debug("Adding automatic calibration setting to %s.", device_uid) + self.devices[device_uid].settings_property['automatic_calibration'] = SettingsProperty( + element_uid=uid_info['UID'], + setter=self.set_setting, + calibration_status=bool(uid_info['properties']['calibrationStatus'])) def _binary_sync(self, uid_info: dict): """ @@ -281,22 +246,18 @@ def _binary_sync(self, uid_info: dict): setting property, so it is named nicely. """ device_uid = get_device_uid_from_setting_uid(uid_info['UID']) - self._logger.debug(f"Adding binary sync setting to {device_uid}") - self.devices[device_uid].settings_property['movement_direction'] = \ - SettingsProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - inverted=uid_info['properties']['value']) + self._logger.debug("Adding binary sync setting to %s.", device_uid) + self.devices[device_uid].settings_property['movement_direction'] = SettingsProperty( + element_uid=uid_info['UID'], + setter=self.set_setting, + inverted=uid_info['properties']['value']) def _binary_async(self, uid_info: dict): """ Process binary async setting (bas) properties. """ device_uid = get_device_uid_from_setting_uid(uid_info['UID']) - self._logger.debug(f"Adding binary async settings to {device_uid}.") - settings_property = SettingsProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], + self._logger.debug("Adding binary async settings to %s.", device_uid) + settings_property = SettingsProperty(element_uid=uid_info['UID'], + setter=self.set_setting, value=uid_info['properties']['value']) # The siren needs to be handled differently, as otherwise their binary async setting will not be named nicely @@ -325,32 +286,26 @@ def _last_activity(self, uid_info: dict): def _led(self, uid_info: dict): """ Process LED information setting (lis) and visual feedback setting (vfs) properties. """ device_uid = get_device_uid_from_setting_uid(uid_info['UID']) - self._logger.debug(f"Adding led settings to {device_uid}.") + self._logger.debug("Adding led settings to %s.", device_uid) try: led_setting = uid_info['properties']['led'] except KeyError: led_setting = uid_info['properties']['feedback'] - self.devices[device_uid].settings_property['led'] = \ - SettingsProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - led_setting=led_setting) + self.devices[device_uid].settings_property['led'] = SettingsProperty(element_uid=uid_info['UID'], + setter=self.set_setting, + led_setting=led_setting) def _meter(self, uid_info: dict): """ Process meter properties. """ device_uid = get_device_uid_from_element_uid(uid_info['UID']) if not hasattr(self.devices[device_uid], "consumption_property"): self.devices[device_uid].consumption_property = {} - self._logger.debug(f"Adding consumption property to {device_uid}.") - self.devices[device_uid].consumption_property[uid_info['UID']] = \ - ConsumptionProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - current=uid_info['properties']['currentValue'], - total=uid_info['properties']['totalValue'], - total_since=uid_info['properties']['sinceTime']) + self._logger.debug("Adding consumption property to %s.", device_uid) + self.devices[device_uid].consumption_property[uid_info['UID']] = ConsumptionProperty( + element_uid=uid_info['UID'], + current=uid_info['properties']['currentValue'], + total=uid_info['properties']['totalValue'], + total_since=uid_info['properties']['sinceTime']) def _multilevel_async(self, uid_info: dict): """ Process multilevel async setting (mas) properties. """ @@ -364,13 +319,10 @@ def _multilevel_async(self, uid_info: dict): else: raise - self._logger.debug(f"Adding {name} setting to {device_uid}.") - self.devices[device_uid].settings_property[name] = \ - SettingsProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - value=uid_info['properties']['value']) + self._logger.debug("Adding %s setting to %s.", name, device_uid) + self.devices[device_uid].settings_property[name] = SettingsProperty(element_uid=uid_info['UID'], + setter=self.set_setting, + value=uid_info['properties']['value']) def _multilevel_sync(self, uid_info: dict): """ Process multilevel sync setting (mss) properties. """ @@ -378,102 +330,84 @@ def _multilevel_sync(self, uid_info: dict): # The siren needs to be handled differently, as otherwise their multilevel sync setting will not be named nicely. if self.devices[device_uid].device_model_uid == "devolo.model.Siren": - self._logger.debug(f"Adding tone settings to {device_uid}.") - self.devices[device_uid].settings_property['tone'] = \ - SettingsProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - tone=uid_info['properties']['value']) + self._logger.debug("Adding tone settings to %s.", device_uid) + self.devices[device_uid].settings_property['tone'] = SettingsProperty(element_uid=uid_info['UID'], + setter=self.set_setting, + tone=uid_info['properties']['value']) # The shutter needs to be handled differently, as otherwise their multilevel sync setting will not be named nicely. elif self.devices[device_uid].device_model_uid in ("devolo.model.OldShutter", "devolo.model.Shutter"): - self._logger.debug(f"Adding shutter duration settings to {device_uid}.") - self.devices[device_uid].settings_property['shutter_duration'] = \ - SettingsProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - shutter_duration=uid_info['properties']['value']) + self._logger.debug("Adding shutter duration settings to %s.", device_uid) + self.devices[device_uid].settings_property['shutter_duration'] = SettingsProperty( + element_uid=uid_info['UID'], + setter=self.set_setting, + shutter_duration=uid_info['properties']['value']) # Other devices are up to now always motion sensors. else: - self._logger.debug(f"Adding motion sensitivity settings to {device_uid}.") - self.devices[device_uid].settings_property['motion_sensitivity'] = \ - SettingsProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - motion_sensitivity=uid_info['properties']['value']) + self._logger.debug("Adding motion sensitivity settings to %s.", device_uid) + self.devices[device_uid].settings_property['motion_sensitivity'] = SettingsProperty( + element_uid=uid_info['UID'], + setter=self.set_setting, + motion_sensitivity=uid_info['properties']['value']) def _multi_level_sensor(self, uid_info: dict): """ Process multi level sensor properties. """ device_uid = get_device_uid_from_element_uid(uid_info['UID']) if not hasattr(self.devices[device_uid], "multi_level_sensor_property"): self.devices[device_uid].multi_level_sensor_property = {} - self._logger.debug(f"Adding multi level sensor property {uid_info.get('UID')} to {device_uid}.") - self.devices[device_uid].multi_level_sensor_property[uid_info['UID']] = \ - MultiLevelSensorProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - value=uid_info['properties']['value'], - unit=uid_info['properties']['unit'], - sensor_type=uid_info['properties']['sensorType']) + self._logger.debug("Adding multi level sensor property %s to %s.", uid_info['UID'], device_uid) + self.devices[device_uid].multi_level_sensor_property[uid_info['UID']] = MultiLevelSensorProperty( + element_uid=uid_info['UID'], + value=uid_info['properties']['value'], + unit=uid_info['properties']['unit'], + sensor_type=uid_info['properties']['sensorType']) def _multi_level_switch(self, uid_info: dict): """ Process multi level switch properties. """ device_uid = get_device_uid_from_element_uid(uid_info['UID']) if not hasattr(self.devices[device_uid], "multi_level_switch_property"): self.devices[device_uid].multi_level_switch_property = {} - self._logger.debug(f"Adding multi level switch property {uid_info.get('UID')} to {device_uid}.") - self.devices[device_uid].multi_level_switch_property[uid_info['UID']] = \ - MultiLevelSwitchProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - value=uid_info['properties']['value'], - switch_type=uid_info['properties']['switchType'], - max=uid_info['properties']['max'], - min=uid_info['properties']['min']) + self._logger.debug("Adding multi level switch property %s to %s.", uid_info['UID'], device_uid) + self.devices[device_uid].multi_level_switch_property[uid_info['UID']] = MultiLevelSwitchProperty( + element_uid=uid_info['UID'], + setter=self.set_multi_level_switch, + value=uid_info['properties']['value'], + switch_type=uid_info['properties']['switchType'], + max=uid_info['properties']['max'], + min=uid_info['properties']['min']) def _parameter(self, uid_info: dict): """ Process custom parameter setting (cps) properties.""" device_uid = get_device_uid_from_setting_uid(uid_info['UID']) - self._logger.debug(f"Adding parameter settings to {device_uid}.") - self.devices[device_uid].settings_property['param_changed'] = \ - SettingsProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - param_changed=uid_info['properties']['paramChanged']) + self._logger.debug("Adding parameter settings to %s.", device_uid) + self.devices[device_uid].settings_property['param_changed'] = SettingsProperty( + element_uid=uid_info['UID'], + setter=self.set_setting, + param_changed=uid_info['properties']['paramChanged']) def _protection(self, uid_info: dict): """ Process protection setting (ps) properties. """ device_uid = get_device_uid_from_setting_uid(uid_info['UID']) - self._logger.debug(f"Adding protection settings to {device_uid}.") - self.devices[device_uid].settings_property['protection'] = \ - SettingsProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - local_switching=uid_info['properties']['localSwitch'], - remote_switching=uid_info['properties']['remoteSwitch']) + self._logger.debug("Adding protection settings to %s.", device_uid) + self.devices[device_uid].settings_property['protection'] = SettingsProperty( + element_uid=uid_info['UID'], + setter=self.set_setting, + local_switching=uid_info['properties']['localSwitch'], + remote_switching=uid_info['properties']['remoteSwitch']) def _remote_control(self, uid_info: dict): """ Process remote control properties. """ device_uid = get_device_uid_from_element_uid(uid_info['UID']) - self._logger.debug(f"Adding remote control to {device_uid}") + self._logger.debug("Adding remote control to %s.", device_uid) if not hasattr(self.devices[device_uid], "remote_control_property"): self.devices[device_uid].remote_control_property = {} - self.devices[device_uid].remote_control_property[uid_info['UID']] = \ - RemoteControlProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - key_count=uid_info['properties']['keyCount'], - key_pressed=uid_info['properties']['keyPressed'], - type=uid_info['properties']['type']) + self.devices[device_uid].remote_control_property[uid_info['UID']] = RemoteControlProperty( + element_uid=uid_info['UID'], + setter=self.set_remote_control, + key_count=uid_info['properties']['keyCount'], + key_pressed=uid_info['properties']['keyPressed'], + type=uid_info['properties']['type']) def _switch_type(self, uid_info: dict): """ @@ -481,28 +415,24 @@ def _switch_type(self, uid_info: dict): switch with four buttons reports a switchType of 2. This confusing behavior is corrected by doubling the value. """ device_uid = get_device_uid_from_setting_uid(uid_info['UID']) - self._logger.debug(f"Adding switch type setting to {device_uid}") - self.devices[device_uid].settings_property['switch_type'] = \ - SettingsProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - value=uid_info['properties']['switchType'] * 2) + self._logger.debug("Adding switch type setting to %s.", device_uid) + self.devices[device_uid].settings_property['switch_type'] = SettingsProperty(element_uid=uid_info['UID'], + setter=self.set_setting, + value=uid_info['properties']['switchType'] + * 2) def _temperature_report(self, uid_info: dict): """ Process temperature report setting (trs) properties. """ device_uid = get_device_uid_from_setting_uid(uid_info['UID']) - self._logger.debug(f"Adding temperature report settings to {device_uid}.") - self.devices[device_uid].settings_property['temperature_report'] = \ - SettingsProperty(session=self._session, - gateway=self.gateway, - mydevolo=self._mydevolo, - element_uid=uid_info['UID'], - temp_report=uid_info['properties']['tempReport'], - target_temp_report=uid_info['properties']['targetTempReport']) + self._logger.debug("Adding temperature report settings to %s.", device_uid) + self.devices[device_uid].settings_property['temperature_report'] = SettingsProperty( + element_uid=uid_info['UID'], + setter=self.set_setting, + temp_report=uid_info['properties']['tempReport'], + target_temp_report=uid_info['properties']['targetTempReport']) def _unknown(self, uid_info: dict): """ Ignore unknown properties. """ ignore = ("devolo.SirenBinarySensor", "devolo.SirenMultiLevelSensor", "ss", "mcs") if not uid_info['UID'].startswith(ignore): - self._logger.debug(f"Found an unexpected element uid: {uid_info.get('UID')}") + self._logger.debug("Found an unexpected element uid: %s", uid_info['UID']) diff --git a/devolo_home_control_api/mydevolo.py b/devolo_home_control_api/mydevolo.py index 73ce135a..4fd2bf66 100644 --- a/devolo_home_control_api/mydevolo.py +++ b/devolo_home_control_api/mydevolo.py @@ -22,9 +22,6 @@ def __init__(self): self.url = "https://www.mydevolo.com" - self.__class__.__instance = self - - @property def user(self) -> str: """ The user (also known as my devolo ID) is used for basic authentication. """ @@ -49,7 +46,6 @@ def password(self, password: str): self._uuid = None self._gateway_ids = [] - def credentials_valid(self) -> bool: """ Check if current credentials are valid. This is done by trying to get the UUID. If that fails, credentials must be @@ -70,7 +66,7 @@ def get_gateway_ids(self) -> list: items = self._call(f"{self.url}/v1/users/{self.uuid()}/hc/gateways/status")['items'] for gateway in items: self._gateway_ids.append(gateway.get("gatewayId")) - self._logger.debug(f'Adding {gateway.get("gatewayId")} to list of gateways.') + self._logger.debug("Adding %s to list of gateways.", gateway.get("gatewayId")) if len(self._gateway_ids) == 0: self._logger.error("Could not get gateway list. No gateway attached to account?") raise IndexError("No gateways found.") @@ -83,7 +79,7 @@ def get_gateway(self, gateway_id: str) -> dict: :param gateway_id: Gateway ID :return: Gateway details """ - self._logger.debug(f"Getting details for gateway {gateway_id}") + self._logger.debug("Getting details for gateway %s", gateway_id) try: details = self._call(f"{self.url}/v1/users/{self.uuid()}/hc/gateways/{gateway_id}") except WrongUrlError: @@ -110,7 +106,7 @@ def get_zwave_products(self, manufacturer: str, product_type: str, product: str) :param product: The product ID in hex. :return: All known product information. """ - self._logger.debug(f"Getting information for {manufacturer}/{product_type}/{product}") + self._logger.debug("Getting information for %s/%s/%s", manufacturer, product_type, product) try: device_info = self._call(f"{self.url}/v1/zwave/products/{manufacturer}/{product_type}/{product}") except WrongUrlError: @@ -128,7 +124,7 @@ def get_zwave_products(self, manufacturer: str, product_type: str, product: str) "productId": product, "productTypeId": product_type, "specificDeviceClass": "Unknown", - "zwaveVersion": "Unknown" + "zwaveVersion": "Unknown", } return device_info @@ -139,9 +135,8 @@ def maintenance(self) -> bool: state = self._call(f"{self.url}/v1/hc/maintenance")['state'] if state == "on": return False - else: - self._logger.warning("devolo Home Control is in maintenance mode.") - return True + self._logger.warning("devolo Home Control is in maintenance mode.") + return True def uuid(self) -> str: """ @@ -152,22 +147,20 @@ def uuid(self) -> str: self._uuid = self._call(f"{self.url.rstrip('/')}/v1/users/uuid")['uuid'] return self._uuid - def _call(self, url: str) -> dict: """ Make a call to any entry point with the user's context. """ - headers = {"content-type": "application/json", - "User-Agent": f"devolo_home_control_api/{__version__}"} - responds = requests.get(url, - auth=(self._user, self._password), - headers=headers, - timeout=60) - - if responds.status_code == requests.codes.forbidden: + headers = { + "content-type": "application/json", + "User-Agent": f"devolo_home_control_api/{__version__}" + } + responds = requests.get(url, auth=(self._user, self._password), headers=headers, timeout=60) + + if responds.status_code == requests.codes.forbidden: # pylint: disable=no-member self._logger.error("Could not get full URL. Wrong username or password?") raise WrongCredentialsError("Wrong username or password.") - if responds.status_code == requests.codes.not_found: + if responds.status_code == requests.codes.not_found: # pylint: disable=no-member raise WrongUrlError(f"Wrong URL: {url}") - if responds.status_code == requests.codes.service_unavailable: + if responds.status_code == requests.codes.service_unavailable: # pylint: disable=no-member # mydevolo sends a 503, if the gateway is offline self._logger.error("The requested gateway seems to be offline.") raise GatewayOfflineError("Gateway offline.") diff --git a/devolo_home_control_api/properties/binary_sensor_property.py b/devolo_home_control_api/properties/binary_sensor_property.py index b716d95d..da098693 100644 --- a/devolo_home_control_api/properties/binary_sensor_property.py +++ b/devolo_home_control_api/properties/binary_sensor_property.py @@ -1,11 +1,6 @@ from datetime import datetime -from typing import Any -from requests import Session - -from ..devices.gateway import Gateway from ..exceptions.device import WrongElementError -from ..mydevolo import Mydevolo from .sensor_property import SensorProperty @@ -13,26 +8,21 @@ class BinarySensorProperty(SensorProperty): """ Object for binary sensors. It stores the binary sensor state. - :param gateway: Instance of a Gateway object - :param session: Instance of a requests.Session object - :param mydevolo: Mydevolo instance for talking to the devolo Cloud :param element_uid: Element UID, something like devolo.BinarySensor:hdm:ZWave:CBC56091/24 :key state: State of the binary sensor :type state: bool """ - def __init__(self, gateway: Gateway, session: Session, mydevolo: Mydevolo, element_uid: str, **kwargs: Any): + def __init__(self, element_uid: str, **kwargs): if not element_uid.startswith(("devolo.BinarySensor:", "devolo.MildewSensor:", "devolo.ShutterMovementFI:", - "devolo.WarningBinaryFI:",)): + "devolo.WarningBinaryFI:")): raise WrongElementError(f"{element_uid} is not a Binary Sensor.") - self._state = False - self.state = kwargs.get("state", False) - - super().__init__(gateway=gateway, session=session, mydevolo=mydevolo, element_uid=element_uid, **kwargs) + super().__init__(element_uid=element_uid, **kwargs) + self._state = kwargs.pop("state", False) @property def last_activity(self) -> datetime: @@ -44,7 +34,7 @@ def last_activity(self, timestamp: int): """ The gateway persists the last activity of some binary sensors. They can be initialized with that value. """ if timestamp != -1: self._last_activity = datetime.utcfromtimestamp(timestamp / 1000) - self._logger.debug(f"self.last_activity of element_uid {self.element_uid} set to {self._last_activity}.") + self._logger.debug("last_activity of element_uid %s set to %s.", self.element_uid, self._last_activity) @property def state(self) -> bool: @@ -56,3 +46,4 @@ def state(self, state: bool): """ Update state of the binary sensor and set point in time of the last_activity. """ self._state = state self._last_activity = datetime.now() + self._logger.debug("state of element_uid %s set to %s.", self.element_uid, state) diff --git a/devolo_home_control_api/properties/binary_switch_property.py b/devolo_home_control_api/properties/binary_switch_property.py index 7caf8c3f..f6df1f51 100644 --- a/devolo_home_control_api/properties/binary_switch_property.py +++ b/devolo_home_control_api/properties/binary_switch_property.py @@ -1,11 +1,7 @@ from datetime import datetime -from typing import Any +from typing import Callable -from requests import Session - -from ..devices.gateway import Gateway from ..exceptions.device import SwitchingProtected, WrongElementError -from ..mydevolo import Mydevolo from .property import Property @@ -13,23 +9,23 @@ class BinarySwitchProperty(Property): """ Object for binary switches. It stores the binary switch state. - :param gateway: Instance of a Gateway object - :param session: Instance of a requests.Session object - :param mydevolo: Mydevolo instance for talking to the devolo Cloud :param element_uid: Element UID, something like devolo.BinarySwitch:hdm:ZWave:CBC56091/24#2 + :param setter: Method to call on setting the state :key enabled: State of the remote protection setting + :type enabled: bool :key state: State the switch has at time of creating this instance + :type state: bool """ - def __init__(self, gateway: Gateway, session: Session, mydevolo: Mydevolo, element_uid: str, **kwargs: Any): + def __init__(self, element_uid: str, setter: Callable, **kwargs: bool): if not element_uid.startswith("devolo.BinarySwitch:"): raise WrongElementError(f"{element_uid} is not a Binary Switch.") - super().__init__(gateway=gateway, session=session, mydevolo=mydevolo, element_uid=element_uid) - - self._state = kwargs.get("state", False) - self.enabled = kwargs.get("enabled", False) + super().__init__(element_uid=element_uid) + self._setter = setter + self._state = kwargs.pop("state", False) + self.enabled = kwargs.pop("enabled", False) @property def state(self) -> bool: @@ -41,7 +37,7 @@ def state(self, state: bool): """ Update state of the binary sensor and set point in time of the last_activity. """ self._state = state self._last_activity = datetime.now() - + self._logger.debug("State of %s set to %s.", self.element_uid, state) def set(self, state: bool): """ @@ -52,11 +48,5 @@ def set(self, state: bool): if not self.enabled: raise SwitchingProtected("This device is protected against remote switching.") - data = {"method": "FIM/invokeOperation", - "params": [self.element_uid, "turnOn" if state else "turnOff", []]} - response = self.post(data) - if response["result"].get("status") == 1: + if self._setter(self.element_uid, state): self.state = state - self._logger.debug(f"Binary switch property {self.element_uid} set to {state}") - else: - self._logger.debug(f"Something went wrong. Response to set command:\n{response}") diff --git a/devolo_home_control_api/properties/consumption_property.py b/devolo_home_control_api/properties/consumption_property.py index 33fda037..ce9d7c3d 100644 --- a/devolo_home_control_api/properties/consumption_property.py +++ b/devolo_home_control_api/properties/consumption_property.py @@ -1,11 +1,7 @@ from datetime import datetime -from typing import Any +from typing import Union -from requests import Session - -from ..devices.gateway import Gateway from ..exceptions.device import WrongElementError -from ..mydevolo import Mydevolo from .property import Property @@ -13,27 +9,26 @@ class ConsumptionProperty(Property): """ Object for consumptions. It stores the current and total consumption and the corresponding units. - :param gateway: Instance of a Gateway object - :param session: Instance of a requests.Session object - :param mydevolo: Mydevolo instance for talking to the devolo Cloud :param element_uid: Element UID, something like devolo.Meter:hdm:ZWave:CBC56091/24#2 :key current: Consumption value valid at time of creating the instance + :type current: float :key total: Total consumption since last reset + :type total: float :key total_since: Timestamp in milliseconds of last reset + :type total_since: int """ - def __init__(self, gateway: Gateway, session: Session, mydevolo: Mydevolo, element_uid: str, **kwargs: Any): + def __init__(self, element_uid: str, **kwargs: Union[int, float]): if not element_uid.startswith("devolo.Meter:"): raise WrongElementError(f"{element_uid} is not a Meter.") - super().__init__(gateway=gateway, session=session, mydevolo=mydevolo, element_uid=element_uid) - self._current = kwargs.get("current", 0.0) + super().__init__(element_uid=element_uid) + + self._current = kwargs.pop("current", 0.0) self.current_unit = "W" - self._total = kwargs.get("total", 0.0) + self._total = kwargs.pop("total", 0.0) self.total_unit = "kWh" - - self._total_since = datetime.utcfromtimestamp(kwargs.get("total_since", 0) / 1000) - + self._total_since = datetime.utcfromtimestamp(kwargs.pop("total_since", 0) / 1000) @property def current(self) -> float: @@ -45,6 +40,7 @@ def current(self, current: float): """ Update current consumption and set point in time of the last_activity. """ self._current = current self._last_activity = datetime.now() + self._logger.debug("current of element_uid %s set to %s.", self.element_uid, current) @property def total(self) -> float: @@ -56,6 +52,7 @@ def total(self, total: float): """ Update total consumption and set point in time of the last_activity. """ self._total = total self._last_activity = datetime.now() + self._logger.debug("total of element_uid %s set to %s.", self.element_uid, total) @property def total_since(self) -> datetime: @@ -66,4 +63,4 @@ def total_since(self) -> datetime: def total_since(self, timestamp: int): """ Convert a timestamp in millisecond to a datetime object. """ self._total_since = datetime.utcfromtimestamp(timestamp / 1000) - self._logger.debug(f"self.total_since of element_uid {self.element_uid} set to {self._total_since}.") + self._logger.debug("total_since of element_uid %s set to %s.", self.element_uid, self._total_since) diff --git a/devolo_home_control_api/properties/humidity_bar_property.py b/devolo_home_control_api/properties/humidity_bar_property.py index 9d89c36e..0441ac7c 100644 --- a/devolo_home_control_api/properties/humidity_bar_property.py +++ b/devolo_home_control_api/properties/humidity_bar_property.py @@ -1,11 +1,6 @@ from datetime import datetime -from typing import Any -from requests import Session - -from ..devices.gateway import Gateway from ..exceptions.device import WrongElementError -from ..mydevolo import Mydevolo from .sensor_property import SensorProperty @@ -13,9 +8,6 @@ class HumidityBarProperty(SensorProperty): """ Object for humidity bars. It stores the zone and the position inside that zone. - :param gateway: Instance of a Gateway object - :param session: Instance of a requests.Session object - :param mydevolo: Mydevolo instance for talking to the devolo Cloud :param element_uid: Fake element UID, something like devolo.HumidityBar:hdm:ZWave:CBC56091/24 :key value: Position inside a zone :type value: int @@ -23,15 +15,14 @@ class HumidityBarProperty(SensorProperty): :type zone: int """ - def __init__(self, gateway: Gateway, session: Session, mydevolo: Mydevolo, element_uid: str, **kwargs: Any): + def __init__(self, element_uid: str, **kwargs): if not element_uid.startswith("devolo.HumidityBar:"): raise WrongElementError(f"{element_uid} is not a humidity bar.") - self._value = kwargs.get("value", 0) - self.zone = kwargs.get("zone", 0) - - super().__init__(gateway=gateway, session=session, mydevolo=mydevolo, element_uid=element_uid, **kwargs) + super().__init__(element_uid=element_uid, **kwargs) + self._value = kwargs.pop("value", 0) + self.zone = kwargs.pop("zone", 0) @property def value(self) -> int: @@ -46,3 +37,4 @@ def value(self, value: int): """ self._value = value self._last_activity = datetime.now() + self._logger.debug("value of element_uid %s set to %s.", self.element_uid, value) diff --git a/devolo_home_control_api/properties/multi_level_sensor_property.py b/devolo_home_control_api/properties/multi_level_sensor_property.py index 616bb1a6..5984f5e8 100644 --- a/devolo_home_control_api/properties/multi_level_sensor_property.py +++ b/devolo_home_control_api/properties/multi_level_sensor_property.py @@ -1,11 +1,6 @@ from datetime import datetime -from typing import Any -from requests import Session - -from ..devices.gateway import Gateway from ..exceptions.device import WrongElementError -from ..mydevolo import Mydevolo from .sensor_property import SensorProperty @@ -14,9 +9,6 @@ class MultiLevelSensorProperty(SensorProperty): Object for multi level sensors. It stores the multi level sensor state and additional information that help displaying the state in the right context. - :param gateway: Instance of a Gateway object - :param session: Instance of a requests.Session object - :param mydevolo: Mydevolo instance for talking to the devolo Cloud :param element_uid: Element UID, something like devolo.MultiLevelSensor:hdm:ZWave:CBC56091/24#MultilevelSensor(1) :key value: Multi level value :type value: float @@ -24,40 +16,50 @@ class MultiLevelSensorProperty(SensorProperty): :type unit: int """ - def __init__(self, gateway: Gateway, session: Session, mydevolo: Mydevolo, element_uid: str, **kwargs: Any): + def __init__(self, element_uid: str, **kwargs): if not element_uid.startswith(("devolo.DewpointSensor:", "devolo.MultiLevelSensor:", "devolo.ValveTemperatureSensor", "devolo.VoltageMultiLevelSensor:")): raise WrongElementError(f"{element_uid} is not a Multi Level Sensor.") - super().__init__(gateway=gateway, session=session, mydevolo=mydevolo, element_uid=element_uid, **kwargs) - - self._value = kwargs.get("value", 0.0) - self._unit = "" - self.unit = kwargs.get("unit", "") + super().__init__(element_uid=element_uid, **kwargs) + self._value = kwargs.pop("value", 0.0) + self._unit = kwargs.pop("unit", 0) @property def unit(self) -> str: """ Human readable unit of the property. """ - return self._unit - - @unit.setter - def unit(self, unit: int): - """ Make the numeric unit human readable, if known. """ - units = {"dewpoint": {0: "°C", 1: "°F"}, - "humidity": {0: "%", 1: "g/m³"}, - "light": {0: "%", 1: "lx"}, - "temperature": {0: "°C", 1: "°F"}, - "Seismic Intensity": {0: ""}, - "voltage": {0: "V", 1: "mV"} - } + units = { + "dewpoint": { + 0: "°C", + 1: "°F" + }, + "humidity": { + 0: "%", + 1: "g/m³" + }, + "light": { + 0: "%", + 1: "lx" + }, + "temperature": { + 0: "°C", + 1: "°F" + }, + "Seismic Intensity": { + 0: "" + }, + "voltage": { + 0: "V", + 1: "mV" + }, + } try: - self._unit = units[self.sensor_type].get(unit, str(unit)) + return units[self.sensor_type].get(self._unit, str(self._unit)) except KeyError: - self._unit = str(unit) - self._logger.debug(f"Unit of {self.element_uid} set to '{self._unit}'.") + return str(self._unit) @property def value(self) -> float: @@ -69,3 +71,4 @@ def value(self, value: float): """ Update value of the multilevel sensor and set point in time of the last_activity. """ self._value = value self._last_activity = datetime.now() + self._logger.debug("value of element_uid %s set to %s.", self.element_uid, value) diff --git a/devolo_home_control_api/properties/multi_level_switch_property.py b/devolo_home_control_api/properties/multi_level_switch_property.py index fb662d00..70c56adc 100644 --- a/devolo_home_control_api/properties/multi_level_switch_property.py +++ b/devolo_home_control_api/properties/multi_level_switch_property.py @@ -1,12 +1,8 @@ from datetime import datetime -from typing import Any, Optional +from typing import Callable, Optional -from requests import Session - -from .property import Property -from ..devices.gateway import Gateway from ..exceptions.device import WrongElementError -from ..mydevolo import Mydevolo +from .property import Property class MultiLevelSwitchProperty(Property): @@ -14,34 +10,31 @@ class MultiLevelSwitchProperty(Property): Object for multi level switches. It stores the multi level state and additional information that help displaying the state in the right context. - :param gateway: Instance of a Gateway object - :param session: Instance of a requests.Session object - :param mydevolo: Mydevolo instance for talking to the devolo Cloud :param element_uid: Element UID, something like devolo.Dimmer:hdm:ZWave:CBC56091/24#2 :key value: Value the multi level switch has at time of creating this instance :type value: float :key switch_type: Type this switch is of, e.g. temperature - :type switch_type: string + :type switch_type: str :key max: Highest possible value, that can be set :type max: float :key min: Lowest possible value, that can be set :type min: float """ - def __init__(self, gateway: Gateway, session: Session, mydevolo: Mydevolo, element_uid: str, **kwargs: Any): + def __init__(self, element_uid: str, setter: Callable, **kwargs): if not element_uid.startswith(("devolo.Blinds:", "devolo.Dimmer:", "devolo.MultiLevelSwitch:", "devolo.SirenMultiLevelSwitch:")): raise WrongElementError(f"{element_uid} is not a multi level switch.") - super().__init__(gateway=gateway, session=session, mydevolo=mydevolo, element_uid=element_uid) - - self._value = kwargs.get("value", 0.0) - self.switch_type = kwargs.get("switch_type", "") - self.max = kwargs.get("max", 100.0) - self.min = kwargs.get("min", 0.0) + super().__init__(element_uid=element_uid) + self._setter = setter + self._value = kwargs.pop("value", 0.0) + self.switch_type = kwargs.pop("switch_type", "") + self.max = kwargs.pop("max", 100.0) + self.min = kwargs.pop("min", 0.0) @property def last_activity(self) -> datetime: @@ -53,13 +46,15 @@ def last_activity(self, timestamp: int): """ The gateway persists the last activity of some multi level switchs. They can be initialized with that value. """ if timestamp != -1: self._last_activity = datetime.utcfromtimestamp(timestamp / 1000) - self._logger.debug(f"self.last_activity of element_uid {self.element_uid} set to {self._last_activity}.") + self._logger.debug("last_activity of element_uid %s set to %s.", self.element_uid, self._last_activity) @property def unit(self) -> Optional[str]: """ Human readable unit of the property. Defaults to percent. """ - units = {"temperature": "°C", - "tone": None} + units = { + "temperature": "°C", + "tone": None, + } return units.get(self.switch_type, "%") @property @@ -72,7 +67,7 @@ def value(self, value: float): """ Update value of the multilevel value and set point in time of the last_activity. """ self._value = value self._last_activity = datetime.now() - + self._logger.debug("Value of %s set to %s.", self.element_uid, value) def set(self, value: float): """ @@ -81,13 +76,8 @@ def set(self, value: float): :param value: Value to set """ if value > self.max or value < self.min: - raise ValueError(f"Set value {value} is too {'low' if value < self.min else 'high'}. The min value is {self.min}. \ - The max value is {self.max}") - data = {"method": "FIM/invokeOperation", - "params": [self.element_uid, "sendValue", [value]]} - response = self.post(data) - if response["result"].get("status") == 1: + raise ValueError((f"Set value {value} is too {'low' if value < self.min else 'high'}. " + f"The min value is {self.min}. The max value is {self.max}")) + + if self._setter(self.element_uid, value): self.value = value - self._logger.debug(f"Multi level switch property {self.element_uid} set to {value}") - else: - self._logger.debug(f"Something went wrong. Response to set command:\n{response}") diff --git a/devolo_home_control_api/properties/property.py b/devolo_home_control_api/properties/property.py index 41d252ee..f369dcfe 100644 --- a/devolo_home_control_api/properties/property.py +++ b/devolo_home_control_api/properties/property.py @@ -1,33 +1,23 @@ +import logging +from abc import ABC from datetime import datetime -from requests import Session - -from ..backend.mprm_rest import MprmRest -from ..devices.gateway import Gateway from ..devices.zwave import get_device_uid_from_element_uid -from ..mydevolo import Mydevolo -class Property(MprmRest): +class Property(ABC): """ - Base object for properties. It is not meant to use this directly. + Abstract base object for properties. - :param gateway: Instance of a Gateway object - :param session: Instance of a requests.Session object - :param mydevolo: Mydevolo instance for talking to the devolo Cloud :param element_uid: Element UID, something like devolo.BinarySwitch:hdm:ZWave:CBC56091/24#2 """ - def __init__(self, gateway: Gateway, session: Session, mydevolo: Mydevolo, element_uid: str): + def __init__(self, element_uid: str): self.element_uid = element_uid self.device_uid = get_device_uid_from_element_uid(element_uid) + self._logger = logging.getLogger(self.__class__.__name__) self._last_activity = datetime.fromtimestamp(0) # Set last activity to 1.1.1970. Will be corrected on update. - self._gateway = gateway - self._session = session - - super().__init__(mydevolo_instance=mydevolo) - @property def last_activity(self) -> datetime: diff --git a/devolo_home_control_api/properties/remote_control_property.py b/devolo_home_control_api/properties/remote_control_property.py index 1e93ca86..232e5302 100644 --- a/devolo_home_control_api/properties/remote_control_property.py +++ b/devolo_home_control_api/properties/remote_control_property.py @@ -1,9 +1,7 @@ from datetime import datetime -from typing import Any +from typing import Callable -from ..devices.gateway import Gateway from ..exceptions.device import WrongElementError -from ..mydevolo import Mydevolo from .property import Property @@ -12,25 +10,23 @@ class RemoteControlProperty(Property): Object for remote controls. It stores the button state and additional information that help displaying the state in the right context. - :param gateway: Instance of a Gateway object - :param session: Instance of a requests.Session object - :param mydevolo: Mydevolo instance for talking to the devolo Cloud :param element_uid: Element UID, something like devolo.RemoteControl:hdm:ZWave:CBC56091/24#2 + :param setter: Method to call on setting the state :key key_count: Number of buttons this remote control has :type key_count: int :key key_pressed: Number of the button pressed :type key_pressed: int """ - def __init__(self, gateway: Gateway, session, mydevolo: Mydevolo, element_uid: str, **kwargs: Any): + def __init__(self, element_uid: str, setter: Callable, **kwargs: int): if not element_uid.startswith("devolo.RemoteControl"): raise WrongElementError(f"{element_uid} is not a remote control.") - super().__init__(gateway=gateway, session=session, mydevolo=mydevolo, element_uid=element_uid) - - self._key_pressed = kwargs.get("key_pressed", 0) - self.key_count = kwargs.get("key_count", 0) + super().__init__(element_uid=element_uid) + self._setter = setter + self._key_pressed = kwargs.pop("key_pressed", 0) + self.key_count = kwargs.pop("key_count", 0) @property def key_pressed(self) -> int: @@ -38,11 +34,11 @@ def key_pressed(self) -> int: return self._key_pressed @key_pressed.setter - def key_pressed(self, key_pressed: float): + def key_pressed(self, key_pressed: int): """ Update value of the multilevel value and set point in time of the last_activity. """ self._key_pressed = key_pressed self._last_activity = datetime.now() - + self._logger.debug("key_pressed of element_uid %s set to %s.", self.element_uid, key_pressed) def set(self, key_pressed: int): """ @@ -53,8 +49,6 @@ def set(self, key_pressed: int): if key_pressed > self.key_count or key_pressed <= 0: raise ValueError(f"Set value {key_pressed} is invalid.") - data = {"method": "FIM/invokeOperation", - "params": [self.element_uid, "pressKey", [key_pressed]]} - self.post(data) - self.key_pressed = key_pressed - self._logger.debug(f"Remote Control property {self.element_uid} set to {key_pressed}") + if self._setter(self.element_uid, key_pressed): + self.key_pressed = key_pressed + self._logger.debug("Remote Control property %s set to %s", self.element_uid, key_pressed) diff --git a/devolo_home_control_api/properties/sensor_property.py b/devolo_home_control_api/properties/sensor_property.py index 8f79cc09..1da9ad5e 100644 --- a/devolo_home_control_api/properties/sensor_property.py +++ b/devolo_home_control_api/properties/sensor_property.py @@ -1,19 +1,13 @@ -from typing import Any +from abc import ABC -from requests import Session - -from ..devices.gateway import Gateway -from ..mydevolo import Mydevolo from .property import Property -class SensorProperty(Property): +class SensorProperty(Property, ABC): """ - Object for sensors. It stores the sensor and sub type. + Abstract object for sensors. It stores the sensor and sub type. - :param gateway: Instance of a Gateway object - :param session: Instance of a requests.Session object - :param mydevolo: Mydevolo instance for talking to the devolo Cloud + :param connection: Collection of instances needed to communicate with the central unit :param element_uid: Element UID :key sensor_type: Type of the sensor sensor, something like 'alarm' :type sensor_type: str @@ -21,8 +15,8 @@ class SensorProperty(Property): :type sub_type: str """ - def __init__(self, gateway: Gateway, session: Session, mydevolo: Mydevolo, element_uid: str, **kwargs: Any): - super().__init__(gateway=gateway, session=session, mydevolo=mydevolo, element_uid=element_uid) + def __init__(self, element_uid: str, **kwargs: str): + super().__init__(element_uid=element_uid) - self.sensor_type = kwargs.get("sensor_type", "") - self.sub_type = kwargs.get("sub_type", "") + self.sensor_type = kwargs.pop("sensor_type", "") + self.sub_type = kwargs.pop("sub_type", "") diff --git a/devolo_home_control_api/properties/settings_property.py b/devolo_home_control_api/properties/settings_property.py index 418d321b..a7e8a58b 100644 --- a/devolo_home_control_api/properties/settings_property.py +++ b/devolo_home_control_api/properties/settings_property.py @@ -1,10 +1,6 @@ -from typing import Any +from typing import Callable -from requests import Session - -from ..devices.gateway import Gateway from ..exceptions.device import WrongElementError -from ..mydevolo import Mydevolo from .property import Property @@ -14,33 +10,50 @@ class SettingsProperty(Property): the gateway. This is to be as flexible to gateway firmware changes as possible. So if new attributes appear or old ones are removed, they should be handled at least in reading them. Nevertheless, a few unwanted attributes are filtered. - :param gateway: Instance of a Gateway object - :param session: Instance of a requests.Session object - :param mydevolo: Mydevolo instance for talking to the devolo Cloud - :param element_uid: Element UID, something like devolo.BinarySwitch:hdm:ZWave:CBC56091/24#2 + :param element_uid: Element UID, something like acs.hdm:ZWave:CBC56091/24 + :param setter: Method to call on setting the state :param **kwargs: Any setting, that shall be available in this object """ - def __init__(self, gateway: Gateway, session: Session, mydevolo: Mydevolo, element_uid: str, **kwargs: Any): - if not element_uid.startswith(("acs", "bas", "bss", "cps", "gds", "lis", "mas", - "mss", "ps", "sts", "stmss", "trs", "vfs")): + def __init__(self, element_uid: str, setter: Callable, **kwargs): + if not element_uid.startswith(("acs", + "bas", + "bss", + "cps", + "gds", + "lis", + "mas", + "mss", + "ps", + "sts", + "stmss", + "trs", + "vfs")): raise WrongElementError() - super().__init__(gateway=gateway, session=session, mydevolo=mydevolo, element_uid=element_uid) + super().__init__(element_uid=element_uid) + self._setter = setter + + if element_uid.startswith("gds") and {"zones", + "zone_id"} <= kwargs.keys(): + self.events_enabled: bool + self.icon: str + self.name: str + self.zone_id: str + self.zone = kwargs.pop("zones")[kwargs['zone_id']] + for key, value in kwargs.items(): setattr(self, key, value) - if element_uid.startswith("gds"): - self.zone = self._gateway.zones[self.zone_id] - - setter_method = {"bas": self._set_bas, - "gds": self._set_gds, - "lis": self._set_lis, - "mss": self._set_mss, - "ps": self._set_ps, - "trs": self._set_trs, - "vfs": self._set_lis - } + setter_method = { + "bas": self._set_bas, + "gds": self._set_gds, + "lis": self._set_lis, + "mss": self._set_mss, + "ps": self._set_ps, + "trs": self._set_trs, + "vfs": self._set_lis, + } # Depending on the type of setting property, this will create a callable named "set". # However, this methods are not working, if the gateway is connected locally, yet. @@ -52,40 +65,46 @@ def __init__(self, gateway: Gateway, session: Session, mydevolo: Mydevolo, eleme for attribute in clean_up_list: delattr(self, attribute) - def _set_bas(self, value: bool): """ Set a binary async setting. This is e.g. the muted setting of a siren or the three way switch setting of a dimmer. :param value: New state """ - self.value = value - data = {"method": "FIM/invokeOperation", - "params": [self.element_uid, "save", [value]]} - self.post(data) + if self._setter(self.element_uid, [value]): + self.value = value # pylint: disable=attribute-defined-outside-init + self._logger.debug("Binary async setting property %s set to %s", self.element_uid, value) - def _set_gds(self, **kwargs: Any): + def _set_gds(self, **kwargs): """ Set one or more general device setting. :key events_enabled: Show events in diary :type events_enabled: bool :key icon: New icon name - :type icon: string + :type icon: str :key name: New device name - :type name: string + :type name: str :key zone_id: New zone_id (ATTENTION: This is NOT the name of the location) - :type events_enabled: string + :type zone_id: str """ - allowed = ["events_enabled", "icon", "name", "zone_id"] - for item in allowed: - setattr(self, item, kwargs.get(item, getattr(self, item))) - data = {"method": "FIM/invokeOperation", - "params": [self.element_uid, "save", [{"events_enabled": self.events_enabled, - "icon": self.icon, - "name": self.name, - "zone_id": self.zone_id}]]} - self.post(data) + events_enabled = kwargs.pop("events_enabled", self.events_enabled) + icon = kwargs.pop("icon", self.icon) + name = kwargs.pop("name", self.name) + zone_id = kwargs.pop("zone_id", self.zone_id) + + settings = { + "events_enabled": events_enabled, + "icon": icon, + "name": name, + "zone_id": zone_id, + } + if self._setter(self.element_uid, [settings]): + self.events_enabled = events_enabled + self.icon = icon + self.name = name + self.zone_id = zone_id + self._logger.debug("General device setting %s changed.", self.element_uid) def _set_lis(self, led_setting: bool): """ @@ -93,10 +112,9 @@ def _set_lis(self, led_setting: bool): :param led_setting: LED indication setting """ - self.led_setting = led_setting - data = {"method": "FIM/invokeOperation", - "params": [self.element_uid, "save", [self.led_setting]]} - self.post(data) + if self._setter(self.element_uid, [led_setting]): + self.led_setting = led_setting # pylint: disable=attribute-defined-outside-init + self._logger.debug("LED indication setting property %s set to %s", self.element_uid, led_setting) def _set_mss(self, motion_sensitivity: int): """ @@ -106,12 +124,11 @@ def _set_mss(self, motion_sensitivity: int): """ if not 0 <= motion_sensitivity <= 100: raise ValueError("Value must be between 0 and 100") - self.motion_sensitivity = motion_sensitivity - data = {"method": "FIM/invokeOperation", - "params": [self.element_uid, "save", [self.motion_sensitivity]]} - self.post(data) + if self._setter(self.element_uid, [motion_sensitivity]): + self.motion_sensitivity = motion_sensitivity # pylint: disable=attribute-defined-outside-init + self._logger.debug("Motion sensitivity setting property %s set to %s", self.element_uid, motion_sensitivity) - def _set_ps(self, **kwargs): + def _set_ps(self, **kwargs: bool): """ Set one or both protection settings. @@ -120,12 +137,22 @@ def _set_ps(self, **kwargs): :key remote_switching: Allow local switching :type remote_switching: bool """ - allowed = ["local_switching", "remote_switching"] - [setattr(self, item, kwargs.get(item, getattr(self, item))) for item in allowed] - data = {"method": "FIM/invokeOperation", - "params": [self.element_uid, "save", [{"localSwitch": self.local_switching, - "remoteSwitch": self.remote_switching}]]} - self.post(data) + # pylint: disable=access-member-before-definition + local_switching = kwargs.pop("local_switching", self.local_switching) + # pylint: disable=access-member-before-definition + remote_switching = kwargs.pop("remote_switching", self.remote_switching) + + if self._setter(self.element_uid, + [{ + "localSwitch": local_switching, + "remoteSwitch": remote_switching, + }]): + self.local_switching: bool = local_switching # pylint: disable=attribute-defined-outside-init + self.remote_switching: bool = remote_switching # pylint: disable=attribute-defined-outside-init + self._logger.debug("Protection setting property %s set to %s (local) and %s (remote).", + self.element_uid, + local_switching, + remote_switching) def _set_trs(self, temp_report: bool): """ @@ -133,7 +160,6 @@ def _set_trs(self, temp_report: bool): :param temp_report: Boolean of the target value """ - self.temp_report = temp_report - data = {"method": "FIM/invokeOperation", - "params": [self.element_uid, "save", [self.temp_report]]} - self.post(data) + if self._setter(self.element_uid, [temp_report]): + self.temp_report = temp_report # pylint: disable=attribute-defined-outside-init + self._logger.debug("Temperature report setting property %s set to %s", self.element_uid, temp_report) diff --git a/devolo_home_control_api/publisher/publisher.py b/devolo_home_control_api/publisher/publisher.py index 8aafe50a..9cfbb005 100644 --- a/devolo_home_control_api/publisher/publisher.py +++ b/devolo_home_control_api/publisher/publisher.py @@ -1,5 +1,5 @@ -from typing import Callable, Dict import logging +from typing import Callable, Dict, KeysView, Union class Publisher: @@ -7,10 +7,10 @@ class Publisher: The Publisher send messages to attached subscribers. """ - def __init__(self, events: list): + def __init__(self, events: Union[list, KeysView]): self._logger = logging.getLogger(self.__class__.__name__) - self._events: Dict = {event: dict() for event in events} - + self._events: Dict = {event: {} + for event in events} def add_event(self, event: str): """ Add a new event to listen to. """ @@ -33,16 +33,16 @@ def register(self, event: str, who: object, callback: Callable = None): :raises AttributeError: The supposed callback is not callable. """ if callback is None: - callback = getattr(who, 'update') + callback = getattr(who, "update") self._get_subscribers_for_specific_event(event)[who] = callback - self._logger.debug(f"Subscriber registered for event {event}") + self._logger.debug("Subscriber registered for event %s", event) def unregister(self, event: str, who: object): """ Remove a subscriber for a specific event. """ del self._get_subscribers_for_specific_event(event)[who] - self._logger.debug(f"Subscriber deleted for event {event}") - + self._logger.debug("Subscriber deleted for event %s", event) - def _get_subscribers_for_specific_event(self, event: str): + def _get_subscribers_for_specific_event(self, event: str) -> Dict: """ All subscribers listening to an event. """ - return self._events[event] + return self._events.get(event, + {}) diff --git a/devolo_home_control_api/publisher/updater.py b/devolo_home_control_api/publisher/updater.py index e441db81..503eb1eb 100644 --- a/devolo_home_control_api/publisher/updater.py +++ b/devolo_home_control_api/publisher/updater.py @@ -1,7 +1,8 @@ import json import logging -from typing import Callable, Optional +from typing import Any, Callable, Optional +from ..backend import MESSAGE_TYPES from ..devices.gateway import Gateway from ..helper.string import camel_case_to_snake_case from ..helper.uid import get_device_type_from_element_uid, get_device_uid_from_element_uid, get_device_uid_from_setting_uid @@ -26,80 +27,46 @@ def __init__(self, devices: dict, gateway: Gateway, publisher: Publisher): self.devices = devices self.on_device_change: Optional[Callable] = None - def update(self, message: dict): """ Update states and values depending on the message type. :param message: Message to process """ - message_type = {"acs.hdm": self._automatic_calibration, - "bas.hdm": self._binary_async, - "bss.hdm": self._binary_sync, - "cps.hdm": self._parameter, - "gds.hdm": self._general_device, - "lis.hdm": self._led, - "ps.hdm": self._protection, - "stmss.hdm": self._multilevel_sync, - "sts.hdm": self._switch_type, - "trs.hdm": self._temperature, - "vfs.hdm": self._led, - "mss.hdm": self._multilevel_sync, - "devolo.BinarySensor": self._binary_sensor, - "devolo.BinarySwitch": self._binary_switch, - "devolo.Blinds": self._multi_level_switch, - "devolo.DevicesPage": self._device_change, - "devolo.Dimmer": self._multi_level_switch, - "devolo.DewpointSensor": self._multi_level_sensor, - "devolo.Grouping": self._grouping, - "devolo.HumidityBarValue": self._humidity_bar, - "devolo.HumidityBarZone": self._humidity_bar, - "devolo.mprm.gw.GatewayAccessibilityFI": self._gateway_accessible, - "devolo.Meter": self._meter, - "devolo.MildewSensor": self._binary_sensor, - "devolo.MultiLevelSensor": self._multi_level_sensor, - "devolo.MultiLevelSwitch": self._multi_level_switch, - "devolo.RemoteControl": self._remote_control, - "devolo.SirenMultiLevelSwitch": self._multi_level_switch, - "devolo.ShutterMovementFI": self._binary_sensor, - "devolo.ValveTemperatureSensor": self._multi_level_sensor, - "devolo.VoltageMultiLevelSensor": self._multi_level_sensor, - "devolo.WarningBinaryFI:": self._binary_sensor, - "hdm": self._device_state} - - unwanted_properties = [".unregistering", "operationStatus"] + unwanted_properties = [ + ".unregistering", + "operationStatus", + ] # Early return on unwanted messages - if "UNREGISTERED" in message['topic'] or \ - message['properties']['property.name'] in unwanted_properties or \ - "smartGroup" in message['properties']['uid']: + if "UNREGISTERED" in message['topic'] \ + or message['properties']['property.name'] in unwanted_properties \ + or "smartGroup" in message['properties']['uid']: return # Handle pending operations messages - if "property.name" in message["properties"] and message['properties']['property.name'] == "pendingOperations": + if "property.name" in message['properties'] and message['properties']['property.name'] == "pendingOperations": self._pending_operations(message) return # Handle all other messages + message_type = MESSAGE_TYPES.get(get_device_type_from_element_uid(message['properties']['uid']), "_unknown") try: - message_type.get(get_device_type_from_element_uid(message['properties']['uid']), self._unknown)(message) + getattr(self, message_type)(message) except (AttributeError, KeyError): # Sometime we receive already messages although the device is not setup yet. pass - def _automatic_calibration(self, message: dict): """ Update a automatic calibration message. """ try: - calibration_status = message["properties"]["property.value.new"]["status"] - self._update_automatic_calibration( - element_uid=message["properties"]["uid"], - calibration_status=calibration_status != 2, - ) + calibration_status = message['properties']['property.value.new']['status'] + self._update_automatic_calibration(element_uid=message['properties']['uid'], + calibration_status=calibration_status != 2) except (KeyError, TypeError): - if type(message["properties"]["property.value.new"]) not in [dict, list]: - self._update_automatic_calibration(element_uid=message["properties"]["uid"], - calibration_status=bool(message["properties"]["property.value.new"])) + if type(message['properties']['property.value.new']) not in [dict, list]: + self._update_automatic_calibration(element_uid=message['properties']['uid'], + calibration_status=bool(message['properties']['property.value.new'])) def _binary_async(self, message: dict): """ Update a binary async setting. """ @@ -112,7 +79,7 @@ def _binary_async(self, message: dict): except KeyError: # Siren setting is not initialized like others. self.devices[device_uid].settings_property['muted'].value = value - self._logger.debug(f"Updating value of {element_uid} to {value}") + self._logger.debug("Updating state of %s to %s", element_uid, value) self._publisher.dispatch(device_uid, (element_uid, value)) def _binary_sync(self, message: dict): @@ -121,7 +88,7 @@ def _binary_sync(self, message: dict): value = bool(message['properties']['property.value.new']) device_uid = get_device_uid_from_setting_uid(element_uid) self.devices[device_uid].settings_property["movement_direction"].direction = value - self._logger.debug(f"Updating value of {element_uid} to {value}") + self._logger.debug("Updating state of %s to %s", element_uid, value) self._publisher.dispatch(device_uid, (element_uid, value)) def _binary_sensor(self, message: dict): @@ -131,18 +98,18 @@ def _binary_sensor(self, message: dict): value = bool(message['properties']['property.value.new']) device_uid = get_device_uid_from_element_uid(element_uid) self.devices[device_uid].binary_sensor_property[element_uid].state = value - self._logger.debug(f"Updating state of {element_uid} to {value}") + self._logger.debug("Updating state of %s to %s", element_uid, value) self._publisher.dispatch(device_uid, (element_uid, value)) def _binary_switch(self, message: dict): """ Update a binary switch's state. """ - if message['properties']['property.name'] == "targetState" and \ - message['properties']['property.value.new'] is not None: + if message['properties']['property.name'] == "targetState" \ + and message['properties']['property.value.new'] is not None: element_uid = message['properties']['uid'] value = bool(message['properties']['property.value.new']) device_uid = get_device_uid_from_element_uid(element_uid) self.devices[device_uid].binary_switch_property[element_uid].state = value - self._logger.debug(f"Updating state of {element_uid} to {value}") + self._logger.debug("Updating state of %s to %s", element_uid, value) self._publisher.dispatch(device_uid, (element_uid, value)) def _pending_operations(self, message: dict): @@ -150,7 +117,11 @@ def _pending_operations(self, message: dict): element_uid = message['properties']['uid'] # Early return on useless messages - if element_uid in ["devolo.PairDevice", "devolo.RemoveDevice"]: + if element_uid in [ + "devolo.PairDevice", + "devolo.RemoveDevice", + "devolo.mprm.gw.GatewayManager", + ]: return pending_operations = bool(message['properties'].get('property.value.new')) @@ -160,47 +131,29 @@ def _pending_operations(self, message: dict): except KeyError: device_uid = get_device_uid_from_setting_uid(element_uid) self.devices[device_uid].pending_operations = pending_operations - self._logger.debug(f"Updating pending operations of device {device_uid} to {pending_operations}") + self._logger.debug("Updating pending operations of device %s to %s", device_uid, pending_operations) self._publisher.dispatch(device_uid, ("pending_operations", pending_operations)) def _current_consumption(self, message: dict): """ Update current consumption. """ - self._update_consumption(element_uid=message['uid'], - consumption="current", - value=message['property.value.new']) - - def _device_change(self, message: dict): - """ Call method if a new device appears or an old one disappears. """ - if not callable(self.on_device_change): - self._logger.error("on_device_change is not set.") - return - - if type(message['properties']['property.value.new']) == list \ - and message['properties']['uid'] == "devolo.DevicesPage": - device_uid, mode = self.on_device_change(device_uids=message['properties']['property.value.new']) - if mode == "add": - self._logger.info(f"{device_uid} added.") - self._publisher.add_event(event=device_uid) - self._publisher.dispatch(device_uid, (device_uid, mode)) - else: - self._publisher.dispatch(device_uid, (device_uid, mode)) - self._publisher.delete_event(event=device_uid) - + self._update_consumption(element_uid=message['uid'], consumption="current", value=message['property.value.new']) def _device_state(self, message: dict): """ Update the device state. """ - propery_name = {"batteryLevel": "battery_level", - "batteryLow": "battery_low", - "status": "status"} + property_name = { + "batteryLevel": "battery_level", + "batteryLow": "battery_low", + "status": "status", + } device_uid = message['properties']['uid'] name = message['properties']['property.name'] value = message['properties']['property.value.new'] try: - self._logger.debug(f"Updating {propery_name[name]} of {device_uid} to {value}") - setattr(self.devices[device_uid], propery_name[name], value) - self._publisher.dispatch(device_uid, (device_uid, value, propery_name[name])) + self._logger.debug("Updating %s of %s to %s", property_name[name], device_uid, value) + setattr(self.devices[device_uid], property_name[name], value) + self._publisher.dispatch(device_uid, (device_uid, value, property_name[name])) except KeyError: self._unknown(message) @@ -209,7 +162,7 @@ def _gateway_accessible(self, message: dict): if message['properties']['property.name'] == "gatewayAccessible": accessible = message['properties']['property.value.new']['accessible'] online_sync = message['properties']['property.value.new']['onlineSync'] - self._logger.debug(f"Updating status and state of gateway to status: {accessible} and state: {online_sync}") + self._logger.debug("Updating status and state of gateway to status: %s and state: %s", accessible, online_sync) self._gateway.online = accessible self._gateway.sync = online_sync @@ -219,11 +172,13 @@ def _general_device(self, message: dict): events_enabled=message['properties']['property.value.new']['eventsEnabled'], icon=message['properties']['property.value.new']['icon'], name=message['properties']['property.value.new']['name'], - zone_id=message['properties']['property.value.new']['zoneID']) + zone_id=message['properties']['property.value.new']['zoneID'], + zones=self._gateway.zones) def _grouping(self, message: dict): """ Update zone (also called room) of a device. """ - self._gateway.zones = {key["id"]: key["name"] for key in message["properties"]["property.value.new"]} + self._gateway.zones = {key['id']: key['name'] + for key in message['properties']['property.value.new']} self._logger.debug("Updating gateway zones.") def _gui_enabled(self, message: dict): @@ -232,7 +187,7 @@ def _gui_enabled(self, message: dict): enabled = message['property.value.new'] for element_uid in self.devices[device_uid].binary_switch_property: self.devices[device_uid].binary_switch_property[element_uid].enabled = enabled - self._logger.debug(f"Updating enabled state of {element_uid} to {enabled}") + self._logger.debug("Updating enabled state of %s to %s", element_uid, enabled) self._publisher.dispatch(device_uid, (element_uid, enabled, "gui_enabled")) def _humidity_bar(self, message: dict): @@ -242,13 +197,33 @@ def _humidity_bar(self, message: dict): device_uid = get_device_uid_from_element_uid(fake_element_uid) if message['properties']['uid'].startswith("devolo.HumidityBarZone"): self.devices[device_uid].humidity_bar_property[fake_element_uid].zone = value - self._logger.debug(f"Updating humidity bar zone of {fake_element_uid} to {value}") + self._logger.debug("Updating humidity bar zone of %s to %s", fake_element_uid, value) elif message['properties']['uid'].startswith("devolo.HumidityBarValue"): self.devices[device_uid].humidity_bar_property[fake_element_uid].value = value - self._logger.debug(f"Updating humidity bar value of {fake_element_uid} to {value}") - self._publisher.dispatch(device_uid, (fake_element_uid, - self.devices[device_uid].humidity_bar_property[fake_element_uid].zone, - self.devices[device_uid].humidity_bar_property[fake_element_uid].value)) + self._logger.debug("Updating humidity bar value of %s to %s", fake_element_uid, value) + self._publisher.dispatch(device_uid, + (fake_element_uid, + self.devices[device_uid].humidity_bar_property[fake_element_uid].zone, + self.devices[device_uid].humidity_bar_property[fake_element_uid].value)) + + def _inspect_devices(self, message: dict): + """ Call method if a new device appears or an old one disappears. """ + if not callable(self.on_device_change): + self._logger.error("on_device_change is not set.") + return + + if not isinstance(message['properties']['property.value.new'], list) \ + or message['properties']['uid'] != "devolo.DevicesPage": + return + + device_uid, mode = self.on_device_change(device_uids=message['properties']['property.value.new']) + if mode == "add": + self._logger.info("%s added.", device_uid) + self._publisher.add_event(event=device_uid) + self._publisher.dispatch(device_uid, (device_uid, mode)) + else: + self._publisher.dispatch(device_uid, (device_uid, mode)) + self._publisher.delete_event(event=device_uid) def _led(self, message: dict): """ Update LED settings. """ @@ -256,25 +231,40 @@ def _led(self, message: dict): element_uid = message['properties']['uid'] value = message['properties']['property.value.new'] device_uid = get_device_uid_from_setting_uid(element_uid) - self._logger.debug(f"Updating {element_uid} to {value}.") + self._logger.debug("Updating %s to %s.", element_uid, value) self.devices[device_uid].settings_property['led'].led_setting = value self._publisher.dispatch(device_uid, (element_uid, value)) def _meter(self, message: dict): """ Update a meter value. """ - property_name = {"currentValue": self._current_consumption, - "totalValue": self._total_consumption, - "sinceTime": self._since_time, - "guiEnabled": self._gui_enabled} + property_name = { + "currentValue": self._current_consumption, + "totalValue": self._total_consumption, + "sinceTime": self._since_time, + "guiEnabled": self._gui_enabled, + } property_name[message['properties']['property.name']](message['properties']) + def _multilevel_async(self, message: dict): + """ Update multilevel async setting (mas) properties. """ + device_uid = get_device_uid_from_setting_uid(message['properties']['uid']) + try: + name = camel_case_to_snake_case(message['properties']['itemId']) + # The Metering Plug has an multilevel async setting without an ID + except KeyError: + if self.devices[device_uid].device_model_uid == "devolo.model.Wall:Plug:Switch:and:Meter": + name = "flash_mode" + else: + raise + self.devices[device_uid].settings_property[name].value = message['properties']['property.value.new'] + def _multi_level_sensor(self, message: dict): """ Update a multi level sensor. """ element_uid = message['properties']['uid'] value = message['properties']['property.value.new'] device_uid = get_device_uid_from_element_uid(element_uid) - self._logger.debug(f"Updating {element_uid} to {value}") + self._logger.debug("Updating %s to %s.", element_uid, value) self.devices[device_uid].multi_level_sensor_property[element_uid].value = value self._publisher.dispatch(device_uid, (element_uid, value)) @@ -284,7 +274,7 @@ def _multi_level_switch(self, message: dict): element_uid = message['properties']['uid'] value = message['properties']['property.value.new'] device_uid = get_device_uid_from_element_uid(element_uid) - self._logger.debug(f"Updating {element_uid} to {value}") + self._logger.debug("Updating %s to %s.", element_uid, value) self.devices[device_uid].multi_level_switch_property[element_uid].value = value self._publisher.dispatch(device_uid, (element_uid, value)) @@ -295,10 +285,12 @@ def _multilevel_sync(self, message: dict): value = message['properties']['property.value.new'] device_uid = get_device_uid_from_setting_uid(element_uid) device_model = self.devices[device_uid].device_model_uid - self._logger.debug(f"Updating {element_uid} to {value}") - sync_type = {"devolo.model.Siren": "tone", - "devolo.model.OldShutter": "shutter_duration", - "devolo.model.Shutter": "shutter_duration"} + self._logger.debug("Updating %s to %s.", element_uid, value) + sync_type = { + "devolo.model.Siren": "tone", + "devolo.model.OldShutter": "shutter_duration", + "devolo.model.Shutter": "shutter_duration", + } try: setattr(self.devices[device_uid].settings_property[sync_type[device_model]], sync_type[device_model], value) @@ -315,7 +307,7 @@ def _parameter(self, message: dict): param_changed = message['properties']['property.value.new'] device_uid = get_device_uid_from_setting_uid(element_uid) self.devices[device_uid].settings_property['param_changed'].param_changed = param_changed - self._logger.debug(f"Updating param_changed of {element_uid} to {param_changed}") + self._logger.debug("Updating %s to %s.", element_uid, param_changed) self._publisher.dispatch(device_uid, (element_uid, param_changed)) def _protection(self, message: dict): @@ -325,13 +317,15 @@ def _protection(self, message: dict): value = message['properties']['property.value.new'] name = message['properties']['property.name'] device_uid = get_device_uid_from_setting_uid(element_uid) - switching_type = {"targetLocalSwitch": "local_switching", - "localSwitch": "local_switching", - "targetRemoteSwitch": "remote_switching", - "remoteSwitch": "remote_switching"} + switching_type = { + "targetLocalSwitch": "local_switching", + "localSwitch": "local_switching", + "targetRemoteSwitch": "remote_switching", + "remoteSwitch": "remote_switching", + } setattr(self.devices[device_uid].settings_property['protection'], switching_type[name], value) - self._logger.debug(f"Updating {switching_type[name]} protection of {element_uid} to {value}") + self._logger.debug("Updating %s protection of %s to %s", switching_type[name], element_uid, value) self._publisher.dispatch(device_uid, (element_uid, value, switching_type[name])) def _remote_control(self, message: dict): @@ -344,8 +338,9 @@ def _remote_control(self, message: dict): device_uid = get_device_uid_from_element_uid(element_uid) old_key_pressed = self.devices[device_uid].remote_control_property[element_uid].key_pressed self.devices[device_uid].remote_control_property[element_uid].key_pressed = key_pressed - self._logger.debug(f"Updating remote control of {element_uid}.\ - Key {f'pressed: {key_pressed}' if key_pressed != 0 else f'released: {old_key_pressed}'}") + self._logger.debug("Updating remote control of %s. Key %s", + element_uid, + f"pressed: {key_pressed}" if key_pressed != 0 else f"released: {old_key_pressed}") self._publisher.dispatch(device_uid, (element_uid, key_pressed)) def _since_time(self, message: dict): @@ -354,7 +349,7 @@ def _since_time(self, message: dict): total_since = message['property.value.new'] device_uid = get_device_uid_from_element_uid(element_uid) self.devices[device_uid].consumption_property[element_uid].total_since = total_since - self._logger.debug(f"Updating total since of {element_uid} to {total_since}") + self._logger.debug("Updating total since of %s to %s", element_uid, total_since) self._publisher.dispatch(device_uid, (element_uid, total_since, "total_since")) def _switch_type(self, message: dict): @@ -364,60 +359,58 @@ def _switch_type(self, message: dict): device_uid = get_device_uid_from_setting_uid(element_uid) self.devices[device_uid].settings_property['switch_type'].value = value self.devices[device_uid].remote_control_property[f'devolo.RemoteControl:{device_uid}'].key_count = value - self._logger.debug(f"Updating switch type of {device_uid} to {value}") + self._logger.debug("Updating switch type of %s to %s", device_uid, value) self._publisher.dispatch(device_uid, (element_uid, value)) - def _temperature(self, message: dict): + def _temperature_report(self, message: dict): """ Update temperature report settings. """ if type(message['properties'].get("property.value.new")) not in [dict, list]: element_uid = message['properties']['uid'] value = message['properties']['property.value.new'] device_uid = get_device_uid_from_setting_uid(element_uid) self.devices[device_uid].settings_property['temperature_report'].temp_report = value - self._logger.debug(f"Updating temperature report of {element_uid} to {value}") + self._logger.debug("Updating temperature report of %s to %s", element_uid, value) self._publisher.dispatch(device_uid, (element_uid, value)) def _total_consumption(self, message: dict): """ Update total consumption. """ - self._update_consumption(element_uid=message['uid'], - consumption="total", - value=message['property.value.new']) + self._update_consumption(element_uid=message['uid'], consumption="total", value=message['property.value.new']) def _unknown(self, message: dict): """ Ignore unknown messages. """ - ignore = ("devolo.DeviceEvents", - "devolo.LastActivity", - "devolo.PairDevice", - "devolo.SirenBinarySensor", - "devolo.SirenMultiLevelSensor", - "devolo.mprm.gw.GatewayManager", - "devolo.mprm.gw.PortalManager", - "hdm", - "ss", - "mcs") + ignore = ( + "devolo.DeviceEvents", + "devolo.PairDevice", + "devolo.SirenBinarySensor", + "devolo.SirenMultiLevelSensor", + "devolo.mprm.gw.GatewayManager", + "devolo.mprm.gw.PortalManager", + "ss", + "mcs", + ) if not message["properties"]["uid"].startswith(ignore): self._logger.debug(json.dumps(message, indent=4)) def _update_automatic_calibration(self, element_uid: str, calibration_status: bool): """ Update automatic calibration setting of a device. """ device_uid = get_device_uid_from_setting_uid(element_uid) - self.devices[device_uid].settings_property["automatic_calibration"].calibration_status = calibration_status - self._logger.debug(f"Updating value of {element_uid} to {calibration_status}") + self.devices[device_uid].settings_property['automatic_calibration'].calibration_status = calibration_status + self._logger.debug("Updating value of %s to %s", element_uid, calibration_status) self._publisher.dispatch(device_uid, (element_uid, calibration_status)) def _update_consumption(self, element_uid: str, consumption: str, value: float): """ Update the consumption of a device. """ device_uid = get_device_uid_from_element_uid(element_uid) setattr(self.devices[device_uid].consumption_property[element_uid], consumption, value) - self._logger.debug(f"Updating {consumption} consumption of {element_uid} to {value}") + self._logger.debug("Updating %s consumption of %s to %s", consumption, element_uid, value) self._publisher.dispatch(device_uid, (element_uid, value, consumption)) - def _update_general_device_settings(self, element_uid, **kwargs: str): + def _update_general_device_settings(self, element_uid: str, **kwargs: Any): """ Update general device settings. """ device_uid = get_device_uid_from_setting_uid(element_uid) for key, value in kwargs.items(): setattr(self.devices[device_uid].settings_property['general_device_settings'], key, value) - self._logger.debug(f"Updating attribute: {key} of {element_uid} to {value}") + self._logger.debug("Updating attribute: %s of %s to %s", key, element_uid, value) self._publisher.dispatch(device_uid, (key, value)) def _voltage_multi_level_sensor(self, message: dict): @@ -426,5 +419,5 @@ def _voltage_multi_level_sensor(self, message: dict): value = message['properties']['property.value.new'] device_uid = get_device_uid_from_element_uid(element_uid) self.devices[device_uid].multi_level_sensor_property[element_uid].value = value - self._logger.debug(f"Updating voltage of {element_uid} to {value}") + self._logger.debug("Updating voltage of %s to %s", element_uid, value) self._publisher.dispatch(device_uid, (element_uid, value)) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3e894770..d23bac56 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.17.0] - 2021/03/12 + +### Added + +- Added devices are now added to the Publisher. +- Multilevel Async Settings are now updated via websocket + +### Fixed + +- websocket_client 0.58.0 support + ## [v0.16.0] - 2020/11/07 ### Added @@ -69,7 +80,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Changed - **BREAKING**: MildewProperty was reimplemented as BinarySensorProperty and DewpointProperty was reimplemented as MultiLevelSensorProperty. -- **BREAKING**: Devices attributes manID and manufacturerId, prodID and productId as well as prodTypeID and productTypeId were merged +- **BREAKING**: Devices attributes manID and manufacturerId, prodID and productId as well as prodTypeID and productTypeId were merged to manufacturer_id, product_id and product_type_id respectively. ### Fixed diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md index fe3e84fb..c97d8da1 100644 --- a/docs/CODE_OF_CONDUCT.md +++ b/docs/CODE_OF_CONDUCT.md @@ -44,4 +44,4 @@ This Code of Conduct is adapted from the [Contributor Covenant][homepage], versi [homepage]: https://www.contributor-covenant.org -For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq \ No newline at end of file +For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 97cccf5d..dbe26d3a 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -22,16 +22,14 @@ If you are using a device we do not support, we want to know about it. Please [c ## Code style guide -We basically follow [PEP8](https://www.python.org/dev/peps/pep-0008/), but deviate in some points for - as we think - good reasons. If you have good reasons to stick strictly to PEP8 or even have good reasons to deviate from our deviation, feel free to convince us. +We set up [yapf](https://github.com/google/yapf) to format the code. We basically follow [PEP8](https://www.python.org/dev/peps/pep-0008/), but deviate in some points for - as we think - good reasons. If you have good reasons to stick strictly to PEP8 or even have good reasons to deviate from our deviation, feel free to convince us. We limit out lines to 127 characters, as that is maximum length still allowing code reviews on GitHub without horizontal scrolling. -As PEP8 allows to use extra blank lines sparingly to separate groups of related functions, we use an extra line between static methods and constructor, constructor and properties, properties and public methods, and public methods and internal methods. +We set up [flake8](https://flake8.pycqa.org/en/latest/), [mypy](http://mypy-lang.org/) and [pylint](https://www.pylint.org/) to lint the code. But again we accept good reasons not to follow the findings and even disable complete tests (e.g. E303 and W503). -We use double string quotes, except when the string contains double string quotes itself or when used as key of a dictionary. - -If you find code where we violated our own rules, feel free to [tell us](https://github.com/2Fake/devolo_home_control_api/issues). +A pre-commit hook should automatically help you following our guide. Additionally, a GitHub action will assist you. ## Testing -We cover our code with unit tests written in pytest, but we do not push them to hard. We want public methods covered, but we skip internal, nested and trivial methods. Often we also skip constructors. If you want to contribute, please make sure to keep the unit tests green and to deliver new ones, if you extend the functionality. +We cover our code with unit tests written in pytest, but we do not push them to hard. We want public methods covered, but we skip nested and trivial methods. Often we also skip constructors. If you want to contribute, please make sure to keep the unit tests green and to deliver new ones, if you extend the functionality. diff --git a/example.py b/example.py index feb8ed49..51fa5035 100644 --- a/example.py +++ b/example.py @@ -10,6 +10,7 @@ class Subscriber: + def __init__(self, name): self.name = name diff --git a/setup.cfg b/setup.cfg index 9af7e6f1..262d57a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,21 @@ -[aliases] -test=pytest \ No newline at end of file +[flake8] +max-cognitive-complexity=5 +max-line-length=127 +ignore=E303,W503 + +[isort] +ignore_whitespace=True +line_length=127 +multi_line_output=1 +order_by_type=True + +[mypy] +ignore_missing_imports=True + +[yapf] +BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF = True +COLUMN_LIMIT = 127 +FORCE_MULTILINE_DICT = True +SPLIT_ALL_COMMA_SEPARATED_VALUES = True +SPLIT_BEFORE_ARITHMETIC_OPERATOR = True +SPLIT_COMPLEX_COMPREHENSION = True diff --git a/setup.py b/setup.py index 4e204ec3..fb2d88c6 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,28 @@ -import setuptools - -from devolo_home_control_api import __version__ +import shlex +from subprocess import check_call +from setuptools import find_packages, setup +from setuptools.command.develop import develop with open("README.md", "r") as fh: long_description = fh.read() -setuptools.setup( + +# Create post develop command class for hooking into the python setup process +# This command will run after dependencies are installed +class PostDevelopCommand(develop): + + def run(self): + try: + check_call(shlex.split("pre-commit install")) + except Exception: + print("Unable to run 'pre-commit install'") + develop.run(self) + + +setup( name="devolo_home_control_api", - version=__version__, + use_scm_version=True, author="Markus Bong, Guido Schmitz", author_email="m.bong@famabo.de, guido.schmitz@fedaix.de", description="devolo Home Control API in Python", @@ -16,24 +30,30 @@ long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/2Fake/devolo_home_control_api", - packages=setuptools.find_packages(exclude=("tests*",)), + packages=find_packages(exclude=("tests*", + )), classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: OS Independent", ], install_requires=[ + "importlib-metadata;python_version<'3.8'", "requests", - "websocket_client", - "zeroconf" - ], - setup_requires=[ - "pytest-runner" - ], - tests_require=[ - "pytest", - "pytest-cov", - "pytest-mock" + "websocket_client>=0.58.0", + "zeroconf", ], - python_requires='>=3.6', + setup_requires=["setuptools_scm"], + extras_require={ + "dev": [ + "pre-commit", + ], + "test": [ + "pytest", + "pytest-cov", + "pytest-mock", + ], + }, + cmdclass={"develop": PostDevelopCommand}, + python_requires=">=3.6", ) diff --git a/tests/conftest.py b/tests/conftest.py index 1a33a5da..a71424f4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,20 +3,20 @@ import pytest - file = pathlib.Path(__file__).parent / "test_data.json" with file.open("r") as fh: test_data = json.load(fh) - -pytest_plugins = ['tests.fixtures.gateway', - 'tests.fixtures.homecontrol', - 'tests.fixtures.mprm', - 'tests.fixtures.mydevolo', - 'tests.fixtures.publisher', - 'tests.fixtures.requests', - 'tests.fixtures.socket', - 'tests.fixtures.updater'] +pytest_plugins = [ + 'tests.fixtures.gateway', + 'tests.fixtures.homecontrol', + 'tests.fixtures.mprm', + 'tests.fixtures.mydevolo', + 'tests.fixtures.publisher', + 'tests.fixtures.requests', + 'tests.fixtures.socket', + 'tests.fixtures.updater', +] @pytest.fixture(autouse=True) @@ -25,24 +25,3 @@ def test_data_fixture(request): request.cls.user = test_data['user'] request.cls.gateway = test_data['gateway'] request.cls.devices = test_data['devices'] - - -@pytest.fixture() -def fill_device_data(request): - """ Load test device data. """ - consumption_property = request.cls.homecontrol.devices[test_data['devices']['mains']['uid']].consumption_property - consumption_property[f"devolo.Meter:{test_data['devices']['mains']['uid']}"].current = 0.58 - consumption_property[f"devolo.Meter:{test_data['devices']['mains']['uid']}"].total = 125.68 - - binary_sensor_property = request.cls.homecontrol.devices[test_data['devices']['sensor']['uid']].binary_sensor_property - binary_sensor_property.get(f"devolo.BinarySensor:{test_data['devices']['sensor']['uid']}").state = False - - binary_switch_property = request.cls.homecontrol.devices[test_data['devices']['mains']['uid']].binary_switch_property - binary_switch_property.get(f"devolo.BinarySwitch:{test_data['devices']['mains']['uid']}").state = False - - humidity_bar_property = request.cls.homecontrol.devices[test_data['devices']['humidity']['uid']].humidity_bar_property - humidity_bar_property.get(f"devolo.HumidityBar:{test_data['devices']['humidity']['uid']}").zone = 1 - humidity_bar_property.get(f"devolo.HumidityBar:{test_data['devices']['humidity']['uid']}").value = 75 - - voltage_property = request.cls.homecontrol.devices[test_data['devices']['mains']['uid']].multi_level_sensor_property - voltage_property.get(f"devolo.VoltageMultiLevelSensor:{test_data['devices']['mains']['uid']}").current = 236 diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/gateway.py b/tests/fixtures/gateway.py index 280be5e1..4f06319c 100644 --- a/tests/fixtures/gateway.py +++ b/tests/fixtures/gateway.py @@ -11,5 +11,5 @@ def gateway_instance(request, mydevolo): @pytest.fixture() def mock_gateway(mocker): - """ Mock ony the constructor of a gateway instance. """ + """ Mock only the constructor of a gateway instance. """ mocker.patch("devolo_home_control_api.devices.gateway.Gateway.__init__", MockGateway.__init__) diff --git a/tests/fixtures/homecontrol.py b/tests/fixtures/homecontrol.py index be7de88d..9dabf7ab 100644 --- a/tests/fixtures/homecontrol.py +++ b/tests/fixtures/homecontrol.py @@ -7,8 +7,13 @@ @pytest.fixture() -def home_control_instance(request, mydevolo, mock_gateway, mock_mprmwebsocket_websocket_connection, - mock_inspect_devices_metering_plug, mock_mprmrest_get_all_zones, mock_mprm__detect_gateway_in_lan, +def home_control_instance(request, + mydevolo, + mock_gateway, + mock_mprmwebsocket_websocket_connection, + mock_inspect_devices_metering_plug, + mock_mprmrest_get_all_zones, + mock_mprm__detect_gateway_in_lan, mock_mprmwebsocket_get_remote_session): """ Create a mocked Home Control instance with static test data. """ request.cls.homecontrol = HomeControl(request.cls.gateway.get("id"), mydevolo_instance=mydevolo) diff --git a/tests/fixtures/mprm.py b/tests/fixtures/mprm.py index 99bde496..dbb12bb5 100644 --- a/tests/fixtures/mprm.py +++ b/tests/fixtures/mprm.py @@ -1,19 +1,16 @@ -import json - import pytest - import requests -from devolo_home_control_api.backend.mprm import Mprm + from devolo_home_control_api.backend.mprm_rest import MprmRest -from devolo_home_control_api.backend.mprm_websocket import MprmWebsocket from ..mocks.mock_gateway import MockGateway -from ..mocks.mock_mprm import MockMprm from ..mocks.mock_mprm_rest import try_local_connection from ..mocks.mock_service_browser import ServiceBrowser from ..mocks.mock_websocket import try_reconnect from ..mocks.mock_websocketapp import MockWebsocketapp from ..mocks.mock_zeroconf import Zeroconf +from ..stubs.mprm import StubMprm +from ..stubs.mprm_websocket import StubMprmWebsocket @pytest.fixture() @@ -39,7 +36,7 @@ def mock_mprm__detect_gateway_in_lan(mocker, request): @pytest.fixture() -def mock_mprm__try_local_connection(mocker, request): +def mock_mprm__try_local_connection(mocker): """ Mock finding gateway's IP. """ mocker.patch("devolo_home_control_api.backend.mprm.Mprm._try_local_connection", try_local_connection) @@ -65,10 +62,14 @@ def mock_mprmrest__extract_data_from_element_uid(mocker, request): """ Mock extracting device data. """ properties = { "test_fetch_binary_switch_state_valid_on": { - "properties": {"state": 1} + "properties": { + "state": 1 + } }, "test_fetch_binary_switch_state_valid_off": { - "properties": {"state": 0} + "properties": { + "state": 0 + } }, "test_fetch_consumption_valid": { "properties": { @@ -111,7 +112,7 @@ def mock_mprmrest__extract_data_from_element_uid(mocker, request): "totalValue": request.cls.devices.get("mains").get("properties").get("total_consumption") } }, - "test__inspect_device": None + "test__inspect_device": None, } mocker.patch("devolo_home_control_api.backend.mprm_rest.MprmRest.get_data_from_uid_list", @@ -121,69 +122,72 @@ def mock_mprmrest__extract_data_from_element_uid(mocker, request): @pytest.fixture() def mock_mprmrest__post(mocker, request): """ Mock getting properties from the mPRM. """ + test_case = request.node.name.split('[')[0] properties = { "test_get_name_and_element_uids": { "result": { - "items": [ - { - "properties": { - "itemName": "test_name", - "zone": "test_zone", - "batteryLevel": "test_battery", - "icon": "test_icon", - "elementUIDs": "test_element_uids", - "settingUIDs": "test_setting_uids", - "deviceModelUID": "test_device_model_uid", - "status": "test_status", - } + "items": [{ + "properties": { + "itemName": "test_name", + "zone": "test_zone", + "batteryLevel": "test_battery", + "icon": "test_icon", + "elementUIDs": "test_element_uids", + "settingUIDs": "test_setting_uids", + "deviceModelUID": "test_device_model_uid", + "status": "test_status", } - ] + }] } }, "test_get_data_from_uid_list": { - "result": {"items": [{"properties": {"itemName": "test_name"}}]} + "result": { + "items": [{ + "properties": { + "itemName": "test_name" + } + }] + } }, "test_get_all_devices": { - "result": {"items": [{"properties": {"deviceUIDs": "deviceUIDs"}}]} + "result": { + "items": [{ + "properties": { + "deviceUIDs": "deviceUIDs" + } + }] + } }, "test_get_all_zones": { "result": { - "items": [ - { - "properties": { - "zones": [ - { - "id": "hz_3", - "name": "Office" - } - ] - } + "items": [{ + "properties": { + "zones": [{ + "id": "hz_3", + "name": "Office" + }] } - ] + }] } - } - } - - mocker.patch("devolo_home_control_api.backend.mprm_rest.MprmRest.post", return_value=properties.get(request.node.name)) - - -@pytest.fixture() -def mock_mprmrest__post_set(mocker, request): - """ Mock setting values. """ - status = { - "test_set_valid": {"result": {"status": 1}}, - "test_set_binary_switch_error": {"result": {"status": 2}}, - "test_set_binary_switch_same": {"result": {"status": 3}}, + }, + "test_set_success": { + "result": { + "status": 1 + } + }, + "test_set_failed": { + "result": { + "status": 0 + } + }, + "test_set_doubled": { + "result": { + "status": 2 + } + }, } - mocker.patch("devolo_home_control_api.backend.mprm_rest.MprmRest.post", return_value=status.get(request.node.name)) - - -@pytest.fixture() -def mock_mprmwebsocket_get_local_session_json_decode_error(mocker): - """ Create an JSONDecodeError on getting a local session. """ - mocker.patch("devolo_home_control_api.backend.mprm_websocket.MprmWebsocket.get_local_session", - side_effect=json.JSONDecodeError("", "", 1)) + mocker.patch("devolo_home_control_api.backend.mprm_rest.MprmRest._post", return_value=properties[test_case]) @pytest.fixture() @@ -209,6 +213,7 @@ def mock_mprmwebsocket_try_reconnect(mocker): def mock_mprmwebsocket_websocketapp(mocker): """ Mock a websocket connection init. """ mocker.patch("websocket.WebSocketApp.__init__", MockWebsocketapp.__init__) + mocker.patch("websocket.WebSocketApp.close", MockWebsocketapp.close) mocker.patch("websocket.WebSocketApp.run_forever", MockWebsocketapp.run_forever) @@ -217,8 +222,7 @@ def mock_mprmwebsocket_websocket_connection(mocker, request): """ Mock a running websocket connection to speed up tests. """ mocker.patch("devolo_home_control_api.backend.mprm_websocket.MprmWebsocket.wait_for_websocket_establishment", return_value=False) - mocker.patch("devolo_home_control_api.backend.mprm_websocket.MprmWebsocket.websocket_connect", - return_value=None) + mocker.patch("devolo_home_control_api.backend.mprm_websocket.MprmWebsocket.websocket_connect", return_value=None) @pytest.fixture() @@ -228,17 +232,20 @@ def mock_mprmwebsocket_websocket_disconnect(mocker): @pytest.fixture() -def mprm_instance(request, mocker, mydevolo, mock_gateway, mock_inspect_devices_metering_plug, +def mprm_instance(request, + mocker, + mydevolo, + mock_gateway, + mock_inspect_devices_metering_plug, mock_mprm__detect_gateway_in_lan): """ Create a mocked mPRM instance with static test data. """ if "TestMprmRest" in request.node.nodeid: - request.cls.mprm = MprmRest(mydevolo_instance=mydevolo) + request.cls.mprm = MprmRest() elif "TestMprmWebsocket" in request.node.nodeid: - request.cls.mprm = MprmWebsocket(mydevolo_instance=mydevolo) + request.cls.mprm = StubMprmWebsocket() else: - mocker.patch("devolo_home_control_api.backend.mprm.Mprm.__init__", MockMprm.__init__) mocker.patch("devolo_home_control_api.backend.mprm_websocket.MprmWebsocket.websocket_connect", return_value=None) - request.cls.mprm = Mprm(mydevolo_instance=mydevolo) + request.cls.mprm = StubMprm() request.cls.mprm.gateway = MockGateway(request.cls.gateway.get("id"), mydevolo=mydevolo) diff --git a/tests/fixtures/mydevolo.py b/tests/fixtures/mydevolo.py index 78ede067..2e207ead 100644 --- a/tests/fixtures/mydevolo.py +++ b/tests/fixtures/mydevolo.py @@ -1,6 +1,5 @@ import pytest - -from devolo_home_control_api.mydevolo import Mydevolo, GatewayOfflineError, WrongCredentialsError, WrongUrlError +from devolo_home_control_api.mydevolo import Mydevolo, WrongCredentialsError, WrongUrlError from ..mocks.mock_mydevolo import MockMydevolo @@ -8,15 +7,9 @@ @pytest.fixture() def mydevolo(request): """ Create real mydevolo object with static test data. """ - mydevolo = Mydevolo() - mydevolo._uuid = request.cls.user.get("uuid") - yield mydevolo - - -@pytest.fixture() -def mock_mydevolo_full_url(mocker): - """ Mock getting a gateway's full URL. """ - mocker.patch("devolo_home_control_api.mydevolo.Mydevolo.get_full_url", side_effect=MockMydevolo.get_full_url) + mydevolo_instance = Mydevolo() + mydevolo_instance._uuid = request.cls.user.get("uuid") + yield mydevolo_instance @pytest.fixture() @@ -24,28 +17,22 @@ def mock_mydevolo__call(mocker, request): """ Mock calls to the mydevolo API. """ mock_mydevolo = MockMydevolo(request) mocker.patch("devolo_home_control_api.mydevolo.Mydevolo._call", side_effect=mock_mydevolo._call) - del mock_mydevolo @pytest.fixture() -def mock_mydevolo__call_raise_GatewayOfflineError(mocker): - """ Respond with GatewayOfflineError on calls to the mydevolo API. """ - mocker.patch("devolo_home_control_api.mydevolo.Mydevolo._call", side_effect=GatewayOfflineError) +def mock_mydevolo__call_raise_WrongUrlError(mocker): + """ Respond with WrongUrlError on calls to the mydevolo API. """ + mocker.patch("devolo_home_control_api.mydevolo.Mydevolo._call", side_effect=WrongUrlError) @pytest.fixture() -def mock_mydevolo__call_raise_WrongCredentialsError(mocker): +def mock_mydevolo_uuid_raise_WrongCredentialsError(mocker): """ Respond with WrongCredentialsError on calls to the mydevolo API. """ mocker.patch("devolo_home_control_api.mydevolo.Mydevolo.uuid", side_effect=WrongCredentialsError) -@pytest.fixture() -def mock_mydevolo__call_raise_WrongUrlError(mocker): - """ Respond with WrongUrlError on calls to the mydevolo API. """ - mocker.patch("devolo_home_control_api.mydevolo.Mydevolo._call", side_effect=WrongUrlError) - - @pytest.fixture() def mock_get_zwave_products(mocker): """ Mock Z-Wave product information call to speed up tests. """ - mocker.patch("devolo_home_control_api.mydevolo.Mydevolo.get_zwave_products", return_value={}) + mocker.patch("devolo_home_control_api.mydevolo.Mydevolo.get_zwave_products", + return_value={}) diff --git a/tests/fixtures/publisher.py b/tests/fixtures/publisher.py index 646a9727..9c96d514 100644 --- a/tests/fixtures/publisher.py +++ b/tests/fixtures/publisher.py @@ -1,6 +1,18 @@ import pytest +@pytest.fixture() +def mock_publisher_add_event(mocker): + """ Mock add event function to keep tests independent from a real event. """ + mocker.patch("devolo_home_control_api.publisher.publisher.Publisher.add_event", return_value=None) + + +@pytest.fixture() +def mock_publisher_delete_event(mocker): + """ Mock delete event function to keep tests independent from a real event. """ + mocker.patch("devolo_home_control_api.publisher.publisher.Publisher.delete_event", return_value=None) + + @pytest.fixture() def mock_publisher_dispatch(mocker): """ Mock dispatch function to keep tests independent from a real subscriber. """ diff --git a/tests/fixtures/requests.py b/tests/fixtures/requests.py index bf0f8e49..28c79f18 100644 --- a/tests/fixtures/requests.py +++ b/tests/fixtures/requests.py @@ -1,7 +1,9 @@ import pytest -from ..mocks.mock_response import (MockResponseConnectTimeout, MockResponseGet, - MockResponseJsonError, MockResponsePost, +from ..mocks.mock_response import (MockResponseConnectTimeout, + MockResponseGet, + MockResponseJsonError, + MockResponsePost, MockResponseReadTimeout, MockResponseServiceUnavailable) @@ -9,66 +11,92 @@ @pytest.fixture() def mock_response_gateway_offline(mocker): """ Mock requests get method with service_unavailable status_code. """ - mocker.patch("requests.get", return_value=MockResponseServiceUnavailable({"link": "test_link"}, status_code=503)) + mocker.patch("requests.get", + return_value=MockResponseServiceUnavailable({"link": "test_link"}, + status_code=503)) @pytest.fixture() def mock_response_json(mocker): """ Mock requests.Session get method with success status_code. """ - mocker.patch("requests.Session", return_value=MockResponseGet({"link": "test_link"}, status_code=200)) + mocker.patch("requests.Session", + return_value=MockResponseGet({"link": "test_link"}, + status_code=200)) @pytest.fixture() def mock_response_requests_ConnectTimeout(mocker): """ Mock requests.Session with ConnectTimeout exception. """ - mocker.patch("requests.Session", return_value=MockResponseConnectTimeout({"link": "test_link"}, status_code=200)) + mocker.patch("requests.Session", + return_value=MockResponseConnectTimeout({"link": "test_link"}, + status_code=200)) @pytest.fixture() def mock_response_json_JSONDecodeError(mocker): """ Mock requests.Session with JSONDecodeError exception. """ - mocker.patch("requests.Session", return_value=MockResponseJsonError({"link": "test_link"}, status_code=200)) + mocker.patch("requests.Session", + return_value=MockResponseJsonError({"link": "test_link"}, + status_code=200)) @pytest.fixture() def mock_response_requests_invalid_id(mocker): """ Mock requests.Session with JSONDecodeError exception. """ - mocker.patch("requests.Session", return_value=MockResponsePost({"link": "test_link"}, status_code=200)) + mocker.patch("requests.Session", + return_value=MockResponsePost({"link": "test_link"}, + status_code=200)) @pytest.fixture() def mock_response_requests_valid(mocker): """ Mock requests.Session post method with success status_code. """ - mocker.patch("requests.Session", return_value=MockResponsePost({"link": "test_link"}, status_code=200)) + mocker.patch("requests.Session", + return_value=MockResponsePost({"link": "test_link"}, + status_code=200)) @pytest.fixture() def mock_response_valid(mocker): """ Mock requests get method with success status_code. """ - mocker.patch("requests.get", return_value=MockResponseGet({"response": "response"}, status_code=200)) + mocker.patch("requests.get", + return_value=MockResponseGet({"response": "response"}, + status_code=200)) @pytest.fixture() def mock_response_wrong_credentials_error(mocker): """ Mock requests get method with forbidden status_code. """ - mocker.patch("requests.get", return_value=MockResponseGet({"link": "test_link"}, status_code=403)) + mocker.patch("requests.get", + return_value=MockResponseGet({"link": "test_link"}, + status_code=403)) @pytest.fixture() def mock_response_wrong_url_error(mocker): """ Mock requests get method with notfound status_code. """ - mocker.patch("requests.get", return_value=MockResponseGet({"link": "test_link"}, status_code=404)) + mocker.patch("requests.get", + return_value=MockResponseGet({"link": "test_link"}, + status_code=404)) @pytest.fixture() def mock_response_requests_ReadTimeout(mocker): """ Mock requests.Session with ReadTimeout exception. """ - mocker.patch("requests.Session", return_value=MockResponseReadTimeout({"link": "test_link"}, status_code=200)) + mocker.patch("requests.Session", + return_value=MockResponseReadTimeout({"link": "test_link"}, + status_code=200)) @pytest.fixture() def mock_session_get(mocker, request): """ Mock requests.Session get method with test data. """ - properties = {'test_get_local_session_valid': {'link': 'test_link'}, - 'test__on_pong': {'link': 'test_link'}} + properties = { + 'test_get_local_session_valid': { + 'link': 'test_link' + }, + 'test__on_pong': { + 'link': 'test_link' + }, + } mocker.patch("requests.Session.get", return_value=properties.get(request.node.name)) diff --git a/tests/mocks/__init__.py b/tests/mocks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/mocks/mock_dummy_device.py b/tests/mocks/mock_dummy_device.py index f1a26e6d..8a5c8c40 100644 --- a/tests/mocks/mock_dummy_device.py +++ b/tests/mocks/mock_dummy_device.py @@ -1,15 +1,11 @@ import json import pathlib -import requests - -from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.properties.binary_switch_property import BinarySwitchProperty from devolo_home_control_api.properties.settings_property import SettingsProperty -from .mock_gateway import MockGateway - def dummy_device(key: str) -> Zwave: """ @@ -24,26 +20,23 @@ def dummy_device(key: str) -> Zwave: mydevolo = Mydevolo() device = Zwave(mydevolo_instance=mydevolo, **test_data['devices'][key]) - gateway = MockGateway(test_data['gateway']['id'], mydevolo=mydevolo) - session = requests.Session() device.binary_switch_property = {} - device.binary_switch_property[f"devolo.BinarySwitch:{test_data['devices'][key]['uid']}"] = \ - BinarySwitchProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"devolo.BinarySwitch:{test_data['devices'][key]['uid']}", - state=test_data['devices'][key]['state'], - enabled=test_data['devices'][key]['guiEnabled']) + device.binary_switch_property[f"devolo.BinarySwitch:{test_data['devices'][key]['uid']}"] = BinarySwitchProperty( + element_uid=f"devolo.BinarySwitch:{test_data['devices'][key]['uid']}", + setter=lambda uid, + state: None, + state=test_data['devices'][key]['state'], + enabled=test_data['devices'][key]['guiEnabled']) device.settings_property = {} - device.settings_property["general_device_settings"] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"gds.{test_data['devices'][key]['uid']}", - icon=test_data['devices'][key]['icon'], - name=test_data['devices'][key]['itemName'], - zone_id=test_data['devices'][key]['zoneId']) + device.settings_property["general_device_settings"] = SettingsProperty( + element_uid=f"gds.{test_data['devices'][key]['uid']}", + setter=lambda uid, + state: None, + icon=test_data['devices'][key]['icon'], + name=test_data['devices'][key]['itemName'], + zone_id=test_data['devices'][key]['zoneId'], + zones=test_data.get("gateway").get("zones")) return device diff --git a/tests/mocks/mock_gateway.py b/tests/mocks/mock_gateway.py index c90945cd..2a1ad771 100644 --- a/tests/mocks/mock_gateway.py +++ b/tests/mocks/mock_gateway.py @@ -5,6 +5,7 @@ class MockGateway: + def __init__(self, gateway_id: str, mydevolo: Mydevolo): file = pathlib.Path(__file__).parent / ".." / "test_data.json" with file.open("r") as fh: @@ -23,6 +24,5 @@ def __init__(self, gateway_id: str, mydevolo: Mydevolo): self.online = True self.zones = test_data.get("gateway").get("zones") - def update_state(self, online: bool): self.online = online diff --git a/tests/mocks/mock_homecontrol.py b/tests/mocks/mock_homecontrol.py index 8e706fde..536eeec4 100644 --- a/tests/mocks/mock_homecontrol.py +++ b/tests/mocks/mock_homecontrol.py @@ -16,16 +16,17 @@ def mock__inspect_devices(self, devices): with file.open("r") as fh: test_data = json.load(fh) - mapping = {"blinds": shutter, - "humidity": humidity_sensor_device, - "mains": metering_plug, - "multi_level_switch": multi_level_switch_device, - "remote": remote_control, - "sensor": multi_level_sensor_device, - "siren": siren} + mapping = { + "blinds": shutter, + "humidity": humidity_sensor_device, + "mains": metering_plug, + "multi_level_switch": multi_level_switch_device, + "remote": remote_control, + "sensor": multi_level_sensor_device, + "siren": siren, + } for device_type, device in test_data['devices'].items(): device_uid = device['uid'] - self.devices[device_uid] = mapping.get(device_type, dummy_device)( - device_uid if device_type in mapping else device_type - ) + self.devices[device_uid] = mapping.get(device_type, + dummy_device)(device_uid if device_type in mapping else device_type) diff --git a/tests/mocks/mock_humidity_sensor_device.py b/tests/mocks/mock_humidity_sensor_device.py index edcfa4d7..89672829 100644 --- a/tests/mocks/mock_humidity_sensor_device.py +++ b/tests/mocks/mock_humidity_sensor_device.py @@ -1,17 +1,13 @@ import json import pathlib -import requests - -from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.properties.binary_sensor_property import BinarySensorProperty from devolo_home_control_api.properties.humidity_bar_property import HumidityBarProperty from devolo_home_control_api.properties.multi_level_sensor_property import MultiLevelSensorProperty from devolo_home_control_api.properties.settings_property import SettingsProperty -from .mock_gateway import MockGateway - def humidity_sensor_device(device_uid: str) -> Zwave: """ @@ -26,46 +22,35 @@ def humidity_sensor_device(device_uid: str) -> Zwave: mydevolo = Mydevolo() device = Zwave(mydevolo_instance=mydevolo, **test_data.get("devices").get("humidity")) - gateway = MockGateway(test_data.get("gateway").get("id"), mydevolo=mydevolo) - session = requests.Session() device.binary_sensor_property = {} device.humidity_bar_property = {} device.multi_level_sensor_property = {} device.settings_property = {} - element_uid = f'devolo.DewpointSensor:{device_uid}' - device.multi_level_sensor_property[element_uid] = \ - MultiLevelSensorProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=element_uid, - value=test_data.get("devices").get("humidity").get("dewpoint")) - - element_uid = f'devolo.MildewSensor:{device_uid}' - device.binary_sensor_property[element_uid] = \ - BinarySensorProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=element_uid, - state=test_data.get("devices").get("humidity").get("mildew")) - - element_uid = f'devolo.HumidityBar:{device_uid}' - device.humidity_bar_property[element_uid] = \ - HumidityBarProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=element_uid, - zone=test_data.get("devices").get("humidity").get("zone"), - value=test_data.get("devices").get("humidity").get("value")) - - device.settings_property["general_device_settings"] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f'gds.{test_data.get("devices").get("humidity").get("uid")}', - icon=test_data.get("devices").get("humidity").get("icon"), - name=test_data.get("devices").get("humidity").get("itemName"), - zone_id=test_data.get("devices").get("humidity").get("zoneId")) + element_uid = f"devolo.DewpointSensor:{device_uid}" + device.multi_level_sensor_property[element_uid] = MultiLevelSensorProperty( + element_uid=element_uid, + value=test_data.get("devices").get("humidity").get("dewpoint")) + + element_uid = f"devolo.MildewSensor:{device_uid}" + device.binary_sensor_property[element_uid] = BinarySensorProperty( + element_uid=element_uid, + state=test_data.get("devices").get("humidity").get("mildew")) + + element_uid = f"devolo.HumidityBar:{device_uid}" + device.humidity_bar_property[element_uid] = HumidityBarProperty( + element_uid=element_uid, + zone=test_data.get("devices").get("humidity").get("zone"), + value=test_data.get("devices").get("humidity").get("value")) + + device.settings_property['general_device_settings'] = SettingsProperty( + element_uid=f'gds.{test_data.get("devices").get("humidity").get("uid")}', + setter=lambda uid, + state: None, + icon=test_data.get("devices").get("humidity").get("icon"), + name=test_data.get("devices").get("humidity").get("itemName"), + zone_id=test_data.get("devices").get("humidity").get("zoneId"), + zones=test_data.get("gateway").get("zones")) return device diff --git a/tests/mocks/mock_metering_plug.py b/tests/mocks/mock_metering_plug.py index e8852122..5f641b73 100644 --- a/tests/mocks/mock_metering_plug.py +++ b/tests/mocks/mock_metering_plug.py @@ -1,17 +1,13 @@ import json import pathlib -import requests - -from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.properties.binary_switch_property import BinarySwitchProperty from devolo_home_control_api.properties.consumption_property import ConsumptionProperty from devolo_home_control_api.properties.multi_level_sensor_property import MultiLevelSensorProperty from devolo_home_control_api.properties.settings_property import SettingsProperty -from .mock_gateway import MockGateway - def metering_plug(device_uid: str) -> Zwave: """ @@ -26,61 +22,52 @@ def metering_plug(device_uid: str) -> Zwave: mydevolo = Mydevolo() device = Zwave(mydevolo_instance=mydevolo, **test_data['devices']['mains']['properties']) - gateway = MockGateway(test_data['gateway']['id'], mydevolo=mydevolo) - session = requests.Session() device.binary_switch_property = {} device.consumption_property = {} device.multi_level_sensor_property = {} device.settings_property = {} - device.binary_switch_property[f'devolo.BinarySwitch:{device_uid}'] = \ - BinarySwitchProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"devolo.BinarySwitch:{device_uid}", - state=test_data['devices']['mains']['properties']['state'], - enabled=test_data['devices']['mains']['properties']['guiEnabled']) - device.consumption_property[f'devolo.Meter:{device_uid}'] = \ - ConsumptionProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"devolo.Meter:{device_uid}", - current=test_data['devices']['mains']['properties']['current_consumption'], - total=test_data['devices']['mains']['properties']['total_consumption'], - total_since=test_data['devices']['mains']['properties']['sinceTime']) + device.binary_switch_property[f'devolo.BinarySwitch:{device_uid}'] = BinarySwitchProperty( + element_uid=f"devolo.BinarySwitch:{device_uid}", + setter=lambda uid, + state: None, + state=test_data['devices']['mains']['properties']['state'], + enabled=test_data['devices']['mains']['properties']['guiEnabled']) + device.consumption_property[f'devolo.Meter:{device_uid}'] = ConsumptionProperty( + element_uid=f"devolo.Meter:{device_uid}", + setter=lambda uid, + state: None, + current=test_data['devices']['mains']['properties']['current_consumption'], + total=test_data['devices']['mains']['properties']['total_consumption'], + total_since=test_data['devices']['mains']['properties']['sinceTime']) device.multi_level_sensor_property[f'devolo.VoltageMultiLevelSensor:{device_uid}'] = \ - MultiLevelSensorProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"devolo.VoltageMultiLevelSensor:{device_uid}", + MultiLevelSensorProperty(element_uid=f"devolo.VoltageMultiLevelSensor:{device_uid}", current=test_data['devices']['mains']['properties']['voltage']) - device.settings_property["param_changed"] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"cps.{device_uid}") - device.settings_property['general_device_settings'] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"gds.{device_uid}", - events_enabled=test_data['devices']['mains']['properties']['eventsEnabled'], - icon=test_data['devices']['mains']['properties']['icon'], - name=test_data['devices']['mains']['properties']['itemName'], - zone_id=test_data['devices']['mains']['properties']['zoneId']) - device.settings_property["led"] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"lis.{device_uid}", - led_setting=test_data['devices']['mains']['properties']['led_setting']) + device.settings_property["param_changed"] = SettingsProperty(element_uid=f"cps.{device_uid}", + setter=lambda uid, + state: None) + device.settings_property['general_device_settings'] = SettingsProperty( + element_uid=f"gds.{device_uid}", + setter=lambda uid, + state: True, + events_enabled=test_data['devices']['mains']['properties']['eventsEnabled'], + icon=test_data['devices']['mains']['properties']['icon'], + name=test_data['devices']['mains']['properties']['itemName'], + zone_id=test_data['devices']['mains']['properties']['zoneId'], + zones=test_data.get("gateway").get("zones")) + device.settings_property["led"] = SettingsProperty(element_uid=f"lis.{device_uid}", + setter=lambda uid, + state: True, + led_setting=test_data['devices']['mains']['properties']['led_setting']) device.settings_property["protection"] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"ps.{device_uid}", + SettingsProperty(element_uid=f"ps.{device_uid}", + setter=lambda uid, state: True, local_switching=test_data['devices']['mains']['properties']['local_switch'], remote_switching=test_data['devices']['mains']['properties']['remote_switch']) + device.settings_property['flash_mode'] = SettingsProperty(element_uid=f"mas.{device_uid}", + setter=lambda uid, + state: None, + valus=test_data['devices']['mains']['flashMode']) return device diff --git a/tests/mocks/mock_mprm.py b/tests/mocks/mock_mprm.py deleted file mode 100644 index 96881bdd..00000000 --- a/tests/mocks/mock_mprm.py +++ /dev/null @@ -1,7 +0,0 @@ -from devolo_home_control_api.backend.mprm_websocket import MprmWebsocket - - -class MockMprm(MprmWebsocket): - def __init__(self, mydevolo_instance): - super(MprmWebsocket, self).__init__(mydevolo_instance=mydevolo_instance) - self.detect_gateway_in_lan(None) diff --git a/tests/mocks/mock_mprm_rest.py b/tests/mocks/mock_mprm_rest.py index 50d35d25..e7e24e15 100644 --- a/tests/mocks/mock_mprm_rest.py +++ b/tests/mocks/mock_mprm_rest.py @@ -1,7 +1,6 @@ import json import pathlib - file = pathlib.Path(__file__).parent / ".." / "test_data.json" with file.open("r") as fh: test_data = json.load(fh) @@ -15,5 +14,9 @@ def mock_get_data_from_uid_list(self, uids): if len(uids) == 1: return [test_data["devices"]["mains"]] else: - return [{"UID": test_data["devices"]["mains"]["settingUIDs"][0], - "properties": {"settings": test_data["devices"]["mains"]["properties"]}}] + return [{ + "UID": test_data["devices"]["mains"]["settingUIDs"][0], + "properties": { + "settings": test_data["devices"]["mains"]["properties"] + }, + }] diff --git a/tests/mocks/mock_multi_level_sensor_device.py b/tests/mocks/mock_multi_level_sensor_device.py index 574c5a3d..a6f51c2c 100644 --- a/tests/mocks/mock_multi_level_sensor_device.py +++ b/tests/mocks/mock_multi_level_sensor_device.py @@ -1,16 +1,12 @@ import json import pathlib -import requests - -from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.properties.binary_sensor_property import BinarySensorProperty from devolo_home_control_api.properties.multi_level_sensor_property import MultiLevelSensorProperty from devolo_home_control_api.properties.settings_property import SettingsProperty -from .mock_gateway import MockGateway - def multi_level_sensor_device(device_uid: str) -> Zwave: """ @@ -25,49 +21,39 @@ def multi_level_sensor_device(device_uid: str) -> Zwave: mydevolo = Mydevolo() device = Zwave(mydevolo_instance=mydevolo, **test_data.get("devices").get("sensor")) - gateway = MockGateway(test_data.get("gateway").get("id"), mydevolo=mydevolo) - session = requests.Session() device.binary_sensor_property = {} device.multi_level_sensor_property = {} device.settings_property = {} - element_uid = f'devolo.BinarySensor:{device_uid}' - device.binary_sensor_property[element_uid] = \ - BinarySensorProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=element_uid, - state=test_data.get("devices").get("sensor").get("state")) - - element_uid = f'devolo.MultiLevelSensor:{device_uid}#MultilevelSensor(1)' - device.multi_level_sensor_property[element_uid] = \ - MultiLevelSensorProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=element_uid, - value=test_data.get("devices").get("sensor").get("value"), - unit=test_data.get("devices").get("sensor").get("unit")) - - device.settings_property['temperature_report'] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"trs.{device_uid}", - temp_report=test_data.get("devices").get("sensor").get("temp_report")) - device.settings_property['motion_sensitivity'] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"mss.{device_uid}", - motion_sensitivity=test_data.get("devices").get("sensor").get("motion_sensitivity")) - device.settings_property["general_device_settings"] = \ - SettingsProperty(gateway=gateway, - session=session, - element_uid=f'gds.{device_uid}', - mydevolo=mydevolo, - icon=test_data.get("devices").get("sensor").get("icon"), - name=test_data.get("devices").get("sensor").get("itemName"), - zone_id=test_data.get("devices").get("sensor").get("zoneId")) + element_uid = f"devolo.BinarySensor:{device_uid}" + device.binary_sensor_property[element_uid] = BinarySensorProperty( + element_uid=element_uid, + state=test_data.get("devices").get("sensor").get("state")) + + element_uid = f"devolo.MultiLevelSensor:{device_uid}#MultilevelSensor(1)" + device.multi_level_sensor_property[element_uid] = MultiLevelSensorProperty( + element_uid=element_uid, + value=test_data.get("devices").get("sensor").get("value"), + unit=test_data.get("devices").get("sensor").get("unit")) + + device.settings_property['temperature_report'] = SettingsProperty( + element_uid=f"trs.{device_uid}", + setter=lambda uid, + state: True, + temp_report=test_data.get("devices").get("sensor").get("temp_report")) + device.settings_property['motion_sensitivity'] = SettingsProperty( + element_uid=f"mss.{device_uid}", + setter=lambda uid, + state: True, + motion_sensitivity=test_data.get("devices").get("sensor").get("motion_sensitivity")) + device.settings_property["general_device_settings"] = SettingsProperty( + element_uid=f'gds.{device_uid}', + setter=lambda uid, + state: None, + icon=test_data.get("devices").get("sensor").get("icon"), + name=test_data.get("devices").get("sensor").get("itemName"), + zone_id=test_data.get("devices").get("sensor").get("zoneId"), + zones=test_data.get("gateway").get("zones")) return device diff --git a/tests/mocks/mock_multi_level_switch_device.py b/tests/mocks/mock_multi_level_switch_device.py index fc7d8546..0eb28677 100644 --- a/tests/mocks/mock_multi_level_switch_device.py +++ b/tests/mocks/mock_multi_level_switch_device.py @@ -1,15 +1,11 @@ import json import pathlib -import requests - -from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.properties.multi_level_switch_property import MultiLevelSwitchProperty from devolo_home_control_api.properties.settings_property import SettingsProperty -from .mock_gateway import MockGateway - def multi_level_switch_device(device_uid: str) -> Zwave: """ @@ -24,28 +20,25 @@ def multi_level_switch_device(device_uid: str) -> Zwave: mydevolo = Mydevolo() device = Zwave(mydevolo_instance=mydevolo, **test_data.get("devices").get("multi_level_switch")) - gateway = MockGateway(test_data.get("gateway").get("id"), mydevolo=mydevolo) - session = requests.Session() device.multi_level_switch_property = {} device.settings_property = {} - device.multi_level_switch_property[f'devolo.MultiLevelSwitch:{device_uid}'] = \ - MultiLevelSwitchProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"devolo.MultiLevelSwitch:{device_uid}", - value=test_data.get("devices").get("multi_level_switch").get("value"), - max=test_data.get("devices").get("multi_level_switch").get("max"), - min=test_data.get("devices").get("multi_level_switch").get("min")) - - device.settings_property["general_device_settings"] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f'gds.{device_uid}', - icon=test_data.get("devices").get("multi_level_switch").get("icon"), - name=test_data.get("devices").get("multi_level_switch").get("itemName"), - zone_id=test_data.get("devices").get("multi_level_switch").get("zoneId")) + device.multi_level_switch_property[f'devolo.MultiLevelSwitch:{device_uid}'] = MultiLevelSwitchProperty( + element_uid=f"devolo.MultiLevelSwitch:{device_uid}", + setter=lambda uid, + state: None, + value=test_data.get("devices").get("multi_level_switch").get("value"), + max=test_data.get("devices").get("multi_level_switch").get("max"), + min=test_data.get("devices").get("multi_level_switch").get("min")) + + device.settings_property['general_device_settings'] = SettingsProperty( + element_uid=f"gds.{device_uid}", + setter=lambda uid, + state: None, + icon=test_data.get("devices").get("multi_level_switch").get("icon"), + name=test_data.get("devices").get("multi_level_switch").get("itemName"), + zone_id=test_data.get("devices").get("multi_level_switch").get("zoneId"), + zones=test_data.get("gateway").get("zones")) return device diff --git a/tests/mocks/mock_mydevolo.py b/tests/mocks/mock_mydevolo.py index 26a69760..bc9ecb11 100644 --- a/tests/mocks/mock_mydevolo.py +++ b/tests/mocks/mock_mydevolo.py @@ -1,7 +1,4 @@ class MockMydevolo: - @staticmethod - def get_full_url(gateway_id): - return gateway_id def __init__(self, request): self._request = request @@ -12,22 +9,31 @@ def _call(self, url): full_url = self._request.cls.gateway.get("full_url") response = { - f'https://www.mydevolo.com/v1/users/{uuid}/hc/gateways/{gateway_id}/fullURL': { + f"https://www.mydevolo.com/v1/users/{uuid}/hc/gateways/{gateway_id}/fullURL": { "url": full_url }, - 'https://www.mydevolo.com/v1/users/uuid': {"uuid": uuid}, - f'https://www.mydevolo.com/v1/users/{uuid}/hc/gateways/status': - {"items": []} if self._request.node.name == "test_gateway_ids_empty" - else {"items": [{"gatewayId": gateway_id}]}, - f'https://www.mydevolo.com/v1/users/{uuid}/hc/gateways/{gateway_id}': { + "https://www.mydevolo.com/v1/users/uuid": { + "uuid": uuid + }, + f"https://www.mydevolo.com/v1/users/{uuid}/hc/gateways/status": { + "items": [] + } if self._request.node.name == "test_gateway_ids_empty" else { + "items": [{ + "gatewayId": gateway_id + }] + }, + f"https://www.mydevolo.com/v1/users/{uuid}/hc/gateways/{gateway_id}": { "gatewayId": gateway_id, "localPasskey": "abcde", "status": "devolo.hc_gateway.status.online", "state": "devolo.hc_gateway.state.idle", }, - 'https://www.mydevolo.com/v1/hc/maintenance': - {"state": "off"} if self._request.node.name == "test_maintenance_off" else {"state": "on"}, - 'https://www.mydevolo.com/v1/zwave/products/0x0060/0x0001/0x000': { + "https://www.mydevolo.com/v1/hc/maintenance": { + "state": "off" + } if self._request.node.name == "test_maintenance[True]" else { + "state": "on" + }, + "https://www.mydevolo.com/v1/zwave/products/0x0060/0x0001/0x000": { "brand": "Everspring", "deviceType": "Door Lock Keypad", "genericDeviceClass": "Entry Control", @@ -39,7 +45,7 @@ def _call(self, url): "productTypeId": "0x0001", "zwaveVersion": "6.51.07", }, - 'https://www.mydevolo.com/v1/zwave/products/0x0175/0x0001/0x0011': { + "https://www.mydevolo.com/v1/zwave/products/0x0175/0x0001/0x0011": { "manufacturerId": "0x0175", "productTypeId": "0x0001", "productId": "0x0011", diff --git a/tests/mocks/mock_remote_control.py b/tests/mocks/mock_remote_control.py index b3059c52..f4e8be0b 100644 --- a/tests/mocks/mock_remote_control.py +++ b/tests/mocks/mock_remote_control.py @@ -1,15 +1,11 @@ import json import pathlib -import requests - -from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.properties.remote_control_property import RemoteControlProperty from devolo_home_control_api.properties.settings_property import SettingsProperty -from .mock_gateway import MockGateway - def remote_control(device_uid: str) -> Zwave: """ @@ -24,35 +20,28 @@ def remote_control(device_uid: str) -> Zwave: mydevolo = Mydevolo() device = Zwave(mydevolo_instance=mydevolo, **test_data['devices']['remote']) - gateway = MockGateway(test_data['gateway']['id'], mydevolo=mydevolo) - session = requests.Session() device.remote_control_property = {} device.settings_property = {} - device.remote_control_property[f'devolo.RemoteControl:{device_uid}'] = \ - RemoteControlProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f'devolo.RemoteControl:{device_uid}', - key_count=test_data['devices']['remote']['key_count'], - key_pressed=0) - - device.settings_property["general_device_settings"] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f'gds.{device_uid}', - icon=test_data['devices']['remote']['icon'], - name=test_data['devices']['remote']['itemName'], - zone_id=test_data['devices']['remote']['zoneId']) - - - device.settings_property["switch_type"] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f'sts.{device_uid}', - value=test_data['devices']['remote']['key_count']) + device.remote_control_property[f'devolo.RemoteControl:{device_uid}'] = RemoteControlProperty( + element_uid=f"devolo.RemoteControl:{device_uid}", + setter=lambda uid, + state: True, + key_count=test_data['devices']['remote']['key_count'], + key_pressed=0) + + device.settings_property["general_device_settings"] = SettingsProperty(element_uid=f"gds.{device_uid}", + setter=lambda uid, + state: None, + icon=test_data['devices']['remote']['icon'], + name=test_data['devices']['remote']['itemName'], + zone_id=test_data['devices']['remote']['zoneId'], + zones=test_data['gateway']['zones']) + + device.settings_property["switch_type"] = SettingsProperty(element_uid=f"sts.{device_uid}", + setter=lambda uid, + state: None, + value=test_data['devices']['remote']['key_count']) return device diff --git a/tests/mocks/mock_response.py b/tests/mocks/mock_response.py index 0ff3093d..d0ec1bbd 100644 --- a/tests/mocks/mock_response.py +++ b/tests/mocks/mock_response.py @@ -1,9 +1,10 @@ from json import JSONDecodeError -from requests import ConnectTimeout, ReadTimeout +from requests.exceptions import ConnectTimeout, ReadTimeout class MockResponse: + def __init__(self, json_data, status_code): self.json_data = json_data self.status_code = status_code @@ -11,39 +12,50 @@ def __init__(self, json_data, status_code): class MockResponseConnectTimeout(MockResponse): + def get(self, url, auth=None, timeout=None): raise ConnectTimeout class MockResponseGet(MockResponse): + def get(self, url, auth=None, timeout=None): - return MockResponseGet({"link": "test_link"}, status_code=200) + return MockResponseGet({"link": "test_link"}, + status_code=200) def json(self): return self.json_data class MockResponseJsonError(MockResponse): + def get(self, url, auth=None, timeout=None): raise JSONDecodeError(msg="message", doc="doc", pos=1) class MockResponsePost(MockResponse): + def post(self, url, auth=None, timeout=None, headers=None, data=None): - return MockResponsePost({"link": "test_link"}, status_code=200) + return MockResponsePost({"link": "test_link"}, + status_code=200) def json(self): - return {"id": 2} + return { + "id": 2 + } class MockResponseReadTimeout(MockResponse): + def post(self, url, auth=None, timeout=None, headers=None, data=None): raise ReadTimeout class MockResponseServiceUnavailable(MockResponse): + def get(self, url, auth=None, timeout=None): - return MockResponseGet({"link": "test_link"}, status_code=503) + return MockResponseGet({"link": "test_link"}, + status_code=503) def json(self): return self.json_data diff --git a/tests/mocks/mock_service_browser.py b/tests/mocks/mock_service_browser.py index 47b59548..d1a6b58b 100644 --- a/tests/mocks/mock_service_browser.py +++ b/tests/mocks/mock_service_browser.py @@ -1,4 +1,5 @@ class ServiceBrowser: + def __init__(self, *args, **kwargs): pass diff --git a/tests/mocks/mock_shutter.py b/tests/mocks/mock_shutter.py index 0aff54ff..627213f0 100644 --- a/tests/mocks/mock_shutter.py +++ b/tests/mocks/mock_shutter.py @@ -1,15 +1,11 @@ import json import pathlib -import requests - -from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.properties.multi_level_switch_property import MultiLevelSwitchProperty from devolo_home_control_api.properties.settings_property import SettingsProperty -from .mock_gateway import MockGateway - def shutter(device_uid: str) -> Zwave: """ @@ -24,56 +20,47 @@ def shutter(device_uid: str) -> Zwave: mydevolo = Mydevolo() device = Zwave(mydevolo_instance=mydevolo, **test_data['devices']['blinds']) - gateway = MockGateway(test_data['gateway']['id'], mydevolo=mydevolo) - session = requests.Session() device.multi_level_switch_property = {} device.settings_property = {} - device.multi_level_switch_property[f'devolo.Blinds:{device_uid}'] = \ - MultiLevelSwitchProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"devolo.Blinds:{device_uid}", - value=test_data['devices']['blinds']['value'], - max=test_data['devices']['blinds']['max'], - min=test_data['devices']['blinds']['min']) + device.multi_level_switch_property[f'devolo.Blinds:{device_uid}'] = MultiLevelSwitchProperty( + element_uid=f"devolo.Blinds:{device_uid}", + setter=lambda uid, + state: None, + value=test_data['devices']['blinds']['value'], + max=test_data['devices']['blinds']['max'], + min=test_data['devices']['blinds']['min']) - device.settings_property['i2'] = \ - SettingsProperty(session=session, - gateway=gateway, - mydevolo=mydevolo, - element_uid=f"bas.{device_uid}", - value=test_data['devices']['blinds']['i2']) + device.settings_property['i2'] = SettingsProperty(element_uid=f"bas.{device_uid}", + setter=lambda uid, + state: None, + value=test_data['devices']['blinds']['i2']) - device.settings_property["general_device_settings"] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f'gds.{device_uid}', - icon=test_data['devices']['blinds']['icon'], - name=test_data['devices']['blinds']['itemName'], - zone_id=test_data['devices']['blinds']['zoneId']) + device.settings_property["general_device_settings"] = SettingsProperty(element_uid=f'gds.{device_uid}', + setter=lambda uid, + state: None, + icon=test_data['devices']['blinds']['icon'], + name=test_data['devices']['blinds']['itemName'], + zone_id=test_data['devices']['blinds']['zoneId'], + zones=test_data['gateway']['zones']) - device.settings_property["automatic_calibration"] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f'acs.{device_uid}', - calibration_status=test_data['devices']['blinds']['calibrationStatus'] == 2) + device.settings_property["automatic_calibration"] = SettingsProperty( + element_uid=f'acs.{device_uid}', + setter=lambda uid, + state: None, + calibration_status=test_data['devices']['blinds']['calibrationStatus'] == 2) - device.settings_property["movement_direction"] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f'bss.{device_uid}', - direction=not bool(test_data['devices']['blinds']['movement_direction'])) + device.settings_property["movement_direction"] = SettingsProperty( + element_uid=f'bss.{device_uid}', + setter=lambda uid, + state: None, + direction=not bool(test_data['devices']['blinds']['movement_direction'])) - device.settings_property["shutter_duration"] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f'mss.{device_uid}', - shutter_duration=test_data['devices']['blinds']['shutter_duration']) + device.settings_property["shutter_duration"] = SettingsProperty( + element_uid=f'mss.{device_uid}', + setter=lambda uid, + state: None, + shutter_duration=test_data['devices']['blinds']['shutter_duration']) return device diff --git a/tests/mocks/mock_siren.py b/tests/mocks/mock_siren.py index 7fae707e..51244468 100644 --- a/tests/mocks/mock_siren.py +++ b/tests/mocks/mock_siren.py @@ -1,15 +1,11 @@ import json import pathlib -import requests - -from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.devices.zwave import Zwave +from devolo_home_control_api.mydevolo import Mydevolo from devolo_home_control_api.properties.multi_level_switch_property import MultiLevelSwitchProperty from devolo_home_control_api.properties.settings_property import SettingsProperty -from .mock_gateway import MockGateway - def siren(device_uid: str) -> Zwave: """ @@ -24,38 +20,31 @@ def siren(device_uid: str) -> Zwave: mydevolo = Mydevolo() device = Zwave(mydevolo_instance=mydevolo, **test_data['devices']['siren']) - gateway = MockGateway(test_data['gateway']['id'], mydevolo=mydevolo) - session = requests.Session() device.multi_level_switch_property = {} - device.multi_level_switch_property[f'devolo.SirenMultiLevelSwitch:{device_uid}'] = \ - MultiLevelSwitchProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"devolo.SirenMultiLevelSwitch:{device_uid}", - state=test_data['devices']['siren']['state']) + device.multi_level_switch_property[f'devolo.SirenMultiLevelSwitch:{device_uid}'] = MultiLevelSwitchProperty( + element_uid=f"devolo.SirenMultiLevelSwitch:{device_uid}", + setter=lambda uid, + state: None, + state=test_data['devices']['siren']['state']) device.settings_property = {} - device.settings_property['muted'] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"bas.{device_uid}", - value=test_data['devices']['siren']['muted']) - device.settings_property["general_device_settings"] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"gds.{device_uid}", - icon=test_data['devices']['siren']['icon'], - name=test_data['devices']['siren']['itemName'], - zone_id=test_data['devices']['siren']['zoneId']) - - device.settings_property["tone"] = \ - SettingsProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=f"mss.{device_uid}", - value=test_data['devices']['siren']['properties']['value']) + device.settings_property['muted'] = SettingsProperty(element_uid=f"bas.{device_uid}", + setter=lambda uid, + state: True, + value=test_data['devices']['siren']['muted']) + + device.settings_property["general_device_settings"] = SettingsProperty(element_uid=f"gds.{device_uid}", + setter=lambda uid, + state: None, + icon=test_data['devices']['siren']['icon'], + name=test_data['devices']['siren']['itemName'], + zone_id=test_data['devices']['siren']['zoneId'], + zones=test_data['gateway']['zones']) + + device.settings_property["tone"] = SettingsProperty(element_uid=f"mss.{device_uid}", + setter=lambda uid, + state: None, + value=test_data['devices']['siren']['properties']['value']) return device diff --git a/tests/mocks/mock_websocket.py b/tests/mocks/mock_websocket.py index 2959339f..bbab628c 100644 --- a/tests/mocks/mock_websocket.py +++ b/tests/mocks/mock_websocket.py @@ -1,19 +1,5 @@ -from devolo_home_control_api.backend.mprm_rest import MprmRest - - -class MockWebsocket(MprmRest): - def __init__(self): - super(MprmRest, self).__init__() - self._ws = None - self._connected = True - self._reachable = True - self._event_sequence = 0 - - def close(self): - pass - - class MockWebsocketError: + def close(self): raise AssertionError diff --git a/tests/mocks/mock_websocketapp.py b/tests/mocks/mock_websocketapp.py index 7bf5d0da..4295ad2e 100644 --- a/tests/mocks/mock_websocketapp.py +++ b/tests/mocks/mock_websocketapp.py @@ -1,5 +1,9 @@ class MockWebsocketapp: - def __init__(self, ws_url, **kwargs): + + def __init__(self, *args, **kwargs): + pass + + def close(self, **kwargs): pass def run_forever(self, **kwargs): diff --git a/tests/mocks/mock_zeroconf.py b/tests/mocks/mock_zeroconf.py index 8188094a..b5a8d222 100644 --- a/tests/mocks/mock_zeroconf.py +++ b/tests/mocks/mock_zeroconf.py @@ -9,6 +9,7 @@ class Zeroconf: + def get_service_info(self, service_type, name): service_info = ServiceInfo(service_type, name) service_info.server = "devolo-homecontrol.local" diff --git a/tests/stubs/__init__.py b/tests/stubs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/stubs/mprm.py b/tests/stubs/mprm.py new file mode 100644 index 00000000..c7784593 --- /dev/null +++ b/tests/stubs/mprm.py @@ -0,0 +1,26 @@ +import json +import pathlib + +from requests import Session + +from devolo_home_control_api.backend.mprm import Mprm +from devolo_home_control_api.mydevolo import Mydevolo + +from ..mocks.mock_gateway import MockGateway + + +class StubMprm(Mprm): + + def __init__(self): + file = pathlib.Path(__file__).parent / ".." / "test_data.json" + with file.open("r") as fh: + test_data = json.load(fh) + + self._mydevolo = Mydevolo() + self._session = Session() + self._zeroconf = None + self.gateway = MockGateway(test_data['gateway']['id'], self._mydevolo) + super().__init__() + + def on_update(self, message): + pass diff --git a/tests/stubs/mprm_websocket.py b/tests/stubs/mprm_websocket.py new file mode 100644 index 00000000..c9528809 --- /dev/null +++ b/tests/stubs/mprm_websocket.py @@ -0,0 +1,28 @@ +from devolo_home_control_api.backend.mprm_websocket import MprmWebsocket + + +class StubMprmWebsocket(MprmWebsocket): + + def __init__(self): + super().__init__() + self._ws = None + self._connected = True + self._reachable = True + self._event_sequence = 0 + self._url = "https://test.test" + + def detect_gateway_in_lan(self): + pass + + def get_local_session(self): + # We are abusing this to raise the expected exception + raise self._ws() + + def get_remote_session(self): + pass + + def on_update(self, message): + pass + + def _post(self, data): + pass diff --git a/tests/test_binary_sensor_property.py b/tests/test_binary_sensor_property.py index 51610fee..813c54ca 100644 --- a/tests/test_binary_sensor_property.py +++ b/tests/test_binary_sensor_property.py @@ -6,15 +6,11 @@ @pytest.mark.usefixtures("home_control_instance") class TestBinarySensorProperty: - def test_binary_sensor_property_invalid(self, gateway_instance, mprm_session, mydevolo): + + def test_binary_sensor_property_invalid(self): with pytest.raises(WrongElementError): - BinarySensorProperty(gateway=gateway_instance, - session=mprm_session, - mydevolo=mydevolo, - element_uid="invalid", - state=True) + BinarySensorProperty(element_uid="invalid", state=True) - @pytest.mark.usefixtures("mock_mprmrest__post_set") def test_get_binary_sensor(self): - assert self.homecontrol.devices.get(self.devices.get("sensor").get("uid"))\ - .binary_sensor_property.get(self.devices.get("sensor").get("elementUIDs")[0]).state + assert self.homecontrol.devices.get(self.devices.get("sensor").get("uid")).binary_sensor_property.get( + self.devices.get("sensor").get("elementUIDs")[0]).state diff --git a/tests/test_binary_switch_property.py b/tests/test_binary_switch_property.py index abc8397a..6204332c 100644 --- a/tests/test_binary_switch_property.py +++ b/tests/test_binary_switch_property.py @@ -1,30 +1,26 @@ import pytest - from devolo_home_control_api.exceptions.device import SwitchingProtected, WrongElementError from devolo_home_control_api.properties.binary_switch_property import BinarySwitchProperty @pytest.mark.usefixtures("home_control_instance") class TestBinarySwitchProperty: - def test_binary_switch_property_invalid(self, gateway_instance, mprm_session, mydevolo): + + def test_binary_switch_property_invalid(self): with pytest.raises(WrongElementError): - BinarySwitchProperty(gateway=gateway_instance, - session=mprm_session, - mydevolo=mydevolo, - element_uid="invalid", - state=True, - enabled=True) + BinarySwitchProperty(element_uid="invalid", setter=lambda uid, state: None, state=True, enabled=True) - @pytest.mark.usefixtures("mock_mprmrest__post_set") - def test_set_valid(self): + def test_set(self): uid = self.devices['mains']['uid'] element_uid = self.devices['mains']['elementUIDs'][1] + self.homecontrol.devices[uid].binary_switch_property[element_uid]._setter = lambda uid, state: True self.homecontrol.devices[uid].binary_switch_property[element_uid].set(True) assert self.homecontrol.devices[uid].binary_switch_property[element_uid].state def test_set_protected(self): uid = self.devices['mains']['uid'] element_uid = self.devices['mains']['elementUIDs'][1] + self.homecontrol.devices[uid].binary_switch_property[element_uid]._setter = lambda uid, state: True self.homecontrol.devices[uid].binary_switch_property[element_uid].enabled = False with pytest.raises(SwitchingProtected): self.homecontrol.devices[uid].binary_switch_property[element_uid].set(True) diff --git a/tests/test_consumption_property.py b/tests/test_consumption_property.py index cda5950c..f374b28e 100644 --- a/tests/test_consumption_property.py +++ b/tests/test_consumption_property.py @@ -1,14 +1,11 @@ import pytest - from devolo_home_control_api.exceptions.device import WrongElementError from devolo_home_control_api.properties.consumption_property import ConsumptionProperty @pytest.mark.usefixtures("home_control_instance") class TestConsumption: - def test_consumption_property_invalid(self, gateway_instance, mprm_session, mydevolo): + + def test_consumption_property_invalid(self): with pytest.raises(WrongElementError): - ConsumptionProperty(gateway=gateway_instance, - session=mprm_session, - mydevolo=mydevolo, - element_uid="invalid") + ConsumptionProperty(element_uid="invalid", setter=lambda uid, state: None) diff --git a/tests/test_data.json b/tests/test_data.json index a4ff4c5d..42626e3b 100644 --- a/tests/test_data.json +++ b/tests/test_data.json @@ -55,12 +55,15 @@ "icon": "icon_1", "inverted": false, "itemName": "Shutter", + "manID": "0x0175", "min": 0, "max": 100, "motorActivity": 30, "movement_direction": 1, "operationStatus": false, "pending_operations": false, + "prodID": "0x0011", + "prodTypeID": "0x0001", "shutter_duration": 3000, "uid": "hdm:ZWave:F6BF9812/10", "value": 50, @@ -79,9 +82,12 @@ "guiEnabled": true, "icon": "icon_1", "itemName": "Humidity", + "manID": "0x0175", "mildew": false, "operationStatus": false, "pending_operations": false, + "prodID": "0x0011", + "prodTypeID": "0x0001", "status": 2, "uid": "hdm:ZWave:F6BF9812/7", "value": 75, @@ -125,7 +131,8 @@ "gds.hdm:ZWave:F6BF9812/2", "cps.hdm:ZWave:F6BF9812/2", "lis.hdm:ZWave:F6BF9812/2", - "ps.hdm:ZWave:F6BF9812/2" + "ps.hdm:ZWave:F6BF9812/2", + "mas.hdm:ZWave:F6BF9812/2" ], "state": 1, "status": 2, @@ -150,16 +157,19 @@ "batteryLevel": 76, "batteryLow": false, "elementUIDs": [ - "devolo.MultiLevelSwitch:hdm:ZWave:F6BF9812/9" + "devolo.MultiLevelSwitch:hdm:ZWave:F6BF9812/9" ], "guiEnabled": true, "icon": "icon-1", "itemName": "Heating", + "manID": "0x0175", "min": 4, "max": 28, "operationStatus": false, "pending_operations": false, - "uid": "hdm:ZWave:F6BF9812/9", + "prodID": "0x0011", + "prodTypeID": "0x0001", + "uid": "hdm:ZWave:F6BF9812/9", "switch_type": "temperature", "value": 20, "zoneId": "hz_2", @@ -169,12 +179,17 @@ "UID": "hdm:ZWave:F6BF9812/5", "batteryLevel": -1, "batteryLow": null, - "elementUIDs": ["devolo.Meter:hdm:ZWave:F6BF9812/5"], + "elementUIDs": [ + "devolo.Meter:hdm:ZWave:F6BF9812/5" + ], "guiEnabled": false, "icon": "icon-1", "itemName": "Offline Device", + "manID": "0x0175", "operationStatus": false, "pending_operations": false, + "prodID": "0x0011", + "prodTypeID": "0x0001", "state": 1, "status": 1, "uid": "hdm:ZWave:F6BF9812/5", @@ -186,13 +201,16 @@ "batteryLevel": 90, "batteryLow": false, "elementUIDs": [ - "devolo.RemoteControl:hdm:ZWave:F6BF9812/11" + "devolo.RemoteControl:hdm:ZWave:F6BF9812/11" ], "guiEnabled": true, "itemName": "Remote Control", "icon": "icon-1", + "manID": "0x0175", "operationStatus": false, "pending_operations": false, + "prodID": "0x0011", + "prodTypeID": "0x0001", "key_count": 4, "uid": "hdm:ZWave:F6BF9812/11", "zoneId": "hz_2", @@ -247,18 +265,21 @@ "batteryLow": null, "deviceModelUID": "devolo.model.Siren", "elementUIDs": [ - "devolo.SirenMultiLevelSwitch:hdm:ZWave:F6BF9812/8", - "devolo.SirenBinarySensor:hdm:ZWave:F6BF9812/8", - "devolo.SirenMultiLevelSensor:hdm:ZWave:F6BF9812/8", + "devolo.SirenMultiLevelSwitch:hdm:ZWave:F6BF9812/8", + "devolo.SirenBinarySensor:hdm:ZWave:F6BF9812/8", + "devolo.SirenMultiLevelSensor:hdm:ZWave:F6BF9812/8", "devolo.LastActivity:hdm:ZWave:F6BF9812/8" ], "icon": "icon-1", "itemName": "Siren", "guiEnabled": true, "last_activity": 1581419650436, + "manID": "0x0175", "muted": true, "operationStatus": false, "pending_operations": false, + "prodID": "0x0011", + "prodTypeID": "0x0001", "uid": "hdm:ZWave:F6BF9812/8", "properties": { "max": 0, @@ -288,11 +309,12 @@ "sync": true, "zones": { "hz_2": "Living Room", - "hz_3": "Kitchen"} + "hz_3": "Kitchen" + } }, "user": { "password": "7fee3cdb598b45a459ffe2aa720c8532", "username": "testuser@test.de", "uuid": "535512AB-165D-11E7-A4E2-000C29D76CCA" } -} \ No newline at end of file +} diff --git a/tests/test_gateway.py b/tests/test_gateway.py index c8f49b49..568b61b4 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -1,11 +1,11 @@ import pytest - from devolo_home_control_api.devices.gateway import Gateway @pytest.mark.usefixtures("mydevolo") @pytest.mark.usefixtures("mock_mydevolo__call") class TestGateway: + def test_update_state_known(self, mydevolo): gateway = Gateway(self.gateway.get("id"), mydevolo_instance=mydevolo) gateway.update_state(False) diff --git a/tests/test_helper.py b/tests/test_helper.py index 9cc52134..fb87072d 100644 --- a/tests/test_helper.py +++ b/tests/test_helper.py @@ -1,11 +1,15 @@ +import pytest + from devolo_home_control_api.helper.string import camel_case_to_snake_case -from devolo_home_control_api.helper.uid import ( - get_device_type_from_element_uid, get_device_uid_from_element_uid, - get_device_uid_from_setting_uid, get_home_id_from_device_uid, - get_sub_device_uid_from_element_uid) +from devolo_home_control_api.helper.uid import (get_device_type_from_element_uid, + get_device_uid_from_element_uid, + get_device_uid_from_setting_uid, + get_home_id_from_device_uid, + get_sub_device_uid_from_element_uid) class TestHelper: + def test_camel_case_to_snake_case(self): assert camel_case_to_snake_case("CamelCase") == "camel_case" assert camel_case_to_snake_case("camelCase") == "camel_case" @@ -13,17 +17,15 @@ def test_camel_case_to_snake_case(self): def test_get_device_type_from_element_uid(self): assert get_device_type_from_element_uid("devolo.Meter:hdm:ZWave:F6BF9812/2#2") == "devolo.Meter" - def test_get_device_uid_from_element_uid(self): - assert get_device_uid_from_element_uid("devolo.Meter:hdm:ZWave:F6BF9812/2#2") == "hdm:ZWave:F6BF9812/2" - - def test_get_device_uid_from_element_uid_secure(self): - assert get_device_uid_from_element_uid("devolo.Meter:hdm:ZWave:F6BF9812/2:secure#2") == "hdm:ZWave:F6BF9812/2" - - def test_get_device_uid_from_setting_uid(self): - assert get_device_uid_from_setting_uid("lis.hdm:ZWave:EB5A9F6C/2") == "hdm:ZWave:EB5A9F6C/2" + @pytest.mark.parametrize("element_uid", + ["devolo.Meter:hdm:ZWave:F6BF9812/2#2", + "devolo.Meter:hdm:ZWave:F6BF9812/2:secure#2"]) + def test_get_device_uid_from_element_uid(self, element_uid): + assert get_device_uid_from_element_uid(element_uid) == "hdm:ZWave:F6BF9812/2" - def test_get_device_uid_from_setting_uid_secure(self): - assert get_device_uid_from_setting_uid("lis.hdm:ZWave:EB5A9F6C/2:secure") == "hdm:ZWave:EB5A9F6C/2" + @pytest.mark.parametrize("setting_uid", ["lis.hdm:ZWave:EB5A9F6C/2", "lis.hdm:ZWave:EB5A9F6C/2:secure"]) + def test_get_device_uid_from_setting_uid(self, setting_uid): + assert get_device_uid_from_setting_uid(setting_uid) == "hdm:ZWave:EB5A9F6C/2" def test_get_sub_device_uid_from_element_uid(self): assert get_sub_device_uid_from_element_uid("devolo.Meter:hdm:ZWave:F6BF9812/2#2") == 2 diff --git a/tests/test_homecontrol.py b/tests/test_homecontrol.py index 217a7592..9402d623 100644 --- a/tests/test_homecontrol.py +++ b/tests/test_homecontrol.py @@ -1,13 +1,20 @@ from datetime import datetime import pytest +from devolo_home_control_api.backend import MESSAGE_TYPES @pytest.mark.usefixtures("mock_inspect_devices_metering_plug") @pytest.mark.usefixtures("home_control_instance") @pytest.mark.usefixtures("mock_mydevolo__call") class TestHomeControl: - # TODO: Clear devices after each test + + def test_hasattr(self): + homecontrol_functions = (func for func in dir(self.homecontrol) + if callable(getattr(self.homecontrol, + func)) and not func.startswith("__")) + assert set(MESSAGE_TYPES.values()).difference(homecontrol_functions) == {"_gateway_accessible", + "_device_state"} def test_binary_sensor_devices(self): assert hasattr(self.homecontrol.binary_sensor_devices[0], "binary_sensor_property") @@ -33,48 +40,70 @@ def test_get_publisher(self): def test__automatic_calibration(self): device = self.devices['blinds'] uid = device['uid'] - self.homecontrol._automatic_calibration({"UID": f"acs.{uid}", - "properties": {"calibrationStatus": device['calibrationStatus']}}) + self.homecontrol._automatic_calibration({ + "UID": f"acs.{uid}", + "properties": { + "calibrationStatus": device['calibrationStatus'] + } + }) assert self.homecontrol.devices.get(uid).settings_property['automatic_calibration'].calibration_status == \ bool(device['calibrationStatus']) def test___binary_sync(self): device = self.devices['blinds'] uid = device['uid'] - self.homecontrol._binary_sync({"UID": f"acs.{uid}", - "properties": {"value": device['inverted']}}) + self.homecontrol._binary_sync({ + "UID": f"acs.{uid}", + "properties": { + "value": device['inverted'] + } + }) assert self.homecontrol.devices.get(uid).settings_property['movement_direction'].inverted is device['inverted'] def test__binary_async_blinds(self): device = self.devices.get("blinds").get("uid") i2 = self.devices.get("blinds").get("i2") - self.homecontrol._binary_async({"UID": f"bas.{device}#i2", - "properties": {"value": not i2}}) + self.homecontrol._binary_async({ + "UID": f"bas.{device}#i2", + "properties": { + "value": not i2 + } + }) assert self.homecontrol.devices.get(device).settings_property.get("i2").value is not i2 def test__binary_async_siren(self): device = self.devices.get("siren").get("uid") muted = self.devices.get("siren").get("muted") - self.homecontrol._binary_async({"UID": f"bas.{device}", - "properties": {"value": not muted}}) + self.homecontrol._binary_async({ + "UID": f"bas.{device}", + "properties": { + "value": not muted + } + }) assert self.homecontrol.devices.get(device).settings_property.get("muted").value is not muted def test__binary_sensor(self): device = self.devices.get("sensor").get("uid") del self.homecontrol.devices[device].binary_sensor_property assert not hasattr(self.homecontrol.devices.get(device), "binary_sensor_property") - self.homecontrol._binary_sensor({"UID": self.devices.get("sensor").get("elementUIDs")[0], - "properties": {"state": self.devices.get("sensor").get("state"), - "sensorType": self.devices.get("sensor").get("sensor_type"), - "subType": ""}}) + self.homecontrol._binary_sensor({ + "UID": self.devices.get("sensor").get("elementUIDs")[0], + "properties": { + "state": self.devices.get("sensor").get("state"), + "sensorType": self.devices.get("sensor").get("sensor_type"), + "subType": "" + }, + }) assert hasattr(self.homecontrol.devices.get(device), "binary_sensor_property") def test__binary_switch(self): device = self.devices['mains']['uid'] del self.homecontrol.devices[device].binary_switch_property assert not hasattr(self.homecontrol.devices[device], "binary_switch_property") - self.homecontrol._binary_switch({"UID": self.devices['mains']['properties']['elementUIDs'][1], - "properties": self.devices['mains']['properties']}) + self.homecontrol._binary_switch({ + "UID": self.devices['mains']['properties']['elementUIDs'][1], + "properties": self.devices['mains']['properties'], + }) assert hasattr(self.homecontrol.devices[device], "binary_switch_property") def test__consumption(self): @@ -82,10 +111,14 @@ def test__consumption(self): device = self.devices.get("mains").get("uid") del self.homecontrol.devices[device].consumption_property assert not hasattr(self.homecontrol.devices.get(device), "consumption_property") - self.homecontrol._meter({"UID": "devolo.Meter:hdm:ZWave:F6BF9812/2", "properties": { - "currentValue": self.devices.get("mains").get("current_consumption"), - "totalValue": self.devices.get("mains").get("total_consumption"), - "sinceTime": self.devices.get("mains").get("properties").get("total_consumption")}}) + self.homecontrol._meter({ + "UID": "devolo.Meter:hdm:ZWave:F6BF9812/2", + "properties": { + "currentValue": self.devices.get("mains").get("current_consumption"), + "totalValue": self.devices.get("mains").get("total_consumption"), + "sinceTime": self.devices.get("mains").get("properties").get("total_consumption"), + } + }) assert hasattr(self.homecontrol.devices.get(device), "consumption_property") def test__general_device(self): @@ -93,11 +126,17 @@ def test__general_device(self): element_uid = f"gds.{device['uid']}" self.homecontrol.devices[device['uid']].settings_property['general_device_settings'].events_enabled = \ not device['properties']['eventsEnabled'] - self.homecontrol._general_device({"UID": element_uid, - "properties": {"settings": {"eventsEnabled": device['properties']['eventsEnabled'], - "name": device['properties']['itemName'], - "zoneID": device['properties']['zoneId'], - "icon": device['properties']['icon']}}}) + self.homecontrol._general_device({ + "UID": element_uid, + "properties": { + "settings": { + "eventsEnabled": device['properties']['eventsEnabled'], + "name": device['properties']['itemName'], + "zoneID": device['properties']['zoneId'], + "icon": device['properties']['icon'], + } + } + }) assert self.homecontrol.devices[device['uid']].settings_property['general_device_settings'].events_enabled == \ device['properties']['eventsEnabled'] @@ -106,35 +145,61 @@ def test__humidity_bar(self): device = self.devices.get("humidity").get("uid") del self.homecontrol.devices[device].humidity_bar_property assert not hasattr(self.homecontrol.devices.get(device), "humidity_bar_property") - self.homecontrol._humidity_bar({"UID": f"devolo.HumidityBarValue:{device}", - "properties": {"sensorType": "humidityBarPos", "value": 75}}) - self.homecontrol._humidity_bar({"UID": f"devolo.HumidityBarZone:{device}", - "properties": {"sensorType": "humidityBarZone", "value": 1}}) + self.homecontrol._humidity_bar({ + "UID": f"devolo.HumidityBarValue:{device}", + "properties": { + "sensorType": "humidityBarPos", + "value": 75, + } + }) + self.homecontrol._humidity_bar({ + "UID": f"devolo.HumidityBarZone:{device}", + "properties": { + "sensorType": "humidityBarZone", + "value": 1, + } + }) assert self.homecontrol.devices.get(device).humidity_bar_property.get(f"devolo.HumidityBar:{device}").value == 75 assert self.homecontrol.devices.get(device).humidity_bar_property.get(f"devolo.HumidityBar:{device}").zone == 1 def test__last_activity_binary_sensor(self): device = self.devices['sensor']['uid'] element_uids = self.devices['sensor']['elementUIDs'] - self.homecontrol._binary_sensor({"UID": element_uids[0], - "properties": {"state": self.devices['sensor']['state'], - "sensorType": self.devices['sensor']['sensor_type'], - "subType": ""}}) - self.homecontrol._last_activity({"UID": element_uids[1], - "properties": {"lastActivityTime": self.devices['sensor']['last_activity']}}) + self.homecontrol._binary_sensor({ + "UID": element_uids[0], + "properties": { + "state": self.devices['sensor']['state'], + "sensorType": self.devices['sensor']['sensor_type'], + "subType": "", + } + }) + self.homecontrol._last_activity({ + "UID": element_uids[1], + "properties": { + "lastActivityTime": self.devices['sensor']['last_activity'], + } + }) assert self.homecontrol.devices[device].binary_sensor_property.get(element_uids[0]).last_activity == \ datetime.utcfromtimestamp(self.devices['sensor']['last_activity'] / 1000) def test__last_activity_siren(self): device = self.devices['siren']['uid'] element_uids = self.devices['siren']['elementUIDs'] - self.homecontrol._multi_level_switch({"UID": element_uids[0], - "properties": {"value": self.devices['siren']['properties']['value'], - "switchType": self.devices['siren']['switch_type'], - "max": self.devices['siren']['properties']['max'], - "min": self.devices['siren']['properties']['min']}}) - self.homecontrol._last_activity({"UID": element_uids[3], - "properties": {"lastActivityTime": self.devices['siren']['last_activity']}}) + self.homecontrol._multi_level_switch({ + "UID": element_uids[0], + "properties": { + "value": self.devices['siren']['properties']['value'], + "switchType": self.devices['siren']['switch_type'], + "max": self.devices['siren']['properties']['max'], + "min": self.devices['siren']['properties']['min'], + } + }) + self.homecontrol._last_activity({ + "UID": element_uids[3], + "properties": { + "lastActivityTime": self.devices['siren']['last_activity'], + } + }) assert self.homecontrol.devices[device].multi_level_switch_property[element_uids[0]].last_activity == \ datetime.utcfromtimestamp(self.devices['siren']['last_activity'] / 1000) @@ -142,53 +207,86 @@ def test__led(self): device = self.devices['mains'] element_uid = f"lis.{device['UID']}" led_setting = device['properties']['led_setting'] - self.homecontrol._led({"UID": element_uid, "properties": {"led": led_setting}}) + self.homecontrol._led({ + "UID": element_uid, + "properties": { + "led": led_setting, + } + }) assert self.homecontrol.devices[device['UID']].settings_property['led'].led_setting == led_setting device = self.devices['sensor'] element_uid = f"vfs.{device['UID']}" led_setting = device['led_setting'] - self.homecontrol._led({"UID": element_uid, "properties": {"feedback": led_setting}}) + self.homecontrol._led({ + "UID": element_uid, + "properties": { + "feedback": led_setting, + } + }) assert self.homecontrol.devices[device['UID']].settings_property['led'].led_setting == led_setting def test__multilevel_async(self): device = self.devices['blinds'] uid = device['uid'] - self.homecontrol._multilevel_async({"UID": f"mas.{uid}", - "properties": {"itemId": "motorActivity", "value": device['motorActivity']}}) + self.homecontrol._multilevel_async({ + "UID": f"mas.{uid}", + "properties": { + "itemId": "motorActivity", + "value": device['motorActivity'], + } + }) assert self.homecontrol.devices[uid].settings_property['motor_activity'].value == device['motorActivity'] def test__multilevel_async_mains(self): device = self.devices['mains'] uid = device['uid'] - self.homecontrol._multilevel_async({"UID": f"mas.{uid}", - "properties": {"itemId": None, "value": device['flashMode']}}) + self.homecontrol._multilevel_async({ + "UID": f"mas.{uid}", + "properties": { + "itemId": None, + "value": device['flashMode'], + } + }) assert self.homecontrol.devices[uid].settings_property['flash_mode'].value == device['flashMode'] def test__multilevel_async_type_error(self): with pytest.raises(TypeError): device = self.devices['sensor'] uid = device['uid'] - self.homecontrol._multilevel_async({"UID": f"mas.{uid}", - "properties": {"itemId": None, "value": device['motion_sensitivity']}}) + self.homecontrol._multilevel_async({ + "UID": f"mas.{uid}", + "properties": { + "itemId": None, + "value": device['motion_sensitivity'], + } + }) def test__multilevel_sync_sensor(self): device = self.devices['sensor']['uid'] - self.homecontrol._multilevel_sync({"UID": self.devices['sensor']['settingUIDs'][2], - "properties": self.devices['sensor']['properties']}) + self.homecontrol._multilevel_sync({ + "UID": self.devices['sensor']['settingUIDs'][2], + "properties": self.devices['sensor']['properties'], + }) assert self.homecontrol.devices[device].settings_property['motion_sensitivity'].motion_sensitivity == \ self.devices['sensor']['properties']['value'] def test__multilevel_sync_shutter(self): device = self.devices['blinds']['uid'] - self.homecontrol._multilevel_sync({"UID": f"mss.{device}", - "properties": {"value": self.devices['blinds']['shutter_duration']}}) + self.homecontrol._multilevel_sync({ + "UID": f"mss.{device}", + "properties": { + "value": self.devices['blinds']['shutter_duration'], + } + }) assert self.homecontrol.devices[device].settings_property['shutter_duration'].shutter_duration == \ self.devices['blinds']['shutter_duration'] def test__multilevel_sync_siren(self): device = self.devices['siren']['uid'] - self.homecontrol._multilevel_sync({"UID": self.devices['siren']['settingUIDs'][0], - "properties": self.devices['siren']['properties']}) + self.homecontrol._multilevel_sync({ + "UID": self.devices['siren']['settingUIDs'][0], + "properties": self.devices['siren']['properties'], + }) assert self.homecontrol.devices[device].settings_property['tone'].tone == self.devices['siren']['properties']['value'] def test__multi_level_sensor(self): @@ -196,36 +294,53 @@ def test__multi_level_sensor(self): device = self.devices.get("sensor").get("uid") del self.homecontrol.devices[device].multi_level_sensor_property assert not hasattr(self.homecontrol.devices.get(device), "multi_level_sensor_property") - self.homecontrol._multi_level_sensor({"UID": self.devices.get("sensor").get("elementUIDs")[2], - "properties": {"value": 90.0, "unit": "%", "sensorType": "light"}}) + self.homecontrol._multi_level_sensor({ + "UID": self.devices.get("sensor").get("elementUIDs")[2], + "properties": { + "value": 90.0, + "unit": "%", + "sensorType": "light", + } + }) assert hasattr(self.homecontrol.devices.get(device), "multi_level_sensor_property") def test__multi_level_switch(self): device = self.devices.get("siren").get("uid") del self.homecontrol.devices[device].multi_level_switch_property assert not hasattr(self.homecontrol.devices.get(device), "multi_level_switch_property") - self.homecontrol._multi_level_switch({"UID": self.devices.get("siren").get("elementUIDs")[0], - "properties": { - "state": self.devices.get("multi_level_switch").get("state"), - "value": self.devices.get("multi_level_switch").get("value"), - "switchType": self.devices.get("multi_level_switch").get("switch_type"), - "max": self.devices.get("multi_level_switch").get("max"), - "min": self.devices.get("multi_level_switch").get("min")}}) + self.homecontrol._multi_level_switch({ + "UID": self.devices.get("siren").get("elementUIDs")[0], + "properties": { + "state": self.devices.get("multi_level_switch").get("state"), + "value": self.devices.get("multi_level_switch").get("value"), + "switchType": self.devices.get("multi_level_switch").get("switch_type"), + "max": self.devices.get("multi_level_switch").get("max"), + "min": self.devices.get("multi_level_switch").get("min"), + } + }) assert hasattr(self.homecontrol.devices.get(device), "multi_level_switch_property") def test__parameter(self): # TODO: Use test data device = self.devices.get("mains").get("uid") - self.homecontrol._parameter({"UID": "cps.hdm:ZWave:F6BF9812/2", - "properties": {"paramChanged": False}}) + self.homecontrol._parameter({ + "UID": "cps.hdm:ZWave:F6BF9812/2", + "properties": { + "paramChanged": False, + } + }) assert hasattr(self.homecontrol.devices.get(device).settings_property.get("param_changed"), "param_changed") def test__protection(self): # TODO: Use test data device = self.devices.get("mains").get("uid") - self.homecontrol._protection({"UID": "ps.hdm:ZWave:F6BF9812/2", - "properties": {"localSwitch": True, - "remoteSwitch": False}}) + self.homecontrol._protection({ + "UID": "ps.hdm:ZWave:F6BF9812/2", + "properties": { + "localSwitch": True, + "remoteSwitch": False, + } + }) assert hasattr(self.homecontrol.devices.get(device).settings_property.get("protection"), "local_switching") assert hasattr(self.homecontrol.devices.get(device).settings_property.get("protection"), "remote_switching") @@ -234,24 +349,36 @@ def test__remote_control(self): element_uid = self.devices.get("remote").get("elementUIDs")[0] del self.homecontrol.devices[device].remote_control_property assert not hasattr(self.homecontrol.devices.get(device), "remote_control_property") - self.homecontrol._remote_control({"UID": element_uid, - "properties": {"keyCount": self.devices.get("remote").get("key_count"), - "keyPressed": 0, - "type": 1}}) + self.homecontrol._remote_control({ + "UID": element_uid, + "properties": { + "keyCount": self.devices.get("remote").get("key_count"), + "keyPressed": 0, + "type": 1, + } + }) assert self.homecontrol.devices.get(device).remote_control_property[element_uid].key_pressed == 0 def test__switch_type(self): device = self.devices['remote']['uid'] - self.homecontrol._switch_type({"UID": f"sts.{device}", - "properties": {"switchType": self.devices['remote']['key_count'] / 2}}) + self.homecontrol._switch_type({ + "UID": f"sts.{device}", + "properties": { + "switchType": self.devices['remote']['key_count'] / 2, + } + }) assert self.homecontrol.devices[device].settings_property['switch_type'].value == self.devices['remote']['key_count'] def test__temperature_report(self): # TODO: Use test data device = self.devices.get("sensor").get("uid") - self.homecontrol._temperature_report({"UID": "trs.hdm:ZWave:F6BF9812/6", - "properties": {"tempReport": True, - "targetTempReport": False}}) + self.homecontrol._temperature_report({ + "UID": "trs.hdm:ZWave:F6BF9812/6", + "properties": { + "tempReport": True, + "targetTempReport": False, + } + }) assert hasattr(self.homecontrol.devices.get(device).settings_property.get("temperature_report"), "temp_report") assert hasattr(self.homecontrol.devices.get(device).settings_property.get("temperature_report"), "target_temp_report") @@ -259,9 +386,9 @@ def test__temperature_report(self): def test_device_change_add(self, mocker): uids = [self.devices.get(device).get("uid") for device in self.devices] uids.append("test_uid") - spy = mocker.spy(self.homecontrol, '_inspect_devices') + spy = mocker.spy(self.homecontrol, "_inspect_devices") self.homecontrol.device_change(uids) - spy.assert_called_once_with(["test_uid"]) + spy.assert_called_once_with(['test_uid']) def test_device_change_remove(self): uids = [self.devices.get(device).get("uid") for device in self.devices] @@ -276,7 +403,7 @@ def test__inspect_devices(self): del self.homecontrol.devices[uid] self.homecontrol._inspect_devices(self.homecontrol.get_all_devices()) try: - self.homecontrol.devices[uid] # pylint: disable=W0104 + self.homecontrol.devices[uid] assert True except KeyError: assert False diff --git a/tests/test_humidity_bar_property.py b/tests/test_humidity_bar_property.py index 96b3ab2f..13c30fb3 100644 --- a/tests/test_humidity_bar_property.py +++ b/tests/test_humidity_bar_property.py @@ -1,14 +1,11 @@ import pytest - from devolo_home_control_api.exceptions.device import WrongElementError from devolo_home_control_api.properties.humidity_bar_property import HumidityBarProperty @pytest.mark.usefixtures("home_control_instance") class TestHumidityBar: - def test_humidity_bar_property_invalid(self, gateway_instance, mprm_session, mydevolo): + + def test_humidity_bar_property_invalid(self): with pytest.raises(WrongElementError): - HumidityBarProperty(gateway=gateway_instance, - session=mprm_session, - mydevolo=mydevolo, - element_uid="invalid") + HumidityBarProperty(element_uid="invalid") diff --git a/tests/test_mprm.py b/tests/test_mprm.py index 8abe668f..a67870c6 100644 --- a/tests/test_mprm.py +++ b/tests/test_mprm.py @@ -7,18 +7,19 @@ @pytest.mark.usefixtures("mprm_instance") class TestMprm: + @pytest.mark.usefixtures("mock_mprm_get_local_session") def test_create_connection_local(self, mprm_session): self.mprm._session = mprm_session - self.mprm._local_ip = self.gateway.get("local_ip") + self.mprm._local_ip = self.gateway["local_ip"] self.mprm.create_connection() @pytest.mark.usefixtures("mock_mprmwebsocket_get_remote_session") @pytest.mark.usefixtures("mock_session_get") def test_create_connection_remote(self, mprm_session, mydevolo): + self.mprm._mydevolo = mydevolo self.mprm._session = mprm_session self.mprm.gateway.external_access = True - self._mydevolo = mydevolo self.mprm.create_connection() def test_create_connection_invalid(self): @@ -28,27 +29,27 @@ def test_create_connection_invalid(self): @pytest.mark.usefixtures("mock_mprm_service_browser") def test_detect_gateway_in_lan(self): - self.mprm._local_ip = self.gateway.get("local_ip") - assert self.mprm.detect_gateway_in_lan("zeroconf") == self.gateway.get("local_ip") + self.mprm._local_ip = self.gateway["local_ip"] + assert self.mprm.detect_gateway_in_lan() == self.gateway["local_ip"] @pytest.mark.usefixtures("mock_session_get") @pytest.mark.usefixtures("mock_response_json") def test_get_local_session_valid(self, mprm_session): self.mprm._session = mprm_session - self.mprm._local_ip = self.gateway.get("local_ip") + self.mprm._local_ip = self.gateway["local_ip"] self.mprm.get_local_session() @pytest.mark.usefixtures("mock_response_requests_ConnectTimeout") def test_get_local_session_ConnectTimeout(self, mprm_session): self.mprm._session = mprm_session - self.mprm._local_ip = self.gateway.get("local_ip") + self.mprm._local_ip = self.gateway["local_ip"] with pytest.raises(ConnectTimeout): self.mprm.get_local_session() @pytest.mark.usefixtures("mock_response_json_JSONDecodeError") def test_get_local_session_JSONDecodeError(self, mprm_session): self.mprm._session = mprm_session - self.mprm._local_ip = self.gateway.get("local_ip") + self.mprm._local_ip = self.gateway["local_ip"] with pytest.raises(GatewayOfflineError): self.mprm.get_local_session() @@ -64,11 +65,11 @@ def test__on_service_state_change(self): zc = zeroconf.Zeroconf() service_type = "_http._tcp.local." self.mprm._on_service_state_change(zc, service_type, service_type, zeroconf.ServiceStateChange.Added) - assert self.mprm._local_ip == self.gateway.get("local_ip") + assert self.mprm._local_ip == self.gateway["local_ip"] @pytest.mark.usefixtures("mock_socket_inet_ntoa") @pytest.mark.usefixtures("mock_response_valid") def test__try_local_connection_success(self, mprm_session): self.mprm._session = mprm_session - self.mprm._try_local_connection([self.gateway.get("local_ip")]) - assert self.mprm._local_ip == self.gateway.get("local_ip") + self.mprm._try_local_connection([self.gateway["local_ip"]]) + assert self.mprm._local_ip == self.gateway["local_ip"] diff --git a/tests/test_mprm_rest.py b/tests/test_mprm_rest.py index d2fa1fbb..51cc2bcd 100644 --- a/tests/test_mprm_rest.py +++ b/tests/test_mprm_rest.py @@ -1,21 +1,23 @@ import pytest - from devolo_home_control_api.exceptions.gateway import GatewayOfflineError @pytest.mark.usefixtures("mprm_instance") class TestMprmRest: - def test_get_name_and_element_uids(self, mock_mprmrest__extract_data_from_element_uid, mock_mprmrest__post): + @pytest.mark.usefixtures("mock_mprmrest__post") + def test_get_name_and_element_uids(self): properties = self.mprm.get_name_and_element_uids("test") - assert properties == {"itemName": "test_name", - "zone": "test_zone", - "batteryLevel": "test_battery", - "icon": "test_icon", - "elementUIDs": "test_element_uids", - "settingUIDs": "test_setting_uids", - "deviceModelUID": "test_device_model_uid", - "status": "test_status"} + assert properties == { + "itemName": "test_name", + "zone": "test_zone", + "batteryLevel": "test_battery", + "icon": "test_icon", + "elementUIDs": "test_element_uids", + "settingUIDs": "test_setting_uids", + "deviceModelUID": "test_device_model_uid", + "status": "test_status", + } @pytest.mark.usefixtures("mock_mprmrest__post") def test_get_all_devices(self): @@ -25,38 +27,70 @@ def test_get_all_devices(self): @pytest.mark.usefixtures("mock_mprmrest__post") def test_get_all_zones(self): zones = self.mprm.get_all_zones() - assert zones == {"hz_3": "Office"} + assert zones == { + "hz_3": "Office", + } @pytest.mark.usefixtures("mock_response_requests_ReadTimeout") def test_post_ReadTimeOut(self, mprm_session, gateway_instance): self.mprm._session = mprm_session self.mprm.gateway = gateway_instance with pytest.raises(GatewayOfflineError): - self.mprm.post({"data": "test"}) - - @pytest.mark.usefixtures("mock_response_requests_ReadTimeout") - def test_post_gateway_offline(self, mprm_session, gateway_instance): - self.mprm._session = mprm_session - self.mprm.gateway = gateway_instance - self.mprm.gateway.online = False - self.mprm.gateway.sync = False - self.mprm.gateway.local_connection = False - with pytest.raises(GatewayOfflineError): - self.mprm.post({"data": "test"}) + self.mprm._post({"data": "test"}) @pytest.mark.usefixtures("mock_response_requests_invalid_id") def test_post_invalid_id(self, mprm_session): self.mprm._session = mprm_session self.mprm._data_id = 0 with pytest.raises(ValueError): - self.mprm.post({"data": "test"}) + self.mprm._post({"data": "test"}) @pytest.mark.usefixtures("mock_response_requests_valid") def test_post_valid(self, mprm_session): self.mprm._session = mprm_session self.mprm._data_id = 1 - assert self.mprm.post({"data": "test"}).get("id") == 2 + assert self.mprm._post({ + "data": "test" + }).get("id") == 2 - def test_get_data_from_uid_list(self, mock_mprmrest__post): + @pytest.mark.usefixtures("mock_mprmrest__post") + @pytest.mark.parametrize("setter", + [ + ("set_binary_switch"), + ("set_multi_level_switch"), + ("set_remote_control"), + ("set_setting"), + ]) + def test_set_success(self, setter): + test_data = { + "set_binary_switch": bool(self.devices['mains']['properties']['state']), + "set_multi_level_switch": self.devices['multi_level_switch']['value'], + "set_remote_control": 1, + "set_setting": self.devices['mains']['properties']['local_switch'], + } + assert getattr(self.mprm, setter)(self.devices['mains']['properties']['elementUIDs'][1], test_data[setter]) + + @pytest.mark.usefixtures("mock_mprmrest__post") + @pytest.mark.parametrize("setter", [("set_binary_switch"), ("set_multi_level_switch")]) + def test_set_doubled(self, setter): + assert not getattr(self.mprm, + setter)(self.devices['mains']['properties']['elementUIDs'][1], + bool(self.devices['mains']['properties']['state'])) + + @pytest.mark.usefixtures("mock_mprmrest__post") + @pytest.mark.parametrize("setter", + [ + ("set_binary_switch"), + ("set_multi_level_switch"), + ("set_remote_control"), + ("set_setting"), + ]) + def test_set_failed(self, setter): + assert not getattr(self.mprm, + setter)(self.devices['mains']['properties']['elementUIDs'][1], + bool(self.devices['mains']['properties']['state'])) + + @pytest.mark.usefixtures("mock_mprmrest__post") + def test_get_data_from_uid_list(self): properties = self.mprm.get_data_from_uid_list(["test"]) assert properties[0].get("properties").get("itemName") == "test_name" diff --git a/tests/test_mprm_websocket.py b/tests/test_mprm_websocket.py index d0ab2d8e..8afc6bca 100644 --- a/tests/test_mprm_websocket.py +++ b/tests/test_mprm_websocket.py @@ -1,24 +1,22 @@ import time import pytest +from requests.exceptions import ConnectionError +from urllib3.connection import ConnectTimeoutError +from websocket import WebSocketApp from devolo_home_control_api.backend.mprm_rest import MprmRest from devolo_home_control_api.backend.mprm_websocket import MprmWebsocket -from .mocks.mock_websocket import MockWebsocket, MockWebsocketError +from .mocks.mock_websocket import MockWebsocketError +from .stubs.mprm_websocket import StubMprmWebsocket @pytest.mark.usefixtures("mprm_instance") class TestMprmWebsocket: - def test_get_local_session(self): - with pytest.raises(NotImplementedError): - self.mprm.get_local_session() - def test_get_remote_session(self): - with pytest.raises(NotImplementedError): - self.mprm.get_remote_session() - - def test_websocket_connect(self, mock_mprmwebsocket_websocketapp, mprm_session, gateway_instance): + @pytest.mark.usefixtures("mock_mprmwebsocket_websocketapp") + def test_websocket_connect(self, mprm_session, gateway_instance): self.mprm._session = mprm_session self.mprm.gateway = gateway_instance with pytest.raises(AssertionError): @@ -35,45 +33,57 @@ def test__exit__(self, mocker): self.mprm.__exit__(None, None, None) assert disconnect_spy.call_count == 1 - def test__on_message(self): - with pytest.raises(NotImplementedError): - message = '{"properties": {"com.prosyst.mbs.services.remote.event.sequence.number": 0}}' - self.mprm._on_message(message) - - def test__on_message_event_sequence(self, mock_mprmwebsocket_on_update): + @pytest.mark.usefixtures("mock_mprmwebsocket_on_update") + def test__on_message_event_sequence(self): event_sequence = self.mprm._event_sequence + self.mprm._event_sequence = 5 message = '{"properties": {"com.prosyst.mbs.services.remote.event.sequence.number": 5}}' - self.mprm._on_message(message) + self.mprm._on_message(None, message) assert event_sequence != self.mprm._event_sequence assert self.mprm._event_sequence == 6 + message = '{"properties": {"com.prosyst.mbs.services.remote.event.sequence.number": 7}}' + self.mprm._on_message(None, message) + assert self.mprm._event_sequence == 8 @pytest.mark.usefixtures("mock_mprmwebsocket_get_remote_session") @pytest.mark.usefixtures("mock_mprmwebsocket_websocket_connection") @pytest.mark.usefixtures("mock_mprmwebsocket_try_reconnect") + @pytest.mark.usefixtures("mock_mprmwebsocket_websocketapp") def test__on_error(self, mocker): - close_spy = mocker.spy(MockWebsocket, "close") + close_spy = mocker.spy(WebSocketApp, "close") reconnect_spy = mocker.spy(MprmWebsocket, "_try_reconnect") connect_spy = mocker.spy(MprmWebsocket, "websocket_connect") - self.mprm._ws = MockWebsocket() - self.mprm._on_error("error") + self.mprm._ws = WebSocketApp() + self.mprm._on_error(self.mprm._ws, "error") assert close_spy.call_count == 1 assert reconnect_spy.call_count == 1 assert connect_spy.call_count == 1 @pytest.mark.usefixtures("mock_mprmwebsocket_websocketapp") @pytest.mark.usefixtures("mock_session_get") - def test__on_pong(self, mocker, mprm_session, gateway_instance): + def test__on_pong(self, mocker, mprm_session, gateway_instance, mydevolo): spy = mocker.spy(MprmRest, "refresh_session") + self.mprm._mydevolo = mydevolo self.mprm._session = mprm_session self.mprm.gateway = gateway_instance - self.mprm._local_ip = self.gateway.get("local_ip") - self.mprm._on_pong() + self.mprm._local_ip = self.gateway["local_ip"] + self.mprm._on_pong(None) assert spy.call_count == 1 - @pytest.mark.usefixtures("mock_mprmwebsocket_get_local_session_json_decode_error") + @pytest.mark.usefixtures("mock_mprmwebsocket_websocketapp") def test__try_reconnect(self, mocker): spy = mocker.spy(time, "sleep") - self.mprm._ws = MockWebsocket() - self.mprm._local_ip = self.gateway.get("local_ip") + self.mprm._ws = ConnectTimeoutError + self.mprm._local_ip = self.gateway["local_ip"] self.mprm._try_reconnect(0.1) spy.assert_called_once_with(0.1) + + @pytest.mark.usefixtures("mock_mprmwebsocket_websocketapp") + def test__try_reconnect_with_detect(self, mocker): + spy_sleep = mocker.spy(time, "sleep") + spy_detect_gateway = mocker.spy(StubMprmWebsocket, "detect_gateway_in_lan") + self.mprm._ws = ConnectionError + self.mprm._local_ip = self.gateway["local_ip"] + self.mprm._try_reconnect(4) + spy_sleep.assert_called_once_with(1) + spy_detect_gateway.assert_called_once() diff --git a/tests/test_multi_level_sensor_property.py b/tests/test_multi_level_sensor_property.py index f6444356..03dcde8b 100644 --- a/tests/test_multi_level_sensor_property.py +++ b/tests/test_multi_level_sensor_property.py @@ -1,23 +1,19 @@ import pytest - from devolo_home_control_api.exceptions.device import WrongElementError from devolo_home_control_api.properties.multi_level_sensor_property import MultiLevelSensorProperty @pytest.mark.usefixtures("home_control_instance") class TestMultiLevelSensorProperty: - def test_multi_level_property_invalid(self, gateway_instance, mprm_session, mydevolo): + + def test_multi_level_property_invalid(self): with pytest.raises(WrongElementError): - MultiLevelSensorProperty(gateway=gateway_instance, - session=mprm_session, - mydevolo=mydevolo, - element_uid="invalid") + MultiLevelSensorProperty(element_uid="invalid", setter=lambda uid, state: None) - def test_unit(self, gateway_instance, mprm_session, mydevolo): - mlsp = MultiLevelSensorProperty(gateway=gateway_instance, - session=mprm_session, - mydevolo=mydevolo, - element_uid=self.devices.get("sensor").get("elementUIDs")[2], + def test_unit(self): + mlsp = MultiLevelSensorProperty(element_uid=self.devices.get("sensor").get("elementUIDs")[2], + setter=lambda uid, + state: None, sensor_type=self.devices.get("sensor").get("sensor_type"), value=self.devices.get("sensor").get("value"), unit=self.devices.get("sensor").get("unit")) diff --git a/tests/test_multi_level_switch_property.py b/tests/test_multi_level_switch_property.py index f7718a0e..8afc5020 100644 --- a/tests/test_multi_level_switch_property.py +++ b/tests/test_multi_level_switch_property.py @@ -1,33 +1,33 @@ import pytest - from devolo_home_control_api.exceptions.device import WrongElementError from devolo_home_control_api.properties.multi_level_switch_property import MultiLevelSwitchProperty @pytest.mark.usefixtures("home_control_instance") class TestMultiLevelSwitchProperty: - def test_multi_level_switch_property_invalid(self, gateway_instance, mprm_session, mydevolo): + + def test_multi_level_switch_property_invalid(self): with pytest.raises(WrongElementError): - MultiLevelSwitchProperty(gateway=gateway_instance, session=mprm_session, mydevolo=mydevolo, element_uid="invalid") + MultiLevelSwitchProperty(element_uid="invalid", setter=lambda uid, state: None) - def test_unit(self, gateway_instance, mprm_session, mydevolo): - element_uid = self.devices.get("siren").get("elementUIDs")[0] - switch_type = self.devices.get("siren").get("switch_type") - mlsp = MultiLevelSwitchProperty(gateway=gateway_instance, session=mprm_session, mydevolo=mydevolo, - element_uid=element_uid, switch_type=switch_type) + def test_unit(self): + element_uid = self.devices['siren']['elementUIDs'][0] + switch_type = self.devices['siren']['switch_type'] + mlsp = MultiLevelSwitchProperty(element_uid=element_uid, setter=lambda uid, state: None, switch_type=switch_type) assert mlsp.unit is None - @pytest.mark.usefixtures("mock_mprmrest__post_set") def test_set_invalid(self): - value = self.devices.get("multi_level_switch").get("max") + 1 + value = self.devices['multi_level_switch']['max'] + 1 + element_uid = self.devices['multi_level_switch']['elementUIDs'][0] + device = self.homecontrol.devices.get(self.devices['multi_level_switch']['uid']) + device.multi_level_switch_property[element_uid]._setter = lambda uid, state: True with pytest.raises(ValueError): - self.homecontrol.devices.get(self.devices.get("multi_level_switch").get("uid"))\ - .multi_level_switch_property.get(self.devices.get("multi_level_switch").get("elementUIDs")[0]).set(value=value) + device.multi_level_switch_property[element_uid].set(value=value) - @pytest.mark.usefixtures("mock_mprmrest__post_set") - def test_set_valid(self): - value = self.devices.get("multi_level_switch").get("value") - self.homecontrol.devices.get(self.devices.get("multi_level_switch").get("uid"))\ - .multi_level_switch_property.get(self.devices.get("multi_level_switch").get("elementUIDs")[0]).set(value=value) - assert self.homecontrol.devices.get(self.devices.get("multi_level_switch").get("uid"))\ - .multi_level_switch_property.get(self.devices.get("multi_level_switch").get("elementUIDs")[0]).value == value + def test_set(self): + value = self.devices['multi_level_switch']['value'] + element_uid = self.devices['multi_level_switch']['elementUIDs'][0] + device = self.homecontrol.devices.get(self.devices['multi_level_switch']['uid']) + device.multi_level_switch_property[element_uid]._setter = lambda uid, state: True + device.multi_level_switch_property[element_uid].set(value=value) + assert device.multi_level_switch_property[element_uid].value == value diff --git a/tests/test_mydevolo.py b/tests/test_mydevolo.py index 464ee7d1..147f7a33 100644 --- a/tests/test_mydevolo.py +++ b/tests/test_mydevolo.py @@ -1,20 +1,19 @@ import pytest - -from devolo_home_control_api.mydevolo import Mydevolo, GatewayOfflineError, WrongUrlError, WrongCredentialsError +from devolo_home_control_api.mydevolo import GatewayOfflineError, Mydevolo, WrongCredentialsError, WrongUrlError class TestMydevolo: - @pytest.mark.usefixtures("mock_mydevolo__call") + def test_credentials_valid(self, mydevolo): assert mydevolo.credentials_valid() - @pytest.mark.usefixtures("mock_mydevolo__call_raise_WrongCredentialsError") + @pytest.mark.usefixtures("mock_mydevolo_uuid_raise_WrongCredentialsError") def test_credentials_invalid(self, mydevolo): assert not mydevolo.credentials_valid() @pytest.mark.usefixtures("mock_mydevolo__call") def test_gateway_ids(self, mydevolo): - assert mydevolo.get_gateway_ids() == [self.gateway.get("id")] + assert mydevolo.get_gateway_ids() == [self.gateway['id']] @pytest.mark.usefixtures("mock_mydevolo__call") def test_gateway_ids_empty(self, mydevolo): @@ -23,23 +22,23 @@ def test_gateway_ids_empty(self, mydevolo): @pytest.mark.usefixtures("mock_mydevolo__call") def test_get_full_url(self, mydevolo): - full_url = mydevolo.get_full_url(self.gateway.get("id")) - assert full_url == self.gateway.get("full_url") + full_url = mydevolo.get_full_url(self.gateway['id']) + assert full_url == self.gateway['full_url'] @pytest.mark.usefixtures("mock_mydevolo__call") def test_get_gateway(self, mydevolo): - details = mydevolo.get_gateway(self.gateway.get("id")) - assert details.get("gatewayId") == self.gateway.get("id") + details = mydevolo.get_gateway(self.gateway['id']) + assert details.get("gatewayId") == self.gateway['id'] @pytest.mark.usefixtures("mock_mydevolo__call_raise_WrongUrlError") def test_get_gateway_invalid(self, mydevolo): with pytest.raises(WrongUrlError): - mydevolo.get_gateway(self.gateway.get("id")) + mydevolo.get_gateway(self.gateway['id']) @pytest.mark.usefixtures("mock_mydevolo__call") def test_get_zwave_products(self, mydevolo): - product = mydevolo.get_zwave_products(manufacturer="0x0060", product_type="0x0001", product="0x000") - assert product.get("name") == "Everspring PIR Sensor SP814" + device_info = mydevolo.get_zwave_products(manufacturer="0x0060", product_type="0x0001", product="0x000") + assert device_info.get("name") == "Everspring PIR Sensor SP814" @pytest.mark.usefixtures("mock_mydevolo__call_raise_WrongUrlError") def test_get_zwave_products_invalid(self, mydevolo): @@ -47,41 +46,36 @@ def test_get_zwave_products_invalid(self, mydevolo): assert device_info.get("name") == "Unknown" @pytest.mark.usefixtures("mock_mydevolo__call") - def test_maintenance_on(self, mydevolo): - assert not mydevolo.maintenance() - - @pytest.mark.usefixtures("mock_mydevolo__call") - def test_maintenance_off(self, mydevolo): - assert mydevolo.maintenance() + @pytest.mark.parametrize("result", [True, False]) + def test_maintenance(self, mydevolo, result): + assert mydevolo.maintenance() == result def test_set_password(self, mydevolo): - mydevolo._gateway_ids = [self.gateway.get("id")] - - mydevolo.password = self.user.get("password") + mydevolo._gateway_ids = [self.gateway['id']] + mydevolo.password = self.user['password'] assert mydevolo._uuid is None assert mydevolo._gateway_ids == [] def test_set_user(self, mydevolo): - mydevolo._gateway_ids = [self.gateway.get("id")] - - mydevolo.user = self.user.get("username") + mydevolo._gateway_ids = [self.gateway['id']] + mydevolo.user = self.user['username'] assert mydevolo._uuid is None assert mydevolo._gateway_ids == [] def test_get_user(self, mydevolo): - mydevolo.user = self.user.get("username") - assert mydevolo.user == self.user.get("username") + mydevolo.user = self.user['username'] + assert mydevolo.user == self.user['username'] def test_get_password(self, mydevolo): - mydevolo.password = self.user.get("password") - assert mydevolo.password == self.user.get("password") + mydevolo.password = self.user['password'] + assert mydevolo.password == self.user['password'] @pytest.mark.usefixtures("mock_mydevolo__call") def test_uuid(self, mydevolo): mydevolo._uuid = None - assert mydevolo.uuid() == self.user.get("uuid") + assert mydevolo.uuid() == self.user['uuid'] @pytest.mark.usefixtures("mock_response_wrong_credentials_error") def test_call_WrongCredentialsError(self): diff --git a/tests/test_publisher.py b/tests/test_publisher.py index 92133ff4..74aca7c9 100644 --- a/tests/test_publisher.py +++ b/tests/test_publisher.py @@ -10,7 +10,9 @@ def test_add_event(self): assert "Test" in self.homecontrol.publisher._events def test_delete_event(self): - self.homecontrol.publisher._events = {"Test": {}} + self.homecontrol.publisher._events = { + "Test": {} + } self.homecontrol.publisher.delete_event("Test") assert "Test" not in self.homecontrol.publisher._events @@ -31,8 +33,8 @@ def test_dispatch(self): self.homecontrol.publisher.dispatch(event="hdm:ZWave:F6BF9812/4", message=()) - class Subscriber: + def __init__(self, name): self.name = name diff --git a/tests/test_remote_control_property.py b/tests/test_remote_control_property.py index 6cc1b2c7..83542f79 100644 --- a/tests/test_remote_control_property.py +++ b/tests/test_remote_control_property.py @@ -1,21 +1,20 @@ import pytest - from devolo_home_control_api.exceptions.device import WrongElementError from devolo_home_control_api.properties.remote_control_property import RemoteControlProperty @pytest.mark.usefixtures("home_control_instance") class TestRemoteControlProperty: - def test_remote_control_property_invalid(self, gateway_instance, mprm_session, mydevolo): + + def test_remote_control_property_invalid(self): with pytest.raises(WrongElementError): - RemoteControlProperty(gateway=gateway_instance, session=mprm_session, mydevolo=mydevolo, element_uid="invalid") + RemoteControlProperty(element_uid="invalid", setter=lambda uid, state: None) def test_set_invalid(self): with pytest.raises(ValueError): self.homecontrol.devices.get(self.devices.get("remote").get("uid"))\ .remote_control_property.get(self.devices.get("remote").get("elementUIDs")[0]).set(5) - @pytest.mark.usefixtures("mock_mprmrest__post_set") def test_set_valid(self): self.homecontrol.devices.get(self.devices.get("remote").get("uid"))\ .remote_control_property.get(self.devices.get("remote").get("elementUIDs")[0]).set(1) diff --git a/tests/test_settings_property.py b/tests/test_settings_property.py index 2090efa5..b6872bb5 100644 --- a/tests/test_settings_property.py +++ b/tests/test_settings_property.py @@ -1,76 +1,65 @@ import pytest - from devolo_home_control_api.exceptions.device import WrongElementError from devolo_home_control_api.properties.settings_property import SettingsProperty @pytest.mark.usefixtures("home_control_instance") class TestSettingsProperty: - def test_settings_property_valid(self, gateway_instance, mprm_session, mydevolo): - setting_property = SettingsProperty(gateway=gateway_instance, - session=mprm_session, - mydevolo=mydevolo, - element_uid=f"lis.{self.devices.get('mains').get('uid')}", + + def test_settings_property_valid(self): + setting_property = SettingsProperty(element_uid=f"lis.{self.devices.get('mains').get('uid')}", + setter=lambda uid, + state: None, led_setting=True, events_enabled=False, param_changed=True, local_switching=False, - remote_switching=True,) + remote_switching=True) assert setting_property.led_setting assert not setting_property.events_enabled assert setting_property.param_changed assert not setting_property.local_switching assert setting_property.remote_switching - def test_settings_property_invalid(self, gateway_instance, mprm_session, mydevolo): + def test_settings_property_invalid(self): with pytest.raises(WrongElementError): - SettingsProperty(gateway=gateway_instance, session=mprm_session, mydevolo=mydevolo, element_uid="invalid") + SettingsProperty(element_uid="invalid", setter=lambda uid, state: None) - @pytest.mark.usefixtures("mock_mprmrest__post_set") def test__set_bas_valid(self): muted = self.devices.get("siren").get("muted") - self.homecontrol.devices.get(self.devices.get("siren").get("uid"))\ - .settings_property.get("muted").set(value=not muted) - assert self.homecontrol.devices.get(self.devices.get("siren").get("uid"))\ - .settings_property.get("muted").value is not muted + self.homecontrol.devices.get(self.devices.get("siren").get("uid")).settings_property.get("muted").set(value=not muted) + assert self.homecontrol.devices.get( + self.devices.get("siren").get("uid")).settings_property.get("muted").value is not muted - @pytest.mark.usefixtures("mock_mprmrest__post_set") def test__set_gds_valid(self): - self.homecontrol.devices.get(self.devices.get("mains").get("uid"))\ - .settings_property.get("general_device_settings").set(events_enabled=False) - assert not self.homecontrol.devices.get(self.devices.get("mains").get("uid"))\ - .settings_property.get("general_device_settings").events_enabled + self.homecontrol.devices.get( + self.devices.get("mains").get("uid")).settings_property.get("general_device_settings").set(events_enabled=False) + assert not self.homecontrol.devices.get( + self.devices.get("mains").get("uid")).settings_property.get("general_device_settings").events_enabled - @pytest.mark.usefixtures("mock_mprmrest__post_set") def test__set_lis_valid(self): - self.homecontrol.devices.get(self.devices.get("mains").get("uid"))\ - .settings_property.get("led").set(led_setting=False) - assert not self.homecontrol.devices.get(self.devices.get("mains").get("uid"))\ - .settings_property.get("led").led_setting + self.homecontrol.devices.get(self.devices.get("mains").get("uid")).settings_property.get("led").set(led_setting=False) + assert not self.homecontrol.devices.get(self.devices.get("mains").get("uid")).settings_property.get("led").led_setting - @pytest.mark.usefixtures("mock_mprmrest__post_set") def test__set_mss_invalid(self): with pytest.raises(ValueError): - self.homecontrol.devices.get(self.devices.get("sensor").get("uid"))\ - .settings_property.get("motion_sensitivity").set(motion_sensitivity=110) + self.homecontrol.devices.get( + self.devices.get("sensor").get("uid")).settings_property.get("motion_sensitivity").set(motion_sensitivity=110) - @pytest.mark.usefixtures("mock_mprmrest__post_set") def test__set_mss_valid(self): - self.homecontrol.devices.get(self.devices.get("sensor").get("uid"))\ - .settings_property.get("motion_sensitivity").set(motion_sensitivity=90) - assert self.homecontrol.devices.get(self.devices.get("sensor").get("uid"))\ - .settings_property.get("motion_sensitivity").motion_sensitivity == 90 + self.homecontrol.devices.get( + self.devices.get("sensor").get("uid")).settings_property.get("motion_sensitivity").set(motion_sensitivity=90) + assert self.homecontrol.devices.get( + self.devices.get("sensor").get("uid")).settings_property.get("motion_sensitivity").motion_sensitivity == 90 - @pytest.mark.usefixtures("mock_mprmrest__post_set") def test__set_ps_valid(self): - self.homecontrol.devices.get(self.devices.get("mains").get("uid"))\ - .settings_property.get("protection").set(local_switching=False) - assert not self.homecontrol.devices.get(self.devices.get("mains").get("uid"))\ - .settings_property.get("protection").local_switching + self.homecontrol.devices.get( + self.devices.get("mains").get("uid")).settings_property.get("protection").set(local_switching=False) + assert not self.homecontrol.devices.get( + self.devices.get("mains").get("uid")).settings_property.get("protection").local_switching - @pytest.mark.usefixtures("mock_mprmrest__post_set") def test__set_trs_valid(self): - self.homecontrol.devices.get(self.devices.get("sensor").get("uid"))\ - .settings_property.get("temperature_report").set(temp_report=False) - assert not self.homecontrol.devices.get(self.devices.get("sensor").get("uid"))\ - .settings_property.get("temperature_report").temp_report + self.homecontrol.devices.get( + self.devices.get("sensor").get("uid")).settings_property.get("temperature_report").set(temp_report=False) + assert not self.homecontrol.devices.get( + self.devices.get("sensor").get("uid")).settings_property.get("temperature_report").temp_report diff --git a/tests/test_updater.py b/tests/test_updater.py index c188b4bd..c130097f 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -1,44 +1,71 @@ -import pytest from datetime import datetime, timezone +import pytest +from devolo_home_control_api.backend import MESSAGE_TYPES + @pytest.mark.usefixtures("home_control_instance") @pytest.mark.usefixtures("mock_publisher_dispatch") class TestUpdater: + def test_hasattr(self): + updater_functions = (func for func in dir(self.homecontrol.updater) + if callable(getattr(self.homecontrol.updater, + func)) and not func.startswith("__")) + assert set(MESSAGE_TYPES.values()).difference(updater_functions) == set() + @pytest.mark.usefixtures("mock_updater_binary_switch") def test_update_device(self, mocker): - message = {"topic": "com/prosyst/mbs/services/fim/FunctionalItemEvent/PROPERTY_CHANGED", - "properties": {"property.name": "state", "uid": f"devolo.BinarySwitch:{self.devices['mains']['uid']}"}} + message = { + "topic": "com/prosyst/mbs/services/fim/FunctionalItemEvent/PROPERTY_CHANGED", + "properties": { + "property.name": "state", + "uid": f"devolo.BinarySwitch:{self.devices['mains']['uid']}", + }, + } spy = mocker.spy(self.homecontrol.updater, '_binary_switch') self.homecontrol.updater.update(message=message) spy.assert_called_once_with(message) @pytest.mark.usefixtures("mock_updater_pending_operations") def test_update_pending_operations(self, mocker): - message = {"topic": "com/prosyst/mbs/services/fim/FunctionalItemEvent/PROPERTY_CHANGED", - "properties": {"property.name": "pendingOperations", - "uid": ""}} + message = { + "topic": "com/prosyst/mbs/services/fim/FunctionalItemEvent/PROPERTY_CHANGED", + "properties": { + "property.name": "pendingOperations", + "uid": "", + }, + } spy = mocker.spy(self.homecontrol.updater, '_pending_operations') self.homecontrol.updater.update(message=message) spy.assert_called_once_with(message) def test_update_unwanted(self, mocker): - message = {"topic": "com/prosyst/mbs/services/fim/FunctionalItemEvent/UNREGISTERED"} + message = { + "topic": "com/prosyst/mbs/services/fim/FunctionalItemEvent/UNREGISTERED", + } spy = mocker.spy(self.homecontrol.updater, '_unknown') self.homecontrol.updater.update(message=message) spy.assert_not_called() def test_update_smart_group(self, mocker): - message = {"topic": "com/prosyst/mbs/services/fim/FunctionalItemEvent/PROPERTY_CHANGED", - "properties": {"uid": "devolo.MultiLevelSwitch:devolo.smartGroup.Group", - "property.name": "value"}} + message = { + "topic": "com/prosyst/mbs/services/fim/FunctionalItemEvent/PROPERTY_CHANGED", + "properties": { + "uid": "devolo.MultiLevelSwitch:devolo.smartGroup.Group", + "property.name": "value", + }, + } spy = mocker.spy(self.homecontrol.updater, '_multi_level_switch') self.homecontrol.updater.update(message=message) spy.assert_not_called() - message = {"topic": "com/prosyst/mbs/services/fim/FunctionalItemEvent/PROPERTY_CHANGED", - "properties": {"uid": "devolo.BinarySwitch:devolo.smartGroup.Group", - "property.name": "value"}} + message = { + "topic": "com/prosyst/mbs/services/fim/FunctionalItemEvent/PROPERTY_CHANGED", + "properties": { + "uid": "devolo.BinarySwitch:devolo.smartGroup.Group", + "property.name": "value", + }, + } spy = mocker.spy(self.homecontrol.updater, '_binary_switch') self.homecontrol.updater.update(message=message) spy.assert_not_called() @@ -46,33 +73,43 @@ def test_update_smart_group(self, mocker): def test__automatic_calibration(self): uid = self.devices['blinds']['uid'] calibration_status = self.devices['blinds']['calibrationStatus'] - self.homecontrol.updater._automatic_calibration(message={"properties": { - "uid": f"acs.{uid}", - "property.value.new": {"status": calibration_status}}}) + self.homecontrol.updater._automatic_calibration( + message={"properties": { + "uid": f"acs.{uid}", + "property.value.new": { + "status": calibration_status, + }, + }}) assert self.homecontrol.devices[uid].settings_property['automatic_calibration'].calibration_status def test__automatic_calibration_key_error(self): uid = self.devices['blinds']['uid'] calibration_status = self.devices['blinds']['calibrationStatus'] - self.homecontrol.updater._automatic_calibration(message={"properties": { - "uid": f"acs.{uid}", - "property.value.new": calibration_status}}) + self.homecontrol.updater._automatic_calibration( + message={"properties": { + "uid": f"acs.{uid}", + "property.value.new": calibration_status, + }}) assert self.homecontrol.devices[uid].settings_property['automatic_calibration'].calibration_status def test__binary_async_blinds(self): uid = self.devices['blinds']['uid'] self.homecontrol.devices[uid].settings_property['i2'].value = self.devices['blinds']['i2'] - self.homecontrol.updater._binary_async(message={"properties": { - "uid": f"bas.{uid}#i2", - "property.value.new": not self.devices['blinds']['i2']}}) + self.homecontrol.updater._binary_async( + message={"properties": { + "uid": f"bas.{uid}#i2", + "property.value.new": not self.devices['blinds']['i2'], + }}) assert not self.homecontrol.devices[uid].settings_property['i2'].value def test__binary_async_siren(self): uid = self.devices['siren']['uid'] self.homecontrol.devices[uid].settings_property['muted'].value = self.devices['siren']['muted'] - self.homecontrol.updater._binary_async(message={"properties": { - "uid": f"bas.{uid}", - "property.value.new": not self.devices['siren']['muted']}}) + self.homecontrol.updater._binary_async( + message={"properties": { + "uid": f"bas.{uid}", + "property.value.new": not self.devices['siren']['muted'], + }}) assert not self.homecontrol.devices[uid].settings_property['muted'].value def test__binary_sensor(self): @@ -80,48 +117,50 @@ def test__binary_sensor(self): device = self.homecontrol.devices[uid].binary_sensor_property[f"devolo.BinarySensor:{uid}"] device.state = True state = device.state - self.homecontrol.updater._binary_sensor(message={"properties": - {"property.name": "state", - "uid": f"devolo.BinarySensor:{uid}", - "property.value.new": 0}}) + self.homecontrol.updater._binary_sensor( + message={"properties": { + "property.name": "state", + "uid": f"devolo.BinarySensor:{uid}", + "property.value.new": 0, + }}) state_new = device.state assert state != state_new assert device.last_activity != datetime.fromtimestamp(0) def test__binary_sync(self): uid = self.devices['blinds']['uid'] - self.homecontrol.updater._binary_sync(message={"properties": - {"uid": f"bss.{uid}", - "property.value.new": self.devices['blinds']['movement_direction']}}) + self.homecontrol.updater._binary_sync(message={ + "properties": { + "uid": f"bss.{uid}", + "property.value.new": self.devices['blinds']['movement_direction'], + }, + }) assert self.homecontrol.devices[uid].settings_property["movement_direction"].direction is \ bool(self.devices['blinds']['movement_direction']) def test__binary_switch(self): uid = self.devices.get("mains").get("uid") - self.homecontrol.devices.get(uid).binary_switch_property \ - .get(f"devolo.BinarySwitch:{uid}").state = True - state = self.homecontrol.devices.get(uid).binary_switch_property \ - .get(f"devolo.BinarySwitch:{uid}").state - self.homecontrol.updater._binary_switch(message={"properties": - {"property.name": "targetState", - "uid": f"devolo.BinarySwitch:{uid}", - "property.value.new": 0}}) - state_new = self.homecontrol.devices.get(uid).binary_switch_property \ - .get(f"devolo.BinarySwitch:{uid}").state + self.homecontrol.devices.get(uid).binary_switch_property.get(f"devolo.BinarySwitch:{uid}").state = True + state = self.homecontrol.devices.get(uid).binary_switch_property.get(f"devolo.BinarySwitch:{uid}").state + self.homecontrol.updater._binary_switch(message={ + "properties": { + "property.name": "targetState", + "uid": f"devolo.BinarySwitch:{uid}", + "property.value.new": 0 + }, + }) + state_new = self.homecontrol.devices.get(uid).binary_switch_property.get(f"devolo.BinarySwitch:{uid}").state assert state != state_new - def test__device_change_error(self, mocker): - self.homecontrol.updater.on_device_change = None - spy = mocker.spy(self.homecontrol.updater._logger, "error") - self.homecontrol.updater._device_change({}) - spy.assert_called_once_with("on_device_change is not set.") - def test__device_state(self): uid = self.devices.get("mains").get("uid") online_state = self.homecontrol.devices.get(uid).status - self.homecontrol.updater._device_state(message={"properties": {"uid": uid, - "property.name": "status", - "property.value.new": 1}}) + self.homecontrol.updater._device_state( + message={"properties": { + "uid": uid, + "property.name": "status", + "property.value.new": 1 + }}) assert self.homecontrol.devices.get(uid).status == 1 assert online_state != self.homecontrol.devices.get(uid).status @@ -129,13 +168,18 @@ def test__general_device(self): uid = self.devices['mains']['uid'] events_enabled = self.devices['mains']['properties']['eventsEnabled'] self.homecontrol.devices[uid].settings_property['general_device_settings'].events_enabled = events_enabled - self.homecontrol.updater._general_device(message={"properties": - {"uid": f"gds.{uid}", - "property.value.new": - {"eventsEnabled": not events_enabled, - "icon": self.devices['mains']['properties']['icon'], - "name": self.devices['mains']['properties']["itemName"], - "zoneID": self.devices['mains']['properties']["zoneId"]}}}) + self.homecontrol.updater._general_device( + message={ + "properties": { + "uid": f"gds.{uid}", + "property.value.new": { + "eventsEnabled": not events_enabled, + "icon": self.devices['mains']['properties']['icon'], + "name": self.devices['mains']['properties']["itemName"], + "zoneID": self.devices['mains']['properties']["zoneId"], + }, + }, + }) assert not self.homecontrol.devices[uid].settings_property['general_device_settings'].events_enabled def test__gateway_accessible(self): @@ -143,9 +187,16 @@ def test__gateway_accessible(self): self.homecontrol.gateway.sync = True accessible = self.homecontrol.gateway.online online_sync = self.homecontrol.gateway.sync - self.homecontrol.updater._gateway_accessible(message={"properties": {"property.name": "gatewayAccessible", - "property.value.new": {"accessible": False, - "onlineSync": False}}}) + self.homecontrol.updater._gateway_accessible( + message={ + "properties": { + "property.name": "gatewayAccessible", + "property.value.new": { + "accessible": False, + "onlineSync": False, + }, + }, + }) accessible_new = self.homecontrol.gateway.online online_sync_new = self.homecontrol.gateway.sync assert accessible != accessible_new @@ -154,9 +205,14 @@ def test__gateway_accessible(self): def test__grouping(self): zone_id = "hz_3" name = self.gateway['zones'][zone_id] - self.homecontrol.updater._grouping(message={"properties": - {"property.value.new": - [{"id": zone_id, "name": self.devices['mains']['properties']['zone']}]}}) + self.homecontrol.updater._grouping(message={ + "properties": { + "property.value.new": [{ + "id": zone_id, + "name": self.devices['mains']['properties']['zone'], + }], + }, + }) assert self.homecontrol.gateway.zones[zone_id] != name assert self.homecontrol.gateway.zones[zone_id] == self.devices['mains']['properties']['zone'] @@ -165,8 +221,10 @@ def test__gui_enabled(self): element_uids = self.devices['mains']['properties']['elementUIDs'] enabled = self.devices['mains']['properties']['guiEnabled'] self.homecontrol.devices[uid].binary_switch_property[element_uids[1]].enabled = enabled - self.homecontrol.updater._gui_enabled(message={"uid": element_uids[0], - "property.value.new": not enabled}) + self.homecontrol.updater._gui_enabled(message={ + "uid": element_uids[0], + "property.value.new": not enabled, + }) assert self.homecontrol.devices[uid].binary_switch_property[element_uids[1]].enabled != enabled def test__humidity_bar(self): @@ -175,13 +233,17 @@ def test__humidity_bar(self): self.homecontrol.devices.get(uid).humidity_bar_property.get(f"devolo.HumidityBar:{uid}").zone = 1 current_value = self.homecontrol.devices.get(uid).humidity_bar_property.get(f"devolo.HumidityBar:{uid}").value current_zone = self.homecontrol.devices.get(uid).humidity_bar_property.get(f"devolo.HumidityBar:{uid}").zone - self.homecontrol.updater._humidity_bar(message={"properties": - {"uid": f"devolo.HumidityBarValue:{uid}", - "property.value.new": 50}}) + self.homecontrol.updater._humidity_bar( + message={"properties": { + "uid": f"devolo.HumidityBarValue:{uid}", + "property.value.new": 50, + }}) current_value_new = self.homecontrol.devices.get(uid).humidity_bar_property.get(f"devolo.HumidityBar:{uid}").value - self.homecontrol.updater._humidity_bar(message={"properties": - {"uid": f"devolo.HumidityBarZone:{uid}", - "property.value.new": 0}}) + self.homecontrol.updater._humidity_bar( + message={"properties": { + "uid": f"devolo.HumidityBarZone:{uid}", + "property.value.new": 0, + }}) current_zone_new = self.homecontrol.devices.get(uid).humidity_bar_property.get(f"devolo.HumidityBar:{uid}").zone assert current_value_new == 50 @@ -189,45 +251,81 @@ def test__humidity_bar(self): assert current_value != current_value_new assert current_zone != current_zone_new + @pytest.mark.usefixtures("mock_publisher_add_event") + @pytest.mark.usefixtures("mock_publisher_delete_event") + @pytest.mark.parametrize("event", ["add", "delete"]) + def test__inspect_devices_valid(self, mocker, event): + self.homecontrol.updater.on_device_change = lambda device_uids: (device_uids, event) + spy = mocker.spy(self.homecontrol.updater._publisher, f"{event}_event") + self.homecontrol.updater._inspect_devices( + message={"properties": { + "uid": "devolo.DevicesPage", + "property.value.new": [], + }}) + spy.assert_called_once() + + def test__inspect_devices_error(self, mocker): + self.homecontrol.updater.on_device_change = None + spy = mocker.spy(self.homecontrol.updater._logger, "error") + self.homecontrol.updater._inspect_devices({}) + spy.assert_called_once_with("on_device_change is not set.") + self.homecontrol.updater.on_device_change = lambda: None + spy = mocker.spy(self.homecontrol.updater, "on_device_change") + self.homecontrol.updater._inspect_devices(message={"properties": { + "uid": "", + "property.value.new": [], + }}) + spy.assert_not_called() + def test__meter(self): uid = self.devices.get("mains").get("uid") - self.homecontrol.devices.get(uid).consumption_property \ - .get(f"devolo.Meter:{uid}").current = 5 - self.homecontrol.devices.get(uid).consumption_property \ - .get(f"devolo.Meter:{uid}").total = 230 - total = self.homecontrol.devices.get(uid).consumption_property \ - .get(f"devolo.Meter:{uid}").total + self.homecontrol.devices.get(uid).consumption_property.get(f"devolo.Meter:{uid}").current = 5 + self.homecontrol.devices.get(uid).consumption_property.get(f"devolo.Meter:{uid}").total = 230 + total = self.homecontrol.devices.get(uid).consumption_property.get(f"devolo.Meter:{uid}").total # Changing current value - self.homecontrol.updater._meter(message={"properties": {"property.name": "currentValue", - "uid": f"devolo.Meter:{uid}", - "property.value.new": 7}}) - current_new = self.homecontrol.devices.get(uid).consumption_property \ - .get(f"devolo.Meter:{uid}").current + self.homecontrol.updater._meter( + message={"properties": { + "property.name": "currentValue", + "uid": f"devolo.Meter:{uid}", + "property.value.new": 7, + }}) + current_new = self.homecontrol.devices.get(uid).consumption_property.get(f"devolo.Meter:{uid}").current # Check if current value has changed assert current_new == 7 # Check if total has not changed - assert total == self.homecontrol.devices.get(uid).consumption_property \ - .get(f"devolo.Meter:{uid}").total + assert total == self.homecontrol.devices.get(uid).consumption_property.get(f"devolo.Meter:{uid}").total # Changing total value - self.homecontrol.updater._meter(message={"properties": {"property.name": "totalValue", - "uid": f"devolo.Meter:{uid}", - "property.value.new": 235}}) - total_new = self.homecontrol.devices.get(uid).consumption_property \ - .get(f"devolo.Meter:{uid}").total + self.homecontrol.updater._meter( + message={"properties": { + "property.name": "totalValue", + "uid": f"devolo.Meter:{uid}", + "property.value.new": 235 + }}) + total_new = self.homecontrol.devices.get(uid).consumption_property.get(f"devolo.Meter:{uid}").total # Check if total value has changed assert total_new == 235 # Check if current value has not changed - assert self.homecontrol.devices.get(uid).consumption_property \ - .get(f"devolo.Meter:{uid}").current == current_new + assert self.homecontrol.devices.get(uid).consumption_property.get(f"devolo.Meter:{uid}").current == current_new + + def test___multilevel_async(self): + uid = self.devices['mains']['UID'] + element_uid = self.devices['mains']['properties']['settingUIDs'][4] + value = self.devices['mains']['flashMode'] + 1 + self.homecontrol.updater._multilevel_async(message={"properties": { + "uid": element_uid, + "property.value.new": value + }}) + assert self.homecontrol.devices[uid].settings_property['flash_mode'].value == value def test__multi_level_sensor(self): uid = self.devices['sensor']['uid'] element_uid = f"devolo.MultiLevelSensor:{uid}#MultilevelSensor(1)" self.homecontrol.devices[uid].multi_level_sensor_property[element_uid].value = 100 current = self.homecontrol.devices[uid].multi_level_sensor_property[element_uid].value - self.homecontrol.updater._multi_level_sensor(message={"properties": - {"uid": element_uid, - "property.value.new": 50}}) + self.homecontrol.updater._multi_level_sensor(message={"properties": { + "uid": element_uid, + "property.value.new": 50 + }}) current_new = self.homecontrol.devices[uid].multi_level_sensor_property[element_uid].value assert current_new == 50 @@ -236,16 +334,15 @@ def test__multi_level_sensor(self): def test__multi_level_switch(self): device = self.devices.get("multi_level_switch") uid = device.get("uid") - self.homecontrol.devices.get(uid).multi_level_switch_property \ - .get(device.get("elementUIDs")[0]).value = \ + self.homecontrol.devices.get(uid).multi_level_switch_property.get(device.get("elementUIDs")[0]).value = \ device.get("value") - current = self.homecontrol.devices.get(uid).multi_level_switch_property \ - .get(device.get("elementUIDs")[0]).value - self.homecontrol.updater._multi_level_switch(message={"properties": - {"uid": device.get("elementUIDs")[0], - "property.value.new": device.get("max")}}) - current_new = self.homecontrol.devices.get(uid).multi_level_switch_property \ - .get(device.get("elementUIDs")[0]).value + current = self.homecontrol.devices.get(uid).multi_level_switch_property.get(device.get("elementUIDs")[0]).value + self.homecontrol.updater._multi_level_switch( + message={"properties": { + "uid": device.get("elementUIDs")[0], + "property.value.new": device.get("max"), + }}) + current_new = self.homecontrol.devices.get(uid).multi_level_switch_property.get(device.get("elementUIDs")[0]).value assert current_new == device.get("max") assert current != current_new @@ -255,9 +352,11 @@ def test__multilevel_sync_sensor(self): uid = device['uid'] value = device['properties']['value'] self.homecontrol.devices[uid].settings_property['motion_sensitivity'].motion_sensitivity = value - self.homecontrol.updater._multilevel_sync(message={"properties": - {"uid": f"mss.{uid}", - "property.value.new": value - 1}}) + self.homecontrol.updater._multilevel_sync( + message={"properties": { + "uid": f"mss.{uid}", + "property.value.new": value - 1, + }}) assert self.homecontrol.devices[uid].settings_property['motion_sensitivity'].motion_sensitivity == value - 1 def test__multilevel_sync_shutter(self): @@ -265,9 +364,11 @@ def test__multilevel_sync_shutter(self): uid = device['uid'] value = device['shutter_duration'] self.homecontrol.devices[uid].settings_property['shutter_duration'].shutter_duration = value - self.homecontrol.updater._multilevel_sync(message={"properties": - {"uid": f"mss.{uid}", - "property.value.new": value - 1}}) + self.homecontrol.updater._multilevel_sync( + message={"properties": { + "uid": f"mss.{uid}", + "property.value.new": value - 1, + }}) assert self.homecontrol.devices[uid].settings_property['shutter_duration'].shutter_duration == value - 1 def test__multilevel_sync_siren(self): @@ -275,9 +376,11 @@ def test__multilevel_sync_siren(self): uid = device['uid'] value = device['properties']['value'] self.homecontrol.devices[uid].settings_property['tone'].tone = value - self.homecontrol.updater._multilevel_sync(message={"properties": - {"uid": f"mss.{uid}", - "property.value.new": value - 1}}) + self.homecontrol.updater._multilevel_sync( + message={"properties": { + "uid": f"mss.{uid}", + "property.value.new": value - 1, + }}) assert self.homecontrol.devices[uid].settings_property['tone'].tone == value - 1 def test__led(self): @@ -285,9 +388,10 @@ def test__led(self): uid = device['uid'] led_setting = device['properties']['led_setting'] self.homecontrol.devices[uid].settings_property['led'].led_setting = led_setting - self.homecontrol.updater._led(message={"properties": - {"uid": f"lis.{uid}", - "property.value.new": not led_setting}}) + self.homecontrol.updater._led(message={"properties": { + "uid": f"lis.{uid}", + "property.value.new": not led_setting, + }}) assert self.homecontrol.devices[uid].settings_property['led'].led_setting is not led_setting def test__parameter(self): @@ -295,9 +399,11 @@ def test__parameter(self): uid = device['uid'] param_changed = device['properties']['param_changed'] self.homecontrol.devices[uid].settings_property['param_changed'].param_changed = param_changed - self.homecontrol.updater._parameter(message={"properties": - {"uid": f"cps.{uid}", - "property.value.new": not param_changed}}) + self.homecontrol.updater._parameter( + message={"properties": { + "uid": f"cps.{uid}", + "property.value.new": not param_changed, + }}) assert self.homecontrol.devices[uid].settings_property['param_changed'].param_changed is not param_changed def test__pending_operations_false(self): @@ -305,8 +411,9 @@ def test__pending_operations_false(self): uid = device['uid'] pending_operation = device['properties']['pending_operations'] self.homecontrol.devices[uid].pending_operation = not pending_operation - self.homecontrol.updater._pending_operations(message={"properties": - {"uid": device['elementUIDs'][1]}}) + self.homecontrol.updater._pending_operations(message={"properties": { + "uid": device['elementUIDs'][1] + }}) assert self.homecontrol.devices[uid].pending_operation def test__pending_operations_true(self): @@ -314,9 +421,13 @@ def test__pending_operations_true(self): uid = device['uid'] pending_operation = device['properties']['pending_operations'] self.homecontrol.devices[uid].pending_operation = pending_operation - self.homecontrol.updater._pending_operations(message={"properties": - {"uid": device['elementUIDs'][1], - "property.value.new": {"status": 1}}}) + self.homecontrol.updater._pending_operations( + message={"properties": { + "uid": device['elementUIDs'][1], + "property.value.new": { + "status": 1 + } + }}) assert not self.homecontrol.devices[uid].pending_operation def test__pending_operations_setting(self): @@ -324,15 +435,20 @@ def test__pending_operations_setting(self): uid = device['uid'] pending_operation = device['properties']['pending_operations'] self.homecontrol.devices[uid].pending_operation = pending_operation - self.homecontrol.updater._pending_operations(message={"properties": - {"uid": device['settingUIDs'][3], - "property.value.new": {"status": 1}}}) + self.homecontrol.updater._pending_operations( + message={"properties": { + "uid": device['settingUIDs'][3], + "property.value.new": { + "status": 1, + }, + }}) assert not self.homecontrol.devices[uid].pending_operation def test__pending_operations_useless(self, mocker): spy = mocker.spy(self.homecontrol.updater._publisher, 'dispatch') - self.homecontrol.updater._pending_operations(message={"properties": - {"uid": "devolo.PairDevice"}}) + self.homecontrol.updater._pending_operations(message={"properties": { + "uid": "devolo.PairDevice", + }}) spy.assert_not_called() def test__protection_local(self): @@ -340,10 +456,13 @@ def test__protection_local(self): uid = device['uid'] local_switch = device['properties']['local_switch'] self.homecontrol.devices[uid].settings_property['protection'].local_switching = local_switch - self.homecontrol.updater._protection(message={"properties": - {"uid": f"ps.{uid}", - "property.name": "targetLocalSwitch", - "property.value.new": not local_switch}}) + self.homecontrol.updater._protection(message={ + "properties": { + "uid": f"ps.{uid}", + "property.name": "targetLocalSwitch", + "property.value.new": not local_switch, + }, + }) assert self.homecontrol.devices[uid].settings_property['protection'].local_switching is not local_switch def test__protection_remote(self): @@ -351,10 +470,13 @@ def test__protection_remote(self): uid = device['uid'] remote_switch = device['properties']['remote_switch'] self.homecontrol.devices[uid].settings_property['protection'].remote_switching = remote_switch - self.homecontrol.updater._protection(message={"properties": - {"uid": f"ps.{uid}", - "property.name": "targetRemoteSwitch", - "property.value.new": not remote_switch}}) + self.homecontrol.updater._protection(message={ + "properties": { + "uid": f"ps.{uid}", + "property.name": "targetRemoteSwitch", + "property.value.new": not remote_switch, + }, + }) assert self.homecontrol.devices[uid].settings_property['protection'].remote_switching is not remote_switch def test__remote_control(self): @@ -362,9 +484,11 @@ def test__remote_control(self): uid = device.get("uid") self.homecontrol.devices.get(uid).remote_control_property \ .get(device.get("elementUIDs")[0]).key_pressed = 0 - self.homecontrol.updater._remote_control(message={"properties": - {"uid": device.get("elementUIDs")[0], - "property.value.new": 1}}) + self.homecontrol.updater._remote_control( + message={"properties": { + "uid": device.get("elementUIDs")[0], + "property.value.new": 1, + }}) assert self.homecontrol.devices.get(uid).remote_control_property \ .get(device.get("elementUIDs")[0]).key_pressed == 1 @@ -374,8 +498,10 @@ def test__since_time(self): uid = device['uid'] now = datetime.now() total_since = self.homecontrol.devices[uid].consumption_property[f'devolo.Meter:{uid}'].total_since - self.homecontrol.updater._since_time({"uid": f"devolo.Meter:{uid}", - "property.value.new": now.replace(tzinfo=timezone.utc).timestamp() * 1000}) + self.homecontrol.updater._since_time({ + "uid": f"devolo.Meter:{uid}", + "property.value.new": now.replace(tzinfo=timezone.utc).timestamp() * 1000, + }) new_total_since = self.homecontrol.devices[uid].consumption_property[f'devolo.Meter:{uid}'].total_since assert total_since != new_total_since assert new_total_since == now @@ -383,31 +509,36 @@ def test__since_time(self): def test__switch_type(self): device = self.devices['remote'] uid = device['uid'] - self.homecontrol.updater._switch_type(message={"properties": - {"uid": f"sts.{uid}", - "property.value.new": device['key_count'] / 4}}) + self.homecontrol.updater._switch_type( + message={"properties": { + "uid": f"sts.{uid}", + "property.value.new": device['key_count'] / 4, + }}) assert self.homecontrol.devices[uid].settings_property['switch_type'].value == device['key_count'] / 2 - def test__temperature(self): + def test__temperature_report(self): device = self.devices['sensor'] uid = device['uid'] self.homecontrol.devices[uid].settings_property['temperature_report'].temp_report = device['temp_report'] - self.homecontrol.updater._temperature(message={"properties": - {"uid": f"trs.{uid}", - "property.value.new": not device['temp_report']}}) + self.homecontrol.updater._temperature_report( + message={"properties": { + "uid": f"trs.{uid}", + "property.value.new": not device['temp_report'], + }}) assert self.homecontrol.devices[uid].settings_property['temperature_report'].temp_report is not device['temp_report'] def test__voltage_multi_level_sensor(self): uid = self.devices.get("mains").get("uid") - self.homecontrol.devices.get(uid).multi_level_sensor_property \ - .get(f"devolo.VoltageMultiLevelSensor:{uid}").value = 231 - current = self.homecontrol.devices.get(uid).multi_level_sensor_property \ - .get(f"devolo.VoltageMultiLevelSensor:{uid}").value - self.homecontrol.updater._voltage_multi_level_sensor(message={"properties": - {"uid": f"devolo.VoltageMultiLevelSensor:{uid}", - "property.value.new": 234}}) - current_new = self.homecontrol.devices.get(uid).multi_level_sensor_property \ - .get(f"devolo.VoltageMultiLevelSensor:{uid}").value + self.homecontrol.devices.get(uid).multi_level_sensor_property.get(f"devolo.VoltageMultiLevelSensor:{uid}").value = 231 + current = self.homecontrol.devices.get(uid).multi_level_sensor_property.get( + f"devolo.VoltageMultiLevelSensor:{uid}").value + self.homecontrol.updater._voltage_multi_level_sensor( + message={"properties": { + "uid": f"devolo.VoltageMultiLevelSensor:{uid}", + "property.value.new": 234 + }}) + current_new = self.homecontrol.devices.get(uid).multi_level_sensor_property.get( + f"devolo.VoltageMultiLevelSensor:{uid}").value assert current_new == 234 assert current != current_new diff --git a/tests/test_zwave.py b/tests/test_zwave.py index 9e9a6f50..ac5e10b9 100644 --- a/tests/test_zwave.py +++ b/tests/test_zwave.py @@ -1,31 +1,24 @@ import pytest - -import requests - from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.properties.binary_switch_property import BinarySwitchProperty -from .mocks.mock_gateway import MockGateway - class TestZwave: + @pytest.mark.usefixtures("home_control_instance") @pytest.mark.usefixtures("mock_mprmrest__extract_data_from_element_uid") @pytest.mark.usefixtures("mock_get_zwave_products") def test_get_property(self, mydevolo): device = Zwave(mydevolo_instance=mydevolo, **self.devices['mains']['properties']) - gateway = MockGateway(self.gateway['id'], mydevolo=mydevolo) - session = requests.Session() device.binary_switch_property = {} element_uid = f"devolo.BinarySwitch:{self.devices['mains']['uid']}" - device.binary_switch_property[element_uid] = \ - BinarySwitchProperty(gateway=gateway, - session=session, - mydevolo=mydevolo, - element_uid=element_uid, - state=self.devices['mains']['properties']['state'], - enabled=self.devices['mains']['properties']['guiEnabled']) + device.binary_switch_property[element_uid] = BinarySwitchProperty( + element_uid=element_uid, + setter=lambda uid, + state: None, + state=self.devices['mains']['properties']['state'], + enabled=self.devices['mains']['properties']['guiEnabled']) assert isinstance(device.get_property("binary_switch")[0], BinarySwitchProperty)