From 64c8d2cc827e9f71bc73d0e335584f079ce27c88 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Sat, 5 Oct 2024 19:55:02 +0200 Subject: [PATCH 01/29] add mac of new devices for discovery --- custom_components/philips_airpurifier_coap/manifest.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/philips_airpurifier_coap/manifest.json b/custom_components/philips_airpurifier_coap/manifest.json index e67eadb1..bd9508b8 100644 --- a/custom_components/philips_airpurifier_coap/manifest.json +++ b/custom_components/philips_airpurifier_coap/manifest.json @@ -14,6 +14,9 @@ { "macaddress": "849DC2*" }, + { + "macaddress": "80A036*" + }, { "hostname": "mxchip*" }, From 5a522b1c3adf2c8112f7938731b00f8d2f9595f7 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Sat, 5 Oct 2024 21:38:30 +0200 Subject: [PATCH 02/29] add new AC0850 variants --- custom_components/philips_airpurifier_coap/const.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/const.py b/custom_components/philips_airpurifier_coap/const.py index 1289f593..2b5ea671 100644 --- a/custom_components/philips_airpurifier_coap/const.py +++ b/custom_components/philips_airpurifier_coap/const.py @@ -1,4 +1,5 @@ """Constants for Philips AirPurifier integration.""" + from __future__ import annotations from enum import StrEnum @@ -97,7 +98,9 @@ class ICON(StrEnum): class FanModel(StrEnum): """Supported fan models.""" - AC0850 = "AC0850" + AC0850_11 = "AC0850/11 AWS_Philips_AIR" + AC0850_11C = "AC0850/11 AWS_Philips_AIR_Combo" + AC0850_31 = "AC0850/31" AC1214 = "AC1214" AC1715 = "AC1715" AC2729 = "AC2729" @@ -601,7 +604,6 @@ class PhilipsApi: } - EXTRA_SENSOR_TYPES: dict[str, SensorDescription] = {} BINARY_SENSOR_TYPES: dict[str, SensorDescription] = { From 5f99750d7c60449dd68fbbf47b07af21923cadbc Mon Sep 17 00:00:00 2001 From: kongo09 Date: Sat, 5 Oct 2024 21:38:57 +0200 Subject: [PATCH 03/29] add classes for AC0850 variants --- .../philips_airpurifier_coap/philips.py | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/philips.py b/custom_components/philips_airpurifier_coap/philips.py index 46fcd4d6..534d355c 100644 --- a/custom_components/philips_airpurifier_coap/philips.py +++ b/custom_components/philips_airpurifier_coap/philips.py @@ -1,4 +1,5 @@ """Collection of classes to manage Philips AirPurifier devices.""" + from __future__ import annotations import asyncio @@ -383,7 +384,11 @@ async def async_turn_off(self, **kwargs) -> None: @property def supported_features(self) -> int: """Return the supported features.""" - features = FanEntityFeature.PRESET_MODE | FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON + features = ( + FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) if self._speeds: features |= FanEntityFeature.SET_SPEED if self.KEY_OSCILLATION is not None: @@ -641,8 +646,13 @@ class PhilipsHumidifierMixin(PhilipsGenericCoAPFanBase): # similar to the AC1715, the AC0850 seems to be a new class of devices that # follows some patterns of its own -class PhilipsAC0850(PhilipsNewGenericCoAPFan): - """AC0850.""" + + +# the AC0850/11 comes in two versions. +# the first version has a Wifi string starting with "AWS_Philips_AIR" +# the second version has a Wifi string starting with "AWS_Philips_AIR_Combo" +class PhilipsAC085011(PhilipsNewGenericCoAPFan): + """AC0850/11 with firmware AWS_Philips_AIR.""" AVAILABLE_PRESET_MODES = { PresetMode.AUTO: { @@ -660,6 +670,29 @@ class PhilipsAC0850(PhilipsNewGenericCoAPFan): UNAVAILABLE_FILTERS = [PhilipsApi.FILTER_NANOPROTECT_PREFILTER] +class PhilipsAC085011C(PhilipsAC085011): + """AC0850/11 with firmware AWS_Philips_AIR_Combo.""" + + AVAILABLE_PRESET_MODES = { + PresetMode.AUTO: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_B: 0, + }, + PresetMode.TURBO: {PhilipsApi.NEW2_POWER: 1, PhilipsApi.NEW2_MODE_B: 18}, + PresetMode.SLEEP: {PhilipsApi.NEW2_POWER: 1, PhilipsApi.NEW2_MODE_B: 17}, + } + AVAILABLE_SPEEDS = { + PresetMode.SLEEP: {PhilipsApi.NEW2_POWER: 1, PhilipsApi.NEW2_MODE_B: 17}, + PresetMode.TURBO: {PhilipsApi.NEW2_POWER: 1, PhilipsApi.NEW2_MODE_B: 18}, + } + # the prefilter data is present but doesn't change for this device, so let's take it out + UNAVAILABLE_FILTERS = [PhilipsApi.FILTER_NANOPROTECT_PREFILTER] + + +class PhilipsAC085031(PhilipsAC085011C): + """AC0850/31.""" + + # the AC1715 seems to be a new class of devices that follows some patterns of its own class PhilipsAC1715(PhilipsNewGenericCoAPFan): """AC1715.""" @@ -1786,7 +1819,9 @@ class PhilipsCX5120(PhilipsNew2GenericCoAPFan): model_to_class = { - FanModel.AC0850: PhilipsAC0850, + FanModel.AC0850_11: PhilipsAC085011, + FanModel.AC0850_11C: PhilipsAC085011C, + FanModel.AC0850_31: PhilipsAC085031, FanModel.AC1214: PhilipsAC1214, FanModel.AC1715: PhilipsAC1715, FanModel.AC2729: PhilipsAC2729, From 195b000012897db0b46fef1ab3f0d19f76a64a30 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Sat, 5 Oct 2024 21:49:57 +0200 Subject: [PATCH 04/29] detect correct version of AC0850 --- .../philips_airpurifier_coap/config_flow.py | 73 ++++++++++++------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/config_flow.py b/custom_components/philips_airpurifier_coap/config_flow.py index d46b7618..5e100c0b 100644 --- a/custom_components/philips_airpurifier_coap/config_flow.py +++ b/custom_components/philips_airpurifier_coap/config_flow.py @@ -1,4 +1,5 @@ """The Philips AirPurifier component.""" + import asyncio import ipaddress import logging @@ -43,13 +44,13 @@ def __init__(self) -> None: self._model: Any = None self._name: Any = None self._device_id: str = None + self._wifi_version: Any = None def _get_schema(self, user_input): """Provide schema for user input.""" - schema = vol.Schema( + return vol.Schema( {vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): cv.string} ) - return schema async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle initial step of auto discovery flow.""" @@ -80,7 +81,7 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes # get the status out of the queue _LOGGER.debug("status for host %s is: %s", self._host, status) - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning( r"Timeout, host %s looks like a Philips AirPurifier but doesn't answer, aborting", self._host, @@ -110,6 +111,9 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes self._model = first_model[:9] _LOGGER.debug("model type extracted: %s", self._model) + # autodetect Wifi version + self._wifi_version = status.get(PhilipsApi.WIFI_VERSION) + # autodetect name self._name = list( filter( @@ -129,18 +133,21 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes ) # check if model is supported - if self._model not in model_to_class: - _LOGGER.info( - "Model %s found, but not supported directly. Trying model family", - self._model, - ) - self._model = self._model[:6] - if self._model not in model_to_class: - _LOGGER.warning( - "Model %s found, but not supported. Aborting discovery", - self._model, - ) - return self.async_abort(reason="model_unsupported") + model_long = self._model + " " + self._wifi_version.split("@")[0] + model = self._model + model_family = self._model[:6] + + if model in model_to_class: + _LOGGER.info("Model %s supported", model) + self._model = model + elif model_long in model_to_class: + _LOGGER.info("Model %s supported", model_long) + self._model = model_long + elif model_family in model_to_class: + _LOGGER.info("Model family %s supported", model_family) + self._model = model_family + else: + return self.async_abort(reason="model_unsupported") # use the device ID as unique_id unique_id = self._device_id @@ -163,7 +170,9 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes _LOGGER.debug("waiting for async_step_confirm") return await self.async_step_confirm() - async def async_step_confirm(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Confirm the dhcp discovered data.""" _LOGGER.debug("async_step_confirm called with user_input: %s", user_input) @@ -192,7 +201,9 @@ async def async_step_confirm(self, user_input: dict[str, Any] = None) -> FlowRes description_placeholders={"model": self._model, "name": self._name}, ) - async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Handle initial step of user config flow.""" errors = {} @@ -225,7 +236,7 @@ async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult if client is not None: await client.shutdown() - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.warning( r"Timeout, host %s doesn't answer, aborting", self._host ) @@ -254,6 +265,9 @@ async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult self._model = first_model[:9] _LOGGER.debug("model type extracted: %s", self._model) + # autodetect Wifi version + self._wifi_version = status.get(PhilipsApi.WIFI_VERSION) + # autodetect name self._name = list( filter( @@ -281,14 +295,21 @@ async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult ) # check if model is supported - if self._model not in model_to_class: - _LOGGER.info( - "Model %s not supported. Trying model family", self._model - ) - self._model = self._model[:6] - if self._model not in model_to_class: - return self.async_abort(reason="model_unsupported") - user_input[CONF_MODEL] = self._model + model_long = self._model + " " + self._wifi_version.split("@")[0] + model = self._model + model_family = self._model[:6] + + if model in model_to_class: + _LOGGER.info("Model %s supported", model) + user_input[CONF_MODEL] = model + elif model_long in model_to_class: + _LOGGER.info("Model %s supported", model_long) + user_input[CONF_MODEL] = model_long + elif model_family in model_to_class: + _LOGGER.info("Model family %s supported", model_family) + user_input[CONF_MODEL] = model_family + else: + return self.async_abort(reason="model_unsupported") # use the device ID as unique_id unique_id = self._device_id From 090d390728bcac4b5e0f06c632d98bd8d9a77a18 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Sat, 5 Oct 2024 21:55:50 +0200 Subject: [PATCH 05/29] put new model variants into readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a8e7c7f..57034fb4 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,9 @@ Note: `configuration.yaml` is no longer supported and your configuration is not ## Supported models -- AC0850 +- AC0850/11 AWS_Philips_AIR +- AC0850/11 AWS_Philips_AIR_Combo +- AC0850/31 - AC1214 - AC1715 - AC2729 From 99cc43d973ad0809149f9b2861e801e97efc53b0 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Sat, 5 Oct 2024 21:55:58 +0200 Subject: [PATCH 06/29] bump version --- custom_components/philips_airpurifier_coap/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/philips_airpurifier_coap/manifest.json b/custom_components/philips_airpurifier_coap/manifest.json index bd9508b8..e44783c1 100644 --- a/custom_components/philips_airpurifier_coap/manifest.json +++ b/custom_components/philips_airpurifier_coap/manifest.json @@ -29,5 +29,5 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/kongo09/philips-airpurifier-coap/issues", "requirements": ["aioairctrl==0.2.5", "getmac==0.9.4"], - "version": "0.18.9" + "version": "0.18.10" } From 269431287ad2b24d19a6fe868868a8caa26f7162 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Sat, 5 Oct 2024 22:43:22 +0200 Subject: [PATCH 07/29] fix some warnings --- .../philips_airpurifier_coap/__init__.py | 21 ++++++++++++------- .../philips_airpurifier_coap/config_flow.py | 2 +- .../philips_airpurifier_coap/const.py | 1 - 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/__init__.py b/custom_components/philips_airpurifier_coap/__init__.py index 2cf7c351..88103292 100644 --- a/custom_components/philips_airpurifier_coap/__init__.py +++ b/custom_components/philips_airpurifier_coap/__init__.py @@ -1,4 +1,5 @@ """Support for Philips AirPurifier with CoAP.""" + from __future__ import annotations import asyncio @@ -25,7 +26,6 @@ DATA_KEY_COORDINATOR, DOMAIN, ICONLIST_URL, - ICONS, ICONS_PATH, ICONS_URL, LOADER_PATH, @@ -37,7 +37,7 @@ _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["fan", "binary_sensor", "sensor", "switch", "light", "select", "number"] +PLATFORMS = ["binary_sensor", "fan", "light", "number", "select", "sensor", "switch"] # icons code thanks to Thomas Loven: @@ -50,17 +50,20 @@ class ListingView(HomeAssistantView): requires_auth = False def __init__(self, url, iconpath) -> None: + """Initialize the ListingView with a URL and icon path.""" self.url = url self.iconpath = iconpath self.name = "Icon Listing" async def get(self, request): + """Handle GET request to provide a JSON list of the used icons.""" icons = [] - for (dirpath, dirnames, filenames) in walk(self.iconpath): + for dirpath, _dirnames, filenames in walk(self.iconpath): icons.extend( [ - {"name": path.join(dirpath[len(self.iconpath):], fn[:-4])} - for fn in filenames if fn.endswith(".svg") + {"name": path.join(dirpath[len(self.iconpath) :], fn[:-4])} + for fn in filenames + if fn.endswith(".svg") ] ) return json.dumps(icons) @@ -70,12 +73,16 @@ async def async_setup(hass: HomeAssistant, config) -> bool: """Set up the icons for the Philips AirPurifier integration.""" _LOGGER.debug("async_setup called") - await hass.http.async_register_static_paths([StaticPathConfig(LOADER_URL, hass.config.path(LOADER_PATH), True)]) + await hass.http.async_register_static_paths( + [StaticPathConfig(LOADER_URL, hass.config.path(LOADER_PATH), True)] + ) add_extra_js_url(hass, LOADER_URL) iset = PAP iconpath = hass.config.path(ICONS_PATH + "/" + iset) - await hass.http.async_register_static_paths([StaticPathConfig(ICONS_URL + "/" + iset, iconpath, True)]) + await hass.http.async_register_static_paths( + [StaticPathConfig(ICONS_URL + "/" + iset, iconpath, True)] + ) hass.http.register_view(ListingView(ICONLIST_URL + "/" + iset, iconpath)) return True diff --git a/custom_components/philips_airpurifier_coap/config_flow.py b/custom_components/philips_airpurifier_coap/config_flow.py index 5e100c0b..038fce0c 100644 --- a/custom_components/philips_airpurifier_coap/config_flow.py +++ b/custom_components/philips_airpurifier_coap/config_flow.py @@ -213,7 +213,7 @@ async def async_step_user( try: # first some sanitycheck on the host input if not host_valid(user_input[CONF_HOST]): - raise InvalidHost() + raise InvalidHost # noqa: TRY301 self._host = user_input[CONF_HOST] _LOGGER.debug("trying to configure host: %s", self._host) diff --git a/custom_components/philips_airpurifier_coap/const.py b/custom_components/philips_airpurifier_coap/const.py index 2b5ea671..35a51add 100644 --- a/custom_components/philips_airpurifier_coap/const.py +++ b/custom_components/philips_airpurifier_coap/const.py @@ -393,7 +393,6 @@ class PhilipsApi: NEW2_AUTO_PLUS_AI = "D03180" NEW2_PREFERRED_INDEX = "D0312A#1" NEW2_GAS_PREFERRED_INDEX = "D0312A#2" - NEW2_ERROR_CODE = "D03240 " PREFERRED_INDEX_MAP = { 0: ("Indoor Allergen Index", ICON.IAI), From 2ae1e6b9240604a3a6a6f2f3cba7836d223e1323 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Sat, 5 Oct 2024 22:52:56 +0200 Subject: [PATCH 08/29] fix some warnings --- .../philips_airpurifier_coap/config_flow.py | 1 - .../philips_airpurifier_coap/philips.py | 27 ++++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/config_flow.py b/custom_components/philips_airpurifier_coap/config_flow.py index 038fce0c..f94cb8f9 100644 --- a/custom_components/philips_airpurifier_coap/config_flow.py +++ b/custom_components/philips_airpurifier_coap/config_flow.py @@ -1,6 +1,5 @@ """The Philips AirPurifier component.""" -import asyncio import ipaddress import logging import re diff --git a/custom_components/philips_airpurifier_coap/philips.py b/custom_components/philips_airpurifier_coap/philips.py index 534d355c..cdf61ffd 100644 --- a/custom_components/philips_airpurifier_coap/philips.py +++ b/custom_components/philips_airpurifier_coap/philips.py @@ -8,7 +8,7 @@ import contextlib from datetime import timedelta import logging -from typing import Any, Optional, Union +from typing import Any from aioairctrl import CoAPClient @@ -268,7 +268,7 @@ def __init__( # noqa: D107 self._unique_id = None @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return the unique ID of the fan.""" return self._unique_id @@ -326,7 +326,7 @@ def __init__( # noqa: D107 self._unique_id = f"{self._model}-{device_id}" except Exception as e: _LOGGER.error("Failed retrieving unique_id: %s", e) - raise PlatformNotReady + raise PlatformNotReady from e def _collect_available_preset_modes(self): preset_modes = {} @@ -360,8 +360,8 @@ def is_on(self) -> bool: async def async_turn_on( self, - percentage: Optional[int] = None, - preset_mode: Optional[str] = None, + percentage: int | None = None, + preset_mode: str | None = None, **kwargs, ): """Turn the fan on.""" @@ -396,12 +396,12 @@ def supported_features(self) -> int: return features @property - def preset_modes(self) -> Optional[list[str]]: + def preset_modes(self) -> list[str] | None: """Return the supported preset modes.""" return self._preset_modes @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the selected preset mode.""" for preset_mode, status_pattern in self._available_preset_modes.items(): for k, v in status_pattern.items(): @@ -413,6 +413,7 @@ def preset_mode(self) -> Optional[str]: break else: return preset_mode + return None async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan.""" @@ -439,7 +440,7 @@ def oscillating(self) -> bool | None: async def async_oscillate(self, oscillating: bool) -> None: """Osciallate the fan.""" if self.KEY_OSCILLATION is None: - return None + return key = next(iter(self.KEY_OSCILLATION)) values = self.KEY_OSCILLATION.get(key) @@ -451,7 +452,7 @@ async def async_oscillate(self, oscillating: bool) -> None: await self.coordinator.client.set_control_value(key, off) @property - def percentage(self) -> Optional[int]: + def percentage(self) -> int | None: """Return the speed percentages.""" for speed, status_pattern in self._available_speeds.items(): for k, v in status_pattern.items(): @@ -476,14 +477,14 @@ async def async_set_percentage(self, percentage: int) -> None: await self.coordinator.client.set_control_values(data=status_pattern) @property - def extra_state_attributes(self) -> Optional[dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the extra state attributes.""" def append( attributes: dict, key: str, philips_key: str, - value_map: Union[dict, Callable[[Any, Any], Any]] = None, + value_map: dict | Callable[[Any, Any], Any] | None = None, ): # some philips keys are not unique, so # serves as a marker and needs to be filtered out philips_clean_key = philips_key.partition("#")[0] @@ -826,8 +827,8 @@ async def async_set_percentage(self, percentage: int) -> None: async def async_turn_on( self, - percentage: Optional[int] = None, - preset_mode: Optional[str] = None, + percentage: int | None = None, + preset_mode: str | None = None, **kwargs, ): """Turn on the device.""" From 9ce9c92e102efbb9ffeeca3be1f91fb25687c697 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Tue, 8 Oct 2024 19:44:23 +0200 Subject: [PATCH 09/29] add support for AC0850/20 --- README.md | 2 ++ custom_components/philips_airpurifier_coap/const.py | 2 ++ .../philips_airpurifier_coap/philips.py | 12 +++++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 57034fb4..e6dc6537 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ Note: `configuration.yaml` is no longer supported and your configuration is not - AC0850/11 AWS_Philips_AIR - AC0850/11 AWS_Philips_AIR_Combo +- AC0850/20 AWS_Philips_AIR +- AC0850/20 AWS_Philips_AIR_Combo - AC0850/31 - AC1214 - AC1715 diff --git a/custom_components/philips_airpurifier_coap/const.py b/custom_components/philips_airpurifier_coap/const.py index 35a51add..02786601 100644 --- a/custom_components/philips_airpurifier_coap/const.py +++ b/custom_components/philips_airpurifier_coap/const.py @@ -100,6 +100,8 @@ class FanModel(StrEnum): AC0850_11 = "AC0850/11 AWS_Philips_AIR" AC0850_11C = "AC0850/11 AWS_Philips_AIR_Combo" + AC0850_20 = "AC0850/20 AWS_Philips_AIR" + AC0850_20C = "AC0850/20 AWS_Philips_AIR_Combo" AC0850_31 = "AC0850/31" AC1214 = "AC1214" AC1715 = "AC1715" diff --git a/custom_components/philips_airpurifier_coap/philips.py b/custom_components/philips_airpurifier_coap/philips.py index cdf61ffd..3c0f31c7 100644 --- a/custom_components/philips_airpurifier_coap/philips.py +++ b/custom_components/philips_airpurifier_coap/philips.py @@ -671,7 +671,7 @@ class PhilipsAC085011(PhilipsNewGenericCoAPFan): UNAVAILABLE_FILTERS = [PhilipsApi.FILTER_NANOPROTECT_PREFILTER] -class PhilipsAC085011C(PhilipsAC085011): +class PhilipsAC085011C(PhilipsNewGenericCoAPFan): """AC0850/11 with firmware AWS_Philips_AIR_Combo.""" AVAILABLE_PRESET_MODES = { @@ -690,6 +690,14 @@ class PhilipsAC085011C(PhilipsAC085011): UNAVAILABLE_FILTERS = [PhilipsApi.FILTER_NANOPROTECT_PREFILTER] +class PhilipsAC085020(PhilipsAC085011): + """AC0850/20 with firmware AWS_Philips_AIR.""" + + +class PhilipsAC085020C(PhilipsAC085011C): + """AC0850/20 with firmware AWS_Philips_AIR_Combo.""" + + class PhilipsAC085031(PhilipsAC085011C): """AC0850/31.""" @@ -1822,6 +1830,8 @@ class PhilipsCX5120(PhilipsNew2GenericCoAPFan): model_to_class = { FanModel.AC0850_11: PhilipsAC085011, FanModel.AC0850_11C: PhilipsAC085011C, + FanModel.AC0850_20: PhilipsAC085020, + FanModel.AC0850_20C: PhilipsAC085020C, FanModel.AC0850_31: PhilipsAC085031, FanModel.AC1214: PhilipsAC1214, FanModel.AC1715: PhilipsAC1715, From 86f676670ddef10bfe2117e2a6ab15eafaf0b6ed Mon Sep 17 00:00:00 2001 From: kongo09 Date: Sat, 12 Oct 2024 20:18:43 +0200 Subject: [PATCH 10/29] fix power on and index for AC0850 Combo models --- custom_components/philips_airpurifier_coap/philips.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/philips.py b/custom_components/philips_airpurifier_coap/philips.py index 3c0f31c7..322f22fe 100644 --- a/custom_components/philips_airpurifier_coap/philips.py +++ b/custom_components/philips_airpurifier_coap/philips.py @@ -631,7 +631,7 @@ class PhilipsNew2GenericCoAPFan(PhilipsGenericCoAPFanBase): AVAILABLE_LIGHTS = [] AVAILABLE_SWITCHES = [] - AVAILABLE_SELECTS = [] + AVAILABLE_SELECTS = [PhilipsApi.NEW2_GAS_PREFERRED_INDEX] KEY_PHILIPS_POWER = PhilipsApi.NEW2_POWER STATE_POWER_ON = 1 @@ -671,7 +671,7 @@ class PhilipsAC085011(PhilipsNewGenericCoAPFan): UNAVAILABLE_FILTERS = [PhilipsApi.FILTER_NANOPROTECT_PREFILTER] -class PhilipsAC085011C(PhilipsNewGenericCoAPFan): +class PhilipsAC085011C(PhilipsNew2GenericCoAPFan): """AC0850/11 with firmware AWS_Philips_AIR_Combo.""" AVAILABLE_PRESET_MODES = { From 89049df01aae797a2606ca6cd7dd7c077ae3ca7b Mon Sep 17 00:00:00 2001 From: kongo09 Date: Sat, 12 Oct 2024 20:44:50 +0200 Subject: [PATCH 11/29] add support for AC0950 --- .../philips_airpurifier_coap/const.py | 2 ++ .../philips_airpurifier_coap/philips.py | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/custom_components/philips_airpurifier_coap/const.py b/custom_components/philips_airpurifier_coap/const.py index 02786601..35020a3d 100644 --- a/custom_components/philips_airpurifier_coap/const.py +++ b/custom_components/philips_airpurifier_coap/const.py @@ -103,6 +103,7 @@ class FanModel(StrEnum): AC0850_20 = "AC0850/20 AWS_Philips_AIR" AC0850_20C = "AC0850/20 AWS_Philips_AIR_Combo" AC0850_31 = "AC0850/31" + AC0950 = "AC0950" AC1214 = "AC1214" AC1715 = "AC1715" AC2729 = "AC2729" @@ -158,6 +159,7 @@ class PresetMode: SLEEP = "sleep" SLEEP_ALLERGY = "allergy sleep" TURBO = "turbo" + MEDIUM = "medium" GAS = "gas" POLLUTION = "pollution" LOW = "low" diff --git a/custom_components/philips_airpurifier_coap/philips.py b/custom_components/philips_airpurifier_coap/philips.py index 322f22fe..304b1a5d 100644 --- a/custom_components/philips_airpurifier_coap/philips.py +++ b/custom_components/philips_airpurifier_coap/philips.py @@ -702,6 +702,31 @@ class PhilipsAC085031(PhilipsAC085011C): """AC0850/31.""" +class PhilipsAC0950(PhilipsNew2GenericCoAPFan): + """AC0950.""" + + AVAILABLE_PRESET_MODES = { + PresetMode.AUTO: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_B: 0, + }, + PresetMode.TURBO: {PhilipsApi.NEW2_POWER: 1, PhilipsApi.NEW2_MODE_B: 18}, + PresetMode.MEDIUM: {PhilipsApi.NEW2_POWER: 1, PhilipsApi.NEW2_MODE_B: 19}, + PresetMode.SLEEP: {PhilipsApi.NEW2_POWER: 1, PhilipsApi.NEW2_MODE_B: 17}, + } + AVAILABLE_SPEEDS = { + PresetMode.SLEEP: {PhilipsApi.NEW2_POWER: 1, PhilipsApi.NEW2_MODE_B: 17}, + PresetMode.MEDIUM: {PhilipsApi.NEW2_POWER: 1, PhilipsApi.NEW2_MODE_B: 19}, + PresetMode.TURBO: {PhilipsApi.NEW2_POWER: 1, PhilipsApi.NEW2_MODE_B: 18}, + } + # the prefilter data is present but doesn't change for this device, so let's take it out + UNAVAILABLE_FILTERS = [PhilipsApi.FILTER_NANOPROTECT_PREFILTER] + + AVAILABLE_SWITCHES = [PhilipsApi.NEW2_CHILD_LOCK, PhilipsApi.NEW2_BEEP] + AVAILABLE_LIGHTS = [PhilipsApi.NEW2_DISPLAY_BACKLIGHT] + AVAILABLE_SELECTS = [PhilipsApi.NEW2_TIMER2] + + # the AC1715 seems to be a new class of devices that follows some patterns of its own class PhilipsAC1715(PhilipsNewGenericCoAPFan): """AC1715.""" @@ -1833,6 +1858,7 @@ class PhilipsCX5120(PhilipsNew2GenericCoAPFan): FanModel.AC0850_20: PhilipsAC085020, FanModel.AC0850_20C: PhilipsAC085020C, FanModel.AC0850_31: PhilipsAC085031, + FanModel.AC0950: PhilipsAC0950, FanModel.AC1214: PhilipsAC1214, FanModel.AC1715: PhilipsAC1715, FanModel.AC2729: PhilipsAC2729, From 80ec254f34354fc1862a53906c73331371fb4ce4 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Sat, 12 Oct 2024 20:45:46 +0200 Subject: [PATCH 12/29] add support for AC0950 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e6dc6537..fe3ccd10 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Note: `configuration.yaml` is no longer supported and your configuration is not - AC0850/20 AWS_Philips_AIR - AC0850/20 AWS_Philips_AIR_Combo - AC0850/31 +- AC0950 - AC1214 - AC1715 - AC2729 From e8065ebb1cbe3577e67b11507698fdf3b6189607 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Sat, 12 Oct 2024 20:53:20 +0200 Subject: [PATCH 13/29] add support for AC3421 --- README.md | 1 + custom_components/philips_airpurifier_coap/const.py | 1 + custom_components/philips_airpurifier_coap/philips.py | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/README.md b/README.md index fe3ccd10..bfb1dbb1 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Note: `configuration.yaml` is no longer supported and your configuration is not - AC3039 - AC3055 - AC3059 +- AC3421 - AC3259 - AC3737 - AC3829 diff --git a/custom_components/philips_airpurifier_coap/const.py b/custom_components/philips_airpurifier_coap/const.py index 35020a3d..2b1d1c8e 100644 --- a/custom_components/philips_airpurifier_coap/const.py +++ b/custom_components/philips_airpurifier_coap/const.py @@ -118,6 +118,7 @@ class FanModel(StrEnum): AC3055 = "AC3055" AC3059 = "AC3059" AC3259 = "AC3259" + AC3421 = "AC3421" AC3737 = "AC3737" AC3829 = "AC3829" AC3836 = "AC3836" diff --git a/custom_components/philips_airpurifier_coap/philips.py b/custom_components/philips_airpurifier_coap/philips.py index 304b1a5d..8e2bb4ef 100644 --- a/custom_components/philips_airpurifier_coap/philips.py +++ b/custom_components/philips_airpurifier_coap/philips.py @@ -1247,6 +1247,10 @@ class PhilipsAC3259(PhilipsGenericCoAPFan): AVAILABLE_SELECTS = [PhilipsApi.GAS_PREFERRED_INDEX] +class PhilipsAC3421(PhilipsAC0950): + """AC3421.""" + + class PhilipsAC3737(PhilipsNew2GenericCoAPFan): """AC3737.""" @@ -1873,6 +1877,7 @@ class PhilipsCX5120(PhilipsNew2GenericCoAPFan): FanModel.AC3055: PhilipsAC3055, FanModel.AC3059: PhilipsAC3059, FanModel.AC3259: PhilipsAC3259, + FanModel.AC3421: PhilipsAC3421, FanModel.AC3737: PhilipsAC3737, FanModel.AC3829: PhilipsAC3829, FanModel.AC3836: PhilipsAC3836, From 7652cb2336bdc42eb725741822aff3c03c3c280d Mon Sep 17 00:00:00 2001 From: kongo09 Date: Wed, 23 Oct 2024 23:08:31 +0200 Subject: [PATCH 14/29] move gas index select into AC0950 --- custom_components/philips_airpurifier_coap/philips.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/philips.py b/custom_components/philips_airpurifier_coap/philips.py index 8e2bb4ef..1cf23a34 100644 --- a/custom_components/philips_airpurifier_coap/philips.py +++ b/custom_components/philips_airpurifier_coap/philips.py @@ -631,7 +631,7 @@ class PhilipsNew2GenericCoAPFan(PhilipsGenericCoAPFanBase): AVAILABLE_LIGHTS = [] AVAILABLE_SWITCHES = [] - AVAILABLE_SELECTS = [PhilipsApi.NEW2_GAS_PREFERRED_INDEX] + AVAILABLE_SELECTS = [] KEY_PHILIPS_POWER = PhilipsApi.NEW2_POWER STATE_POWER_ON = 1 @@ -724,7 +724,7 @@ class PhilipsAC0950(PhilipsNew2GenericCoAPFan): AVAILABLE_SWITCHES = [PhilipsApi.NEW2_CHILD_LOCK, PhilipsApi.NEW2_BEEP] AVAILABLE_LIGHTS = [PhilipsApi.NEW2_DISPLAY_BACKLIGHT] - AVAILABLE_SELECTS = [PhilipsApi.NEW2_TIMER2] + AVAILABLE_SELECTS = [PhilipsApi.NEW2_GAS_PREFERRED_INDEX, PhilipsApi.NEW2_TIMER2] # the AC1715 seems to be a new class of devices that follows some patterns of its own From 9b4887d3eeb2b0159fcf8610d221cf0a93f998b2 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Wed, 23 Oct 2024 23:18:18 +0200 Subject: [PATCH 15/29] fix private member access --- custom_components/philips_airpurifier_coap/philips.py | 6 +++--- custom_components/philips_airpurifier_coap/timer.py | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/philips.py b/custom_components/philips_airpurifier_coap/philips.py index 1cf23a34..6c610a89 100644 --- a/custom_components/philips_airpurifier_coap/philips.py +++ b/custom_components/philips_airpurifier_coap/philips.py @@ -46,7 +46,7 @@ class Coordinator: def __init__(self, client: CoAPClient, host: str, mac: str) -> None: # noqa: D107 self.client = client self._host = host - self._mac = mac + self.mac = mac # It's None before the first successful update. # Components should call async_first_refresh to make sure the first @@ -68,7 +68,7 @@ def __init__(self, client: CoAPClient, host: str, mac: str) -> None: # noqa: D1 callback=self.reconnect, autostart=True, ) - self._timer_disconnected._auto_restart = True + self._timer_disconnected.setAutoRestart(True) _LOGGER.debug("init: finished for host %s", self._host) async def shutdown(self): @@ -209,7 +209,7 @@ def __init__(self, coordinator: Coordinator) -> None: # noqa: D107 )[0] self._firmware = coordinator.status["WifiVersion"] self._manufacturer = "Philips" - self._mac = coordinator._mac + self._mac = coordinator.mac @property def should_poll(self) -> bool: diff --git a/custom_components/philips_airpurifier_coap/timer.py b/custom_components/philips_airpurifier_coap/timer.py index 978324b8..4c41bd7b 100644 --- a/custom_components/philips_airpurifier_coap/timer.py +++ b/custom_components/philips_airpurifier_coap/timer.py @@ -1,4 +1,5 @@ """Timer class to handle instable Philips CoaP API.""" + import asyncio import contextlib import logging @@ -78,3 +79,7 @@ def start(self): """Start the task.""" if self._task is None: self._task = asyncio.ensure_future(self._job()) + + def setAutoRestart(self, auto_restart): + """Set the autorestart.""" + self._auto_restart = auto_restart From a9180dd85efd263e802a3cb7ef59ed4cfa824aac Mon Sep 17 00:00:00 2001 From: kongo09 Date: Wed, 23 Oct 2024 23:37:19 +0200 Subject: [PATCH 16/29] add AC0951 --- README.md | 1 + custom_components/philips_airpurifier_coap/const.py | 1 + custom_components/philips_airpurifier_coap/philips.py | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/README.md b/README.md index bfb1dbb1..0fcae3de 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Note: `configuration.yaml` is no longer supported and your configuration is not - AC0850/20 AWS_Philips_AIR_Combo - AC0850/31 - AC0950 +- AC0951 - AC1214 - AC1715 - AC2729 diff --git a/custom_components/philips_airpurifier_coap/const.py b/custom_components/philips_airpurifier_coap/const.py index 2b1d1c8e..00647e5a 100644 --- a/custom_components/philips_airpurifier_coap/const.py +++ b/custom_components/philips_airpurifier_coap/const.py @@ -104,6 +104,7 @@ class FanModel(StrEnum): AC0850_20C = "AC0850/20 AWS_Philips_AIR_Combo" AC0850_31 = "AC0850/31" AC0950 = "AC0950" + AC0951 = "AC0951" AC1214 = "AC1214" AC1715 = "AC1715" AC2729 = "AC2729" diff --git a/custom_components/philips_airpurifier_coap/philips.py b/custom_components/philips_airpurifier_coap/philips.py index 6c610a89..b66e4306 100644 --- a/custom_components/philips_airpurifier_coap/philips.py +++ b/custom_components/philips_airpurifier_coap/philips.py @@ -727,6 +727,10 @@ class PhilipsAC0950(PhilipsNew2GenericCoAPFan): AVAILABLE_SELECTS = [PhilipsApi.NEW2_GAS_PREFERRED_INDEX, PhilipsApi.NEW2_TIMER2] +class PhilipsAC0951(PhilipsAC0950): + """AC0951.""" + + # the AC1715 seems to be a new class of devices that follows some patterns of its own class PhilipsAC1715(PhilipsNewGenericCoAPFan): """AC1715.""" @@ -1863,6 +1867,7 @@ class PhilipsCX5120(PhilipsNew2GenericCoAPFan): FanModel.AC0850_20C: PhilipsAC085020C, FanModel.AC0850_31: PhilipsAC085031, FanModel.AC0950: PhilipsAC0950, + FanModel.AC0951: PhilipsAC0951, FanModel.AC1214: PhilipsAC1214, FanModel.AC1715: PhilipsAC1715, FanModel.AC2729: PhilipsAC2729, From a7bceb2636acca576cd43c749dee7ce46bc3fb6d Mon Sep 17 00:00:00 2001 From: kongo09 Date: Wed, 23 Oct 2024 23:55:38 +0200 Subject: [PATCH 17/29] fix code warnings --- custom_components/philips_airpurifier_coap/number.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/number.py b/custom_components/philips_airpurifier_coap/number.py index 2ee11a88..41317aaa 100644 --- a/custom_components/philips_airpurifier_coap/number.py +++ b/custom_components/philips_airpurifier_coap/number.py @@ -1,4 +1,5 @@ """Philips Air Purifier & Humidifier Numbers.""" + from __future__ import annotations from collections.abc import Callable @@ -105,8 +106,7 @@ def __init__( # noqa: D107 @property def native_value(self) -> float | None: """Return the current number.""" - value = self._device_status.get(self.kind) - return value + return self._device_status.get(self.kind) async def async_set_native_value(self, value: float) -> None: """Select a number.""" @@ -118,10 +118,8 @@ async def async_set_native_value(self, value: float) -> None: value = self._attr_native_min_value if value % self._attr_native_step > 0: value = value // self._attr_native_step * self._attr_native_step - if value > 0 and value < self._min: - value = self._min - if value > self._attr_native_max_value: - value = self._attr_native_max_value + value = max(value, self._min) if value > 0 else value + value = min(value, self._attr_native_max_value) _LOGGER.debug("setting number with: %s", value) From 19d0358c291c1c8b62493665b2baf32195273f0a Mon Sep 17 00:00:00 2001 From: kongo09 Date: Thu, 24 Oct 2024 20:59:23 +0200 Subject: [PATCH 18/29] convert to list comprehension --- custom_components/philips_airpurifier_coap/number.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/number.py b/custom_components/philips_airpurifier_coap/number.py index 41317aaa..c506ee41 100644 --- a/custom_components/philips_airpurifier_coap/number.py +++ b/custom_components/philips_airpurifier_coap/number.py @@ -56,11 +56,11 @@ async def async_setup_entry( cls_available_numbers = getattr(cls, "AVAILABLE_NUMBERS", []) available_numbers.extend(cls_available_numbers) - numbers = [] - - for number in NUMBER_TYPES: - if number in available_numbers: - numbers.append(PhilipsNumber(coordinator, name, model, number)) + numbers = [ + PhilipsNumber(coordinator, name, model, number) + for number in NUMBER_TYPES + if number in available_numbers + ] async_add_entities(numbers, update_before_add=False) From 022174c2443086ae978ee0dacb40b19d6fd9bfd9 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Thu, 24 Oct 2024 21:02:28 +0200 Subject: [PATCH 19/29] simplify conditions --- custom_components/philips_airpurifier_coap/light.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/light.py b/custom_components/philips_airpurifier_coap/light.py index 6cc7b339..e2f4da12 100644 --- a/custom_components/philips_airpurifier_coap/light.py +++ b/custom_components/philips_airpurifier_coap/light.py @@ -1,4 +1,5 @@ """Philips Air Purifier & Humidifier Switches.""" + from __future__ import annotations from collections.abc import Callable @@ -118,8 +119,7 @@ def is_on(self) -> bool: # _LOGGER.debug("is_on, kind: %s - status: %s - on: %s", self.kind, status, self._on) if self._dimmable: return status > 0 - else: - return status == int(self._on) + return status == int(self._on) @property def brightness(self) -> int | None: @@ -127,8 +127,7 @@ def brightness(self) -> int | None: if self._dimmable: brightness = int(self._device_status.get(self.kind)) return round(255 * brightness / 100) - else: - return None + return None async def async_turn_on(self, **kwargs) -> None: """Turn the light on.""" From 51489220e8c1c7e93e0e406def3c19915cd9dc60 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Thu, 24 Oct 2024 21:04:06 +0200 Subject: [PATCH 20/29] simplify list comprehension --- custom_components/philips_airpurifier_coap/select.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/select.py b/custom_components/philips_airpurifier_coap/select.py index 1447cd81..e84a1f3f 100644 --- a/custom_components/philips_airpurifier_coap/select.py +++ b/custom_components/philips_airpurifier_coap/select.py @@ -1,4 +1,5 @@ """Philips Air Purifier & Humidifier Selects.""" + from __future__ import annotations from collections.abc import Callable @@ -55,11 +56,11 @@ async def async_setup_entry( cls_available_selects = getattr(cls, "AVAILABLE_SELECTS", []) available_selects.extend(cls_available_selects) - selects = [] - - for select in SELECT_TYPES: - if select in available_selects: - selects.append(PhilipsSelect(coordinator, name, model, select)) + selects = [ + PhilipsSelect(coordinator, name, model, select) + for select in SELECT_TYPES + if select in available_selects + ] async_add_entities(selects, update_before_add=False) From da3629c4b6c823c0a0f3d1b97cb41cfd56fd9f60 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Thu, 24 Oct 2024 21:07:49 +0200 Subject: [PATCH 21/29] simplify list comprehension --- custom_components/philips_airpurifier_coap/light.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/light.py b/custom_components/philips_airpurifier_coap/light.py index e2f4da12..7c7ef1c6 100644 --- a/custom_components/philips_airpurifier_coap/light.py +++ b/custom_components/philips_airpurifier_coap/light.py @@ -59,11 +59,11 @@ async def async_setup_entry( cls_available_lights = getattr(cls, "AVAILABLE_LIGHTS", []) available_lights.extend(cls_available_lights) - lights = [] - - for light in LIGHT_TYPES: - if light in available_lights: - lights.append(PhilipsLight(coordinator, name, model, light)) + lights = [ + PhilipsLight(coordinator, name, model, light) + for light in LIGHT_TYPES + if light in available_lights + ] async_add_entities(lights, update_before_add=False) From b90e2c0abeb08f6ab996682976923eaa287e7e2e Mon Sep 17 00:00:00 2001 From: kongo09 Date: Thu, 24 Oct 2024 21:35:38 +0200 Subject: [PATCH 22/29] introduce medium dimmed light --- .../philips_airpurifier_coap/const.py | 11 ++++++++++ .../philips_airpurifier_coap/light.py | 22 ++++++++++++------- .../philips_airpurifier_coap/philips.py | 2 +- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/const.py b/custom_components/philips_airpurifier_coap/const.py index 00647e5a..079f525e 100644 --- a/custom_components/philips_airpurifier_coap/const.py +++ b/custom_components/philips_airpurifier_coap/const.py @@ -91,6 +91,7 @@ class ICON(StrEnum): SWITCH_ON = "on" SWITCH_OFF = "off" +SWITCH_MEDIUM = "medium" OPTIONS = "options" DIMMABLE = "dimmable" @@ -370,6 +371,7 @@ class PhilipsApi: NEW2_POWER = "D03102" NEW2_DISPLAY_BACKLIGHT = "D0312D" NEW2_DISPLAY_BACKLIGHT2 = "D03105" + NEW2_DISPLAY_BACKLIGHT3 = "D03105#1" # dimmable in 3 steps NEW2_TEMPERATURE = "D03224" NEW2_SOFTWARE_VERSION = "D01S12" NEW2_CHILD_LOCK = "D03103" @@ -802,6 +804,15 @@ class PhilipsApi: SWITCH_OFF: 0, DIMMABLE: True, }, + PhilipsApi.NEW2_DISPLAY_BACKLIGHT3: { + ATTR_ICON: ICON.LIGHT_DIMMING_BUTTON, + FanAttributes.LABEL: FanAttributes.DISPLAY_BACKLIGHT, + CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, + SWITCH_ON: 123, + SWITCH_OFF: 0, + SWITCH_MEDIUM: 115, + DIMMABLE: True, + }, } SELECT_TYPES: dict[str, SelectDescription] = { diff --git a/custom_components/philips_airpurifier_coap/light.py b/custom_components/philips_airpurifier_coap/light.py index 7c7ef1c6..022df6b5 100644 --- a/custom_components/philips_airpurifier_coap/light.py +++ b/custom_components/philips_airpurifier_coap/light.py @@ -25,6 +25,7 @@ DIMMABLE, DOMAIN, LIGHT_TYPES, + SWITCH_MEDIUM, SWITCH_OFF, SWITCH_ON, FanAttributes, @@ -85,6 +86,7 @@ def __init__( # noqa: D107 self._description = LIGHT_TYPES[light] self._on = self._description.get(SWITCH_ON) self._off = self._description.get(SWITCH_OFF) + self._medium = self._description.get(SWITCH_MEDIUM) self._dimmable = self._description.get(DIMMABLE) self._attr_device_class = self._description.get(ATTR_DEVICE_CLASS) self._attr_icon = self._description.get(ATTR_ICON) @@ -95,6 +97,7 @@ def __init__( # noqa: D107 if self._dimmable is None: self._dimmable = False + self._medium = None if self._dimmable: self._attr_color_mode = ColorMode.BRIGHTNESS @@ -110,34 +113,37 @@ def __init__( # noqa: D107 _LOGGER.error("Failed retrieving unique_id: %s", e) raise PlatformNotReady self._attrs: dict[str, Any] = {} - self.kind = light + self.kind = light.partition("#")[0] @property def is_on(self) -> bool: """Return if the light is on.""" status = int(self._device_status.get(self.kind)) # _LOGGER.debug("is_on, kind: %s - status: %s - on: %s", self.kind, status, self._on) - if self._dimmable: - return status > 0 - return status == int(self._on) + return status != int(self._off) @property def brightness(self) -> int | None: """Return the brightness of the light.""" if self._dimmable: brightness = int(self._device_status.get(self.kind)) - return round(255 * brightness / 100) + if self._medium and brightness == int(self._medium): + return 128 + return round(255 * brightness / int(self._on)) return None async def async_turn_on(self, **kwargs) -> None: """Turn the light on.""" if self._dimmable: if ATTR_BRIGHTNESS in kwargs: - value = round(100 * int(kwargs[ATTR_BRIGHTNESS]) / 255) + if self._medium and kwargs[ATTR_BRIGHTNESS] < 255: + value = self._medium + else: + value = round(int(self._on) * int(kwargs[ATTR_BRIGHTNESS]) / 255) else: - value = 100 + value = int(self._on) else: - value = self._on + value = int(self._on) _LOGGER.debug("async_turn_on, kind: %s - value: %s", self.kind, value) await self.coordinator.client.set_control_value(self.kind, value) diff --git a/custom_components/philips_airpurifier_coap/philips.py b/custom_components/philips_airpurifier_coap/philips.py index b66e4306..2b41cf1d 100644 --- a/custom_components/philips_airpurifier_coap/philips.py +++ b/custom_components/philips_airpurifier_coap/philips.py @@ -723,7 +723,7 @@ class PhilipsAC0950(PhilipsNew2GenericCoAPFan): UNAVAILABLE_FILTERS = [PhilipsApi.FILTER_NANOPROTECT_PREFILTER] AVAILABLE_SWITCHES = [PhilipsApi.NEW2_CHILD_LOCK, PhilipsApi.NEW2_BEEP] - AVAILABLE_LIGHTS = [PhilipsApi.NEW2_DISPLAY_BACKLIGHT] + AVAILABLE_LIGHTS = [PhilipsApi.NEW2_DISPLAY_BACKLIGHT3] AVAILABLE_SELECTS = [PhilipsApi.NEW2_GAS_PREFERRED_INDEX, PhilipsApi.NEW2_TIMER2] From 5aac4f31528b621d91cf1239c5c80373085fdffe Mon Sep 17 00:00:00 2001 From: kongo09 Date: Thu, 24 Oct 2024 22:30:30 +0200 Subject: [PATCH 23/29] fix lights --- custom_components/philips_airpurifier_coap/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/light.py b/custom_components/philips_airpurifier_coap/light.py index 022df6b5..d25eac43 100644 --- a/custom_components/philips_airpurifier_coap/light.py +++ b/custom_components/philips_airpurifier_coap/light.py @@ -120,7 +120,7 @@ def is_on(self) -> bool: """Return if the light is on.""" status = int(self._device_status.get(self.kind)) # _LOGGER.debug("is_on, kind: %s - status: %s - on: %s", self.kind, status, self._on) - return status != int(self._off) + return int(status) != int(self._off) @property def brightness(self) -> int | None: @@ -143,7 +143,7 @@ async def async_turn_on(self, **kwargs) -> None: else: value = int(self._on) else: - value = int(self._on) + value = self._on _LOGGER.debug("async_turn_on, kind: %s - value: %s", self.kind, value) await self.coordinator.client.set_control_value(self.kind, value) From 534b69eb64e6c78494131fa4f80ccb30c32b410f Mon Sep 17 00:00:00 2001 From: kongo09 Date: Thu, 24 Oct 2024 23:55:37 +0200 Subject: [PATCH 24/29] improve exceptions --- .../philips_airpurifier_coap/binary_sensor.py | 6 ++++-- .../philips_airpurifier_coap/light.py | 10 ++++++--- .../philips_airpurifier_coap/number.py | 10 ++++++--- .../philips_airpurifier_coap/select.py | 18 ++++++++++------ .../philips_airpurifier_coap/sensor.py | 21 ++++++++++++------- 5 files changed, 43 insertions(+), 22 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/binary_sensor.py b/custom_components/philips_airpurifier_coap/binary_sensor.py index a8690aa1..1736ed09 100644 --- a/custom_components/philips_airpurifier_coap/binary_sensor.py +++ b/custom_components/philips_airpurifier_coap/binary_sensor.py @@ -1,4 +1,5 @@ """Philips Air Purifier & Humidifier Binary Sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -89,9 +90,10 @@ def __init__( # noqa: D107 try: device_id = self._device_status[PhilipsApi.DEVICE_ID] self._attr_unique_id = f"{self._model}-{device_id}-{kind.lower()}" - except Exception as e: + except KeyError as e: _LOGGER.error("Failed retrieving unique_id: %s", e) - raise PlatformNotReady + raise PlatformNotReady from e + self._attrs: dict[str, Any] = {} self.kind = kind diff --git a/custom_components/philips_airpurifier_coap/light.py b/custom_components/philips_airpurifier_coap/light.py index d25eac43..09a005be 100644 --- a/custom_components/philips_airpurifier_coap/light.py +++ b/custom_components/philips_airpurifier_coap/light.py @@ -109,9 +109,13 @@ def __init__( # noqa: D107 try: device_id = self._device_status[PhilipsApi.DEVICE_ID] self._attr_unique_id = f"{self._model}-{device_id}-{light.lower()}" - except Exception as e: - _LOGGER.error("Failed retrieving unique_id: %s", e) - raise PlatformNotReady + except KeyError as e: + _LOGGER.error("Failed retrieving unique_id due to missing key: %s", e) + raise PlatformNotReady from e + except TypeError as e: + _LOGGER.error("Failed retrieving unique_id due to type error: %s", e) + raise PlatformNotReady from e + self._attrs: dict[str, Any] = {} self.kind = light.partition("#")[0] diff --git a/custom_components/philips_airpurifier_coap/number.py b/custom_components/philips_airpurifier_coap/number.py index c506ee41..6af49dfc 100644 --- a/custom_components/philips_airpurifier_coap/number.py +++ b/custom_components/philips_airpurifier_coap/number.py @@ -97,9 +97,13 @@ def __init__( # noqa: D107 try: device_id = self._device_status[PhilipsApi.DEVICE_ID] self._attr_unique_id = f"{self._model}-{device_id}-{number.lower()}" - except Exception as e: - _LOGGER.error("Failed retrieving unique_id: %s", e) - raise PlatformNotReady + except KeyError as e: + _LOGGER.error("Failed retrieving unique_id due to missing key: %s", e) + raise PlatformNotReady from e + except TypeError as e: + _LOGGER.error("Failed retrieving unique_id due to type error: %s", e) + raise PlatformNotReady from e + self._attrs: dict[str, Any] = {} self.kind = number diff --git a/custom_components/philips_airpurifier_coap/select.py b/custom_components/philips_airpurifier_coap/select.py index e84a1f3f..0b733444 100644 --- a/custom_components/philips_airpurifier_coap/select.py +++ b/custom_components/philips_airpurifier_coap/select.py @@ -99,9 +99,13 @@ def __init__( # noqa: D107 try: device_id = self._device_status[PhilipsApi.DEVICE_ID] self._attr_unique_id = f"{self._model}-{device_id}-{select.lower()}" - except Exception as e: - _LOGGER.error("Failed retrieving unique_id: %s", e) - raise PlatformNotReady + except KeyError as e: + _LOGGER.error("Failed retrieving unique_id due to missing key: %s", e) + raise PlatformNotReady from e + except TypeError as e: + _LOGGER.error("Failed retrieving unique_id due to type error: %s", e) + raise PlatformNotReady from e + self._attrs: dict[str, Any] = {} self.kind = select.partition("#")[0] @@ -109,6 +113,7 @@ def __init__( # noqa: D107 def current_option(self) -> str: """Return the currently selected option.""" option = self._device_status.get(self.kind) + _LOGGER.debug("current_option: %s", option) if option in self._options: return self._options[option] return None @@ -129,9 +134,10 @@ async def async_select_option(self, option: str) -> None: option_key, ) await self.coordinator.client.set_control_value(self.kind, option_key) - except Exception as e: - # TODO: catching Exception is actually too broad and needs to be tightened - _LOGGER.error("Failed setting option: '%s' with error: %s", option, e) + except KeyError as e: + _LOGGER.error("Invalid option key: '%s' with error: %s", option, e) + except ValueError as e: + _LOGGER.error("Invalid value for option: '%s' with error: %s", option, e) @property def icon(self) -> str: diff --git a/custom_components/philips_airpurifier_coap/sensor.py b/custom_components/philips_airpurifier_coap/sensor.py index 0b464def..f433612b 100644 --- a/custom_components/philips_airpurifier_coap/sensor.py +++ b/custom_components/philips_airpurifier_coap/sensor.py @@ -1,4 +1,5 @@ """Philips Air Purifier & Humidifier Sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -117,9 +118,10 @@ def __init__( # noqa: D107 try: device_id = self._device_status[PhilipsApi.DEVICE_ID] self._attr_unique_id = f"{self._model}-{device_id}-{kind.lower()}" - except Exception as e: - _LOGGER.error("Failed retrieving unique_id: %s", e) - raise PlatformNotReady + except KeyError as e: + _LOGGER.error("Failed retrieving unique_id due to missing key: %s", e) + raise PlatformNotReady from e + self._attrs: dict[str, Any] = {} self.kind = kind @@ -180,9 +182,13 @@ def __init__( # noqa: D107 self._attr_unique_id = ( f"{self._model}-{device_id}-{self._description[FanAttributes.LABEL]}" ) - except Exception as e: - _LOGGER.error("Failed retrieving unique_id: %s", e) - raise PlatformNotReady + except KeyError as e: + _LOGGER.error("Failed retrieving unique_id due to missing key: %s", e) + raise PlatformNotReady from e + except TypeError as e: + _LOGGER.error("Failed retrieving unique_id due to type error: %s", e) + raise PlatformNotReady from e + self._attrs: dict[str, Any] = {} @property @@ -190,8 +196,7 @@ def native_value(self) -> StateType: """Return the native value of the filter sensor.""" if self._has_total: return self._percentage - else: - return self._time_remaining + return self._time_remaining @property def extra_state_attributes(self) -> dict[str, Any]: From 17f3b969cce41bef79495a61e50ee08abf9ee5f5 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Thu, 24 Oct 2024 23:57:32 +0200 Subject: [PATCH 25/29] simplify list comprehensions --- .../philips_airpurifier_coap/binary_sensor.py | 12 ++++---- .../philips_airpurifier_coap/sensor.py | 30 +++++++++++-------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/binary_sensor.py b/custom_components/philips_airpurifier_coap/binary_sensor.py index 1736ed09..ea1e374a 100644 --- a/custom_components/philips_airpurifier_coap/binary_sensor.py +++ b/custom_components/philips_airpurifier_coap/binary_sensor.py @@ -55,13 +55,11 @@ async def async_setup_entry( # noqa: D103 cls_available_binary_sensors = getattr(cls, "AVAILABLE_BINARY_SENSORS", []) available_binary_sensors.extend(cls_available_binary_sensors) - binary_sensors = [] - - for binary_sensor in BINARY_SENSOR_TYPES: - if binary_sensor in status and binary_sensor in available_binary_sensors: - binary_sensors.append( - PhilipsBinarySensor(coordinator, name, model, binary_sensor) - ) + binary_sensors = [ + PhilipsBinarySensor(coordinator, name, model, binary_sensor) + for binary_sensor in BINARY_SENSOR_TYPES + if binary_sensor in status and binary_sensor in available_binary_sensors + ] async_add_entities(binary_sensors, update_before_add=False) diff --git a/custom_components/philips_airpurifier_coap/sensor.py b/custom_components/philips_airpurifier_coap/sensor.py index f433612b..c4f0f874 100644 --- a/custom_components/philips_airpurifier_coap/sensor.py +++ b/custom_components/philips_airpurifier_coap/sensor.py @@ -67,19 +67,23 @@ async def async_setup_entry( # noqa: D103 cls_extra_sensors = getattr(cls, "EXTRA_SENSORS", []) extra_sensors.extend(cls_extra_sensors) - sensors = [] - - for sensor in SENSOR_TYPES: - if sensor in status and sensor not in unavailable_sensors: - sensors.append(PhilipsSensor(coordinator, name, model, sensor)) - - for sensor in EXTRA_SENSOR_TYPES: - if sensor in status and sensor in extra_sensors: - sensors.append(PhilipsSensor(coordinator, name, model, sensor)) - - for _filter in FILTER_TYPES: - if _filter in status and _filter not in unavailable_filters: - sensors.append(PhilipsFilterSensor(coordinator, name, model, _filter)) + sensors = ( + [ + PhilipsSensor(coordinator, name, model, sensor) + for sensor in SENSOR_TYPES + if sensor in status and sensor not in unavailable_sensors + ] + + [ + PhilipsSensor(coordinator, name, model, sensor) + for sensor in EXTRA_SENSOR_TYPES + if sensor in status and sensor in extra_sensors + ] + + [ + PhilipsFilterSensor(coordinator, name, model, _filter) + for _filter in FILTER_TYPES + if _filter in status and _filter not in unavailable_filters + ] + ) async_add_entities(sensors, update_before_add=False) From 1ded2abf4b779fcec8823db3e072a0cc7fed4c71 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Fri, 25 Oct 2024 00:01:33 +0200 Subject: [PATCH 26/29] change path.join --- custom_components/philips_airpurifier_coap/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/__init__.py b/custom_components/philips_airpurifier_coap/__init__.py index 88103292..b1673e12 100644 --- a/custom_components/philips_airpurifier_coap/__init__.py +++ b/custom_components/philips_airpurifier_coap/__init__.py @@ -7,7 +7,8 @@ from ipaddress import IPv6Address, ip_address import json import logging -from os import path, walk +from os import walk +from pathlib import Path from aioairctrl import CoAPClient from getmac import get_mac_address @@ -61,7 +62,7 @@ async def get(self, request): for dirpath, _dirnames, filenames in walk(self.iconpath): icons.extend( [ - {"name": path.join(dirpath[len(self.iconpath) :], fn[:-4])} + {"name": (Path(dirpath[len(self.iconpath) :]) / fn[:-4]).as_posix()} for fn in filenames if fn.endswith(".svg") ] From 892b7bb11c8b535065e6d6cf13f09f68ca619929 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Fri, 25 Oct 2024 00:02:45 +0200 Subject: [PATCH 27/29] improve exception --- custom_components/philips_airpurifier_coap/switch.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/switch.py b/custom_components/philips_airpurifier_coap/switch.py index 9de71c74..c6c886ce 100644 --- a/custom_components/philips_airpurifier_coap/switch.py +++ b/custom_components/philips_airpurifier_coap/switch.py @@ -1,4 +1,5 @@ """Philips Air Purifier & Humidifier Switches.""" + from __future__ import annotations from collections.abc import Callable @@ -93,9 +94,10 @@ def __init__( # noqa: D107 try: device_id = self._device_status[PhilipsApi.DEVICE_ID] self._attr_unique_id = f"{self._model}-{device_id}-{switch.lower()}" - except Exception as e: + except KeyError as e: _LOGGER.error("Failed retrieving unique_id: %s", e) - raise PlatformNotReady + raise PlatformNotReady from e + self._attrs: dict[str, Any] = {} self.kind = switch From b0648b710d73ced5d785a8578957c7e3b5a2e225 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Fri, 25 Oct 2024 00:03:10 +0200 Subject: [PATCH 28/29] simplify list comprehension --- custom_components/philips_airpurifier_coap/switch.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/switch.py b/custom_components/philips_airpurifier_coap/switch.py index c6c886ce..9aa617b5 100644 --- a/custom_components/philips_airpurifier_coap/switch.py +++ b/custom_components/philips_airpurifier_coap/switch.py @@ -58,11 +58,11 @@ async def async_setup_entry( cls_available_switches = getattr(cls, "AVAILABLE_SWITCHES", []) available_switches.extend(cls_available_switches) - switches = [] - - for switch in SWITCH_TYPES: - if switch in available_switches: - switches.append(PhilipsSwitch(coordinator, name, model, switch)) + switches = [ + PhilipsSwitch(coordinator, name, model, switch) + for switch in SWITCH_TYPES + if switch in available_switches + ] async_add_entities(switches, update_before_add=False) From 5b5e317743dba0f000b25aaeb89d9ed134df2036 Mon Sep 17 00:00:00 2001 From: kongo09 Date: Fri, 25 Oct 2024 00:04:58 +0200 Subject: [PATCH 29/29] fix list type --- custom_components/philips_airpurifier_coap/model.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/philips_airpurifier_coap/model.py b/custom_components/philips_airpurifier_coap/model.py index b43a0f48..93c25b3d 100644 --- a/custom_components/philips_airpurifier_coap/model.py +++ b/custom_components/philips_airpurifier_coap/model.py @@ -1,4 +1,5 @@ """Type definitions for Philips AirPurifier integration.""" + from __future__ import annotations from collections.abc import Callable @@ -24,7 +25,7 @@ class SensorDescription(_SensorDescription, total=False): unit: str state_class: str value: Callable[[Any, DeviceStatus], StateType] - icon_map: [int, str] + icon_map: list[tuple[int, str]] # warn_value: int # warn_icon: str @@ -35,7 +36,7 @@ class FilterDescription(TypedDict): prefix: str postfix: str icon: str - icon_map: [int, str] + icon_map: list[tuple[int, str]] # warn_icon: str # warn_value: int