Add new humidifier entity integration (#28693)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Shulyaka 2020-06-23 03:59:16 +03:00 committed by GitHub
parent 747490ab34
commit c28493098a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 596 additions and 2 deletions

View File

@ -185,6 +185,7 @@ homeassistant/components/http/* @home-assistant/core
homeassistant/components/huawei_lte/* @scop @fphammerle
homeassistant/components/huawei_router/* @abmantis
homeassistant/components/hue/* @balloob
homeassistant/components/humidifier/* @home-assistant/core @Shulyaka
homeassistant/components/hunterdouglas_powerview/* @bdraco
homeassistant/components/hvv_departures/* @vigonotion
homeassistant/components/hydrawise/* @ptcryan

View File

@ -18,6 +18,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [
"climate",
"cover",
"fan",
"humidifier",
"light",
"lock",
"media_player",

View File

@ -0,0 +1,124 @@
"""Demo platform that offers a fake humidifier device."""
from homeassistant.components.humidifier import HumidifierEntity
from homeassistant.components.humidifier.const import (
DEVICE_CLASS_DEHUMIDIFIER,
DEVICE_CLASS_HUMIDIFIER,
SUPPORT_MODES,
)
SUPPORT_FLAGS = 0
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Demo humidifier devices."""
async_add_entities(
[
DemoHumidifier(
name="Humidifier",
mode=None,
target_humidity=68,
device_class=DEVICE_CLASS_HUMIDIFIER,
),
DemoHumidifier(
name="Dehumidifier",
mode=None,
target_humidity=54,
device_class=DEVICE_CLASS_DEHUMIDIFIER,
),
DemoHumidifier(
name="Hygrostat",
mode="home",
available_modes=["home", "eco"],
target_humidity=50,
),
]
)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Demo humidifier devices config entry."""
await async_setup_platform(hass, {}, async_add_entities)
class DemoHumidifier(HumidifierEntity):
"""Representation of a demo humidifier device."""
def __init__(
self,
name,
mode,
target_humidity,
available_modes=None,
is_on=True,
device_class=None,
):
"""Initialize the humidifier device."""
self._name = name
self._state = is_on
self._support_flags = SUPPORT_FLAGS
if mode is not None:
self._support_flags = self._support_flags | SUPPORT_MODES
self._target_humidity = target_humidity
self._mode = mode
self._available_modes = available_modes
self._device_class = device_class
@property
def supported_features(self):
"""Return the list of supported features."""
return self._support_flags
@property
def should_poll(self):
"""Return the polling state."""
return False
@property
def name(self):
"""Return the name of the humidity device."""
return self._name
@property
def target_humidity(self):
"""Return the humidity we try to reach."""
return self._target_humidity
@property
def mode(self):
"""Return current mode."""
return self._mode
@property
def available_modes(self):
"""Return available modes."""
return self._available_modes
@property
def is_on(self):
"""Return true if the humidifier is on."""
return self._state
@property
def device_class(self):
"""Return the device class of the humidifier."""
return self._device_class
async def async_turn_on(self, **kwargs):
"""Turn the device on."""
self._state = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs):
"""Turn the device off."""
self._state = False
self.async_write_ha_state()
async def async_set_humidity(self, humidity):
"""Set new humidity level."""
self._target_humidity = humidity
self.async_write_ha_state()
async def async_set_mode(self, mode):
"""Update mode."""
self._mode = mode
self.async_write_ha_state()

View File

@ -50,9 +50,21 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
SIGNIFICANT_DOMAINS = ("climate", "device_tracker", "thermostat", "water_heater")
SIGNIFICANT_DOMAINS = (
"climate",
"device_tracker",
"humidifier",
"thermostat",
"water_heater",
)
IGNORE_DOMAINS = ("zone", "scene")
NEED_ATTRIBUTE_DOMAINS = {"climate", "water_heater", "thermostat", "script"}
NEED_ATTRIBUTE_DOMAINS = {
"climate",
"humidifier",
"script",
"thermostat",
"water_heater",
}
SCRIPT_DOMAIN = "script"
ATTR_CAN_CANCEL = "can_cancel"

View File

@ -0,0 +1,175 @@
"""Provides functionality to interact with humidifier devices."""
from datetime import timedelta
import logging
from typing import Any, Dict, List, Optional
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_ON,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.loader import bind_hass
from .const import (
ATTR_AVAILABLE_MODES,
ATTR_HUMIDITY,
ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY,
ATTR_MODE,
DEFAULT_MAX_HUMIDITY,
DEFAULT_MIN_HUMIDITY,
DEVICE_CLASS_DEHUMIDIFIER,
DEVICE_CLASS_HUMIDIFIER,
DOMAIN,
SERVICE_SET_HUMIDITY,
SERVICE_SET_MODE,
SUPPORT_MODES,
)
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=60)
DEVICE_CLASSES = [DEVICE_CLASS_HUMIDIFIER, DEVICE_CLASS_DEHUMIDIFIER]
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
@bind_hass
def is_on(hass, entity_id):
"""Return if the humidifier is on based on the statemachine.
Async friendly.
"""
return hass.states.is_state(entity_id, STATE_ON)
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Set up humidifier devices."""
component = hass.data[DOMAIN] = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
await component.async_setup(config)
component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on")
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off")
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
component.async_register_entity_service(
SERVICE_SET_MODE,
{vol.Required(ATTR_MODE): cv.string},
"async_set_mode",
[SUPPORT_MODES],
)
component.async_register_entity_service(
SERVICE_SET_HUMIDITY,
{
vol.Required(ATTR_HUMIDITY): vol.All(
vol.Coerce(int), vol.Range(min=0, max=100)
)
},
"async_set_humidity",
)
return True
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
class HumidifierEntity(ToggleEntity):
"""Representation of a humidifier device."""
@property
def capability_attributes(self) -> Dict[str, Any]:
"""Return capability attributes."""
supported_features = self.supported_features or 0
data = {
ATTR_MIN_HUMIDITY: self.min_humidity,
ATTR_MAX_HUMIDITY: self.max_humidity,
}
if supported_features & SUPPORT_MODES:
data[ATTR_AVAILABLE_MODES] = self.available_modes
return data
@property
def state_attributes(self) -> Dict[str, Any]:
"""Return the optional state attributes."""
supported_features = self.supported_features or 0
data = {}
if self.target_humidity is not None:
data[ATTR_HUMIDITY] = self.target_humidity
if supported_features & SUPPORT_MODES:
data[ATTR_MODE] = self.mode
return data
@property
def target_humidity(self) -> Optional[int]:
"""Return the humidity we try to reach."""
return None
@property
def mode(self) -> Optional[str]:
"""Return the current mode, e.g., home, auto, baby.
Requires SUPPORT_MODES.
"""
raise NotImplementedError
@property
def available_modes(self) -> Optional[List[str]]:
"""Return a list of available modes.
Requires SUPPORT_MODES.
"""
raise NotImplementedError
def set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
raise NotImplementedError()
async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
await self.hass.async_add_executor_job(self.set_humidity, humidity)
def set_mode(self, mode: str) -> None:
"""Set new mode."""
raise NotImplementedError()
async def async_set_mode(self, mode: str) -> None:
"""Set new mode."""
await self.hass.async_add_executor_job(self.set_mode, mode)
@property
def min_humidity(self) -> int:
"""Return the minimum humidity."""
return DEFAULT_MIN_HUMIDITY
@property
def max_humidity(self) -> int:
"""Return the maximum humidity."""
return DEFAULT_MAX_HUMIDITY

View File

@ -0,0 +1,30 @@
"""Provides the constants needed for component."""
MODE_NORMAL = "normal"
MODE_ECO = "eco"
MODE_AWAY = "away"
MODE_BOOST = "boost"
MODE_COMFORT = "comfort"
MODE_HOME = "home"
MODE_SLEEP = "sleep"
MODE_AUTO = "auto"
MODE_BABY = "baby"
ATTR_MODE = "mode"
ATTR_AVAILABLE_MODES = "available_modes"
ATTR_HUMIDITY = "humidity"
ATTR_MAX_HUMIDITY = "max_humidity"
ATTR_MIN_HUMIDITY = "min_humidity"
DEFAULT_MIN_HUMIDITY = 0
DEFAULT_MAX_HUMIDITY = 100
DOMAIN = "humidifier"
DEVICE_CLASS_HUMIDIFIER = "humidifier"
DEVICE_CLASS_DEHUMIDIFIER = "dehumidifier"
SERVICE_SET_MODE = "set_mode"
SERVICE_SET_HUMIDITY = "set_humidity"
SUPPORT_MODES = 1

View File

@ -0,0 +1,7 @@
{
"domain": "humidifier",
"name": "Humidifier",
"documentation": "https://www.home-assistant.io/integrations/humidifier",
"codeowners": ["@home-assistant/core", "@Shulyaka"],
"quality_scale": "internal"
}

View File

@ -0,0 +1,42 @@
# Describes the format for available humidifier services
set_mode:
description: Set mode for humidifier device.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'humidifier.bedroom'
mode:
description: New mode
example: 'away'
set_humidity:
description: Set target humidity of humidifier device.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'humidifier.bedroom'
humidity:
description: New target humidity for humidifier device.
example: 50
turn_on:
description: Turn humidifier device on.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'humidifier.bedroom'
turn_off:
description: Turn humidifier device off.
fields:
entity_id:
description: Name(s) of entities to change.
example: 'humidifier.bedroom'
toggle:
description: Toggles a humidifier device.
fields:
entity_id:
description: Name(s) of entities to toggle.
example: 'humidifier.bedroom'

View File

@ -0,0 +1,166 @@
"""The tests for the demo humidifier component."""
import pytest
import voluptuous as vol
from homeassistant.components.humidifier.const import (
ATTR_HUMIDITY,
ATTR_MAX_HUMIDITY,
ATTR_MIN_HUMIDITY,
ATTR_MODE,
DOMAIN,
MODE_AWAY,
MODE_ECO,
SERVICE_SET_HUMIDITY,
SERVICE_SET_MODE,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
)
from homeassistant.setup import async_setup_component
ENTITY_DEHUMIDIFIER = "humidifier.dehumidifier"
ENTITY_HYGROSTAT = "humidifier.hygrostat"
ENTITY_HUMIDIFIER = "humidifier.humidifier"
@pytest.fixture(autouse=True)
async def setup_demo_humidifier(hass):
"""Initialize setup demo humidifier."""
assert await async_setup_component(
hass, DOMAIN, {"humidifier": {"platform": "demo"}}
)
await hass.async_block_till_done()
def test_setup_params(hass):
"""Test the initial parameters."""
state = hass.states.get(ENTITY_DEHUMIDIFIER)
assert state.state == STATE_ON
assert state.attributes.get(ATTR_HUMIDITY) == 54
def test_default_setup_params(hass):
"""Test the setup with default parameters."""
state = hass.states.get(ENTITY_DEHUMIDIFIER)
assert state.attributes.get(ATTR_MIN_HUMIDITY) == 0
assert state.attributes.get(ATTR_MAX_HUMIDITY) == 100
async def test_set_target_humidity_bad_attr(hass):
"""Test setting the target humidity without required attribute."""
state = hass.states.get(ENTITY_DEHUMIDIFIER)
assert state.attributes.get(ATTR_HUMIDITY) == 54
with pytest.raises(vol.Invalid):
await hass.services.async_call(
DOMAIN,
SERVICE_SET_HUMIDITY,
{ATTR_HUMIDITY: None, ATTR_ENTITY_ID: ENTITY_DEHUMIDIFIER},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_DEHUMIDIFIER)
assert state.attributes.get(ATTR_HUMIDITY) == 54
async def test_set_target_humidity(hass):
"""Test the setting of the target humidity."""
state = hass.states.get(ENTITY_DEHUMIDIFIER)
assert state.attributes.get(ATTR_HUMIDITY) == 54
await hass.services.async_call(
DOMAIN,
SERVICE_SET_HUMIDITY,
{ATTR_HUMIDITY: 64, ATTR_ENTITY_ID: ENTITY_DEHUMIDIFIER},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_DEHUMIDIFIER)
assert state.attributes.get(ATTR_HUMIDITY) == 64
async def test_set_hold_mode_away(hass):
"""Test setting the hold mode away."""
await hass.services.async_call(
DOMAIN,
SERVICE_SET_MODE,
{ATTR_MODE: MODE_AWAY, ATTR_ENTITY_ID: ENTITY_HYGROSTAT},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_HYGROSTAT)
assert state.attributes.get(ATTR_MODE) == MODE_AWAY
async def test_set_hold_mode_eco(hass):
"""Test setting the hold mode eco."""
await hass.services.async_call(
DOMAIN,
SERVICE_SET_MODE,
{ATTR_MODE: MODE_ECO, ATTR_ENTITY_ID: ENTITY_HYGROSTAT},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_HYGROSTAT)
assert state.attributes.get(ATTR_MODE) == MODE_ECO
async def test_turn_on(hass):
"""Test turn on device."""
await hass.services.async_call(
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_DEHUMIDIFIER}, blocking=True
)
state = hass.states.get(ENTITY_DEHUMIDIFIER)
assert state.state == STATE_OFF
await hass.services.async_call(
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_DEHUMIDIFIER}, blocking=True
)
state = hass.states.get(ENTITY_DEHUMIDIFIER)
assert state.state == STATE_ON
async def test_turn_off(hass):
"""Test turn off device."""
await hass.services.async_call(
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_DEHUMIDIFIER}, blocking=True
)
state = hass.states.get(ENTITY_DEHUMIDIFIER)
assert state.state == STATE_ON
await hass.services.async_call(
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_DEHUMIDIFIER}, blocking=True
)
state = hass.states.get(ENTITY_DEHUMIDIFIER)
assert state.state == STATE_OFF
async def test_toggle(hass):
"""Test toggle device."""
await hass.services.async_call(
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_DEHUMIDIFIER}, blocking=True
)
state = hass.states.get(ENTITY_DEHUMIDIFIER)
assert state.state == STATE_ON
await hass.services.async_call(
DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_DEHUMIDIFIER}, blocking=True
)
state = hass.states.get(ENTITY_DEHUMIDIFIER)
assert state.state == STATE_OFF
await hass.services.async_call(
DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_DEHUMIDIFIER}, blocking=True
)
state = hass.states.get(ENTITY_DEHUMIDIFIER)
assert state.state == STATE_ON

View File

@ -0,0 +1 @@
"""The tests for humidifier component."""

View File

@ -0,0 +1,35 @@
"""The tests for the humidifier component."""
from unittest.mock import MagicMock
from homeassistant.components.humidifier import HumidifierEntity
class MockHumidifierEntity(HumidifierEntity):
"""Mock Humidifier device to use in tests."""
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
return 0
async def test_sync_turn_on(hass):
"""Test if async turn_on calls sync turn_on."""
humidifier = MockHumidifierEntity()
humidifier.hass = hass
humidifier.turn_on = MagicMock()
await humidifier.async_turn_on()
assert humidifier.turn_on.called
async def test_sync_turn_off(hass):
"""Test if async turn_off calls sync turn_off."""
humidifier = MockHumidifierEntity()
humidifier.hass = hass
humidifier.turn_off = MagicMock()
await humidifier.async_turn_off()
assert humidifier.turn_off.called