From ae33e90519fa24457a381414ab1611dbc74b3273 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 16 Jun 2020 14:03:45 +0200 Subject: [PATCH 1/4] Add debug warning --- custom_components/tahoma/__init__.py | 10 ++++++---- custom_components/tahoma/tahoma_api.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/custom_components/tahoma/__init__.py b/custom_components/tahoma/__init__.py index 6d697e519..ceb780a6a 100644 --- a/custom_components/tahoma/__init__.py +++ b/custom_components/tahoma/__init__.py @@ -3,6 +3,7 @@ import voluptuous as vol import logging +import json from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -87,11 +88,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id]["devices"].append(_device) else: - _LOGGER.warning( - "Unsupported TaHoma device (%s - %s - %s)", + _LOGGER.debug( + "Unsupported Tahoma device (%s). Create an issue on Github with the following information. \n\n %s \n %s \n %s", _device.type, - _device.uiclass, - _device.widget, + _device.type + " - " + _device.uiclass + " - " + _device.widget, + json.dumps(_device.command_def) + ',', + json.dumps(_device.states_def), ) for component in PLATFORMS: diff --git a/custom_components/tahoma/tahoma_api.py b/custom_components/tahoma/tahoma_api.py index b0442476b..90bcf3856 100644 --- a/custom_components/tahoma/tahoma_api.py +++ b/custom_components/tahoma/tahoma_api.py @@ -687,6 +687,16 @@ def widget(self): """Get device widget type""" return self.__widget + @property + def command_def(self): + """Get device widget type""" + return self.__command_def + + @property + def states_def(self): + """Get device widget type""" + return self.__states_def + # def execute_action(self, action): # """Exceute action.""" # self.__protocol From 8a0a3803409c1cbc88e4dc691c1cf362335ca930 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 16 Jun 2020 14:06:57 +0200 Subject: [PATCH 2/4] Update readme --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 5d17edb2f..34dc8b70b 100644 --- a/README.md +++ b/README.md @@ -66,3 +66,15 @@ If your device is not supported, it will show the following message in the loggi | Siren | | MusicPlayer | | VentilationSystem | + +## Advanced + +### Enable debug logging + +The [logger](https://www.home-assistant.io/integrations/logger/) integration lets you define the level of logging activities in Home Assistant. Turning on debug mode will show more information about unsupported devices in your logbook. + +```yaml +logger: + logs: + custom_components.tahoma: debug +``` From f25aa286be5b5e3787f023b37f3e29e0218bcd4e Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 16 Jun 2020 14:10:14 +0200 Subject: [PATCH 3/4] Update documentation --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 34dc8b70b..c81b06d74 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ The [logger](https://www.home-assistant.io/integrations/logger/) integration let ```yaml logger: + default: critical logs: custom_components.tahoma: debug ``` From 13ccf782dd32f290c28a394454f9057cf4aa9e62 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Wed, 17 Jun 2020 08:48:09 +0200 Subject: [PATCH 4/4] fixed style. --- .github/workflows/black.yml | 28 ++ .pre-commit-config.yaml | 38 ++ custom_components/tahoma/binary_sensor.py | 8 +- custom_components/tahoma/config_flow.py | 10 +- custom_components/tahoma/const.py | 31 +- custom_components/tahoma/cover.py | 34 +- custom_components/tahoma/light.py | 13 +- custom_components/tahoma/lock.py | 4 +- custom_components/tahoma/sensor.py | 32 +- custom_components/tahoma/switch.py | 3 +- custom_components/tahoma/tahoma_api.py | 530 +++++++++++----------- custom_components/tahoma/tahoma_device.py | 14 +- requirement.txt | 1 + setup.cfg | 38 ++ 14 files changed, 446 insertions(+), 338 deletions(-) create mode 100644 .github/workflows/black.yml create mode 100644 .pre-commit-config.yaml create mode 100644 requirement.txt create mode 100644 setup.cfg diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 000000000..27aa9f351 --- /dev/null +++ b/.github/workflows/black.yml @@ -0,0 +1,28 @@ +name: Linters (flake8, black, isort) + +on: + push: + pull_request: + schedule: + - cron: '0 0 * * *' + +jobs: + black-format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: ricardochaves/python-lint@v1.1.0 + with: + python-root-list: ./custom_components/tahoma + use-pylint: false + use-pycodestyle: false + use-flake8: true + use-black: true + use-mypy: false + use-isort: true + extra-pylint-options: "" + extra-pycodestyle-options: "" + extra-flake8-options: "" + extra-black-options: "" + extra-mypy-options: "" + extra-isort-options: "" \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..11c808f26 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +repos: + - repo: https://github.com/asottile/pyupgrade + rev: v2.3.0 + hooks: + - id: pyupgrade + args: [--py37-plus] + - repo: https://github.com/psf/black + rev: 19.10b0 + hooks: + - id: black + args: + - --safe + - --quiet + files: ^((custom_components)/.+)?[^/]+\.py$ + - repo: https://github.com/codespell-project/codespell + rev: v1.16.0 + hooks: + - id: codespell + args: + - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing + - --skip="./.*,*.csv,*.json,*.md" + - --quiet-level=2 + exclude_types: [csv, json] + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.1 + hooks: + - id: flake8 + additional_dependencies: + - flake8-docstrings==1.5.0 + - pydocstyle==5.0.2 + files: ^(custom_components)/.+\.py$ + - repo: https://github.com/timothycrosley/isort + rev: 4.3.21-2 + hooks: + - id: isort + args: + - --recursive + files: ^((custom_components)/.+)?[^/]+\.py$ diff --git a/custom_components/tahoma/binary_sensor.py b/custom_components/tahoma/binary_sensor.py index d49c7aa4c..10c4454f7 100644 --- a/custom_components/tahoma/binary_sensor.py +++ b/custom_components/tahoma/binary_sensor.py @@ -6,12 +6,12 @@ from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON from .const import ( + CORE_CONTACT_STATE, + CORE_OCCUPANCY_STATE, + CORE_SMOKE_STATE, DOMAIN, - TAHOMA_TYPES, TAHOMA_BINARY_SENSOR_DEVICE_CLASSES, - CORE_SMOKE_STATE, - CORE_OCCUPANCY_STATE, - CORE_CONTACT_STATE, + TAHOMA_TYPES, ) from .tahoma_device import TahomaDevice diff --git a/custom_components/tahoma/config_flow.py b/custom_components/tahoma/config_flow.py index 83e23884f..607b19d07 100644 --- a/custom_components/tahoma/config_flow.py +++ b/custom_components/tahoma/config_flow.py @@ -1,6 +1,7 @@ """Config flow for TaHoma integration.""" import logging +from requests.exceptions import RequestException import voluptuous as vol from homeassistant import config_entries, core, exceptions @@ -8,15 +9,13 @@ from .const import DOMAIN # pylint:disable=unused-import from .tahoma_api import TahomaApi -from requests.exceptions import RequestException _LOGGER = logging.getLogger(__name__) # TODO adjust the data schema to the data that you need DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str - }) + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) async def validate_input(hass: core.HomeAssistant, data): @@ -28,7 +27,7 @@ async def validate_input(hass: core.HomeAssistant, data): password = data.get(CONF_PASSWORD) try: - controller = await hass.async_add_executor_job(TahomaApi, username, password) + await hass.async_add_executor_job(TahomaApi, username, password) except RequestException: _LOGGER.exception("Error when trying to log in to the TaHoma API") @@ -74,6 +73,7 @@ async def async_step_user(self, user_input=None): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/custom_components/tahoma/const.py b/custom_components/tahoma/const.py index 6d794c061..620f65711 100644 --- a/custom_components/tahoma/const.py +++ b/custom_components/tahoma/const.py @@ -1,27 +1,26 @@ +"""Constants for the TaHoma integration.""" + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_SMOKE, +) from homeassistant.components.cover import ( DEVICE_CLASS_AWNING, DEVICE_CLASS_BLIND, DEVICE_CLASS_CURTAIN, DEVICE_CLASS_GARAGE, + DEVICE_CLASS_GATE, DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, - DEVICE_CLASS_GATE, -) - -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_OCCUPANCY, - DEVICE_CLASS_OPENING, ) from homeassistant.const import ( - DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, ) -"""Constants for the TaHoma integration.""" - DOMAIN = "tahoma" # Used to map the Somfy uiClass to the Home Assistant platform @@ -41,7 +40,7 @@ "ContactSensor": "binary_sensor", "SmokeSensor": "binary_sensor", "OccupancySensor": "binary_sensor", - "WindowHandle" : "binary_sensor", + "WindowHandle": "binary_sensor", "ExteriorVenetianBlind": "cover", "Awning": "cover", "Gate": "cover", @@ -49,7 +48,7 @@ "Generic": "cover", "SwingingShutter": "cover", "ElectricitySensor": "sensor", - "AirSensor": "sensor" + "AirSensor": "sensor", } @@ -66,7 +65,7 @@ "VeluxInteriorBlind": DEVICE_CLASS_BLIND, "Gate": DEVICE_CLASS_GATE, "Curtain": DEVICE_CLASS_CURTAIN, - "SwingingShutter" : DEVICE_CLASS_SHUTTER + "SwingingShutter": DEVICE_CLASS_SHUTTER, } # Used to map the Somfy widget or uiClass to the Home Assistant device classes @@ -74,7 +73,7 @@ "SmokeSensor": DEVICE_CLASS_SMOKE, "OccupancySensor": DEVICE_CLASS_OCCUPANCY, "ContactSensor": DEVICE_CLASS_OPENING, - "WindowHandle": DEVICE_CLASS_OPENING + "WindowHandle": DEVICE_CLASS_OPENING, } # Used to map the Somfy widget or uiClass to the Home Assistant device classes @@ -83,7 +82,7 @@ "HumiditySensor": DEVICE_CLASS_HUMIDITY, "LightSensor": DEVICE_CLASS_ILLUMINANCE, "ElectricitySensor": DEVICE_CLASS_POWER, - "AirSensor": "carbon dioxide" + "AirSensor": "carbon dioxide", } # TaHoma Attributes diff --git a/custom_components/tahoma/cover.py b/custom_components/tahoma/cover.py index ea2a9f409..58a80467d 100644 --- a/custom_components/tahoma/cover.py +++ b/custom_components/tahoma/cover.py @@ -10,42 +10,41 @@ DEVICE_CLASS_GARAGE, DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, - CoverEntity, - SUPPORT_OPEN, SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, - SUPPORT_OPEN_TILT, - SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT, - SUPPORT_SET_TILT_POSITION, + CoverEntity, ) from homeassistant.util.dt import utcnow -from .tahoma_device import TahomaDevice - from .const import ( - DOMAIN, - TAHOMA_TYPES, - TAHOMA_COVER_DEVICE_CLASSES, - ATTR_MEM_POS, - ATTR_LOCK_START_TS, ATTR_LOCK_END_TS, ATTR_LOCK_LEVEL, ATTR_LOCK_ORIG, + ATTR_LOCK_START_TS, + ATTR_MEM_POS, + COMMAND_SET_CLOSURE, + COMMAND_SET_ORIENTATION, + COMMAND_SET_PEDESTRIAN_POSITION, + COMMAND_SET_POSITION, CORE_CLOSURE_STATE, CORE_DEPLOYMENT_STATE, + CORE_MEMORIZED_1_POSITION_STATE, CORE_PEDESTRIAN_POSITION_STATE, CORE_PRIORITY_LOCK_TIMER_STATE, CORE_SLATS_ORIENTATION_STATE, - CORE_MEMORIZED_1_POSITION_STATE, + DOMAIN, IO_PRIORITY_LOCK_LEVEL_STATE, IO_PRIORITY_LOCK_ORIGINATOR_STATE, - COMMAND_SET_CLOSURE, - COMMAND_SET_POSITION, - COMMAND_SET_ORIENTATION, - COMMAND_SET_PEDESTRIAN_POSITION, + TAHOMA_COVER_DEVICE_CLASSES, + TAHOMA_TYPES, ) +from .tahoma_device import TahomaDevice _LOGGER = logging.getLogger(__name__) @@ -169,6 +168,7 @@ def current_cover_position(self): @property def current_cover_tilt_position(self): """Return current position of cover tilt. + None is unknown, 0 is closed, 100 is fully open. """ return getattr(self, "_tilt_position", None) diff --git a/custom_components/tahoma/light.py b/custom_components/tahoma/light.py index 072c86d30..bc31a1f92 100644 --- a/custom_components/tahoma/light.py +++ b/custom_components/tahoma/light.py @@ -1,15 +1,14 @@ """TaHoma light platform that implements dimmable TaHoma lights.""" -import logging from datetime import timedelta +import logging from homeassistant.components.light import ( - LightEntity, ATTR_BRIGHTNESS, ATTR_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, + LightEntity, ) - from homeassistant.const import STATE_OFF, STATE_ON from .const import DOMAIN, TAHOMA_TYPES @@ -36,9 +35,10 @@ async def async_setup_entry(hass, entry, async_add_entities): class TahomaLight(TahomaDevice, LightEntity): - """Representation of a Tahome light""" + """Representation of a Tahome light.""" def __init__(self, tahoma_device, controller): + """Initialize a device.""" super().__init__(tahoma_device, controller) self._skip_update = False @@ -70,7 +70,7 @@ def supported_features(self) -> int: return supported_features - async def async_turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs) -> None: """Turn the light on.""" self._state = True self._skip_update = True @@ -86,7 +86,7 @@ async def async_turn_on(self, **kwargs) -> None: self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs) -> None: """Turn the light off.""" self._state = False self._skip_update = True @@ -106,6 +106,7 @@ def effect(self) -> str: def update(self): """Fetch new state data for this light. + This is the only method that should fetch new data for Home Assistant. """ # Postpone the immediate state check for changes that take time. diff --git a/custom_components/tahoma/lock.py b/custom_components/tahoma/lock.py index 7307b5965..a164c712c 100644 --- a/custom_components/tahoma/lock.py +++ b/custom_components/tahoma/lock.py @@ -85,9 +85,7 @@ def is_locked(self): @property def device_state_attributes(self): """Return the lock state attributes.""" - attr = { - ATTR_BATTERY_LEVEL: self._battery_level, - } + attr = {ATTR_BATTERY_LEVEL: self._battery_level} super_attr = super().device_state_attributes if super_attr is not None: attr.update(super_attr) diff --git a/custom_components/tahoma/sensor.py b/custom_components/tahoma/sensor.py index 6505b0404..a6075f08a 100644 --- a/custom_components/tahoma/sensor.py +++ b/custom_components/tahoma/sensor.py @@ -3,18 +3,24 @@ import logging from typing import Optional -from homeassistant.const import ATTR_BATTERY_LEVEL, TEMP_CELSIUS, UNIT_PERCENTAGE, POWER_WATT, CONCENTRATION_PARTS_PER_MILLION +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + CONCENTRATION_PARTS_PER_MILLION, + POWER_WATT, + TEMP_CELSIUS, + UNIT_PERCENTAGE, +) from homeassistant.helpers.entity import Entity from .const import ( - DOMAIN, - TAHOMA_TYPES, - TAHOMA_SENSOR_DEVICE_CLASSES, - CORE_RELATIVE_HUMIDITY_STATE, + CORE_CO2_CONCENTRATION_STATE, + CORE_ELECTRIC_POWER_CONSUMPTION_STATE, CORE_LUMINANCE_STATE, + CORE_RELATIVE_HUMIDITY_STATE, CORE_TEMPERATURE_STATE, - CORE_ELECTRIC_POWER_CONSUMPTION_STATE, - CORE_CO2_CONCENTRATION_STATE + DOMAIN, + TAHOMA_SENSOR_DEVICE_CLASSES, + TAHOMA_TYPES, ) from .tahoma_device import TahomaDevice @@ -104,16 +110,14 @@ def update(self): if CORE_RELATIVE_HUMIDITY_STATE in self.tahoma_device.active_states: self.current_value = float( "{:.2f}".format( - self.tahoma_device.active_states.get( - CORE_RELATIVE_HUMIDITY_STATE) + self.tahoma_device.active_states.get(CORE_RELATIVE_HUMIDITY_STATE) ) ) if CORE_TEMPERATURE_STATE in self.tahoma_device.active_states: self.current_value = float( "{:.2f}".format( - self.tahoma_device.active_states.get( - CORE_TEMPERATURE_STATE) + self.tahoma_device.active_states.get(CORE_TEMPERATURE_STATE) ) ) @@ -121,12 +125,12 @@ def update(self): self.current_value = float( "{:.2f}".format( self.tahoma_device.active_states.get( - CORE_ELECTRIC_POWER_CONSUMPTION_STATE) + CORE_ELECTRIC_POWER_CONSUMPTION_STATE + ) ) ) if CORE_CO2_CONCENTRATION_STATE in self.tahoma_device.active_states: self.current_value = int( - self.tahoma_device.active_states.get( - CORE_CO2_CONCENTRATION_STATE) + self.tahoma_device.active_states.get(CORE_CO2_CONCENTRATION_STATE) ) diff --git a/custom_components/tahoma/switch.py b/custom_components/tahoma/switch.py index 248421307..6575cb10d 100644 --- a/custom_components/tahoma/switch.py +++ b/custom_components/tahoma/switch.py @@ -1,9 +1,8 @@ """Support for TaHoma switches.""" import logging -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.components.switch import DEVICE_CLASS_SWITCH from .const import DOMAIN, TAHOMA_TYPES from .tahoma_device import TahomaDevice diff --git a/custom_components/tahoma/tahoma_api.py b/custom_components/tahoma/tahoma_api.py index 90bcf3856..f9dba080f 100644 --- a/custom_components/tahoma/tahoma_api.py +++ b/custom_components/tahoma/tahoma_api.py @@ -5,18 +5,19 @@ """ import json -import requests import urllib.parse -BASE_URL = 'https://tahomalink.com/enduser-mobile-web/enduserAPI/' # /doc for API doc -BASE_HEADERS = {'User-Agent': 'mine'} +import requests + +BASE_URL = "https://tahomalink.com/enduser-mobile-web/enduserAPI/" # /doc for API doc +BASE_HEADERS = {"User-Agent": "mine"} class TahomaApi: """Connection to TaHoma API.""" def __init__(self, userName, userPassword, **kwargs): - """Initalize the TaHoma protocol. + """Initialize the TaHoma protocol. :param userName: TaHoma username :param userPassword: Password @@ -37,30 +38,38 @@ def login(self): """Login to TaHoma API.""" if self.__logged_in: return - login = {'userId': self.__username, 'userPassword': self.__password} + login = {"userId": self.__username, "userPassword": self.__password} header = BASE_HEADERS.copy() - request = requests.post(BASE_URL + 'login', - data=login, - headers=header, - timeout=10) + request = requests.post( + BASE_URL + "login", data=login, headers=header, timeout=10 + ) try: result = request.json() except ValueError as error: raise Exception( - "Not a valid result for login, " + - "protocol error: " + request.status_code + ' - ' + - request.reason + "(" + error + ")") - - if 'error' in result.keys(): - raise Exception("Could not login: " + result['error']) + "Not a valid result for login, " + + "protocol error: " + + request.status_code + + " - " + + request.reason + + "(" + + error + + ")" + ) + + if "error" in result.keys(): + raise Exception("Could not login: " + result["error"]) if request.status_code != 200: raise Exception( - "Could not login, HTTP code: " + - str(request.status_code) + ' - ' + request.reason) + "Could not login, HTTP code: " + + str(request.status_code) + + " - " + + request.reason + ) - if 'success' not in result.keys() or not result['success']: + if "success" not in result.keys() or not result["success"]: raise Exception("Could not login, no success") cookie = request.headers.get("set-cookie") @@ -71,9 +80,10 @@ def login(self): self.__logged_in = True return self.__logged_in - def send_request(self, method, url: str, headers, data=None, timeout: int = 10, - retries: int = 3): - """Wrap the http requests and retries + def send_request( + self, method, url: str, headers, data=None, timeout: int = 10, retries: int = 3 + ): + """Wrap the http requests and retries. :param method: The method to use for the request: post, get, delete. :param url: The url to send the POST to. @@ -91,47 +101,47 @@ def send_request(self, method, url: str, headers, data=None, timeout: int = 10, try: result = request.json() except ValueError as error: - raise Exception( - "Not a valid result, protocol error: " + str(error)) + raise Exception("Not a valid result, protocol error: " + str(error)) return result elif retries == 0: raise Exception( - "Maximum number of consecutive retries reached. Error is:\n" + request.text) + "Maximum number of consecutive retries reached. Error is:\n" + + request.text + ) else: self.send_request(method, url, headers, data, timeout, retries - 1) def get_user(self): - """Get the user informations from the server. + """Get the user information from the server. - :return: a dict with all the informations + :return: a dict with all the information :rtype: dict raises ValueError in case of protocol issues :Example: - >>> "creationTime":