From 64c8110e41401748dcc984b66a9143dab984e1bb Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Dec 2024 17:34:25 +0100 Subject: [PATCH 1/3] Disable recommended --- pyproject.toml | 2 +- scripts/tests_and_coverage.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8c0243c41..a43c7ee0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -235,7 +235,7 @@ lint.select = [ "G", # flake8-logging-format "I", # isort "ICN001", # import concentions; {name} should be imported as {asname} - "ISC001", # Implicitly concatenated string literals on one line + # "ISC001", # Implicitly concatenated string literals on one line "N804", # First argument of a class method should be named cls "N805", # First argument of a method should be named self "N815", # Variable {name} in class scope should not be mixedCase diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh index c0d0d4f48..80c4b5509 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -37,6 +37,7 @@ fi if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "linting" ] ; then # Black first to ensure nothings roughing up ruff echo "... ruff-ing ..." + ruff format plugwise/ tests/ ruff check plugwise/ tests/ echo "... pylint-ing ..." From 56ab6c17bdb98bd9820df2ade5c29d5fd0004386 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Dec 2024 17:47:33 +0100 Subject: [PATCH 2/3] Try Core method --- scripts/tests_and_coverage.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh index 80c4b5509..dee45120c 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -37,8 +37,8 @@ fi if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "linting" ] ; then # Black first to ensure nothings roughing up ruff echo "... ruff-ing ..." - ruff format plugwise/ tests/ - ruff check plugwise/ tests/ + ruff format --quiet plugwise tests + ruff check plugwise tests echo "... pylint-ing ..." pylint plugwise/ tests/ From 722a69b153f434c4280ba5c12083f5035bb6a4e8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 8 Dec 2024 17:50:24 +0100 Subject: [PATCH 3/3] Save updated files --- plugwise/__init__.py | 152 ++++++++++++++++++++--------------- plugwise/common.py | 33 +++++--- plugwise/constants.py | 6 +- plugwise/data.py | 35 +++++--- plugwise/helper.py | 73 +++++++++++++---- plugwise/legacy/data.py | 1 + plugwise/legacy/helper.py | 25 +++--- plugwise/legacy/smile.py | 19 +++-- plugwise/smile.py | 29 ++++--- plugwise/util.py | 12 ++- tests/test_adam.py | 18 +++-- tests/test_anna.py | 13 ++- tests/test_generic.py | 4 +- tests/test_init.py | 71 ++++++++++------ tests/test_legacy_anna.py | 4 +- tests/test_legacy_generic.py | 4 +- tests/test_legacy_stretch.py | 7 +- 17 files changed, 314 insertions(+), 192 deletions(-) diff --git a/plugwise/__init__.py b/plugwise/__init__.py index 562ad7938..0139bba0d 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -2,6 +2,7 @@ Plugwise backend module for Home Assistant Core. """ + from __future__ import annotations from plugwise.constants import ( @@ -63,7 +64,7 @@ def __init__( self._timeout, self._username, self._websession, - ) + ) self._cooling_present = False self._elga = False @@ -125,52 +126,56 @@ async def connect(self) -> Version | None: # Determine smile specifics await self._smile_detect(result, dsmrmain) - self._smile_api = SmileAPI( - self._host, - self._password, - self._request, - self._websession, - self._cooling_present, - self._elga, - self._is_thermostat, - self._last_active, - self._loc_data, - self._on_off_device, - self._opentherm_device, - self._schedule_old_states, - self.gateway_id, - self.smile_fw_version, - self.smile_hostname, - self.smile_hw_version, - self.smile_mac_address, - self.smile_model, - self.smile_model_id, - self.smile_name, - self.smile_type, - self.smile_version, - self._port, - self._username, - ) if not self.smile_legacy else SmileLegacyAPI( - self._host, - self._password, - self._request, - self._websession, - self._is_thermostat, - self._loc_data, - self._on_off_device, - self._opentherm_device, - self._stretch_v2, - self._target_smile, - self.smile_fw_version, - self.smile_hostname, - self.smile_hw_version, - self.smile_mac_address, - self.smile_model, - self.smile_name, - self.smile_type, - self.smile_zigbee_mac_address, - self._port, - self._username, + self._smile_api = ( + SmileAPI( + self._host, + self._password, + self._request, + self._websession, + self._cooling_present, + self._elga, + self._is_thermostat, + self._last_active, + self._loc_data, + self._on_off_device, + self._opentherm_device, + self._schedule_old_states, + self.gateway_id, + self.smile_fw_version, + self.smile_hostname, + self.smile_hw_version, + self.smile_mac_address, + self.smile_model, + self.smile_model_id, + self.smile_name, + self.smile_type, + self.smile_version, + self._port, + self._username, + ) + if not self.smile_legacy + else SmileLegacyAPI( + self._host, + self._password, + self._request, + self._websession, + self._is_thermostat, + self._loc_data, + self._on_off_device, + self._opentherm_device, + self._stretch_v2, + self._target_smile, + self.smile_fw_version, + self.smile_hostname, + self.smile_hw_version, + self.smile_mac_address, + self.smile_model, + self.smile_name, + self.smile_type, + self.smile_zigbee_mac_address, + self._port, + self._username, + ) ) # Update all endpoints on first connect @@ -203,7 +208,7 @@ async def _smile_detect(self, result: etree, dsmrmain: etree) -> None: ) raise UnsupportedDeviceError - version_major= str(self.smile_fw_version.major) + version_major = str(self.smile_fw_version.major) self._target_smile = f"{model}_v{version_major}" LOGGER.debug("Plugwise identified as %s", self._target_smile) if self._target_smile not in SMILES: @@ -318,9 +323,9 @@ async def async_update(self) -> PlugwiseData: return data -######################################################################################################## -### API Set and HA Service-related Functions ### -######################################################################################################## + ######################################################################################################## + ### API Set and HA Service-related Functions ### + ######################################################################################################## async def set_select( self, @@ -333,7 +338,9 @@ async def set_select( try: await self._smile_api.set_select(key, loc_id, option, state) except ConnectionFailedError as exc: - raise ConnectionFailedError(f"Failed to set select option '{option}': {str(exc)}") from exc + raise ConnectionFailedError( + f"Failed to set select option '{option}': {str(exc)}" + ) from exc async def set_schedule_state( self, @@ -345,8 +352,9 @@ async def set_schedule_state( try: await self._smile_api.set_schedule_state(loc_id, state, name) except ConnectionFailedError as exc: # pragma no cover - raise ConnectionFailedError(f"Failed to set schedule state: {str(exc)}") from exc # pragma no cover - + raise ConnectionFailedError( + f"Failed to set schedule state: {str(exc)}" + ) from exc # pragma no cover async def set_preset(self, loc_id: str, preset: str) -> None: """Set the given Preset on the relevant Thermostat.""" @@ -360,7 +368,9 @@ async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None: try: await self._smile_api.set_temperature(loc_id, items) except ConnectionFailedError as exc: - raise ConnectionFailedError(f"Failed to set temperature: {str(exc)}") from exc + raise ConnectionFailedError( + f"Failed to set temperature: {str(exc)}" + ) from exc async def set_number( self, @@ -372,14 +382,18 @@ async def set_number( try: await self._smile_api.set_number(dev_id, key, temperature) except ConnectionFailedError as exc: - raise ConnectionFailedError(f"Failed to set number '{key}': {str(exc)}") from exc + raise ConnectionFailedError( + f"Failed to set number '{key}': {str(exc)}" + ) from exc async def set_temperature_offset(self, dev_id: str, offset: float) -> None: """Set the Temperature offset for thermostats that support this feature.""" try: # pragma no cover await self._smile_api.set_offset(dev_id, offset) # pragma: no cover except ConnectionFailedError as exc: # pragma no cover - raise ConnectionFailedError(f"Failed to set temperature offset: {str(exc)}") from exc # pragma no cover + raise ConnectionFailedError( + f"Failed to set temperature offset: {str(exc)}" + ) from exc # pragma no cover async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str @@ -388,39 +402,51 @@ async def set_switch_state( try: await self._smile_api.set_switch_state(appl_id, members, model, state) except ConnectionFailedError as exc: - raise ConnectionFailedError(f"Failed to set switch state: {str(exc)}") from exc + raise ConnectionFailedError( + f"Failed to set switch state: {str(exc)}" + ) from exc async def set_gateway_mode(self, mode: str) -> None: """Set the gateway mode.""" try: # pragma no cover await self._smile_api.set_gateway_mode(mode) # pragma: no cover except ConnectionFailedError as exc: # pragma no cover - raise ConnectionFailedError(f"Failed to set gateway mode: {str(exc)}") from exc # pragma no cover + raise ConnectionFailedError( + f"Failed to set gateway mode: {str(exc)}" + ) from exc # pragma no cover async def set_regulation_mode(self, mode: str) -> None: """Set the heating regulation mode.""" try: # pragma no cover await self._smile_api.set_regulation_mode(mode) # pragma: no cover except ConnectionFailedError as exc: # pragma no cover - raise ConnectionFailedError(f"Failed to set regulation mode: {str(exc)}") from exc # pragma no cover + raise ConnectionFailedError( + f"Failed to set regulation mode: {str(exc)}" + ) from exc # pragma no cover async def set_dhw_mode(self, mode: str) -> None: """Set the domestic hot water heating regulation mode.""" try: # pragma no cover await self._smile_api.set_dhw_mode(mode) # pragma: no cover except ConnectionFailedError as exc: # pragma no cover - raise ConnectionFailedError(f"Failed to set dhw mode: {str(exc)}") from exc # pragma no cover + raise ConnectionFailedError( + f"Failed to set dhw mode: {str(exc)}" + ) from exc # pragma no cover async def delete_notification(self) -> None: """Delete the active Plugwise Notification.""" try: await self._smile_api.delete_notification() except ConnectionFailedError as exc: - raise ConnectionFailedError(f"Failed to delete notification: {str(exc)}") from exc + raise ConnectionFailedError( + f"Failed to delete notification: {str(exc)}" + ) from exc async def reboot_gateway(self) -> None: """Reboot the Plugwise Gateway.""" try: await self._smile_api.reboot_gateway() except ConnectionFailedError as exc: - raise ConnectionFailedError(f"Failed to reboot gateway: {str(exc)}") from exc + raise ConnectionFailedError( + f"Failed to reboot gateway: {str(exc)}" + ) from exc diff --git a/plugwise/common.py b/plugwise/common.py index 8266f9a4a..d1300b592 100644 --- a/plugwise/common.py +++ b/plugwise/common.py @@ -2,6 +2,7 @@ Plugwise Smile protocol helpers. """ + from __future__ import annotations from typing import cast @@ -83,21 +84,23 @@ def _appl_heater_central_info( appl.hardware = module_data["hardware_version"] appl.model_id = module_data["vendor_model"] if not legacy else None appl.model = ( - "Generic heater/cooler" - if self._cooling_present - else "Generic heater" + "Generic heater/cooler" if self._cooling_present else "Generic heater" ) return appl - def _appl_thermostat_info(self, appl: Munch, xml_1: etree, xml_2: etree = None) -> Munch: + def _appl_thermostat_info( + self, appl: Munch, xml_1: etree, xml_2: etree = None + ) -> Munch: """Helper-function for _appliance_info_finder().""" locator = "./logs/point_log[type='thermostat']/thermostat" xml_2 = return_valid(xml_2, self._domain_objects) module_data = self._get_module_data(xml_1, locator, xml_2) appl.vendor_name = module_data["vendor_name"] appl.model = module_data["vendor_model"] - if appl.model != "ThermoTouch": # model_id for Anna not present as stand-alone device + if ( + appl.model != "ThermoTouch" + ): # model_id for Anna not present as stand-alone device appl.model_id = appl.model appl.model = check_model(appl.model, appl.vendor_name) @@ -108,7 +111,9 @@ def _appl_thermostat_info(self, appl: Munch, xml_1: etree, xml_2: etree = None) return appl - def _collect_power_values(self, data: GwEntityData, loc: Munch, tariff: str, legacy: bool = False) -> None: + def _collect_power_values( + self, data: GwEntityData, loc: Munch, tariff: str, legacy: bool = False + ) -> None: """Something.""" for loc.peak_select in ("nl_peak", "nl_offpeak"): loc.locator = ( @@ -220,9 +225,7 @@ def _create_gw_entities(self, appl: Munch) -> None: self.gw_entities[appl.entity_id][appl_key] = value self._count += 1 - def _entity_switching_group( - self, entity: GwEntityData, data: GwEntityData - ) -> None: + def _entity_switching_group(self, entity: GwEntityData, data: GwEntityData) -> None: """Helper-function for _get_device_zone_data(). Determine switching group device data. @@ -268,7 +271,9 @@ def _get_group_switches(self) -> dict[str, GwEntityData]: return switch_groups - def _get_lock_state(self, xml: etree, data: GwEntityData, stretch_v2: bool = False) -> None: + def _get_lock_state( + self, xml: etree, data: GwEntityData, stretch_v2: bool = False + ) -> None: """Helper-function for _get_measurement_data(). Adam & Stretches: obtain the relay-switch lock state. @@ -323,7 +328,9 @@ def _get_module_data( return module_data - def _get_zigbee_data(self, module: etree, module_data: ModuleData, legacy: bool) -> None: + def _get_zigbee_data( + self, module: etree, module_data: ModuleData, legacy: bool + ) -> None: """Helper-function for _get_module_data().""" if legacy: # Stretches @@ -334,5 +341,5 @@ def _get_zigbee_data(self, module: etree, module_data: ModuleData, legacy: bool) module_data["zigbee_mac_address"] = coord.find("mac_address").text # Adam elif (zb_node := module.find("./protocols/zig_bee_node")) is not None: - module_data["zigbee_mac_address"] = zb_node.find("mac_address").text - module_data["reachable"] = zb_node.find("reachable").text == "true" + module_data["zigbee_mac_address"] = zb_node.find("mac_address").text + module_data["reachable"] = zb_node.find("reachable").text == "true" diff --git a/plugwise/constants.py b/plugwise/constants.py index e332f812b..63c6af3e9 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -1,4 +1,5 @@ """Plugwise Smile constants.""" + from __future__ import annotations from collections import namedtuple @@ -167,9 +168,7 @@ "intended_boiler_state": DATA( "heating_state", NONE ), # Legacy Anna: shows when heating is active, we don't show dhw_state, cannot be determined reliably - "flame_state": UOM( - NONE - ), # Also present when there is a single gas-heater + "flame_state": UOM(NONE), # Also present when there is a single gas-heater "intended_boiler_temperature": UOM( TEMP_CELSIUS ), # Non-zero when heating, zero when dhw-heating @@ -585,4 +584,3 @@ class PlugwiseData: devices: dict[str, GwEntityData] gateway: GatewayData - diff --git a/plugwise/data.py b/plugwise/data.py index 2b3740a11..a46404ae7 100644 --- a/plugwise/data.py +++ b/plugwise/data.py @@ -2,6 +2,7 @@ Plugwise Smile protocol data-collection helpers. """ + from __future__ import annotations import re @@ -27,7 +28,6 @@ def __init__(self) -> None: """Init.""" SmileHelper.__init__(self) - def _all_entity_data(self) -> None: """Helper-function for get_all_gateway_entities(). @@ -78,7 +78,13 @@ def _update_gw_entities(self) -> None: mac_list and "low_battery" in entity["binary_sensors"] and entity["zigbee_mac_address"] in mac_list - and entity["dev_class"] in ("thermo_sensor", "thermostatic_radiator_valve", "zone_thermometer", "zone_thermostat") + and entity["dev_class"] + in ( + "thermo_sensor", + "thermostatic_radiator_valve", + "zone_thermometer", + "zone_thermostat", + ) ) if is_battery_low: entity["binary_sensors"]["low_battery"] = True @@ -98,7 +104,11 @@ def _detect_low_batteries(self) -> list[str]: message: str | None = notification.get("message") warning: str | None = notification.get("warning") notify = message or warning - if notify is not None and all(x in notify for x in matches) and (mac_addresses := mac_pattern.findall(notify)): + if ( + notify is not None + and all(x in notify for x in matches) + and (mac_addresses := mac_pattern.findall(notify)) + ): mac_address = mac_addresses[0] # re.findall() outputs a list if mac_address is not None: @@ -114,9 +124,7 @@ def _add_or_update_notifications( """Helper-function adding or updating the Plugwise notifications.""" if ( entity_id == self.gateway_id - and ( - self._is_thermostat or self.smile_type == "power" - ) + and (self._is_thermostat or self.smile_type == "power") ) or ( "binary_sensors" in entity and "plugwise_notification" in entity["binary_sensors"] @@ -152,7 +160,6 @@ def _update_for_cooling(self, entity: GwEntityData) -> None: sensors["setpoint_high"] = temp_dict["setpoint_high"] self._count += 2 # add 4, remove 2 - def _get_location_data(self, loc_id: str) -> GwEntityData: """Helper-function for _all_entity_data() and async_update(). @@ -235,7 +242,10 @@ def _get_adam_data(self, entity: GwEntityData, data: GwEntityData) -> None: data["binary_sensors"]["heating_state"] = self._heating_valves() != 0 # Add cooling_enabled binary_sensor if "binary_sensors" in data: - if "cooling_enabled" not in data["binary_sensors"] and self._cooling_present: + if ( + "cooling_enabled" not in data["binary_sensors"] + and self._cooling_present + ): data["binary_sensors"]["cooling_enabled"] = self._cooling_enabled # Show the allowed regulation_modes and gateway_modes @@ -248,10 +258,7 @@ def _get_adam_data(self, entity: GwEntityData, data: GwEntityData) -> None: self._count += 1 def _climate_data( - self, - location_id: str, - entity: GwEntityData, - data: GwEntityData + self, location_id: str, entity: GwEntityData, data: GwEntityData ) -> None: """Helper-function for _get_entity_data(). @@ -282,7 +289,9 @@ def _climate_data( if sel_schedule in (NONE, OFF): data["climate_mode"] = "heat" if self._cooling_present: - data["climate_mode"] = "cool" if self.check_reg_mode("cooling") else "heat_cool" + data["climate_mode"] = ( + "cool" if self.check_reg_mode("cooling") else "heat_cool" + ) if self.check_reg_mode("off"): data["climate_mode"] = "off" diff --git a/plugwise/helper.py b/plugwise/helper.py index 4c39b08ec..20fda20f0 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -2,6 +2,7 @@ Plugwise Smile protocol helpers. """ + from __future__ import annotations import asyncio @@ -179,7 +180,9 @@ async def _request_validate(self, resp: ClientResponse, method: str) -> etree: # Command accepted gives empty body with status 202 return case 401: - msg = "Invalid Plugwise login, please retry with the correct credentials." + msg = ( + "Invalid Plugwise login, please retry with the correct credentials." + ) LOGGER.error("%s", msg) raise InvalidAuthentication case 405: @@ -392,7 +395,9 @@ def _appliance_info_finder(self, appl: Munch, appliance: etree) -> Munch: return self._appl_thermostat_info(appl, appliance) case "heater_central": # Collect heater_central device info - self._appl_heater_central_info(appl, appliance, False) # False means non-legacy device + self._appl_heater_central_info( + appl, appliance, False + ) # False means non-legacy device self._appl_dhw_mode_info(appl, appliance) # Skip orphaned heater_central (Core Issue #104433) if appl.entity_id != self._heater_id: @@ -430,7 +435,9 @@ def _appl_gateway_info(self, appl: Munch, appliance: etree) -> Munch: # Adam: collect the ZigBee MAC address of the Smile if self.smile(ADAM): - if (found := self._domain_objects.find(".//protocols/zig_bee_coordinator")) is not None: + if ( + found := self._domain_objects.find(".//protocols/zig_bee_coordinator") + ) is not None: appl.zigbee_mac = found.find("mac_address").text # Also, collect regulation_modes and check for cooling, indicating cooling-mode is present @@ -463,7 +470,9 @@ def _appl_dhw_mode_info(self, appl: Munch, appliance: etree) -> Munch: Collect dhw control operation modes - Anna + Loria. """ dhw_mode_list: list[str] = [] - locator = "./actuator_functionalities/domestic_hot_water_mode_control_functionality" + locator = ( + "./actuator_functionalities/domestic_hot_water_mode_control_functionality" + ) if (search := appliance.find(locator)) is not None: if search.find("allowed_modes") is not None: for mode in search.find("allowed_modes"): @@ -510,7 +519,7 @@ def _get_measurement_data(self, entity_id: str) -> GwEntityData: # !! DON'T CHANGE below two if-lines, will break stuff !! if self.smile_type == "power": if entity["dev_class"] == "smartmeter": - data.update(self._power_data_from_location(entity["location"])) + data.update(self._power_data_from_location(entity["location"])) return data @@ -641,8 +650,11 @@ def _get_actuator_functionalities( # Skip max_dhw_temperature, not initially valid, # skip thermostat for all but zones with thermostats if item == "max_dhw_temperature" or ( - item == "thermostat" and ( - entity["dev_class"] != "climate" if self.smile(ADAM) else entity["dev_class"] != "thermostat" + item == "thermostat" + and ( + entity["dev_class"] != "climate" + if self.smile(ADAM) + else entity["dev_class"] != "thermostat" ) ): continue @@ -800,7 +812,9 @@ def _update_elga_cooling(self, data: GwEntityData) -> None: data["model"] = "Generic heater/cooler" # Cooling_enabled in xml does NOT show the correct status! # Setting it specifically: - self._cooling_enabled = data["binary_sensors"]["cooling_enabled"] = data["elga_status_code"] in (8, 9) + self._cooling_enabled = data["binary_sensors"]["cooling_enabled"] = data[ + "elga_status_code" + ] in (8, 9) data["binary_sensors"]["cooling_state"] = self._cooling_active = ( data["elga_status_code"] == 8 ) @@ -816,11 +830,15 @@ def _update_loria_cooling(self, data: GwEntityData) -> None: """Loria/Thermastage: base cooling-related on cooling_state and modulation_level.""" # For Loria/Thermastage it's not clear if cooling_enabled in xml shows the correct status, # setting it specifically: - self._cooling_enabled = data["binary_sensors"]["cooling_enabled"] = data["binary_sensors"]["cooling_state"] + self._cooling_enabled = data["binary_sensors"]["cooling_enabled"] = data[ + "binary_sensors" + ]["cooling_state"] self._cooling_active = data["sensors"]["modulation_level"] == 100 # For Loria the above does not work (pw-beta issue #301) if "cooling_ena_switch" in data["switches"]: - self._cooling_enabled = data["binary_sensors"]["cooling_enabled"] = data["switches"]["cooling_ena_switch"] + self._cooling_enabled = data["binary_sensors"]["cooling_enabled"] = data[ + "switches" + ]["cooling_ena_switch"] self._cooling_active = data["binary_sensors"]["cooling_state"] def _cleanup_data(self, data: GwEntityData) -> None: @@ -870,7 +888,10 @@ def _scan_thermostats(self) -> None: "dev_class": "climate", "model": "ThermoZone", "name": loc_data["name"], - "thermostats": {"primary": loc_data["primary"], "secondary": loc_data["secondary"]}, + "thermostats": { + "primary": loc_data["primary"], + "secondary": loc_data["secondary"], + }, "vendor": "Plugwise", } self._count += 3 @@ -910,10 +931,12 @@ def _rank_thermostat( if thermo_matching[appl_class] == thermo_loc["primary_prio"]: thermo_loc["primary"].append(appliance_id) # Pre-elect new primary - elif (thermo_rank := thermo_matching[appl_class]) > thermo_loc["primary_prio"]: + elif (thermo_rank := thermo_matching[appl_class]) > thermo_loc[ + "primary_prio" + ]: thermo_loc["primary_prio"] = thermo_rank # Demote former primary - if (tl_primary := thermo_loc["primary"]): + if tl_primary := thermo_loc["primary"]: thermo_loc["secondary"] += tl_primary thermo_loc["primary"] = [] @@ -1010,9 +1033,17 @@ def _rule_ids_by_name(self, name: str, loc_id: str) -> dict[str, dict[str, str]] for rule in self._domain_objects.findall(f'./rule[name="{name}"]'): active = rule.find("active").text if rule.find(locator) is not None: - schedule_ids[rule.attrib["id"]] = {"location": loc_id, "name": name, "active": active} + schedule_ids[rule.attrib["id"]] = { + "location": loc_id, + "name": name, + "active": active, + } else: - schedule_ids[rule.attrib["id"]] = {"location": NONE, "name": name, "active": active} + schedule_ids[rule.attrib["id"]] = { + "location": NONE, + "name": name, + "active": active, + } return schedule_ids @@ -1029,9 +1060,17 @@ def _rule_ids_by_tag(self, tag: str, loc_id: str) -> dict[str, dict[str, str]]: name = rule.find("name").text active = rule.find("active").text if rule.find(locator2) is not None: - schedule_ids[rule.attrib["id"]] = {"location": loc_id, "name": name, "active": active} + schedule_ids[rule.attrib["id"]] = { + "location": loc_id, + "name": name, + "active": active, + } else: - schedule_ids[rule.attrib["id"]] = {"location": NONE, "name": name, "active": active} + schedule_ids[rule.attrib["id"]] = { + "location": NONE, + "name": name, + "active": active, + } return schedule_ids diff --git a/plugwise/legacy/data.py b/plugwise/legacy/data.py index 66b089ead..cc26ec534 100644 --- a/plugwise/legacy/data.py +++ b/plugwise/legacy/data.py @@ -2,6 +2,7 @@ Plugwise Smile protocol data-collection helpers for legacy devices. """ + from __future__ import annotations # Dict as class diff --git a/plugwise/legacy/helper.py b/plugwise/legacy/helper.py index 861b943dd..832b6dd5a 100644 --- a/plugwise/legacy/helper.py +++ b/plugwise/legacy/helper.py @@ -2,6 +2,7 @@ Plugwise Smile protocol helpers. """ + from __future__ import annotations from typing import cast @@ -169,10 +170,7 @@ def _all_locations(self) -> None: loc.loc_id = location.attrib["id"] # Filter the valid single location for P1 legacy: services not empty locator = "./services" - if ( - self.smile_type == "power" - and len(location.find(locator)) == 0 - ): + if self.smile_type == "power" and len(location.find(locator)) == 0: continue if loc.name == "Home": @@ -212,15 +210,15 @@ def _create_legacy_gateway(self) -> None: def _appliance_info_finder(self, appliance: etree, appl: Munch) -> Munch: """Collect entity info (Smile/Stretch, Thermostats, OpenTherm/On-Off): firmware, model and vendor name.""" match appl.pwclass: - # Collect thermostat entity info + # Collect thermostat entity info case _ as dev_class if dev_class in THERMOSTAT_CLASSES: return self._appl_thermostat_info(appl, appliance, self._modules) - # Collect heater_central entity info + # Collect heater_central entity info case "heater_central": return self._appl_heater_central_info( appl, appliance, True, self._appliances, self._modules ) # True means legacy device - # Collect info from Stretches + # Collect info from Stretches case _: return self._energy_entity_info_finder(appliance, appl) @@ -231,7 +229,9 @@ def _energy_entity_info_finder(self, appliance: etree, appl: Munch) -> Munch: """ if self.smile_type in ("power", "stretch"): locator = "./services/electricity_point_meter" - module_data = self._get_module_data(appliance, locator, self._modules, legacy=True) + module_data = self._get_module_data( + appliance, locator, self._modules, legacy=True + ) appl.zigbee_mac = module_data["zigbee_mac_address"] # Filter appliance without zigbee_mac, it's an orphaned device if appl.zigbee_mac is None and self.smile_type != "power": @@ -362,10 +362,7 @@ def _appliance_measurements( self._count_data_items(data) def _get_actuator_functionalities( - self, - xml: etree, - entity: GwEntityData, - data: GwEntityData + self, xml: etree, entity: GwEntityData, data: GwEntityData ) -> None: """Helper-function for _get_measurement_data().""" for item in ACTIVE_ACTUATORS: @@ -458,7 +455,9 @@ def _schedules(self) -> tuple[list[str], str]: active = result.text == "on" # Show an empty schedule as no schedule found - directives = search.find(f'./rule[@id="{rule_id}"]/directives/when/then') is not None + directives = ( + search.find(f'./rule[@id="{rule_id}"]/directives/when/then') is not None + ) if directives and name is not None: available = [name, OFF] selected = name if active else OFF diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index c1df96fdc..5afe5690d 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -2,6 +2,7 @@ Plugwise backend module for Home Assistant Core - covering the legacy P1, Anna, and Stretch devices. """ + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -138,9 +139,9 @@ async def async_update(self) -> PlugwiseData: gateway=self.gw_data, ) -######################################################################################################## -### API Set and HA Service-related Functions ### -######################################################################################################## + ######################################################################################################## + ### API Set and HA Service-related Functions ### + ######################################################################################################## async def delete_notification(self) -> None: """Set-function placeholder for legacy devices.""" @@ -181,12 +182,16 @@ async def set_preset(self, _: str, preset: str) -> None: async def set_regulation_mode(self, mode: str) -> None: """Set-function placeholder for legacy devices.""" - async def set_select(self, key: str, loc_id: str, option: str, state: str | None) -> None: + async def set_select( + self, key: str, loc_id: str, option: str, state: str | None + ) -> None: """Set the thermostat schedule option.""" # schedule name corresponds to select option await self.set_schedule_state("dummy", state, option) - async def set_schedule_state(self, _: str, state: str | None, name: str | None) -> None: + async def set_schedule_state( + self, _: str, state: str | None, name: str | None + ) -> None: """Activate/deactivate the Schedule. Determined from - DOMAIN_OBJECTS. @@ -205,7 +210,9 @@ async def set_schedule_state(self, _: str, state: str | None, name: str | None) schedule_rule_id = rule.attrib["id"] if schedule_rule_id is None: - raise PlugwiseError("Plugwise: no schedule with this name available.") # pragma: no cover + raise PlugwiseError( + "Plugwise: no schedule with this name available." + ) # pragma: no cover new_state = "false" if state == "on": diff --git a/plugwise/smile.py b/plugwise/smile.py index bea641f04..9f20bfd3d 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -2,6 +2,7 @@ Plugwise backend module for Home Assistant Core. """ + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -94,7 +95,6 @@ def __init__( self.smile_version = smile_version SmileData.__init__(self) - async def full_xml_update(self) -> None: """Perform a first fetch of all XML data, needed for initialization.""" self._domain_objects = await self.request(DOMAIN_OBJECTS) @@ -132,14 +132,16 @@ async def async_update(self) -> PlugwiseData: await self.full_xml_update() self.get_all_gateway_entities() # Set self._cooling_enabled -required for set_temperature, - #also, check for a failed data-retrieval + # also, check for a failed data-retrieval if "heater_id" in self.gw_data: heat_cooler = self.gw_entities[self.gw_data["heater_id"]] if ( "binary_sensors" in heat_cooler and "cooling_enabled" in heat_cooler["binary_sensors"] ): - self._cooling_enabled = heat_cooler["binary_sensors"]["cooling_enabled"] + self._cooling_enabled = heat_cooler["binary_sensors"][ + "cooling_enabled" + ] except KeyError as err: raise DataMissingError("No Plugwise data received") from err @@ -148,9 +150,9 @@ async def async_update(self) -> PlugwiseData: gateway=self.gw_data, ) -######################################################################################################## -### API Set and HA Service-related Functions ### -######################################################################################################## + ######################################################################################################## + ### API Set and HA Service-related Functions ### + ######################################################################################################## async def delete_notification(self) -> None: """Delete the active Plugwise Notification.""" @@ -222,7 +224,9 @@ async def set_preset(self, loc_id: str, preset: str) -> None: await self.call_request(uri, method="put", data=data) - async def set_select(self, key: str, loc_id: str, option: str, state: str | None) -> None: + async def set_select( + self, key: str, loc_id: str, option: str, state: str | None + ) -> None: """Set a dhw/gateway/regulation mode or the thermostat schedule option.""" match key: case "select_dhw_mode": @@ -254,7 +258,12 @@ async def set_gateway_mode(self, mode: str) -> None: valid = "" if mode == "away": time_1 = self._domain_objects.find("./gateway/time").text - away_time = dt.datetime.fromisoformat(time_1).astimezone(dt.UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z") + away_time = ( + dt.datetime.fromisoformat(time_1) + .astimezone(dt.UTC) + .isoformat(timespec="milliseconds") + .replace("+00:00", "Z") + ) valid = ( f"{away_time}{end_time}" ) @@ -448,8 +457,8 @@ async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None: if setpoint is None: raise PlugwiseError( - "Plugwise: failed setting temperature: no valid input provided" - ) # pragma: no cover" + "Plugwise: failed setting temperature: no valid input provided" + ) # pragma: no cover" temperature = str(setpoint) uri = self._thermostat_uri(loc_id) diff --git a/plugwise/util.py b/plugwise/util.py index 7beec330e..e2d1a125a 100644 --- a/plugwise/util.py +++ b/plugwise/util.py @@ -1,4 +1,5 @@ """Plugwise protocol helpers.""" + from __future__ import annotations import datetime as dt @@ -41,9 +42,7 @@ def check_alternative_location(loc: Munch, legacy: bool) -> Munch: loc.found = False return loc - loc.locator = ( - f'./{loc.log_type}[type="{loc.measurement}"]/period/measurement' - ) + loc.locator = f'./{loc.log_type}[type="{loc.measurement}"]/period/measurement' if legacy: loc.locator = ( f"./{loc.meas_list[0]}_{loc.log_type}/" @@ -67,11 +66,11 @@ def in_alternative_location(loc: Munch, legacy: bool) -> bool: """ present = "log" in loc.log_type and ( "gas" in loc.measurement or "phase" in loc.measurement - ) + ) if legacy: present = "meter" in loc.log_type and ( "point" in loc.log_type or "gas" in loc.measurement - ) + ) return present @@ -222,8 +221,7 @@ def skip_obsolete_measurements(xml: etree, measurement: str) -> bool: locator = f".//logs/point_log[type='{measurement}']/updated_date" if ( measurement in OBSOLETE_MEASUREMENTS - and (updated_date_key := xml.find(locator)) - is not None + and (updated_date_key := xml.find(locator)) is not None ): updated_date = updated_date_key.text.split("T")[0] date_1 = dt.datetime.strptime(updated_date, "%Y-%m-%d") diff --git a/tests/test_adam.py b/tests/test_adam.py index 3d3cfa0f8..94a9075c0 100644 --- a/tests/test_adam.py +++ b/tests/test_adam.py @@ -76,9 +76,9 @@ async def test_connect_adam_plus_anna_new(self): ) assert result - smile._schedule_old_states["f2bf9048bef64cc5b6d5110154e33c81"][ - "Badkamer" - ] = "off" + smile._schedule_old_states["f2bf9048bef64cc5b6d5110154e33c81"]["Badkamer"] = ( + "off" + ) result_1 = await self.tinker_thermostat_schedule( smile, "f2bf9048bef64cc5b6d5110154e33c81", @@ -153,7 +153,9 @@ async def test_connect_adam_plus_anna_new(self): self.smile_setup = "adam_plus_anna_new" testdata = self.load_testdata(SMILE_TYPE, self.smile_setup) server, smile, client = await self.connect_wrapper(raise_timeout=True) - await self.device_test(smile, "2023-12-17 00:00:01", testdata, skip_testing=True) + await self.device_test( + smile, "2023-12-17 00:00:01", testdata, skip_testing=True + ) tinkered = await self.tinker_max_boiler_temp(smile, unhappy=True) assert tinkered @@ -228,7 +230,9 @@ async def test_connect_adam_zone_per_device(self): await self.disconnect(server, client) server, smile, client = await self.connect_wrapper(raise_timeout=True) - await self.device_test(smile, "2022-05-16 00:00:01", testdata, skip_testing=True) + await self.device_test( + smile, "2022-05-16 00:00:01", testdata, skip_testing=True + ) result = await self.tinker_thermostat( smile, "c50f167537524366a5af7aa3942feb1e", @@ -382,7 +386,9 @@ async def test_connect_adam_plus_anna(self): await self.disconnect(server, client) server, smile, client = await self.connect_wrapper(raise_timeout=True) - await self.device_test(smile, "2020-03-22 00:00:01", testdata, skip_testing=True) + await self.device_test( + smile, "2020-03-22 00:00:01", testdata, skip_testing=True + ) result = await self.tinker_thermostat( smile, "009490cc2f674ce6b576863fbb64f867", diff --git a/tests/test_anna.py b/tests/test_anna.py index 177eaa007..a5e81702b 100644 --- a/tests/test_anna.py +++ b/tests/test_anna.py @@ -70,7 +70,9 @@ async def test_connect_anna_v4(self): server, smile, client = await self.connect_wrapper(raise_timeout=True) # Reset self.smile_setup self.smile_setup = "anna_v4" - await self.device_test(smile, "2020-04-05 00:00:01", testdata, skip_testing=True) + await self.device_test( + smile, "2020-04-05 00:00:01", testdata, skip_testing=True + ) result = await self.tinker_thermostat( smile, "eb5309212bf5407bb143e5bfa3b18aee", @@ -81,7 +83,9 @@ async def test_connect_anna_v4(self): assert result result = await self.tinker_temp_offset( - smile, "01b85360fdd243d0aaad4d6ac2a5ba7e", unhappy=True, + smile, + "01b85360fdd243d0aaad4d6ac2a5ba7e", + unhappy=True, ) assert result @@ -497,7 +501,9 @@ async def test_connect_anna_loria_heating_idle(self): await self.disconnect(server, client) server, smile, client = await self.connect_wrapper(raise_timeout=True) - await self.device_test(smile, "2022-05-16 00:00:01", testdata, skip_testing=True) + await self.device_test( + smile, "2022-05-16 00:00:01", testdata, skip_testing=True + ) tinkered = await self.tinker_dhw_mode(smile, unhappy=True) assert tinkered @@ -505,7 +511,6 @@ async def test_connect_anna_loria_heating_idle(self): await smile.close_connection() await self.disconnect(server, client) - @pytest.mark.asyncio async def test_connect_anna_loria_cooling_active(self): """Test an Anna with a Loria in heating mode - state idle.""" diff --git a/tests/test_generic.py b/tests/test_generic.py index f5b861b68..e098095f6 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -9,9 +9,7 @@ from .test_init import _LOGGER, TestPlugwise, pw_exceptions -class TestPlugwiseGeneric( - TestPlugwise -): # pylint: disable=attribute-defined-outside-init +class TestPlugwiseGeneric(TestPlugwise): # pylint: disable=attribute-defined-outside-init """Tests for generic functionality.""" @pytest.mark.asyncio diff --git a/tests/test_init.py b/tests/test_init.py index e7bf771b9..638af6c94 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,5 +1,6 @@ # pylint: disable=protected-access """Test Plugwise Home Assistant module and generate test JSON fixtures.""" + import importlib import json @@ -109,27 +110,19 @@ async def setup_app( # Introducte timeout with 2 seconds, test by setting response to 10ms # Don't actually wait 2 seconds as this will prolongue testing if not raise_timeout: - app.router.add_route( - "POST", CORE_GATEWAYS_TAIL, self.smile_http_accept - ) + app.router.add_route("POST", CORE_GATEWAYS_TAIL, self.smile_http_accept) app.router.add_route("PUT", CORE_LOCATIONS_TAIL, self.smile_http_accept) app.router.add_route( "DELETE", CORE_NOTIFICATIONS_TAIL, self.smile_http_accept ) app.router.add_route("PUT", CORE_RULES_TAIL, self.smile_http_accept) - app.router.add_route( - "PUT", CORE_APPLIANCES_TAIL, self.smile_http_accept - ) + app.router.add_route("PUT", CORE_APPLIANCES_TAIL, self.smile_http_accept) else: - app.router.add_route( - "POST", CORE_GATEWAYS_TAIL, self.smile_timeout - ) + app.router.add_route("POST", CORE_GATEWAYS_TAIL, self.smile_timeout) app.router.add_route("PUT", CORE_LOCATIONS_TAIL, self.smile_timeout) app.router.add_route("PUT", CORE_RULES_TAIL, self.smile_timeout) app.router.add_route("PUT", CORE_APPLIANCES_TAIL, self.smile_timeout) - app.router.add_route( - "DELETE", CORE_NOTIFICATIONS_TAIL, self.smile_timeout - ) + app.router.add_route("DELETE", CORE_NOTIFICATIONS_TAIL, self.smile_timeout) return app @@ -373,7 +366,9 @@ async def connect_legacy( ) # Happy flow - app = await self.setup_legacy_app(broken, timeout, raise_timeout, fail_auth, stretch) + app = await self.setup_legacy_app( + broken, timeout, raise_timeout, fail_auth, stretch + ) server = aiohttp.test_utils.TestServer( app, port=port, scheme="http", host="127.0.0.1" @@ -631,9 +626,13 @@ def test_and_assert(test_dict, data, header): heat_cooler = data.devices[data.gateway["heater_id"]] if "binary_sensors" in heat_cooler: if "cooling_enabled" in heat_cooler["binary_sensors"]: - self._cooling_enabled = heat_cooler["binary_sensors"]["cooling_enabled"] + self._cooling_enabled = heat_cooler["binary_sensors"][ + "cooling_enabled" + ] if "cooling_state" in heat_cooler["binary_sensors"]: - self._cooling_active = heat_cooler["binary_sensors"]["cooling_state"] + self._cooling_active = heat_cooler["binary_sensors"][ + "cooling_state" + ] self._write_json("all_data", {"devices": data.devices, "gateway": data.gateway}) @@ -692,7 +691,9 @@ async def tinker_switch( except pw_exceptions.PlugwiseError: _LOGGER.info(" + locked, not switched as expected") return False - except pw_exceptions.ConnectionFailedError: # leave for-loop at connect-error + except ( + pw_exceptions.ConnectionFailedError + ): # leave for-loop at connect-error if unhappy: return True # test is pass! _LOGGER.info(" + failed as expected") @@ -750,7 +751,9 @@ async def tinker_thermostat_preset(self, smile, loc_id, unhappy=False): except pw_exceptions.PlugwiseError: _LOGGER.info(" + found invalid preset, as expected") tinker_preset_passed = True - except pw_exceptions.ConnectionFailedError: # leave for-loop at connect-error + except ( + pw_exceptions.ConnectionFailedError + ): # leave for-loop at connect-error if unhappy: _LOGGER.info(" + tinker_thermostat_preset failed as expected") return True @@ -778,13 +781,17 @@ async def tinker_thermostat_schedule( new_schedule = new_schedule[1:] _LOGGER.info("- Adjusting schedule to %s", f"{new_schedule}{warning}") try: - await smile.set_select("select_schedule", loc_id, new_schedule, state) + await smile.set_select( + "select_schedule", loc_id, new_schedule, state + ) tinker_schedule_passed = True _LOGGER.info(" + working as intended") except pw_exceptions.PlugwiseError: _LOGGER.info(" + failed as expected") tinker_schedule_passed = True - except pw_exceptions.ConnectionFailedError: # leave for-loop at connect-error + except ( + pw_exceptions.ConnectionFailedError + ): # leave for-loop at connect-error tinker_schedule_passed = False if unhappy: _LOGGER.info(" + failed as expected before intended failure") @@ -812,7 +819,9 @@ async def tinker_legacy_thermostat_schedule(self, smile, unhappy=False): except pw_exceptions.PlugwiseError: _LOGGER.info(" + failed as expected") tinker_schedule_passed = True - except pw_exceptions.ConnectionFailedError: # leave for-loop at connect-error + except ( + pw_exceptions.ConnectionFailedError + ): # leave for-loop at connect-error tinker_schedule_passed = False if unhappy: _LOGGER.info(" + failed as expected before intended failure") @@ -896,7 +905,9 @@ async def tinker_dhw_mode(smile, unhappy=False): except pw_exceptions.PlugwiseError: _LOGGER.info(" + tinker_dhw_mode found invalid mode, as expected") tinker_dhw_mode_passed = False - except pw_exceptions.ConnectionFailedError: # leave for-loop at connect-error + except ( + pw_exceptions.ConnectionFailedError + ): # leave for-loop at connect-error if unhappy: _LOGGER.info(" + failed as expected before intended failure") return True @@ -925,7 +936,9 @@ async def tinker_regulation_mode(smile, unhappy=False): " + tinker_regulation_mode found invalid mode, as expected" ) tinker_reg_mode_passed = False - except pw_exceptions.ConnectionFailedError: # leave for-loop at connect-error + except ( + pw_exceptions.ConnectionFailedError + ): # leave for-loop at connect-error if unhappy: _LOGGER.info(" + failed as expected before intended failure") return True @@ -941,7 +954,11 @@ async def tinker_max_boiler_temp(smile, unhappy=False): tinker_max_boiler_temp_passed = False new_temp = 60.0 _LOGGER.info("- Adjusting temperature to %s", new_temp) - for test in ["maximum_boiler_temperature", "max_dhw_temperature", "bogus_temperature"]: + for test in [ + "maximum_boiler_temperature", + "max_dhw_temperature", + "bogus_temperature", + ]: _LOGGER.info(" + for %s", test) try: await smile.set_number("dummy", test, new_temp) @@ -950,7 +967,9 @@ async def tinker_max_boiler_temp(smile, unhappy=False): except pw_exceptions.PlugwiseError: _LOGGER.info(" + tinker_max_boiler_temp failed as intended") tinker_max_boiler_temp_passed = False - except pw_exceptions.ConnectionFailedError: # leave for-loop at connect-error + except ( + pw_exceptions.ConnectionFailedError + ): # leave for-loop at connect-error if unhappy: _LOGGER.info(" + failed as expected before intended failure") return True @@ -997,7 +1016,9 @@ async def tinker_gateway_mode(smile, unhappy=False): except pw_exceptions.PlugwiseError: _LOGGER.info(" + found invalid mode, as expected") tinker_gateway_mode_passed = False - except pw_exceptions.ConnectionFailedError: # leave for-loop at connect-error + except ( + pw_exceptions.ConnectionFailedError + ): # leave for-loop at connect-error if unhappy: _LOGGER.info(" + failed as expected before intended failure") return True diff --git a/tests/test_legacy_anna.py b/tests/test_legacy_anna.py index 32b1a26da..6f05eccd5 100644 --- a/tests/test_legacy_anna.py +++ b/tests/test_legacy_anna.py @@ -39,7 +39,9 @@ async def test_connect_legacy_anna(self): await self.disconnect(server, client) server, smile, client = await self.connect_legacy_wrapper(raise_timeout=True) - await self.device_test(smile, "2020-03-22 00:00:01", testdata, skip_testing=True) + await self.device_test( + smile, "2020-03-22 00:00:01", testdata, skip_testing=True + ) result = await self.tinker_legacy_thermostat(smile, unhappy=True) assert result await smile.close_connection() diff --git a/tests/test_legacy_generic.py b/tests/test_legacy_generic.py index 825d18959..f5a433b72 100644 --- a/tests/test_legacy_generic.py +++ b/tests/test_legacy_generic.py @@ -5,9 +5,7 @@ from .test_init import TestPlugwise, pw_exceptions -class TestPlugwiseGeneric( - TestPlugwise -): # pylint: disable=attribute-defined-outside-init +class TestPlugwiseGeneric(TestPlugwise): # pylint: disable=attribute-defined-outside-init """Tests for generic functionality.""" @pytest.mark.asyncio diff --git a/tests/test_legacy_stretch.py b/tests/test_legacy_stretch.py index 961bb6378..f5056dee6 100644 --- a/tests/test_legacy_stretch.py +++ b/tests/test_legacy_stretch.py @@ -7,9 +7,7 @@ SMILE_TYPE = "stretch" -class TestPlugwiseStretch( - TestPlugwise -): # pylint: disable=attribute-defined-outside-init +class TestPlugwiseStretch(TestPlugwise): # pylint: disable=attribute-defined-outside-init """Tests for Stretch.""" @pytest.mark.asyncio @@ -34,7 +32,8 @@ async def test_connect_stretch_v31(self): assert self.entity_items == 83 switch_change = await self.tinker_switch( - smile, "059e4d03c7a34d278add5c7a4a781d19", + smile, + "059e4d03c7a34d278add5c7a4a781d19", ) assert not switch_change