diff --git a/custom_components/meross_cloud/common.py b/custom_components/meross_cloud/common.py index 08bfcae012..c610a262df 100644 --- a/custom_components/meross_cloud/common.py +++ b/custom_components/meross_cloud/common.py @@ -31,7 +31,8 @@ HA_COVER = "cover" HA_CLIMATE = "climate" HA_FAN = "fan" -MEROSS_PLATFORMS = (HA_SWITCH, HA_LIGHT, HA_COVER, HA_FAN, HA_SENSOR, HA_CLIMATE) +HA_HUMIDIFIER = "humidifier" +MEROSS_PLATFORMS = (HA_SWITCH, HA_LIGHT, HA_COVER, HA_SENSOR, HA_CLIMATE, HA_HUMIDIFIER) CONNECTION_TIMEOUT_THRESHOLD = 5 CONF_STORED_CREDS = "stored_credentials" diff --git a/custom_components/meross_cloud/fan.py b/custom_components/meross_cloud/fan.py deleted file mode 100644 index 985422b2b1..0000000000 --- a/custom_components/meross_cloud/fan.py +++ /dev/null @@ -1,136 +0,0 @@ -import logging -from typing import Any, Optional, List, Dict - -from meross_iot.controller.device import BaseDevice -from meross_iot.controller.mixins.spray import SprayMixin -from meross_iot.manager import MerossManager -from meross_iot.model.enums import SprayMode -from meross_iot.model.http.device import HttpDeviceInfo - -from homeassistant.components.fan import FanEntity, FanEntityFeature -from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import MerossDevice -from .common import (DOMAIN, MANAGER, HA_FAN, DEVICE_LIST_COORDINATOR) - -_LOGGER = logging.getLogger(__name__) - - -class MerossHumidifierDevice(SprayMixin, BaseDevice): - """ - Type hints helper - """ - pass - - -class HumidifierEntityWrapper(MerossDevice, FanEntity): - """Wrapper class to adapt the Meross humidifier into the Homeassistant platform""" - - _device: MerossHumidifierDevice - - def __init__(self, - channel: int, - device: MerossHumidifierDevice, - device_list_coordinator: DataUpdateCoordinator[Dict[str, HttpDeviceInfo]]): - super().__init__( - device=device, - channel=channel, - device_list_coordinator=device_list_coordinator, - platform=HA_FAN) - - async def async_turn_off(self, **kwargs) -> None: - await self._device.async_set_mode(mode=SprayMode.OFF, channel=self._channel_id, skip_rate_limits=True) - - async def async_turn_on(self, speed: Optional[str] = None, **kwargs: Any) -> None: - if speed is None: - mode = SprayMode.CONTINUOUS - else: - mode = SprayMode[speed] - await self._device.async_set_mode(mode=mode, channel=self._channel_id, skip_rate_limits=True) - - async def async_set_speed(self, speed: str) -> None: - mode = SprayMode[speed] - await self._device.async_set_mode(mode=mode, channel=self._channel_id, skip_rate_limits=True) - - def set_direction(self, direction: str) -> None: - # Not supported - pass - - def set_speed(self, speed: str) -> None: - # Not implemented: use async method instead... - pass - - def turn_on(self, speed: Optional[str] = None, **kwargs) -> None: - # Not implemented: use async method instead... - pass - - def turn_off(self, **kwargs: Any) -> None: - # Not implemented: use async method instead... - pass - - @property - def supported_features(self) -> int: - return FanEntityFeature.PRESET_MODE - - @property - def is_on(self) -> Optional[bool]: - mode = self._device.get_current_mode(channel=self._channel_id) - if mode is None: - return None - return mode != SprayMode.OFF - - @property - def percentage(self) -> Optional[int]: - mode = self._device.get_current_mode(channel=self._channel_id) - if mode == SprayMode.OFF: - return 0 - elif mode == SprayMode.INTERMITTENT: - return 50 - elif mode == SprayMode.CONTINUOUS: - return 100 - else: - raise ValueError("Invalid SprayMode value.") - - @property - def preset_mode(self) -> Optional[str]: - mode = self._device.get_current_mode(channel=self._channel_id) - if mode is not None: - return mode.name - else: - return None - - @property - def preset_modes(self) -> List[str]: - return [x.name for x in SprayMode] - - -async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_entities): - def entity_adder_callback(): - """Discover and adds new Meross entities""" - manager: MerossManager = hass.data[DOMAIN][MANAGER] # type - coordinator = hass.data[DOMAIN][DEVICE_LIST_COORDINATOR] - devices = manager.find_devices(device_class=SprayMixin) - - new_entities = [] - - for d in devices: - channels = [c.index for c in d.channels] if len(d.channels) > 0 else [0] - for channel_index in channels: - w = HumidifierEntityWrapper(device=d, channel=channel_index, device_list_coordinator=coordinator) - if w.unique_id not in hass.data[DOMAIN]["ADDED_ENTITIES_IDS"]: - new_entities.append(w) - - async_add_entities(new_entities, True) - - coordinator = hass.data[DOMAIN][DEVICE_LIST_COORDINATOR] - coordinator.async_add_listener(entity_adder_callback) - # Run the entity adder a first time during setup - entity_adder_callback() - -# TODO: Implement entry unload -# TODO: Unload entry -# TODO: Remove entry - - -def setup_platform(hass, config, async_add_entities, discovery_info=None): - pass diff --git a/custom_components/meross_cloud/humidifier.py b/custom_components/meross_cloud/humidifier.py new file mode 100644 index 0000000000..17337c642d --- /dev/null +++ b/custom_components/meross_cloud/humidifier.py @@ -0,0 +1,184 @@ +import logging +from typing import Any, Optional, List, Dict + +from meross_iot.controller.device import BaseDevice +from meross_iot.controller.mixins.spray import SprayMixin +from meross_iot.controller.mixins.diffuser_spray import DiffuserSprayMixin +from meross_iot.manager import MerossManager +from meross_iot.model.enums import SprayMode, DiffuserSprayMode +from meross_iot.model.http.device import HttpDeviceInfo + +from homeassistant.components.humidifier import HumidifierEntity, HumidifierEntityFeature, HumidifierDeviceClass +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from . import MerossDevice +from .common import (DOMAIN, MANAGER, HA_HUMIDIFIER, DEVICE_LIST_COORDINATOR) + +_LOGGER = logging.getLogger(__name__) + + +SPRAY_MODE_FROM_HA = { + "CONTINUOUS": SprayMode.CONTINUOUS, + "INTERMITTENT": SprayMode.INTERMITTENT +} + +SPRAY_MODE_TO_HA = { + SprayMode.CONTINUOUS: "CONTINUOUS", + SprayMode.INTERMITTENT: "INTERMITTENT" +} + +OILSPRAY_MODE_FROM_HA = { + "HEAVY SPRAY": DiffuserSprayMode.STRONG, + "LIGHT SPRAY": DiffuserSprayMode.LIGHT, +} + +OILSPRAY_MODE_TO_HA = { + DiffuserSprayMode.STRONG: "HEAVY SPRAY", + DiffuserSprayMode.LIGHT: "LIGHT SPRAY" +} + + +class MerossHumidifierDevice(SprayMixin, BaseDevice): + """ + Type hints helper for humidifier + """ + pass + + +class MerossOilDiffuserDevice(DiffuserSprayMixin, BaseDevice): + """ + Type hints helper for oil diffuser + """ + pass + + +class HumidifierEntityWrapper(MerossDevice, HumidifierEntity): + """Wrapper class to adapt the Meross humidifier into the Homeassistant platform""" + + _device: MerossHumidifierDevice + _attr_device_class = HumidifierDeviceClass.HUMIDIFIER + _attr_supported_features: HumidifierEntityFeature = HumidifierEntityFeature.MODES + _attr_available_modes = ["CONTINUOUS", "INTERMITTENT"] + + def __init__(self, + channel: int, + device: MerossHumidifierDevice, + device_list_coordinator: DataUpdateCoordinator[Dict[str, HttpDeviceInfo]]): + super().__init__( + device=device, + channel=channel, + device_list_coordinator=device_list_coordinator, + platform=HA_HUMIDIFIER) + + async def async_turn_off(self, **kwargs) -> None: + await self._device.async_set_mode(mode=SprayMode.OFF, channel=self._channel_id, skip_rate_limits=True) + + async def async_turn_on(self, **kwargs: Any) -> None: + mode = self.mode + if mode is None: + mode = SprayMode.CONTINUOUS + await self._device.async_set_mode(mode=mode, channel=self._channel_id, skip_rate_limits=True) + + async def async_set_mode(self, mode: str) -> None: + parsed_mode = SPRAY_MODE_FROM_HA[mode] + await self._device.async_set_mode(mode=parsed_mode, channel=self._channel_id, skip_rate_limits=True) + + @property + def mode(self) -> str | None: + """Return the current mode + + Requires HumidifierEntityFeature.MODES. + """ + return SPRAY_MODE_TO_HA.get(self._device.get_current_mode()) + + @property + def is_on(self) -> Optional[bool]: + mode = self._device.get_current_mode(channel=self._channel_id) + if mode is None: + return None + return mode != SprayMode.OFF + + +class OilDiffuserEntityWrapper(MerossDevice, HumidifierEntity): + """Wrapper class to adapt the Meross OilDiffuser into the Homeassistant platform""" + + _device: MerossOilDiffuserDevice + _attr_device_class = HumidifierDeviceClass.HUMIDIFIER + _attr_supported_features: HumidifierEntityFeature = HumidifierEntityFeature.MODES + _attr_available_modes = ["HEAVY SPRAY", "LIGHT SPRAY"] + + def __init__(self, + channel: int, + device: MerossOilDiffuserDevice, + device_list_coordinator: DataUpdateCoordinator[Dict[str, HttpDeviceInfo]]): + super().__init__( + device=device, + channel=channel, + device_list_coordinator=device_list_coordinator, + platform=HA_HUMIDIFIER) + + async def async_turn_off(self, **kwargs) -> None: + await self._device.async_set_spray_mode(mode=DiffuserSprayMode.OFF, channel=self._channel_id, skip_rate_limits=True) + + async def async_turn_on(self, **kwargs: Any) -> None: + await self._device.async_set_spray_mode(mode=DiffuserSprayMode.LIGHT, channel=self._channel_id, skip_rate_limits=True) + + async def async_set_mode(self, mode: str) -> None: + parsed_mode = OILSPRAY_MODE_FROM_HA[mode] + await self._device.async_set_spray_mode(mode=parsed_mode, channel=self._channel_id, skip_rate_limits=True) + + @property + def mode(self) -> str | None: + """Return the current mode + + Requires HumidifierEntityFeature.MODES. + """ + return OILSPRAY_MODE_TO_HA.get(self._device.get_current_spray_mode()) + + @property + def is_on(self) -> Optional[bool]: + mode = self._device.get_current_spray_mode(channel=self._channel_id) + if mode is None: + return None + return mode != DiffuserSprayMode.OFF + + +async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_entities): + def entity_adder_callback(): + """Discover and adds new Meross entities""" + manager: MerossManager = hass.data[DOMAIN][MANAGER] # type + coordinator = hass.data[DOMAIN][DEVICE_LIST_COORDINATOR] + new_entities = [] + + # Add Humidifiers + devices = manager.find_devices(device_class=SprayMixin) + for d in devices: + channels = [c.index for c in d.channels] if len(d.channels) > 0 else [0] + for channel_index in channels: + w = HumidifierEntityWrapper(device=d, channel=channel_index, device_list_coordinator=coordinator) + if w.unique_id not in hass.data[DOMAIN]["ADDED_ENTITIES_IDS"]: + new_entities.append(w) + + # Add OilDiffuser + devices = manager.find_devices(device_class=DiffuserSprayMixin) + for d in devices: + channels = [c.index for c in d.channels] if len(d.channels) > 0 else [0] + for channel_index in channels: + w = OilDiffuserEntityWrapper(device=d, channel=channel_index, device_list_coordinator=coordinator) + if w.unique_id not in hass.data[DOMAIN]["ADDED_ENTITIES_IDS"]: + new_entities.append(w) + + async_add_entities(new_entities, True) + + coordinator = hass.data[DOMAIN][DEVICE_LIST_COORDINATOR] + coordinator.async_add_listener(entity_adder_callback) + # Run the entity adder a first time during setup + entity_adder_callback() + +# TODO: Implement entry unload +# TODO: Unload entry +# TODO: Remove entry + + +def setup_platform(hass, config, async_add_entities, discovery_info=None): + pass