Add power strip with 2 outlets to kitchen_sink (#110346)

This commit is contained in:
Erik Montnemery 2024-02-12 20:00:13 +01:00 committed by GitHub
parent 3086d24231
commit e27e799dd5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 625 additions and 29 deletions

View File

@ -27,10 +27,12 @@ DOMAIN = "kitchen_sink"
COMPONENTS_WITH_DEMO_PLATFORM = [
Platform.BUTTON,
Platform.IMAGE,
Platform.LAWN_MOWER,
Platform.LOCK,
Platform.SENSOR,
Platform.SWITCH,
Platform.WEATHER,
]

View File

@ -0,0 +1,56 @@
"""Demo platform that offers a fake button entity."""
from __future__ import annotations
from homeassistant.components import persistent_notification
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the demo button platform."""
async_add_entities(
[
DemoButton(
unique_id="2_ch_power_strip",
device_name="2CH Power strip",
entity_name="Restart",
),
]
)
class DemoButton(ButtonEntity):
"""Representation of a demo button entity."""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(
self,
unique_id: str,
device_name: str,
entity_name: str | None,
) -> None:
"""Initialize the Demo button entity."""
self._attr_unique_id = unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=device_name,
)
self._attr_name = entity_name
async def async_press(self) -> None:
"""Send out a persistent notification."""
persistent_notification.async_create(
self.hass, "Button pressed", title="Button"
)
self.hass.bus.async_fire("demo_button_pressed")

View File

@ -0,0 +1,23 @@
"""Create device without entities."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import DOMAIN
def async_create_device(
hass: HomeAssistant,
config_entry_id: str,
device_name: str | None,
unique_id: str,
) -> dr.DeviceEntry:
"""Create a device."""
device_registry = dr.async_get(hass)
return device_registry.async_get_or_create(
config_entry_id=config_entry_id,
name=device_name,
identifiers={(DOMAIN, unique_id)},
)

View File

@ -11,9 +11,10 @@ from homeassistant.const import UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.typing import UNDEFINED, StateType, UndefinedType
from . import DOMAIN
from .device import async_create_device
async def async_setup_entry(
@ -22,31 +23,63 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Everything but the Kitchen Sink config entry."""
async_create_device(
hass, config_entry.entry_id, "2CH Power strip", "2_ch_power_strip"
)
async_add_entities(
[
DemoSensor(
"statistics_issue_1",
"Statistics issue 1",
100,
None,
SensorStateClass.MEASUREMENT,
UnitOfPower.WATT, # Not a volume unit
device_unique_id="outlet_1",
unique_id="outlet_1_power",
device_name="Outlet 1",
entity_name=UNDEFINED,
state=50,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
unit_of_measurement=UnitOfPower.WATT,
via_device="2_ch_power_strip",
),
DemoSensor(
"statistics_issue_2",
"Statistics issue 2",
100,
None,
SensorStateClass.MEASUREMENT,
"dogs", # Can't be converted to cats
device_unique_id="outlet_2",
unique_id="outlet_2_power",
device_name="Outlet 2",
entity_name=UNDEFINED,
state=1500,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
unit_of_measurement=UnitOfPower.WATT,
via_device="2_ch_power_strip",
),
DemoSensor(
"statistics_issue_3",
"Statistics issue 3",
100,
None,
None, # Wrong state class
UnitOfPower.WATT,
device_unique_id="statistics_issues",
unique_id="statistics_issue_1",
device_name="Statistics issues",
entity_name="Issue 1",
state=100,
device_class=None,
state_class=SensorStateClass.MEASUREMENT,
unit_of_measurement=UnitOfPower.WATT,
),
DemoSensor(
device_unique_id="statistics_issues",
unique_id="statistics_issue_2",
device_name="Statistics issues",
entity_name="Issue 2",
state=100,
device_class=None,
state_class=SensorStateClass.MEASUREMENT,
unit_of_measurement="dogs",
),
DemoSensor(
device_unique_id="statistics_issues",
unique_id="statistics_issue_3",
device_name="Statistics issues",
entity_name="Issue 3",
state=100,
device_class=None,
state_class=None,
unit_of_measurement=UnitOfPower.WATT,
),
]
)
@ -55,26 +88,34 @@ async def async_setup_entry(
class DemoSensor(SensorEntity):
"""Representation of a Demo sensor."""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(
self,
*,
device_unique_id: str,
unique_id: str,
name: str,
device_name: str,
entity_name: str | None | UndefinedType,
state: StateType,
device_class: SensorDeviceClass | None,
state_class: SensorStateClass | None,
unit_of_measurement: str | None,
via_device: str | None = None,
) -> None:
"""Initialize the sensor."""
self._attr_device_class = device_class
self._attr_name = name
if entity_name is not UNDEFINED:
self._attr_name = entity_name
self._attr_native_unit_of_measurement = unit_of_measurement
self._attr_native_value = state
self._attr_state_class = state_class
self._attr_unique_id = unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=name,
identifiers={(DOMAIN, device_unique_id)},
name=device_name,
)
if via_device:
self._attr_device_info["via_device"] = (DOMAIN, via_device)

View File

@ -0,0 +1,88 @@
"""Demo platform that has some fake switches."""
from __future__ import annotations
from typing import Any
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN
from .device import async_create_device
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the demo switch platform."""
async_create_device(
hass, config_entry.entry_id, "2CH Power strip", "2_ch_power_strip"
)
async_add_entities(
[
DemoSwitch(
unique_id="outlet_1",
device_name="Outlet 1",
entity_name=None,
state=False,
assumed=False,
via_device="2_ch_power_strip",
),
DemoSwitch(
unique_id="outlet_2",
device_name="Outlet 2",
entity_name=None,
state=True,
assumed=False,
via_device="2_ch_power_strip",
),
]
)
class DemoSwitch(SwitchEntity):
"""Representation of a demo switch."""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(
self,
*,
unique_id: str,
device_name: str,
entity_name: str | None,
state: bool,
assumed: bool,
translation_key: str | None = None,
device_class: SwitchDeviceClass | None = None,
via_device: str | None = None,
) -> None:
"""Initialize the Demo switch."""
self._attr_assumed_state = assumed
self._attr_device_class = device_class
self._attr_translation_key = translation_key
self._attr_is_on = state
self._attr_unique_id = unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=device_name,
)
if via_device:
self._attr_device_info["via_device"] = (DOMAIN, via_device)
self._attr_name = entity_name
def turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
self._attr_is_on = True
self.schedule_update_ha_state()
def turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
self._attr_is_on = False
self.schedule_update_ha_state()

View File

@ -3,35 +3,61 @@
set({
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Statistics issue 1',
'device_class': 'power',
'friendly_name': 'Outlet 1 Power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.statistics_issue_1',
'entity_id': 'sensor.outlet_1_power',
'last_changed': <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_updated': <ANY>,
'state': '1500',
}),
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_updated': <ANY>,
'state': '100',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Statistics issue 2',
'friendly_name': 'Statistics issues Issue 2',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'dogs',
}),
'context': <ANY>,
'entity_id': 'sensor.statistics_issue_2',
'entity_id': 'sensor.statistics_issues_issue_2',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '100',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Statistics issue 3',
'friendly_name': 'Statistics issues Issue 3',
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.statistics_issue_3',
'entity_id': 'sensor.statistics_issues_issue_3',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '100',

View File

@ -0,0 +1,199 @@
# serializer version: 1
# name: test_state
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Outlet 1',
}),
'context': <ANY>,
'entity_id': 'switch.outlet_1',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_state.1
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.outlet_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'kitchen_sink',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'outlet_1',
'unit_of_measurement': None,
})
# ---
# name: test_state.2
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'kitchen_sink',
'outlet_1',
),
}),
'is_new': False,
'manufacturer': None,
'model': None,
'name': 'Outlet 1',
'name_by_user': None,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': <ANY>,
})
# ---
# name: test_state.3
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'kitchen_sink',
'2_ch_power_strip',
),
}),
'is_new': False,
'manufacturer': None,
'model': None,
'name': '2CH Power strip',
'name_by_user': None,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
})
# ---
# name: test_state.4
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Outlet 2',
}),
'context': <ANY>,
'entity_id': 'switch.outlet_2',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_state.5
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.outlet_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'kitchen_sink',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'outlet_2',
'unit_of_measurement': None,
})
# ---
# name: test_state.6
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'kitchen_sink',
'outlet_2',
),
}),
'is_new': False,
'manufacturer': None,
'model': None,
'name': 'Outlet 2',
'name_by_user': None,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': <ANY>,
})
# ---
# name: test_state.7
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'kitchen_sink',
'2_ch_power_strip',
),
}),
'is_new': False,
'manufacturer': None,
'model': None,
'name': '2CH Power strip',
'name_by_user': None,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
})
# ---

View File

@ -0,0 +1,59 @@
"""The tests for the demo button component."""
from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.components.kitchen_sink import DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
ENTITY_RESTART = "button.2ch_power_strip_restart"
@pytest.fixture
async def button_only() -> None:
"""Enable only the button platform."""
with patch(
"homeassistant.components.kitchen_sink.COMPONENTS_WITH_DEMO_PLATFORM",
[Platform.BUTTON],
):
yield
@pytest.fixture(autouse=True)
async def setup_comp(hass: HomeAssistant, button_only):
"""Set up demo component."""
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
await hass.async_block_till_done()
def test_setup_params(hass: HomeAssistant) -> None:
"""Test the initial parameters."""
state = hass.states.get(ENTITY_RESTART)
assert state
assert state.state == STATE_UNKNOWN
async def test_press(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None:
"""Test pressing the button."""
state = hass.states.get(ENTITY_RESTART)
assert state
assert state.state == STATE_UNKNOWN
now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
freezer.move_to(now)
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: ENTITY_RESTART},
blocking=True,
)
state = hass.states.get(ENTITY_RESTART)
assert state
assert state.state == now.isoformat()

View File

@ -0,0 +1,102 @@
"""The tests for the demo switch component."""
from unittest.mock import patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.kitchen_sink import DOMAIN
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
SWITCH_ENTITY_IDS = ["switch.outlet_1", "switch.outlet_2"]
@pytest.fixture
async def switch_only() -> None:
"""Enable only the switch platform."""
with patch(
"homeassistant.components.kitchen_sink.COMPONENTS_WITH_DEMO_PLATFORM",
[Platform.SWITCH],
):
yield
@pytest.fixture(autouse=True)
async def setup_comp(hass, switch_only):
"""Set up demo component."""
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
await hass.async_block_till_done()
async def test_state(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test switch state."""
for entity_id in SWITCH_ENTITY_IDS:
state = hass.states.get(entity_id)
assert state == snapshot
entity_entry = entity_registry.async_get(entity_id)
assert entity_entry == snapshot
sub_device_entry = device_registry.async_get(entity_entry.device_id)
assert sub_device_entry == snapshot
main_device_entry = device_registry.async_get(sub_device_entry.via_device_id)
assert main_device_entry == snapshot
@pytest.mark.parametrize("switch_entity_id", SWITCH_ENTITY_IDS)
async def test_turn_on(hass: HomeAssistant, switch_entity_id) -> None:
"""Test switch turn on method."""
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: switch_entity_id},
blocking=True,
)
state = hass.states.get(switch_entity_id)
assert state.state == STATE_OFF
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: switch_entity_id},
blocking=True,
)
state = hass.states.get(switch_entity_id)
assert state.state == STATE_ON
@pytest.mark.parametrize("switch_entity_id", SWITCH_ENTITY_IDS)
async def test_turn_off(hass: HomeAssistant, switch_entity_id) -> None:
"""Test switch turn off method."""
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: switch_entity_id},
blocking=True,
)
state = hass.states.get(switch_entity_id)
assert state.state == STATE_ON
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: switch_entity_id},
blocking=True,
)
state = hass.states.get(switch_entity_id)
assert state.state == STATE_OFF