Add config flow to generic hygrostat (#119017)
* Add config flow to hygrostat Co-authored-by: Franck Nijhof <frenck@frenck.nl>pull/120234/head
parent
4474e8c7ef
commit
84d1d11138
|
@ -3,6 +3,7 @@
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.humidifier import HumidifierDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
|
@ -73,3 +74,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up from a config entry."""
|
||||
await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,))
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(
|
||||
entry, (Platform.HUMIDIFIER,)
|
||||
)
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
"""Config flow for Generic hygrostat."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.humidifier import HumidifierDeviceClass
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.const import CONF_NAME, PERCENTAGE
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaConfigFlowHandler,
|
||||
SchemaFlowFormStep,
|
||||
)
|
||||
|
||||
from . import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_DRY_TOLERANCE,
|
||||
CONF_HUMIDIFIER,
|
||||
CONF_MIN_DUR,
|
||||
CONF_SENSOR,
|
||||
CONF_WET_TOLERANCE,
|
||||
DEFAULT_TOLERANCE,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
OPTIONS_SCHEMA = {
|
||||
vol.Required(CONF_DEVICE_CLASS): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[
|
||||
HumidifierDeviceClass.HUMIDIFIER,
|
||||
HumidifierDeviceClass.DEHUMIDIFIER,
|
||||
],
|
||||
translation_key=CONF_DEVICE_CLASS,
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=SENSOR_DOMAIN, device_class=SensorDeviceClass.HUMIDITY
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_HUMIDIFIER): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=SWITCH_DOMAIN)
|
||||
),
|
||||
vol.Required(
|
||||
CONF_DRY_TOLERANCE, default=DEFAULT_TOLERANCE
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0,
|
||||
max=100,
|
||||
step=0.5,
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
)
|
||||
),
|
||||
vol.Required(
|
||||
CONF_WET_TOLERANCE, default=DEFAULT_TOLERANCE
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0,
|
||||
max=100,
|
||||
step=0.5,
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_MIN_DUR): selector.DurationSelector(
|
||||
selector.DurationSelectorConfig(allow_negative=False)
|
||||
),
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
vol.Required(CONF_NAME): selector.TextSelector(),
|
||||
**OPTIONS_SCHEMA,
|
||||
}
|
||||
|
||||
|
||||
CONFIG_FLOW = {
|
||||
"user": SchemaFlowFormStep(vol.Schema(CONFIG_SCHEMA)),
|
||||
}
|
||||
|
||||
OPTIONS_FLOW = {
|
||||
"init": SchemaFlowFormStep(vol.Schema(OPTIONS_SCHEMA)),
|
||||
}
|
||||
|
||||
|
||||
class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config or options flow."""
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
return cast(str, options["name"])
|
|
@ -3,10 +3,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Mapping
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from homeassistant.components.humidifier import (
|
||||
ATTR_HUMIDITY,
|
||||
|
@ -18,6 +18,7 @@ from homeassistant.components.humidifier import (
|
|||
HumidifierEntity,
|
||||
HumidifierEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_MODE,
|
||||
|
@ -39,7 +40,7 @@ from homeassistant.core import (
|
|||
State,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers import condition, config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
|
@ -83,6 +84,38 @@ async def async_setup_platform(
|
|||
"""Set up the generic hygrostat platform."""
|
||||
if discovery_info:
|
||||
config = discovery_info
|
||||
await _async_setup_config(
|
||||
hass, config, config.get(CONF_UNIQUE_ID), async_add_entities
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize config entry."""
|
||||
|
||||
await _async_setup_config(
|
||||
hass,
|
||||
config_entry.options,
|
||||
config_entry.entry_id,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
def _time_period_or_none(value: Any) -> timedelta | None:
|
||||
if value is None:
|
||||
return None
|
||||
return cast(timedelta, cv.time_period(value))
|
||||
|
||||
|
||||
async def _async_setup_config(
|
||||
hass: HomeAssistant,
|
||||
config: Mapping[str, Any],
|
||||
unique_id: str | None,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
name: str = config[CONF_NAME]
|
||||
switch_entity_id: str = config[CONF_HUMIDIFIER]
|
||||
sensor_entity_id: str = config[CONF_SENSOR]
|
||||
|
@ -90,15 +123,18 @@ async def async_setup_platform(
|
|||
max_humidity: float | None = config.get(CONF_MAX_HUMIDITY)
|
||||
target_humidity: float | None = config.get(CONF_TARGET_HUMIDITY)
|
||||
device_class: HumidifierDeviceClass | None = config.get(CONF_DEVICE_CLASS)
|
||||
min_cycle_duration: timedelta | None = config.get(CONF_MIN_DUR)
|
||||
sensor_stale_duration: timedelta | None = config.get(CONF_STALE_DURATION)
|
||||
min_cycle_duration: timedelta | None = _time_period_or_none(
|
||||
config.get(CONF_MIN_DUR)
|
||||
)
|
||||
sensor_stale_duration: timedelta | None = _time_period_or_none(
|
||||
config.get(CONF_STALE_DURATION)
|
||||
)
|
||||
dry_tolerance: float = config[CONF_DRY_TOLERANCE]
|
||||
wet_tolerance: float = config[CONF_WET_TOLERANCE]
|
||||
keep_alive: timedelta | None = config.get(CONF_KEEP_ALIVE)
|
||||
keep_alive: timedelta | None = _time_period_or_none(config.get(CONF_KEEP_ALIVE))
|
||||
initial_state: bool | None = config.get(CONF_INITIAL_STATE)
|
||||
away_humidity: int | None = config.get(CONF_AWAY_HUMIDITY)
|
||||
away_fixed: bool | None = config.get(CONF_AWAY_FIXED)
|
||||
unique_id: str | None = config.get(CONF_UNIQUE_ID)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
"domain": "generic_hygrostat",
|
||||
"name": "Generic hygrostat",
|
||||
"codeowners": ["@Shulyaka"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/generic_hygrostat",
|
||||
"integration_type": "helper",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"title": "Generic hygrostat",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Add generic hygrostat",
|
||||
"description": "Create a entity that control the humidity via a switch and sensor.",
|
||||
"data": {
|
||||
"device_class": "Device class",
|
||||
"dry_tolerance": "Dry tolerance",
|
||||
"humidifier": "Switch",
|
||||
"min_cycle_duration": "Minimum cycle duration",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"target_sensor": "Humidity sensor",
|
||||
"wet_tolerance": "Wet tolerance"
|
||||
},
|
||||
"data_description": {
|
||||
"dry_tolerance": "The minimum amount of difference between the humidity read by the sensor specified in the target sensor option and the target humidity that must change prior to being switched on.",
|
||||
"humidifier": "Humidifier or dehumidifier switch; must be a toggle device.",
|
||||
"min_cycle_duration": "Set a minimum amount of time that the switch specified in the humidifier option must be in its current state prior to being switched either off or on.",
|
||||
"target_sensor": "Sensor with current humidity.",
|
||||
"wet_tolerance": "The minimum amount of difference between the humidity read by the sensor specified in the target sensor option and the target humidity that must change prior to being switched off."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"device_class": "[%key:component::generic_hygrostat::config::step::user::data::device_class%]",
|
||||
"dry_tolerance": "[%key:component::generic_hygrostat::config::step::user::data::dry_tolerance%]",
|
||||
"humidifier": "[%key:component::generic_hygrostat::config::step::user::data::humidifier%]",
|
||||
"min_cycle_duration": "[%key:component::generic_hygrostat::config::step::user::data::min_cycle_duration%]",
|
||||
"target_sensor": "[%key:component::generic_hygrostat::config::step::user::data::target_sensor%]",
|
||||
"wet_tolerance": "[%key:component::generic_hygrostat::config::step::user::data::wet_tolerance%]"
|
||||
},
|
||||
"data_description": {
|
||||
"dry_tolerance": "[%key:component::generic_hygrostat::config::step::user::data_description::dry_tolerance%]",
|
||||
"humidifier": "[%key:component::generic_hygrostat::config::step::user::data_description::humidifier%]",
|
||||
"min_cycle_duration": "[%key:component::generic_hygrostat::config::step::user::data_description::min_cycle_duration%]",
|
||||
"target_sensor": "[%key:component::generic_hygrostat::config::step::user::data_description::target_sensor%]",
|
||||
"wet_tolerance": "[%key:component::generic_hygrostat::config::step::user::data_description::wet_tolerance%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"device_class": {
|
||||
"options": {
|
||||
"humidifier": "Humidifier",
|
||||
"dehumidifier": "Dehumidifier"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ To update, run python3 -m script.hassfest
|
|||
FLOWS = {
|
||||
"helper": [
|
||||
"derivative",
|
||||
"generic_hygrostat",
|
||||
"generic_thermostat",
|
||||
"group",
|
||||
"integration",
|
||||
|
|
|
@ -2127,12 +2127,6 @@
|
|||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"generic_hygrostat": {
|
||||
"name": "Generic hygrostat",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"geniushub": {
|
||||
"name": "Genius Hub",
|
||||
"integration_type": "hub",
|
||||
|
@ -7160,6 +7154,11 @@
|
|||
"config_flow": true,
|
||||
"iot_class": "calculated"
|
||||
},
|
||||
"generic_hygrostat": {
|
||||
"integration_type": "helper",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"generic_thermostat": {
|
||||
"integration_type": "helper",
|
||||
"config_flow": true,
|
||||
|
@ -7265,6 +7264,7 @@
|
|||
"filesize",
|
||||
"garages_amsterdam",
|
||||
"generic",
|
||||
"generic_hygrostat",
|
||||
"generic_thermostat",
|
||||
"google_travel_time",
|
||||
"group",
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
# serializer version: 1
|
||||
# name: test_config_flow[create]
|
||||
FlowResultSnapshot({
|
||||
'result': ConfigEntrySnapshot({
|
||||
'title': 'My hygrostat',
|
||||
}),
|
||||
'title': 'My hygrostat',
|
||||
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_config_flow[init]
|
||||
FlowResultSnapshot({
|
||||
'type': <FlowResultType.FORM: 'form'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_options[create_entry]
|
||||
FlowResultSnapshot({
|
||||
'result': True,
|
||||
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_options[dehumidifier]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'action': <HumidifierAction.OFF: 'off'>,
|
||||
'current_humidity': 10.0,
|
||||
'device_class': 'dehumidifier',
|
||||
'friendly_name': 'My hygrostat',
|
||||
'humidity': 100,
|
||||
'max_humidity': 100,
|
||||
'min_humidity': 0,
|
||||
'supported_features': <HumidifierEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'humidifier.my_hygrostat',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_options[humidifier]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'action': <HumidifierAction.OFF: 'off'>,
|
||||
'current_humidity': 10.0,
|
||||
'device_class': 'humidifier',
|
||||
'friendly_name': 'My hygrostat',
|
||||
'humidity': 100,
|
||||
'max_humidity': 100,
|
||||
'min_humidity': 0,
|
||||
'supported_features': <HumidifierEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'humidifier.my_hygrostat',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_options[init]
|
||||
FlowResultSnapshot({
|
||||
'type': <FlowResultType.FORM: 'form'>,
|
||||
})
|
||||
# ---
|
|
@ -0,0 +1,106 @@
|
|||
"""Test the generic hygrostat config flow."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from syrupy.filters import props
|
||||
|
||||
from homeassistant.components.generic_hygrostat import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_DRY_TOLERANCE,
|
||||
CONF_HUMIDIFIER,
|
||||
CONF_NAME,
|
||||
CONF_SENSOR,
|
||||
CONF_WET_TOLERANCE,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
SNAPSHOT_FLOW_PROPS = props("type", "title", "result", "error")
|
||||
|
||||
|
||||
async def test_config_flow(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None:
|
||||
"""Test the config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result == snapshot(name="init", include=SNAPSHOT_FLOW_PROPS)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.generic_hygrostat.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: "My hygrostat",
|
||||
CONF_DRY_TOLERANCE: 2,
|
||||
CONF_WET_TOLERANCE: 4,
|
||||
CONF_HUMIDIFIER: "switch.run",
|
||||
CONF_SENSOR: "sensor.humidity",
|
||||
CONF_DEVICE_CLASS: "dehumidifier",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result == snapshot(name="create", include=SNAPSHOT_FLOW_PROPS)
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
assert config_entry.data == {}
|
||||
assert config_entry.title == "My hygrostat"
|
||||
|
||||
|
||||
async def test_options(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None:
|
||||
"""Test reconfiguring."""
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={
|
||||
CONF_DEVICE_CLASS: "dehumidifier",
|
||||
CONF_DRY_TOLERANCE: 2.0,
|
||||
CONF_HUMIDIFIER: "switch.run",
|
||||
CONF_NAME: "My hygrostat",
|
||||
CONF_SENSOR: "sensor.humidity",
|
||||
CONF_WET_TOLERANCE: 4.0,
|
||||
},
|
||||
title="My hygrostat",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
# set some initial values
|
||||
hass.states.async_set(
|
||||
"sensor.humidity",
|
||||
"10",
|
||||
{"unit_of_measurement": "%", "device_class": "humidity"},
|
||||
)
|
||||
hass.states.async_set("switch.run", "on")
|
||||
|
||||
# check that it is setup
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("humidifier.my_hygrostat") == snapshot(name="dehumidifier")
|
||||
|
||||
# switch to humidifier
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result == snapshot(name="init", include=SNAPSHOT_FLOW_PROPS)
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_DRY_TOLERANCE: 2,
|
||||
CONF_WET_TOLERANCE: 4,
|
||||
CONF_HUMIDIFIER: "switch.run",
|
||||
CONF_SENSOR: "sensor.humidity",
|
||||
CONF_DEVICE_CLASS: "humidifier",
|
||||
},
|
||||
)
|
||||
assert result == snapshot(name="create_entry", include=SNAPSHOT_FLOW_PROPS)
|
||||
|
||||
# Check config entry is reloaded with new options
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("humidifier.my_hygrostat") == snapshot(name="humidifier")
|
Loading…
Reference in New Issue