Skip to content

Commit

Permalink
Merge pull request #226 from kongo09/185-expose-cx5120-as-a-climate-d…
Browse files Browse the repository at this point in the history
…evice

185 expose cx5120 as a climate device
  • Loading branch information
kongo09 authored Dec 9, 2024
2 parents 53bd30f + 80ce71d commit 8e68e74
Show file tree
Hide file tree
Showing 13 changed files with 457 additions and 22 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,5 +253,8 @@ The integration also provides the original Philips icons for your use in the fro
| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/heating.svg) | heating |
| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/gas.svg) | gas |
| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/circle.svg) | circle |
| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/temp_high.svg) | temp_high |
| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/temp_medium.svg) | temp_medium |
| ![Preview](./custom_components/philips_airpurifier_coap/icons/pap/temp_low.svg) | temp_low |

Note: you might have to clear your browser cache after installation to see the icons.
266 changes: 266 additions & 0 deletions custom_components/philips_airpurifier_coap/climate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
"""Philips Air Purifier & Humidifier Heater."""

from __future__ import annotations

from collections.abc import Callable
import logging
from typing import Any

from homeassistant.components.climate import (
SWING_OFF,
SWING_ON,
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity

from .config_entry_data import ConfigEntryData
from .const import (
DOMAIN,
HEATER_TYPES,
SWITCH_OFF,
SWITCH_ON,
FanAttributes,
PresetMode,
)
from .philips import PhilipsGenericControlBase, 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 climate platform."""

config_entry_data: ConfigEntryData = hass.data[DOMAIN][entry.entry_id]

model = config_entry_data.device_information.model

model_class = model_to_class.get(model)
if model_class:
available_heaters = []
available_preset_modes = {}
available_oscillation = {}

for cls in reversed(model_class.__mro__):
# Get the available heaters from the base classes
cls_available_heaters = getattr(cls, "AVAILABLE_HEATERS", [])
available_heaters.extend(cls_available_heaters)

# Get the available preset modes from the base classes
cls_available_preset_modes = getattr(cls, "AVAILABLE_PRESET_MODES", [])
available_preset_modes.update(cls_available_preset_modes)

# Get the available oscillation from the base classes
cls_available_oscillation = getattr(cls, "KEY_OSCILLATION", {})
_LOGGER.debug("Available oscillation: %s", cls_available_oscillation)
if cls_available_oscillation:
available_oscillation.update(cls_available_oscillation)

heaters = [
PhilipsHeater(
hass,
entry,
config_entry_data,
heater,
available_preset_modes,
available_oscillation,
)
for heater in HEATER_TYPES
if heater in available_heaters
]
async_add_entities(heaters, update_before_add=False)

else:
_LOGGER.error("Unsupported model: %s", model)
return


class PhilipsHeater(PhilipsGenericControlBase, ClimateEntity):
"""Define a Philips AirPurifier heater."""

_attr_is_on: bool | None = False
_attr_temperature_unit: str = UnitOfTemperature.CELSIUS
_attr_hvac_modes: list[HVACMode] = [
HVACMode.OFF,
HVACMode.HEAT,
HVACMode.AUTO,
HVACMode.FAN_ONLY,
]
_attr_target_temperature_step: float = 1.0

def __init__(
self,
hass: HomeAssistant,
config: ConfigEntry,
config_entry_data: ConfigEntryData,
heater: str,
available_preset_modes: list[str],
available_oscillation: dict[str, dict[str, Any]],
) -> None:
"""Initialize the select."""

super().__init__(hass, config, config_entry_data)

self._model = config_entry_data.device_information.model
latest_status = config_entry_data.latest_status

self._description = HEATER_TYPES[heater]

device_id = config_entry_data.device_information.device_id
self._attr_unique_id = f"{self._model}-{device_id}-{heater.lower()}"

# preset modes in the climate entity are used for HVAC, so we use fan modes
self._preset_modes = available_preset_modes
self._attr_preset_modes = list(self._preset_modes.keys())

self._power_key = self._description[FanAttributes.POWER]
self._temperature_target_key = heater.partition("#")[0]

self._attr_min_temp = self._description[FanAttributes.MIN_TEMPERATURE]
self._attr_max_temp = self._description[FanAttributes.MAX_TEMPERATURE]
self._attr_target_temperature = latest_status.get(self._temperature_target_key)
self._attr_current_temperature = latest_status.get(
self._description[FanAttributes.TEMPERATURE]
)

self._attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)

# some devices can oscillate
if available_oscillation:
self._oscillation_key = list(available_oscillation.keys())[0]
self._oscillation_modes = available_oscillation[self._oscillation_key]
self._attr_supported_features |= ClimateEntityFeature.SWING_MODE
self._attr_swing_modes = [SWING_ON, SWING_OFF]

@property
def target_temperature(self) -> int | None:
"""Return the target temperature."""
return self._device_status.get(self._temperature_target_key)

@property
def hvac_mode(self) -> HVACMode | None:
"""Return the current HVAC mode."""
if not self.is_on:
return HVACMode.OFF
if self.preset_mode == PresetMode.AUTO:
return HVACMode.AUTO
if self.preset_mode == PresetMode.VENTILATION:
return HVACMode.FAN_ONLY
return HVACMode.HEAT

async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode of the heater."""
if hvac_mode == HVACMode.OFF:
await self.async_turn_off()
elif hvac_mode == HVACMode.AUTO:
await self.async_set_preset_mode(PresetMode.AUTO)
elif hvac_mode == HVACMode.FAN_ONLY:
await self.async_set_preset_mode(PresetMode.VENTILATION)
elif hvac_mode == HVACMode.HEAT:
await self.async_set_preset_mode(PresetMode.LOW)

@property
def preset_mode(self) -> str | None:
"""Return the current fan mode."""

for fan_mode, status_pattern in self._preset_modes.items():
for k, v in status_pattern.items():
status = self._device_status.get(k)
if status != v:
break
else:
return fan_mode

# no mode found
return None

async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the fan mode of the heater."""
if preset_mode not in self._attr_preset_modes:
return

status_pattern = self._preset_modes.get(preset_mode)
await self.coordinator.client.set_control_values(data=status_pattern)
self._device_status.update(status_pattern)
self._handle_coordinator_update()

@property
def swing_mode(self) -> str | None:
"""Return the current swing mode."""
if not self._oscillation_key:
return None

value = self._device_status.get(self._oscillation_key)
if value == self._oscillation_modes[SWITCH_OFF]:
return SWING_OFF

return SWING_ON

async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set the swing mode of the heater."""
if swing_mode not in self._attr_swing_modes:
return

if swing_mode == SWING_ON:
value = self._oscillation_modes[SWITCH_ON]
else:
value = self._oscillation_modes[SWITCH_OFF]

await self.coordinator.client.set_control_value(self._oscillation_key, value)
self._device_status[self._oscillation_key] = value
self._handle_coordinator_update()

@property
def is_on(self) -> bool | None:
"""Return the device state."""
if (
self._device_status.get(self._power_key)
== self._description[FanAttributes.OFF]
):
return False

return True

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the device."""
await self.coordinator.client.set_control_values(
data={
self._power_key: self._description[FanAttributes.ON],
}
)
self._device_status[self._power_key] = self._description[FanAttributes.ON]
self._handle_coordinator_update()

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the device."""
await self.coordinator.client.set_control_values(
data={
self._power_key: self._description[FanAttributes.OFF],
}
)
self._device_status[self._power_key] = self._description[FanAttributes.OFF]
self._handle_coordinator_update()

async def async_set_temperature(self, **kwargs) -> None:
"""Select target temperature."""
temperature = int(kwargs.get(ATTR_TEMPERATURE))

target = max(self._attr_min_temp, min(temperature, self._attr_max_temp))
await self.coordinator.client.set_control_value(
self._temperature_target_key, target
)
self._device_status[self._temperature_target_key] = temperature
self._handle_coordinator_update()
22 changes: 19 additions & 3 deletions custom_components/philips_airpurifier_coap/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

from .model import (
FilterDescription,
HeaterDescription,
HumidifierDescription,
LightDescription,
NumberDescription,
Expand Down Expand Up @@ -304,7 +305,10 @@ class FanAttributes(StrEnum):
MAX = "max"
STEP = "step"
TIMER = "timer"
TEMPERATURE = "temperature"
TARGET_TEMP = "target_temperature"
MIN_TEMPERATURE = "min_temperature"
MAX_TEMPERATURE = "max_temperature"
STANDBY_SENSORS = "standby_sensors"
AUTO_PLUS = "auto_plus"
WATER_TANK = "water_tank"
Expand Down Expand Up @@ -384,11 +388,11 @@ class PhilipsApi:
}

OSCILLATION_MAP = {
SWITCH_ON: "17920",
SWITCH_OFF: "0",
SWITCH_ON: 17920,
SWITCH_OFF: 0,
}
OSCILLATION_MAP2 = {
SWITCH_ON: [17242, 23040],
SWITCH_ON: 17242,
SWITCH_OFF: 0,
}
OSCILLATION_MAP3 = {
Expand Down Expand Up @@ -1016,3 +1020,15 @@ class PhilipsApi:
FanAttributes.STEP: 5,
},
}

HEATER_TYPES: dict[str, HeaterDescription] = {
PhilipsApi.NEW2_TARGET_TEMP: {
FanAttributes.TEMPERATURE: PhilipsApi.TEMPERATURE,
FanAttributes.POWER: PhilipsApi.NEW2_POWER,
FanAttributes.ON: 1,
FanAttributes.OFF: 0,
FanAttributes.MIN_TEMPERATURE: 1,
FanAttributes.MAX_TEMPERATURE: 37,
FanAttributes.STEP: 1,
},
}
30 changes: 30 additions & 0 deletions custom_components/philips_airpurifier_coap/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
},
"humidifier": {
"pap": {
"state": {
"off": "pap:power_button"
},
"state_attributes": {
"mode": {
"state": {
Expand Down Expand Up @@ -74,6 +77,33 @@
}
}
},
"climate": {
"pap": {
"state": {
"off": "pap:power_button",
"auto": "pap:auto_mode",
"fan_only": "pap:circulate",
"heat": "pap:heating"
},
"state_attributes": {
"preset_mode": {
"state": {
"auto": "pap:auto_mode",
"high": "pap:temp_high",
"medium": "pap:temp_medium",
"low": "pap:temp_low",
"ventilation": "pap:circulate"
}
},
"swing_mode": {
"state": {
"off": "pap:rotate",
"on": "pap:rotate"
}
}
}
}
},
"binary_sensor": {
"water_tank": {
"state": {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 8e68e74

Please sign in to comment.