diff --git a/README.md b/README.md index fb70b77..f37acd1 100644 --- a/README.md +++ b/README.md @@ -74,10 +74,14 @@ Note: `configuration.yaml` is no longer supported and your configuration is not - AC3854/51 - AC3858/50 - AC3858/51 +- AC3858/86 - AC4236 - AC4550 - AC4558 - AC5659 +- AMF765 +- AMF870 +- CX5120 ## Is your model not supported yet? @@ -170,31 +174,41 @@ The `fan` entity has some additional attributes not captured with sensors. Speci The integration also provides the original Philips icons for your use in the frontend. The icons can be accessed with the prefix `pap:` and should be visible in the icon picker. Credit for this part of the code goes to @thomasloven -![Preview](./custom_components/philips_airpurifier_coap/icons/pap/power_button.svg) power_button
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/child_lock_button.svg) child_lock_button
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/auto_mode_button.svg) auto_mode_button
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/fan_speed_button.svg) fan_speed_button
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/humidity_button.svg) humidity_button
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/light_dimming_button.svg) light_dimming_button
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/two_in_one_mode_button.svg) two_in_one_mode_button
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/timer_reset_button.svg) timer_reset_button
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/sleep_mode.svg) sleep_mode
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/auto_mode.svg) auto_mode
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/speed_1.svg) speed_1
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/speed_2.svg) speed_2
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/speed_3.svg) speed_3
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/allergen_mode.svg) allergen_mode
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/purification_only_mode.svg) purification_only_mode
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/two_in_one_mode.svg) two_in_one_mode
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/bacteria_virus_mode.svg) bacteria_virus_mode
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/nanoprotect_filter.svg) nanoprotect_filter
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/filter_replacement.svg) filter_replacement
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/water_refill.svg) water_refill
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/prefilter_cleaning.svg) prefilter_clearning
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/prefilter_wick_cleaning.svg) prefilter_wick_cleaning
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/pm25.svg) pm25
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/iai.svg) iai
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/wifi.svg) wifi
-![Preview](./custom_components/philips_airpurifier_coap/icons/pap/reset.svg) reset
+| icon | name | +|------------------------------------------------------------------------------------------------|-------------------------| +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/power_button.svg) | power_button | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/child_lock_button.svg) | child_lock_button | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/auto_mode_button.svg) | auto_mode_button | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/fan_speed_button.svg) | fan_speed_button | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/humidity_button.svg) | humidity_button | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/light_dimming_button.svg) | light_dimming_button | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/two_in_one_mode_button.svg) | two_in_one_mode_button | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/timer_reset_button.svg) | timer_reset_button | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/sleep_mode.svg) | sleep_mode | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/auto_mode.svg) | auto_mode | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/speed_1.svg) | speed_1 | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/speed_2.svg) | speed_2 | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/speed_3.svg) | speed_3 | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/allergen_mode.svg) | allergen_mode | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/purification_only_mode.svg) | purification_only_mode | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/two_in_one_mode.svg) | two_in_one_mode | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/bacteria_virus_mode.svg) | bacteria_virus_mode | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/nanoprotect_filter.svg) | nanoprotect_filter | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/filter_replacement.svg) | filter_replacement | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/water_refill.svg) | water_refill | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/prefilter_cleaning.svg) | prefilter_clearning | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/prefilter_wick_cleaning.svg) | prefilter_wick_cleaning | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/pm25.svg) | pm25 | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/iai.svg) | iai | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/wifi.svg) | wifi | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/reset.svg) | reset | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/circulate.svg) | circulate | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/clean.svg) | clean | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/mode.svg) | mode | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/pm25b.svg) | pm25b | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/rotate.svg) | rotate | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/oscillate.svg) | oscillate | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/heating.svg) | heating | +| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/gas.svg) | gas | Note: you might have to clear your browser cache after installation to see the icons. diff --git a/custom_components/philips_airpurifier_coap/__init__.py b/custom_components/philips_airpurifier_coap/__init__.py index ac48208..51b8e60 100644 --- a/custom_components/philips_airpurifier_coap/__init__.py +++ b/custom_components/philips_airpurifier_coap/__init__.py @@ -32,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["fan", "sensor", "switch", "light", "select"] +PLATFORMS = ["fan", "sensor", "switch", "light", "select", "number"] # icons code thanks to Thomas Loven: diff --git a/custom_components/philips_airpurifier_coap/config_flow.py b/custom_components/philips_airpurifier_coap/config_flow.py index 06ac9f8..a90017a 100644 --- a/custom_components/philips_airpurifier_coap/config_flow.py +++ b/custom_components/philips_airpurifier_coap/config_flow.py @@ -91,14 +91,34 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes _LOGGER.warning(r"Failed to connect: %s", ex) raise exceptions.ConfigEntryNotReady from ex - # autodetect model and name - self._model = list( + # autodetect model + model_map = map( + status.get, + [ + PhilipsApi.MODEL_ID, + PhilipsApi.NEW_MODEL_ID, + PhilipsApi.NEW2_MODEL_ID, + ], + ) + _LOGGER.debug("model_map retrieved: %s", model_map) + model_filter = filter(None, model_map) + _LOGGER.debug("model_filter applied: %s", model_filter) + model_list = list(model_filter) + _LOGGER.debug("model_list built: %s", model_list) + first_model = model_list[0] + _LOGGER.debug("first model selected: %s", first_model) + self._model = first_model[:9] + _LOGGER.debug("model type extracted: %s", self._model) + + # autodetect name + self._name = list( filter( - None, map(status.get, [PhilipsApi.MODEL_ID, PhilipsApi.NEW_MODEL_ID]) + None, + map( + status.get, + [PhilipsApi.NAME, PhilipsApi.NEW_NAME, PhilipsApi.NEW2_NAME], + ), ) - )[0][:9] - self._name = list( - filter(None, map(status.get, [PhilipsApi.NAME, PhilipsApi.NEW_NAME])) )[0] self._device_id = status[PhilipsApi.DEVICE_ID] _LOGGER.debug( @@ -215,16 +235,37 @@ async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult _LOGGER.warning(r"Failed to connect: %s", ex) raise exceptions.ConfigEntryNotReady from ex - # autodetect model and name - self._model = list( - filter( - None, - map(status.get, [PhilipsApi.MODEL_ID, PhilipsApi.NEW_MODEL_ID]), - ) - )[0][:9] + # autodetect model + model_map = map( + status.get, + [ + PhilipsApi.MODEL_ID, + PhilipsApi.NEW_MODEL_ID, + PhilipsApi.NEW2_MODEL_ID, + ], + ) + _LOGGER.debug("model_map retrieved: %s", model_map) + model_filter = filter(None, model_map) + _LOGGER.debug("model_filter applied: %s", model_filter) + model_list = list(model_filter) + _LOGGER.debug("model_list built: %s", model_list) + first_model = model_list[0] + _LOGGER.debug("first model selected: %s", first_model) + self._model = first_model[:9] + _LOGGER.debug("model type extracted: %s", self._model) + + # autodetect name self._name = list( filter( - None, map(status.get, [PhilipsApi.NAME, PhilipsApi.NEW_NAME]) + None, + map( + status.get, + [ + PhilipsApi.NAME, + PhilipsApi.NEW_NAME, + PhilipsApi.NEW2_NAME, + ], + ), ) )[0] self._device_id = status[PhilipsApi.DEVICE_ID] diff --git a/custom_components/philips_airpurifier_coap/const.py b/custom_components/philips_airpurifier_coap/const.py index 90e8709..e894310 100644 --- a/custom_components/philips_airpurifier_coap/const.py +++ b/custom_components/philips_airpurifier_coap/const.py @@ -3,6 +3,7 @@ from enum import StrEnum +from homeassistant.components.number import NumberDeviceClass from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorDeviceClass, @@ -17,13 +18,13 @@ PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, UnitOfTemperature, - UnitOfTime, ) from homeassistant.helpers.entity import EntityCategory from .model import ( FilterDescription, LightDescription, + NumberDescription, SelectDescription, SensorDescription, SwitchDescription, @@ -62,8 +63,16 @@ class ICON(StrEnum): WATER_REFILL = "pap:water_refill" PREFILTER_CLEANING = "pap:prefilter_cleaning" PREFILTER_WICK_CLEANING = "pap:prefilter_wick_cleaning" - PM25 = "pap:pm25" + PM25 = "pap:pm25b" IAI = "pap:iai" + # PM25B = "pap:pm25b" + CIRCULATE = "pap:circulate" + CLEAN = "pap:clean" + MODE = "pap:mode" + ROTATE = "pap:rotate" + OSCILLATE = "pap:oscillate" + GAS = "pap:gas" + HEATING = "pap:heating" DATA_EXTRA_MODULE_URL = "frontend_extra_module_url" @@ -109,10 +118,14 @@ class FanModel(StrEnum): AC3854_51 = "AC3854/51" AC3858_50 = "AC3858/50" AC3858_51 = "AC3858/51" + AC3858_86 = "AC3858/86" AC4236 = "AC4236" AC4550 = "AC4550" AC4558 = "AC4558" AC5659 = "AC5659" + AMF765 = "AMF765" + AMF870 = "AMF870" + CX5120 = "CX5120" class PresetMode: @@ -122,9 +135,17 @@ class PresetMode: SPEED_GENTLE_1 = "gentle/speed 1" SPEED_2 = "speed 2" SPEED_3 = "speed 3" + SPEED_4 = "speed 4" + SPEED_5 = "speed 5" + SPEED_6 = "speed 6" + SPEED_7 = "speed 7" + SPEED_8 = "spped 8" + SPEED_9 = "spped 9" + SPEED_10 = "speed 10" ALLERGEN = "allergen" AUTO = "auto" AUTO_GENERAL = "auto general" + AUTO_PLUS = "auto+" BACTERIA = "bacteria" GENTLE = "gentle" NIGHT = "night" @@ -133,6 +154,9 @@ class PresetMode: TURBO = "turbo" GAS = "gas" POLLUTION = "pollution" + LOW = "low" + HIGH = "high" + VENTILATION = "ventilation" ICON_MAP = { ALLERGEN: ICON.ALLERGEN_MODE, @@ -177,8 +201,10 @@ class FanService(StrEnum): class FanAttributes(StrEnum): """The attributes of a fan.""" + ACTUAL_FAN_SPEED = "actual_fan_speed" AIR_QUALITY_INDEX = "air_quality_index" CHILD_LOCK = "child_lock" + BEEP = "beep" DEVICE_ID = "device_id" DEVICE_VERSION = "device_version" DISPLAY_BACKLIGHT = "display_backlight" @@ -207,6 +233,7 @@ class FanAttributes(StrEnum): MODEL_ID = "model_id" NAME = "name" PM25 = "PM2.5" + GAS = "gas_level" PREFERRED_INDEX = "preferred_index" PRODUCT_ID = "product_id" RUNTIME = "runtime" @@ -221,6 +248,18 @@ class FanAttributes(StrEnum): WARN_VALUE = "warn_value" WARN_ICON = "warn_icon" RSSI = "rssi" + SWING = "swing" + TURBO = "turbo" + OSCILLATION = "oscillation" + VALUE_LIST = "value_list" + OFF = "off" + MIN = "min" + MAX = "max" + STEP = "step" + TIMER = "timer" + TARGET_TEMP = "target_temperature" + STANDBY_SENSORS = "standby_sensors" + AUTO_PLUS = "auto_plus" class FanUnits(StrEnum): @@ -293,6 +332,12 @@ class PhilipsApi: SWITCH_ON: "1", SWITCH_OFF: "0", } + + OSCILLATION_MAP = { + SWITCH_ON: "17920", + SWITCH_OFF: "0", + } + # the AC1715 seems to follow a new scheme, this should later be refactored NEW_NAME = "D01-03" NEW_MODEL_ID = "D01-05" @@ -305,14 +350,48 @@ class PhilipsApi: NEW_PM25 = "D03-33" NEW_PREFERRED_INDEX = "D03-42" + # there is a third generation of devices with yet another scheme + NEW2_NAME = "D01S03" + NEW2_MODEL_ID = "D01S05" + NEW2_POWER = "D03102" + NEW2_DISPLAY_BACKLIGHT = "D0312D" + NEW2_DISPLAY_BACKLIGHT2 = "D03105" + NEW2_TEMPERATURE = "D03224" + NEW2_SOFTWARE_VERSION = "D01S12" + NEW2_CHILD_LOCK = "D03103" + NEW2_BEEP = "D03130" + NEW2_INDOOR_ALLERGEN_INDEX = "D03120" + NEW2_PM25 = "D03221" + NEW2_GAS = "D03122" + NEW2_HUMIDITY = "D03125" + NEW2_FILTER_NANOPROTECT_PREFILTER = "D0520D" + NEW2_FILTER_NANOPROTECT = "D0540E" + NEW2_FILTER_NANOPROTECT_PREFILTER_TOTAL = "D05207" + NEW2_FILTER_NANOPROTECT_TOTAL = "D05408" + NEW2_FAN_SPEED = "D0310D" + NEW2_SWING = "D0320F" + NEW2_CIRCULATION = "D0310A#1" + NEW2_HEATING = "D0310A#2" + NEW2_OSCILLATION = "D0320F" + NEW2_MODE_A = "D0310A" + NEW2_MODE_B = "D0310C" + NEW2_MODE_C = "D0310D" + NEW2_TIMER = "D03110#1" + NEW2_TIMER2 = "D03110#2" + NEW2_TARGET_TEMP = "D0310E" + NEW2_STANDBY_SENSORS = "D03134" + NEW2_AUTO_PLUS_AI = "D03180" + NEW2_PREFERRED_INDEX = "D0312A#1" + NEW2_GAS_PREFERRED_INDEX = "D0312A#2" + PREFERRED_INDEX_MAP = { - "0": ("Indoor Allergen Index", ICON.IAI), - "1": ("PM2.5", ICON.PM25), + 0: ("Indoor Allergen Index", ICON.IAI), + 1: ("PM2.5", ICON.PM25), } GAS_PREFERRED_INDEX_MAP = { - "0": ("Indoor Allergen Index", ICON.IAI), - "1": ("PM2.5", ICON.PM25), - "2": ("Gas", "mdi:air"), + 0: ("Indoor Allergen Index", ICON.IAI), + 1: ("PM2.5", ICON.PM25), + 2: ("Gas", ICON.GAS), } NEW_PREFERRED_INDEX_MAP = { "IAI": ("Indoor Allergen Index", ICON.IAI), @@ -322,6 +401,46 @@ class PhilipsApi: "P": ("Purification", ICON.PURIFICATION_ONLY_MODE), "PH": ("Purification and Humidification", ICON.TWO_IN_ONE_MODE), } + CIRCULATION_MAP = { + 1: ("Fan", ICON.CLEAN), + 2: ("Circulation", ICON.CIRCULATE), + } + HEATING_MAP = { + 1: ("Fan", ICON.CLEAN), + 2: ("Circulation", ICON.CIRCULATE), + 3: ("Heating", ICON.HEATING), + } + TIMER_MAP = { + 0: ("Off", "mdi:clock-plus"), + 1: ("0.5h", "mdi:clock-time-one"), + 2: ("1h", "mdi:clock-time-one"), + 3: ("2h", "mdi:clock-time-two"), + 4: ("3h", "mdi:clock-time-three"), + 5: ("4h", "mdi:clock-time-four"), + 6: ("5h", "mdi:clock-time-five"), + 7: ("6h", "mdi:clock-time-six"), + 8: ("7h", "mdi:clock-time-seven"), + 9: ("8h", "mdi:clock-time-eight"), + 10: ("9h", "mdi:clock-time-nine"), + 11: ("10h", "mdi:clock-time-ten"), + 12: ("11h", "mdi:clock-time-eleven"), + 13: ("12h", "mdi:clock-time-twelve"), + } + TIMER2_MAP = { + 0: ("Off", "mdi:clock-plus"), + 2: ("1h", "mdi:clock-time-one"), + 3: ("2h", "mdi:clock-time-two"), + 4: ("3h", "mdi:clock-time-three"), + 5: ("4h", "mdi:clock-time-four"), + 6: ("5h", "mdi:clock-time-five"), + 7: ("6h", "mdi:clock-time-six"), + 8: ("7h", "mdi:clock-time-seven"), + 9: ("8h", "mdi:clock-time-eight"), + 10: ("9h", "mdi:clock-time-nine"), + 11: ("10h", "mdi:clock-time-ten"), + 12: ("11h", "mdi:clock-time-eleven"), + 13: ("12h", "mdi:clock-time-twelve"), + } HUMIDITY_TARGET_MAP = { 40: ("40%", ICON.HUMIDITY_BUTTON), 50: ("50%", ICON.HUMIDITY_BUTTON), @@ -354,6 +473,11 @@ class PhilipsApi: FanAttributes.LABEL: FanAttributes.INDOOR_ALLERGEN_INDEX, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, + PhilipsApi.NEW2_INDOOR_ALLERGEN_INDEX: { + FanAttributes.ICON_MAP: {0: ICON.IAI}, + FanAttributes.LABEL: FanAttributes.INDOOR_ALLERGEN_INDEX, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, PhilipsApi.PM25: { ATTR_DEVICE_CLASS: SensorDeviceClass.PM25, FanAttributes.ICON_MAP: {0: ICON.PM25}, @@ -368,6 +492,18 @@ class PhilipsApi: FanAttributes.UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, + PhilipsApi.NEW2_PM25: { + ATTR_DEVICE_CLASS: SensorDeviceClass.PM25, + FanAttributes.ICON_MAP: {0: ICON.PM25}, + FanAttributes.LABEL: FanAttributes.PM25, + FanAttributes.UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + PhilipsApi.NEW2_GAS: { + FanAttributes.ICON_MAP: {0: ICON.GAS}, + FanAttributes.LABEL: FanAttributes.GAS, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, PhilipsApi.TOTAL_VOLATILE_ORGANIC_COMPOUNDS: { ATTR_DEVICE_CLASS: SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, FanAttributes.ICON_MAP: {0: "mdi:blur"}, @@ -382,6 +518,13 @@ class PhilipsApi: ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, FanAttributes.UNIT: PERCENTAGE, }, + PhilipsApi.NEW2_HUMIDITY: { + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, + FanAttributes.ICON_MAP: {0: "mdi:water-percent"}, + FanAttributes.LABEL: FanAttributes.HUMIDITY, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + FanAttributes.UNIT: PERCENTAGE, + }, PhilipsApi.TEMPERATURE: { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, FanAttributes.ICON_MAP: { @@ -393,6 +536,31 @@ class PhilipsApi: ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, FanAttributes.UNIT: UnitOfTemperature.CELSIUS, }, + PhilipsApi.NEW2_TEMPERATURE: { + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + FanAttributes.ICON_MAP: { + 0: "mdi:thermometer-low", + 17: "mdi:thermometer", + 23: "mdi:thermometer-high", + }, + FanAttributes.LABEL: ATTR_TEMPERATURE, + FanAttributes.VALUE: lambda value, _: value / 10, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + FanAttributes.UNIT: UnitOfTemperature.CELSIUS, + }, + # PhilipsApi.NEW2_FAN_SPEED: { + # FanAttributes.ICON_MAP: { + # 0: ICON.FAN_SPEED_BUTTON, + # 1: ICON.SPEED_1, + # 6: ICON.SPEED_2, + # 18: ICON.SPEED_3, + # }, + # FanAttributes.VALUE: lambda value, _: value + # if int(value) < 18 + # else FanAttributes.TURBO, + # FanAttributes.LABEL: FanAttributes.ACTUAL_FAN_SPEED, + # ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + # }, # diagnostic information PhilipsApi.WATER_LEVEL: { FanAttributes.ICON_MAP: {0: ICON.WATER_REFILL, 10: "mdi:water"}, @@ -472,6 +640,24 @@ class PhilipsApi: FanAttributes.TOTAL: PhilipsApi.FILTER_NANOPROTECT_CLEAN_TOTAL, FanAttributes.TYPE: "", }, + PhilipsApi.NEW2_FILTER_NANOPROTECT: { + FanAttributes.ICON_MAP: { + 0: ICON.FILTER_REPLACEMENT, + 10: ICON.NANOPROTECT_FILTER, + }, + FanAttributes.LABEL: FanAttributes.FILTER_NANOPROTECT, + FanAttributes.TOTAL: PhilipsApi.NEW2_FILTER_NANOPROTECT_TOTAL, + FanAttributes.TYPE: "", + }, + PhilipsApi.NEW2_FILTER_NANOPROTECT_PREFILTER: { + FanAttributes.ICON_MAP: { + 0: ICON.PREFILTER_CLEANING, + 10: ICON.NANOPROTECT_FILTER, + }, + FanAttributes.LABEL: FanAttributes.FILTER_NANOPROTECT_CLEAN, + FanAttributes.TOTAL: PhilipsApi.NEW2_FILTER_NANOPROTECT_PREFILTER_TOTAL, + FanAttributes.TYPE: "", + }, } SWITCH_TYPES: dict[str, SwitchDescription] = { @@ -482,6 +668,41 @@ class PhilipsApi: SWITCH_ON: True, SWITCH_OFF: False, }, + PhilipsApi.NEW2_CHILD_LOCK: { + ATTR_ICON: ICON.CHILD_LOCK_BUTTON, + FanAttributes.LABEL: FanAttributes.CHILD_LOCK, + CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, + SWITCH_ON: 1, + SWITCH_OFF: 0, + }, + PhilipsApi.NEW2_BEEP: { + ATTR_ICON: "mdi:volume-high", + FanAttributes.LABEL: FanAttributes.BEEP, + CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, + SWITCH_ON: 100, + SWITCH_OFF: 0, + }, + # Oscillation is part of the fan model, so the switch is removed here + # + # PhilipsApi.NEW2_SWING: { + # ATTR_ICON: ICON.ROTATE, + # FanAttributes.LABEL: FanAttributes.SWING, + # CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, + # SWITCH_ON: 17920, + # SWITCH_OFF: 0, + # }, + PhilipsApi.NEW2_STANDBY_SENSORS: { + ATTR_ICON: "mdi:power-settings", + FanAttributes.LABEL: FanAttributes.STANDBY_SENSORS, + SWITCH_ON: 1, + SWITCH_OFF: 0, + }, + PhilipsApi.NEW2_AUTO_PLUS_AI: { + ATTR_ICON: "mdi:format-annotation-plus", + FanAttributes.LABEL: FanAttributes.AUTO_PLUS, + SWITCH_ON: 1, + SWITCH_OFF: 0, + }, } LIGHT_TYPES: dict[str, LightDescription] = { @@ -507,6 +728,22 @@ class PhilipsApi: SWITCH_ON: 100, SWITCH_OFF: 0, }, + PhilipsApi.NEW2_DISPLAY_BACKLIGHT: { + ATTR_ICON: ICON.LIGHT_DIMMING_BUTTON, + FanAttributes.LABEL: FanAttributes.DISPLAY_BACKLIGHT, + CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, + SWITCH_ON: 100, + SWITCH_OFF: 0, + DIMMABLE: True, + }, + PhilipsApi.NEW2_DISPLAY_BACKLIGHT2: { + ATTR_ICON: ICON.LIGHT_DIMMING_BUTTON, + FanAttributes.LABEL: FanAttributes.DISPLAY_BACKLIGHT, + CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, + SWITCH_ON: 100, + SWITCH_OFF: 0, + DIMMABLE: True, + }, } SELECT_TYPES: dict[str, SelectDescription] = { @@ -525,7 +762,12 @@ class PhilipsApi: CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, OPTIONS: PhilipsApi.PREFERRED_INDEX_MAP, }, - PhilipsApi.PREFERRED_INDEX: { + PhilipsApi.NEW_PREFERRED_INDEX: { + FanAttributes.LABEL: FanAttributes.PREFERRED_INDEX, + CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, + OPTIONS: PhilipsApi.NEW_PREFERRED_INDEX_MAP, + }, + PhilipsApi.NEW2_PREFERRED_INDEX: { FanAttributes.LABEL: FanAttributes.PREFERRED_INDEX, CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, OPTIONS: PhilipsApi.PREFERRED_INDEX_MAP, @@ -535,9 +777,53 @@ class PhilipsApi: CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, OPTIONS: PhilipsApi.GAS_PREFERRED_INDEX_MAP, }, - PhilipsApi.NEW_PREFERRED_INDEX: { + PhilipsApi.NEW2_GAS_PREFERRED_INDEX: { FanAttributes.LABEL: FanAttributes.PREFERRED_INDEX, CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, - OPTIONS: PhilipsApi.NEW_PREFERRED_INDEX_MAP, + OPTIONS: PhilipsApi.GAS_PREFERRED_INDEX_MAP, + }, + PhilipsApi.NEW2_CIRCULATION: { + FanAttributes.LABEL: FanAttributes.FUNCTION, + CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, + OPTIONS: PhilipsApi.CIRCULATION_MAP, + }, + PhilipsApi.NEW2_HEATING: { + FanAttributes.LABEL: FanAttributes.FUNCTION, + CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, + OPTIONS: PhilipsApi.HEATING_MAP, + }, + PhilipsApi.NEW2_TIMER: { + FanAttributes.LABEL: FanAttributes.TIMER, + CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, + OPTIONS: PhilipsApi.TIMER_MAP, + }, + PhilipsApi.NEW2_TIMER2: { + FanAttributes.LABEL: FanAttributes.TIMER, + CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, + OPTIONS: PhilipsApi.TIMER2_MAP, + }, +} + +NUMBER_TYPES: dict[str, NumberDescription] = { + PhilipsApi.NEW2_OSCILLATION: { + FanAttributes.LABEL: FanAttributes.OSCILLATION, + ATTR_ICON: ICON.OSCILLATE, + CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, + FanAttributes.UNIT: "°", + FanAttributes.OFF: 0, + FanAttributes.MIN: 30, + FanAttributes.MAX: 350, + FanAttributes.STEP: 5, + }, + PhilipsApi.NEW2_TARGET_TEMP: { + FanAttributes.LABEL: FanAttributes.TARGET_TEMP, + ATTR_ICON: "mdi:thermometer", + CONF_ENTITY_CATEGORY: EntityCategory.CONFIG, + ATTR_DEVICE_CLASS: NumberDeviceClass.TEMPERATURE, + FanAttributes.UNIT: "°C", + FanAttributes.OFF: 1, + FanAttributes.MIN: 1, + FanAttributes.MAX: 37, + FanAttributes.STEP: 1, }, } diff --git a/custom_components/philips_airpurifier_coap/icons/pap/circulate.svg b/custom_components/philips_airpurifier_coap/icons/pap/circulate.svg new file mode 100644 index 0000000..e2cc318 --- /dev/null +++ b/custom_components/philips_airpurifier_coap/icons/pap/circulate.svg @@ -0,0 +1,4 @@ + + + + diff --git a/custom_components/philips_airpurifier_coap/icons/pap/clean.svg b/custom_components/philips_airpurifier_coap/icons/pap/clean.svg new file mode 100644 index 0000000..ade5e26 --- /dev/null +++ b/custom_components/philips_airpurifier_coap/icons/pap/clean.svg @@ -0,0 +1,4 @@ + + + + diff --git a/custom_components/philips_airpurifier_coap/icons/pap/gas.svg b/custom_components/philips_airpurifier_coap/icons/pap/gas.svg new file mode 100644 index 0000000..1aa23ec --- /dev/null +++ b/custom_components/philips_airpurifier_coap/icons/pap/gas.svg @@ -0,0 +1,4 @@ + + + + diff --git a/custom_components/philips_airpurifier_coap/icons/pap/heating.svg b/custom_components/philips_airpurifier_coap/icons/pap/heating.svg new file mode 100644 index 0000000..d161a9e --- /dev/null +++ b/custom_components/philips_airpurifier_coap/icons/pap/heating.svg @@ -0,0 +1,4 @@ + + + + diff --git a/custom_components/philips_airpurifier_coap/icons/pap/mode.svg b/custom_components/philips_airpurifier_coap/icons/pap/mode.svg new file mode 100644 index 0000000..7095a29 --- /dev/null +++ b/custom_components/philips_airpurifier_coap/icons/pap/mode.svg @@ -0,0 +1,4 @@ + + + + diff --git a/custom_components/philips_airpurifier_coap/icons/pap/oscillate.svg b/custom_components/philips_airpurifier_coap/icons/pap/oscillate.svg new file mode 100644 index 0000000..ca7f6d3 --- /dev/null +++ b/custom_components/philips_airpurifier_coap/icons/pap/oscillate.svg @@ -0,0 +1,4 @@ + + + + diff --git a/custom_components/philips_airpurifier_coap/icons/pap/pm25b.svg b/custom_components/philips_airpurifier_coap/icons/pap/pm25b.svg new file mode 100644 index 0000000..b7acf73 --- /dev/null +++ b/custom_components/philips_airpurifier_coap/icons/pap/pm25b.svg @@ -0,0 +1,4 @@ + + + + diff --git a/custom_components/philips_airpurifier_coap/icons/pap/rotate.svg b/custom_components/philips_airpurifier_coap/icons/pap/rotate.svg new file mode 100644 index 0000000..bf213ce --- /dev/null +++ b/custom_components/philips_airpurifier_coap/icons/pap/rotate.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/custom_components/philips_airpurifier_coap/manifest.json b/custom_components/philips_airpurifier_coap/manifest.json index bac990c..44fd951 100644 --- a/custom_components/philips_airpurifier_coap/manifest.json +++ b/custom_components/philips_airpurifier_coap/manifest.json @@ -1,23 +1,26 @@ { - "domain": "philips_airpurifier_coap", - "name": "Philips AirPurifier (with CoAP)", - "codeowners": ["@kongo09"], - "config_flow": true, - "dependencies": ["frontend", "http"], - "dhcp": [ - { - "macaddress": "B0F893*" - }, - { - "macaddress": "047863*" - }, - { - "hostname": "mxchip*" - } - ], - "documentation": "https://github.com/kongo09/philips-airpurifier-coap", - "iot_class": "local_push", - "issue_tracker": "https://github.com/kongo09/philips-airpurifier-coap/issues", - "requirements": ["aioairctrl==0.2.5"], - "version": "0.10.9" + "domain": "philips_airpurifier_coap", + "name": "Philips AirPurifier (with CoAP)", + "codeowners": ["@kongo09"], + "config_flow": true, + "dependencies": ["frontend", "http"], + "dhcp": [ + { + "macaddress": "B0F893*" + }, + { + "macaddress": "047863*" + }, + { + "macaddress": "849DC2*" + }, + { + "hostname": "mxchip*" + } + ], + "documentation": "https://github.com/kongo09/philips-airpurifier-coap", + "iot_class": "local_push", + "issue_tracker": "https://github.com/kongo09/philips-airpurifier-coap/issues", + "requirements": ["aioairctrl==0.2.5"], + "version": "0.10.9" } diff --git a/custom_components/philips_airpurifier_coap/model.py b/custom_components/philips_airpurifier_coap/model.py index 069cefc..b43a0f4 100644 --- a/custom_components/philips_airpurifier_coap/model.py +++ b/custom_components/philips_airpurifier_coap/model.py @@ -65,3 +65,16 @@ class SelectDescription(TypedDict): label: str entity_category: str options: dict[Any, tuple[str, str]] + + +class NumberDescription(TypedDict): + """Number class.""" + + icon: str + label: str + entity_category: str + unit: str + off: int + min: int + max: int + step: int diff --git a/custom_components/philips_airpurifier_coap/number.py b/custom_components/philips_airpurifier_coap/number.py new file mode 100644 index 0000000..2ee11a8 --- /dev/null +++ b/custom_components/philips_airpurifier_coap/number.py @@ -0,0 +1,128 @@ +"""Philips Air Purifier & Humidifier Numbers.""" +from __future__ import annotations + +from collections.abc import Callable +import logging +from typing import Any + +from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + CONF_ENTITY_CATEGORY, + CONF_HOST, + CONF_NAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.entity import Entity + +from .const import ( + CONF_MODEL, + DATA_KEY_COORDINATOR, + DOMAIN, + NUMBER_TYPES, + FanAttributes, + PhilipsApi, +) +from .philips import Coordinator, PhilipsEntity, model_to_class + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[list[Entity], bool], None], +) -> None: + """Set up the number platform.""" + _LOGGER.debug("async_setup_entry called for platform number") + + host = entry.data[CONF_HOST] + model = entry.data[CONF_MODEL] + name = entry.data[CONF_NAME] + + data = hass.data[DOMAIN][host] + + coordinator = data[DATA_KEY_COORDINATOR] + + model_class = model_to_class.get(model) + if model_class: + available_numbers = [] + + for cls in reversed(model_class.__mro__): + 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)) + + async_add_entities(numbers, update_before_add=False) + + else: + _LOGGER.error("Unsupported model: %s", model) + return + + +class PhilipsNumber(PhilipsEntity, NumberEntity): + """Define a Philips AirPurifier number.""" + + def __init__( # noqa: D107 + self, coordinator: Coordinator, name: str, model: str, number: str + ) -> None: + super().__init__(coordinator) + self._model = model + self._description = NUMBER_TYPES[number] + self._attr_device_class = self._description.get(ATTR_DEVICE_CLASS) + label = FanAttributes.LABEL + label = label.partition("#")[0] + self._attr_name = f"{name} {self._description[label].replace('_', ' ').title()}" + self._attr_entity_category = self._description.get(CONF_ENTITY_CATEGORY) + self._attr_icon = self._description.get(ATTR_ICON) + self._attr_mode = "slider" # hardwired for now + self._attr_native_unit_of_measurement = self._description.get( + FanAttributes.UNIT + ) + + self._attr_native_min_value = self._description.get(FanAttributes.OFF) + self._min = self._description.get(FanAttributes.MIN) + self._attr_native_max_value = self._description.get(FanAttributes.MAX) + self._attr_native_step = self._description.get(FanAttributes.STEP) + + 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 + self._attrs: dict[str, Any] = {} + self.kind = number + + @property + def native_value(self) -> float | None: + """Return the current number.""" + value = self._device_status.get(self.kind) + return value + + async def async_set_native_value(self, value: float) -> None: + """Select a number.""" + + _LOGGER.debug("async_set_native_value called with: %s", value) + + # Catch the boundaries + if value is None or value < self._attr_native_min_value: + 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 + + _LOGGER.debug("setting number with: %s", value) + + await self.coordinator.client.set_control_value(self.kind, int(value)) diff --git a/custom_components/philips_airpurifier_coap/philips.py b/custom_components/philips_airpurifier_coap/philips.py index 842c353..ae6f9fc 100644 --- a/custom_components/philips_airpurifier_coap/philips.py +++ b/custom_components/philips_airpurifier_coap/philips.py @@ -23,6 +23,7 @@ from .const import ( DOMAIN, ICON, + SWITCH_OFF, SWITCH_ON, FanAttributes, FanModel, @@ -183,7 +184,10 @@ def __init__(self, coordinator: Coordinator) -> None: # noqa: D107 self._name = list( filter( None, - map(coordinator.status.get, [PhilipsApi.NAME, PhilipsApi.NEW_NAME]), + map( + coordinator.status.get, + [PhilipsApi.NAME, PhilipsApi.NEW_NAME, PhilipsApi.NEW2_NAME], + ), ) )[0] # self._modelName = coordinator.status["modelid"] @@ -192,7 +196,11 @@ def __init__(self, coordinator: Coordinator) -> None: # noqa: D107 None, map( coordinator.status.get, - [PhilipsApi.MODEL_ID, PhilipsApi.NEW_MODEL_ID], + [ + PhilipsApi.MODEL_ID, + PhilipsApi.NEW_MODEL_ID, + PhilipsApi.NEW2_MODEL_ID, + ], ), ) )[0] @@ -272,15 +280,20 @@ class PhilipsGenericCoAPFanBase(PhilipsGenericFan): """Class as basis to manage a generic Philips CoAP fan.""" AVAILABLE_PRESET_MODES = {} + REPLACE_PRESET = None AVAILABLE_SPEEDS = {} + REPLACE_SPEED = None AVAILABLE_ATTRIBUTES = [] AVAILABLE_SWITCHES = [] AVAILABLE_LIGHTS = [] + AVAILABLE_NUMBERS = [] KEY_PHILIPS_POWER = PhilipsApi.POWER STATE_POWER_ON = "1" STATE_POWER_OFF = "0" + KEY_OSCILLATION = None + def __init__( # noqa: D107 self, coordinator: Coordinator, @@ -366,6 +379,8 @@ def supported_features(self) -> int: features = FanEntityFeature.PRESET_MODE if self._speeds: features |= FanEntityFeature.SET_SPEED + if self.KEY_OSCILLATION is not None: + features |= FanEntityFeature.OSCILLATE return features @property @@ -378,7 +393,11 @@ def preset_mode(self) -> Optional[str]: """Return the selected preset mode.""" for preset_mode, status_pattern in self._available_preset_modes.items(): for k, v in status_pattern.items(): - if self._device_status.get(k) != v: + # check if the speed sensor also used for presets is different from the setting field + if self.REPLACE_PRESET is not None and k == self.REPLACE_PRESET[0]: + k = self.REPLACE_PRESET[1] + status = self._device_status.get(k) + if status != v: break else: return preset_mode @@ -394,16 +413,46 @@ def speed_count(self) -> int: """Return the number of speed options.""" return len(self._speeds) + @property + def oscillating(self) -> bool | None: + """Return if the fan is oscillating.""" + if self.KEY_OSCILLATION is None: + return None + + key = next(iter(self.KEY_OSCILLATION)) + status = self._device_status.get(key) + on = self.KEY_OSCILLATION.get(key).get(SWITCH_ON) + return status is not None and status == on + + async def async_oscillate(self, oscillating: bool) -> None: + """Osciallate the fan.""" + if self.KEY_OSCILLATION is None: + return None + + key = next(iter(self.KEY_OSCILLATION)) + values = self.KEY_OSCILLATION.get(key) + on = values.get(SWITCH_ON) + off = values.get(SWITCH_OFF) + if oscillating: + await self.coordinator.client.set_control_value(key, on) + else: + await self.coordinator.client.set_control_value(key, off) + @property def percentage(self) -> Optional[int]: """Return the speed percentages.""" for speed, status_pattern in self._available_speeds.items(): for k, v in status_pattern.items(): + # check if the speed sensor is different from the speed setting field + if self.REPLACE_SPEED is not None and k == self.REPLACE_SPEED[0]: + k = self.REPLACE_SPEED[1] if self._device_status.get(k) != v: break else: return ordered_list_item_to_percentage(self._speeds, speed) + return None + async def async_set_percentage(self, percentage: int) -> None: """Return the selected speed percentage.""" if percentage == 0: @@ -537,6 +586,45 @@ class PhilipsNewGenericCoAPFan(PhilipsGenericCoAPFanBase): STATE_POWER_OFF = "OFF" +class PhilipsNew2GenericCoAPFan(PhilipsGenericCoAPFanBase): + """Class to manage another new generic CoAP fan.""" + + AVAILABLE_PRESET_MODES = {} + AVAILABLE_SPEEDS = {} + + AVAILABLE_ATTRIBUTES = [ + # device information + (FanAttributes.NAME, PhilipsApi.NEW2_NAME), + (FanAttributes.MODEL_ID, PhilipsApi.NEW2_MODEL_ID), + (FanAttributes.PRODUCT_ID, PhilipsApi.PRODUCT_ID), + (FanAttributes.DEVICE_ID, PhilipsApi.DEVICE_ID), + (FanAttributes.SOFTWARE_VERSION, PhilipsApi.NEW2_SOFTWARE_VERSION), + (FanAttributes.WIFI_VERSION, PhilipsApi.WIFI_VERSION), + # (FanAttributes.ERROR_CODE, PhilipsApi.ERROR_CODE), + # (FanAttributes.ERROR, PhilipsApi.ERROR_CODE, PhilipsApi.ERROR_CODE_MAP), + # device configuration + ( + FanAttributes.PREFERRED_INDEX, + PhilipsApi.NEW2_GAS_PREFERRED_INDEX, + PhilipsApi.GAS_PREFERRED_INDEX_MAP, + ), + # device sensors + ( + FanAttributes.RUNTIME, + PhilipsApi.RUNTIME, + lambda x, _: str(timedelta(seconds=round(x / 1000))), + ), + ] + + AVAILABLE_LIGHTS = [] + AVAILABLE_SWITCHES = [] + AVAILABLE_SELECTS = [] + + KEY_PHILIPS_POWER = PhilipsApi.NEW2_POWER + STATE_POWER_ON = 1 + STATE_POWER_OFF = 0 + + class PhilipsHumidifierMixin(PhilipsGenericCoAPFanBase): """Mixin for humidifiers.""" @@ -1310,6 +1398,10 @@ class PhilipsAC385851(PhilipsAC385x51): """AC3858/51.""" +class PhilipsAC385886(PhilipsAC385x51): + """AC3858/86.""" + + class PhilipsAC4236(PhilipsGenericCoAPFan): """AC4236.""" @@ -1468,6 +1560,168 @@ class PhilipsAC5659(PhilipsGenericCoAPFan): AVAILABLE_SELECTS = [PhilipsApi.PREFERRED_INDEX] +class PhilipsAMFxxx(PhilipsNew2GenericCoAPFan): + """AMF family.""" + + # REPLACE_PRESET = [PhilipsApi.NEW2_MODE_B, PhilipsApi.NEW2_FAN_SPEED] + AVAILABLE_PRESET_MODES = { + # PresetMode.AUTO_PLUS: { + # PhilipsApi.NEW2_POWER: 1, + # PhilipsApi.NEW2_MODE_B: 0, + # PhilipsApi.NEW2_AUTO_PLUS_AI: 1, + # # PhilipsApi.NEW2_MODE_C: 3, + # }, + PresetMode.AUTO: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_B: 0, + # PhilipsApi.NEW2_AUTO_PLUS_AI: 0, + # PhilipsApi.NEW2_MODE_C: 3, + }, + PresetMode.SLEEP: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_B: 17, + # PhilipsApi.NEW2_MODE_C: 1, + }, + PresetMode.TURBO: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_B: 18, + # PhilipsApi.NEW2_MODE_C: 18, + }, + } + # REPLACE_SPEED = [PhilipsApi.NEW2_MODE_B, PhilipsApi.NEW2_FAN_SPEED] + AVAILABLE_SPEEDS = { + PresetMode.SPEED_1: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_B: 1, + # PhilipsApi.NEW2_MODE_C: 1, + }, + PresetMode.SPEED_2: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_B: 2, + # PhilipsApi.NEW2_MODE_C: 2, + }, + PresetMode.SPEED_3: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_B: 3, + # PhilipsApi.NEW2_MODE_C: 3, + }, + PresetMode.SPEED_4: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_B: 4, + # PhilipsApi.NEW2_MODE_C: 4, + }, + PresetMode.SPEED_5: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_B: 5, + # PhilipsApi.NEW2_MODE_C: 5, + }, + PresetMode.SPEED_6: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_B: 6, + # PhilipsApi.NEW2_MODE_C: 6, + }, + PresetMode.SPEED_7: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_B: 7, + # PhilipsApi.NEW2_MODE_C: 7, + }, + PresetMode.SPEED_8: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_B: 8, + # PhilipsApi.NEW2_MODE_C: 8, + }, + PresetMode.SPEED_9: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_B: 9, + # PhilipsApi.NEW2_MODE_C: 9, + }, + PresetMode.SPEED_10: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_B: 10, + # PhilipsApi.NEW2_MODE_C: 10, + }, + # PresetMode.TURBO: { + # PhilipsApi.NEW2_POWER: 1, + # PhilipsApi.NEW2_MODE_B: 18, + # }, + } + + AVAILABLE_LIGHTS = [PhilipsApi.NEW2_DISPLAY_BACKLIGHT] + AVAILABLE_SWITCHES = [ + PhilipsApi.NEW2_CHILD_LOCK, + PhilipsApi.NEW2_BEEP, + PhilipsApi.NEW2_STANDBY_SENSORS, + PhilipsApi.NEW2_AUTO_PLUS_AI, + ] + AVAILABLE_SELECTS = [PhilipsApi.NEW2_TIMER] + AVAILABLE_NUMBERS = [PhilipsApi.NEW2_OSCILLATION] + + +class PhilipsAMF765(PhilipsAMFxxx): + """AMF765.""" + + AVAILABLE_SELECTS = [PhilipsApi.NEW2_CIRCULATION] + UNAVAILABLE_SENSORS = [PhilipsApi.NEW2_GAS] + + +class PhilipsAMF870(PhilipsAMFxxx): + """AMF870.""" + + AVAILABLE_SELECTS = [ + PhilipsApi.NEW2_GAS_PREFERRED_INDEX, + PhilipsApi.NEW2_HEATING, + ] + AVAILABLE_NUMBERS = [PhilipsApi.NEW2_TARGET_TEMP] + + +class PhilipsCX5120(PhilipsNew2GenericCoAPFan): + """CX5120.""" + + AVAILABLE_PRESET_MODES = { + PresetMode.AUTO: { + PhilipsApi.NEW2_POWER: 1, + PhilipsApi.NEW2_MODE_A: 3, + PhilipsApi.NEW2_MODE_B: 0, + }, + PresetMode.HIGH: { + PhilipsApi.POWER: 1, + PhilipsApi.NEW2_MODE_A: 3, + PhilipsApi.NEW2_MODE_B: 65, + }, + PresetMode.LOW: { + PhilipsApi.POWER: 1, + PhilipsApi.NEW2_MODE_A: 3, + PhilipsApi.NEW2_MODE_B: 66, + }, + PresetMode.VENTILATION: { + PhilipsApi.POWER: 1, + PhilipsApi.NEW2_MODE_A: 1, + PhilipsApi.NEW2_MODE_B: -127, + }, + } + AVAILABLE_SPEEDS = { + PresetMode.HIGH: { + PhilipsApi.POWER: 1, + PhilipsApi.NEW2_MODE_A: 3, + PhilipsApi.NEW2_MODE_B: 65, + }, + PresetMode.LOW: { + PhilipsApi.POWER: 1, + PhilipsApi.NEW2_MODE_A: 3, + PhilipsApi.NEW2_MODE_B: 66, + }, + } + KEY_OSCILLATION = { + PhilipsApi.NEW2_OSCILLATION: PhilipsApi.OSCILLATION_MAP, + } + + AVAILABLE_LIGHTS = [PhilipsApi.NEW2_DISPLAY_BACKLIGHT2] + AVAILABLE_SWITCHES = [PhilipsApi.NEW2_BEEP] + UNAVAILABLE_SENSORS = [PhilipsApi.NEW2_FAN_SPEED, PhilipsApi.NEW2_GAS] + AVAILABLE_SELECTS = [PhilipsApi.NEW2_TIMER2] + AVAILABLE_NUMBERS = [PhilipsApi.NEW2_TARGET_TEMP] + + model_to_class = { FanModel.AC0850: PhilipsAC0850, FanModel.AC1214: PhilipsAC1214, @@ -1490,8 +1744,12 @@ class PhilipsAC5659(PhilipsGenericCoAPFan): FanModel.AC3854_51: PhilipsAC385451, FanModel.AC3858_50: PhilipsAC385850, FanModel.AC3858_51: PhilipsAC385851, + FanModel.AC3858_86: PhilipsAC385886, FanModel.AC4236: PhilipsAC4236, FanModel.AC4550: PhilipsAC4550, FanModel.AC4558: PhilipsAC4558, FanModel.AC5659: PhilipsAC5659, + FanModel.AMF765: PhilipsAMF765, + FanModel.AMF870: PhilipsAMF870, + FanModel.CX5120: PhilipsCX5120, } diff --git a/custom_components/philips_airpurifier_coap/sensor.py b/custom_components/philips_airpurifier_coap/sensor.py index 5709a42..ca05c88 100644 --- a/custom_components/philips_airpurifier_coap/sensor.py +++ b/custom_components/philips_airpurifier_coap/sensor.py @@ -51,19 +51,22 @@ async def async_setup_entry( # noqa: D103 coordinator = data[DATA_KEY_COORDINATOR] status = coordinator.status - sensors = [] - - for sensor in SENSOR_TYPES: - if sensor in status: - sensors.append(PhilipsSensor(coordinator, name, model, sensor)) - model_class = model_to_class.get(model) unavailable_filters = [] + unavailable_sensors = [] if model_class: for cls in reversed(model_class.__mro__): cls_unavailable_filters = getattr(cls, "UNAVAILABLE_FILTERS", []) unavailable_filters.extend(cls_unavailable_filters) + cls_unavailable_sensors = getattr(cls, "UNAVAILABLE_SENSORS", []) + unavailable_sensors.extend(cls_unavailable_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 _filter in FILTER_TYPES: if _filter in status and _filter not in unavailable_filters: