Add subentry support to kitchen sink

This commit is contained in:
Erik 2024-12-02 10:07:54 +01:00
parent a4653bb8dc
commit a12a42710f
7 changed files with 233 additions and 6 deletions

View File

@ -70,11 +70,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set the config entry up.""" """Set the config entry up."""
# Set up demo platforms with config entry # Set up demo platforms with config entry
await hass.config_entries.async_forward_entry_setups( await hass.config_entries.async_forward_entry_setups(
config_entry, COMPONENTS_WITH_DEMO_PLATFORM entry, COMPONENTS_WITH_DEMO_PLATFORM
) )
# Create issues # Create issues
@ -85,7 +85,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
await _insert_statistics(hass) await _insert_statistics(hass)
# Start a reauth flow # Start a reauth flow
config_entry.async_start_reauth(hass) entry.async_start_reauth(hass)
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
# Notify backup listeners # Notify backup listeners
hass.async_create_task(_notify_backup_listeners(hass), eager_start=False) hass.async_create_task(_notify_backup_listeners(hass), eager_start=False)
@ -93,6 +95,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
return True return True
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle update."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload config entry.""" """Unload config entry."""
# Notify backup listeners # Notify backup listeners

View File

@ -12,7 +12,9 @@ from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
ConfigSubentryFlow,
OptionsFlow, OptionsFlow,
SubentryFlowResult,
) )
from homeassistant.core import callback from homeassistant.core import callback
@ -35,6 +37,21 @@ class KitchenSinkConfigFlow(ConfigFlow, domain=DOMAIN):
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return OptionsFlowHandler() return OptionsFlowHandler()
@staticmethod
@callback
def async_get_subentry_flow(
config_entry: ConfigEntry, subentry_type: str
) -> ConfigSubentryFlow:
"""Get the subentry flow for this handler."""
return SubentryFlowHandler()
@classmethod
@callback
def async_supported_subentries(cls, config_entry: ConfigEntry) -> tuple[str, ...]:
"""Return subentries supported by this handler."""
return ("add_entity",)
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Set the config entry up from yaml.""" """Set the config entry up from yaml."""
return self.async_create_entry(title="Kitchen Sink", data=import_data) return self.async_create_entry(title="Kitchen Sink", data=import_data)
@ -94,3 +111,31 @@ class OptionsFlowHandler(OptionsFlow):
} }
), ),
) )
class SubentryFlowHandler(ConfigSubentryFlow):
"""Handle subentry flow."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Manage the options."""
return await self.async_step_add_sensor()
async def async_step_add_sensor(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Add a new sensor."""
if user_input is not None:
title = user_input.pop("name")
return self.async_create_entry(data=user_input, title=title)
return self.async_show_form(
step_id="add_sensor",
data_schema=vol.Schema(
{
vol.Required("name"): str,
vol.Required("state"): int,
}
),
)

View File

@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfPower from homeassistant.const import UnitOfPower
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import UNDEFINED, StateType, UndefinedType from homeassistant.helpers.typing import UNDEFINED, StateType, UndefinedType
from . import DOMAIN from . import DOMAIN
@ -21,7 +21,8 @@ from .device import async_create_device
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, # pylint: disable-next=hass-argument-type
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up the Everything but the Kitchen Sink config entry.""" """Set up the Everything but the Kitchen Sink config entry."""
async_create_device( async_create_device(
@ -90,6 +91,23 @@ async def async_setup_entry(
] ]
) )
for subentry_id, subentry in config_entry.subentries.items():
async_add_entities(
[
DemoSensor(
device_unique_id=subentry_id,
unique_id=subentry_id,
device_name=subentry.title,
entity_name=None,
state=subentry.data["state"],
device_class=None,
state_class=None,
unit_of_measurement=None,
)
],
subentry_id=subentry_id,
)
class DemoSensor(SensorEntity): class DemoSensor(SensorEntity):
"""Representation of a Demo sensor.""" """Representation of a Demo sensor."""

View File

@ -9,6 +9,20 @@
} }
} }
}, },
"config_subentries": {
"add_entity": {
"title": "Add entity",
"step": {
"add_sensor": {
"description": "Configure the new sensor",
"data": {
"name": "[%key:common::config_flow::data::name%]",
"state": "Initial state"
}
}
}
}
},
"options": { "options": {
"step": { "step": {
"init": { "init": {

View File

@ -69,3 +69,84 @@
}), }),
}) })
# --- # ---
# name: test_states_with_subentry
set({
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Outlet 1 Power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.outlet_1_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '50',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Outlet 2 Power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.outlet_2_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1500',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Sensor test',
}),
'context': <ANY>,
'entity_id': 'sensor.sensor_test',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '15',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Statistics issues Issue 1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.statistics_issues_issue_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '100',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Statistics issues Issue 2',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'dogs',
}),
'context': <ANY>,
'entity_id': 'sensor.statistics_issues_issue_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '100',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Statistics issues Issue 3',
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.statistics_issues_issue_3',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '100',
}),
})
# ---

View File

@ -104,3 +104,36 @@ async def test_options_flow(hass: HomeAssistant) -> None:
assert config_entry.options == {"section_1": {"bool": True, "int": 15}} assert config_entry.options == {"section_1": {"bool": True, "int": 15}}
await hass.async_block_till_done() await hass.async_block_till_done()
@pytest.mark.usefixtures("no_platforms")
async def test_subentry_flow(hass: HomeAssistant) -> None:
"""Test config flow options."""
config_entry = MockConfigEntry(domain=DOMAIN)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.subentries.async_init(
(config_entry.entry_id, "add_entity")
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "add_sensor"
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
user_input={"name": "Sensor 1", "state": 15},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
subentry_id = list(config_entry.subentries.keys())[0]
assert config_entry.subentries == {
subentry_id: config_entries.ConfigSubentry(
data={"state": 15},
subentry_id=subentry_id,
title="Sensor 1",
unique_id=None,
)
}
await hass.async_block_till_done()

View File

@ -5,11 +5,14 @@ from unittest.mock import patch
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant import config_entries
from homeassistant.components.kitchen_sink import DOMAIN from homeassistant.components.kitchen_sink import DOMAIN
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
@pytest.fixture @pytest.fixture
async def sensor_only() -> None: async def sensor_only() -> None:
@ -21,14 +24,40 @@ async def sensor_only() -> None:
yield yield
@pytest.fixture(autouse=True) @pytest.fixture
async def setup_comp(hass: HomeAssistant, sensor_only): async def setup_comp(hass: HomeAssistant, sensor_only):
"""Set up demo component.""" """Set up demo component."""
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
await hass.async_block_till_done() await hass.async_block_till_done()
@pytest.mark.usefixtures("setup_comp")
async def test_states(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None: async def test_states(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None:
"""Test the expected sensor entities are added.""" """Test the expected sensor entities are added."""
states = hass.states.async_all() states = hass.states.async_all()
assert set(states) == snapshot assert set(states) == snapshot
@pytest.mark.usefixtures("sensor_only")
async def test_states_with_subentry(
hass: HomeAssistant, snapshot: SnapshotAssertion
) -> None:
"""Test the expected sensor entities are added."""
config_entry = MockConfigEntry(
domain=DOMAIN,
subentries_data=[
config_entries.ConfigSubentryData(
data={"state": 15},
subentry_id="blabla",
title="Sensor test",
unique_id=None,
)
],
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
states = hass.states.async_all()
assert set(states) == snapshot