Add config flow to generic hygrostat (#119017)

* Add config flow to hygrostat

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
pull/120234/head
Joakim Plate 2024-06-23 12:56:41 +02:00 committed by GitHub
parent 4474e8c7ef
commit 84d1d11138
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 400 additions and 13 deletions

View File

@ -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,)
)

View File

@ -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"])

View File

@ -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(
[

View File

@ -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"
}

View File

@ -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"
}
}
}
}

View File

@ -6,6 +6,7 @@ To update, run python3 -m script.hassfest
FLOWS = {
"helper": [
"derivative",
"generic_hygrostat",
"generic_thermostat",
"group",
"integration",

View File

@ -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",

View File

@ -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'>,
})
# ---

View File

@ -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")