Skip to content

Commit

Permalink
Humidifier Support (#200)
Browse files Browse the repository at this point in the history
* Initial support for Humidifiers. 

Note I don't have a humidifier, so need other contributors to test.
  • Loading branch information
JeffSteinbok authored Sep 12, 2024
1 parent eb5e62c commit abb2338
Show file tree
Hide file tree
Showing 16 changed files with 1,331 additions and 36 deletions.
3 changes: 3 additions & 0 deletions custom_components/dreo/dreobasedevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ def should_poll(self):
async def async_added_to_hass(self):
"""Register callbacks."""

# Create a callback to update state in HA and add it a callback in
# the PyDreo device. This will cause all handle_server_update responses
# to update the state in HA.
@callback
def update_state():
# Tell HA we're ready to update
Expand Down
2 changes: 1 addition & 1 deletion custom_components/dreo/dreofan.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def extra_state_attributes(self) -> dict[str, Any]:
def supported_features(self) -> int:
"""Return the list of supported features."""
supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF
if (self.device.preset_mode is not None):
if (self.device.preset_modes is not None):
supported_features = supported_features | FanEntityFeature.PRESET_MODE
if (self.device.oscillating is not None):
supported_features = supported_features | FanEntityFeature.OSCILLATE
Expand Down
146 changes: 146 additions & 0 deletions custom_components/dreo/humidifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""HomeAssistant Humidifier platform for Dreo Humidifiers."""
from __future__ import annotations
import logging

from .haimports import * # pylint: disable=W0401,W0614

from homeassistant.components.humidifier import (
HumidifierEntity,
HumidifierEntityFeature
)

from .pydreo import PyDreo, PyDreoBaseDevice, PyDreoHumidifier
from .pydreo.constant import DreoDeviceType
from .dreobasedevice import DreoBaseDeviceHA

from .const import (
LOGGER,
DOMAIN,
PYDREO_MANAGER
)

_LOGGER = logging.getLogger(LOGGER)

def get_entries(pydreo_devices : list[PyDreoBaseDevice]) -> list[DreoHumidifierHA]:
"""Get the Dreo Humidifier entities for the devices."""
humidifier_entities_ha : DreoHumidifierHA = []

for pydreo_device in pydreo_devices:
if (pydreo_device.type == DreoDeviceType.HUMIDIFIER):
_LOGGER.debug("Humidifier:get_entries: Found a %s - %s", pydreo_device.type, pydreo_device.name)
humidifier_entities_ha.append(DreoHumidifierHA(pydreo_device))

return humidifier_entities_ha

async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
_discovery_info=None,
) -> None:
"""Set up the Dreo Humidifier platform."""
_LOGGER.info("Starting Dreo Humidifier Platform")
_LOGGER.debug("Dreo Humidifier:async_setup_entry")

pydreo_manager : PyDreo = hass.data[DOMAIN][PYDREO_MANAGER]

humidifier_entities_ha = get_entries(pydreo_manager.devices)

_LOGGER.debug("Humidifier:async_setup_entry: Adding Humidifiers (%s)", humidifier_entities_ha.count)
async_add_entities(humidifier_entities_ha)

# Implementation of the Humidifier
class DreoHumidifierHA(DreoBaseDeviceHA, HumidifierEntity):
"""Representation of a Dreo Humidifier entity."""

def __init__(self, pyDreoDevice: PyDreoHumidifier) -> None:
super().__init__(pyDreoDevice)
self.device = pyDreoDevice
_LOGGER.info(
"DreoHumidifierHA:__init__(%s)",
pyDreoDevice.name
)

_LOGGER.info(
"new DreoHumidifierHA instance(%s), mode %s, available_modes [%s]",
self.name,
self.mode,
self.available_modes,
)

@property
def device_info(self) -> DeviceInfo:
"""Return device information for this humidifier."""
return DeviceInfo(
identifiers={(DOMAIN, self.device.serial_number)},
manufacturer=self.device.brand,
model=f"{self.device.series_name} ({self.device.model}) {self.device.product_name}",
name=self.device.device_name,
)

@property
def supported_features(self) -> int:
"""Return the list of supported features."""
supported_features = 0
if self.device.modes is not None:
supported_features |= HumidifierEntityFeature.MODES

return supported_features

@property
def is_on(self) -> bool:
"""Return True if device is on."""
return self.device.is_on

@property
def mode(self) -> str | None:
"""Get the current mode."""
return self.device.mode

@property
def available_modes(self) -> int:
"""Return the list of supported modes."""
return self.device.modes

@property
def current_humidity(self) -> float:
"""Return the current humidity."""
return self.device.humidity

@property
def target_humidity(self) -> float:
"""Return the humidity level we try to reach."""
return self.device.target_humidity

def turn_on(self, **kwargs: any) -> None:
"""Turn the device on."""
_LOGGER.debug("DreoHumidiferHA:turn_on(%s)", self.device.name)
self.device.is_on = True

def turn_off(self, **kwargs: any) -> None:
"""Turn the device off."""
_LOGGER.debug("DreoHumidiferHA:turn_off(%s)", self.device.name)
self.device.is_on = False

def set_mode(self, mode: str) -> None:
"""Set the mode of the device."""
_LOGGER.debug(
"DreoHumidiferHA:set_mode(%s) --> %s", self.device.name, mode
)

if not self.device.is_on:
self.device.is_on = True

if mode not in self.available_modes:
raise ValueError(
f"{mode} is not one of the valid preset modes: {self.available_modes}"
)

self.device.mode = mode

def set_humidity(self, humidity: float) -> None:
"""Set the humidity level."""
_LOGGER.debug(
"DreoHumidiferHA:set_humidity(%s) --> %s", self.device.name, humidity
)
self.device.target_humidity = humidity
53 changes: 29 additions & 24 deletions custom_components/dreo/pydreo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
from .models import *
from .commandtransport import CommandTransport
from .pydreobasedevice import PyDreoBaseDevice, UnknownModelError, UnknownProductError
from .pydreounknowndevice import PyDreoUnknownDevice
from .pydreotowerfan import PyDreoTowerFan
from .pydreoaircirculator import PyDreoAirCirculator
from .pydreoceilingfan import PyDreoCeilingFan
from .pydreoairpurifier import PyDreoAirPurifier
from .pydreoheater import PyDreoHeater
from .pydreoairconditioner import PyDreoAC
from .pydreochefmaker import PyDreoChefMaker
from .pydreohumidifier import PyDreoHumidifier

_LOGGER = logging.getLogger(LOGGER_NAME)

Expand All @@ -33,7 +35,8 @@
DreoDeviceType.CEILING_FAN: PyDreoCeilingFan,
DreoDeviceType.HEATER: PyDreoHeater,
DreoDeviceType.AIR_CONDITIONER: PyDreoAC,
DreoDeviceType.CHEF_MAKER: PyDreoChefMaker
DreoDeviceType.CHEF_MAKER: PyDreoChefMaker,
DreoDeviceType.HUMIDIFIER: PyDreoHumidifier
}

class PyDreo: # pylint: disable=function-redefined
Expand Down Expand Up @@ -146,31 +149,33 @@ def _process_devices(self, dev_list: list) -> bool:

_LOGGER.debug("Found device with model %s", model)

# Get the prefix of the model number to match against the supported devices.
# Not all models will have known prefixes.
model_prefix = None
for prefix in SUPPORTED_MODEL_PREFIXES:
if model[:len(prefix):] == prefix:
model_prefix = prefix
_LOGGER.debug("Prefix %s assigned from model %s", model_prefix, model)
break

if model is None:
raise UnknownModelError(model)

device_details = None
if model in SUPPORTED_DEVICES:
_LOGGER.debug("Device %s found!", model)
device_details = SUPPORTED_DEVICES[model]
elif model_prefix is not None and model_prefix in SUPPORTED_DEVICES:
_LOGGER.debug("Device %s found! via prefix %s", model, model_prefix)
device_details = SUPPORTED_DEVICES[model_prefix]
else:
raise UnknownModelError(model)
if model is not None:
# Get the prefix of the model number to match against the supported devices.
# Not all models will have known prefixes.
model_prefix = None
for prefix in SUPPORTED_MODEL_PREFIXES:
if model[:len(prefix):] == prefix:
model_prefix = prefix
_LOGGER.debug("Prefix %s assigned from model %s", model_prefix, model)
break

device_details = None
if model in SUPPORTED_DEVICES:
_LOGGER.debug("Device %s found!", model)
device_details = SUPPORTED_DEVICES[model]
elif model_prefix is not None and model_prefix in SUPPORTED_DEVICES:
_LOGGER.debug("Device %s found! via prefix %s", model, model_prefix)
device_details = SUPPORTED_DEVICES[model_prefix]

# If device_details is None at this point, we have an unknown device model.
# Unsupported/Unknown Device. Load the state, but store it in an "unsupported objects"
# list for later use in diagnostics.
if device_details is not None:
device_class = _DREO_DEVICE_TYPE_TO_CLASS.get(device_details.device_type, None)

device_class = _DREO_DEVICE_TYPE_TO_CLASS.get(device_details.device_type, None)
if device_class is None:
raise UnknownProductError(device_details.device_type)
device_class = PyDreoUnknownDevice

device : PyDreoBaseDevice = device_class(device_details, dev, self)

self.load_device_state(device)
Expand Down
8 changes: 6 additions & 2 deletions custom_components/dreo/pydreo/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
CHILDLOCKON_KEY = "childlockon"
TEMPOFFSET_KEY = "tempoffset"
HUMIDITY_KEY = "rh"
TARGET_AUTO_HUMIDITY_KEY = "rhautolevel"
TARGET_HUMIDITY_KEY = "rhlevel"

# Preferences Names
Expand Down Expand Up @@ -249,7 +250,8 @@ class HVACMode(StrEnum):
"device_control_mode_turbo": "turbo",
"base_reverse": "reverse",
"device_control_custom": "custom",
"fan_2in1_breeze": "2-in-1 Breeze Mode"
"fan_2in1_breeze": "2-in-1 Breeze Mode",
"device_control_mode_manual": "manual"
}

class DreoDeviceType(StrEnum):
Expand All @@ -260,4 +262,6 @@ class DreoDeviceType(StrEnum):
CEILING_FAN = "Ceiling Fan"
HEATER = "Heater"
AIR_CONDITIONER = "Air Conditioner"
CHEF_MAKER = "Chef Maker"
CHEF_MAKER = "Chef Maker"
HUMIDIFIER = "Humidifier"
UNKNOWN = "Unknown"
7 changes: 5 additions & 2 deletions custom_components/dreo/pydreo/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ def __init__(
"DR-HCF",
"DR-HSH",
"WH",
"DR-HAC"
"DR-HAC",
"DR-HHM"
}

SUPPORTED_DEVICES = {
Expand Down Expand Up @@ -241,5 +242,7 @@ def __init__(
device_type=DreoDeviceType.CHEF_MAKER,
cooking_modes=COOKING_MODES,
cooking_range=COOKING_RANGES,
)
),

"DR-HHM": DreoDeviceDetails(device_type=DreoDeviceType.HUMIDIFIER),
}
Loading

0 comments on commit abb2338

Please sign in to comment.