Add Button entity component platform (#57642)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
4c5aca93df
commit
d126d88977
20 changed files with 708 additions and 0 deletions
|
@ -24,6 +24,7 @@ homeassistant.components.bmw_connected_drive.*
|
||||||
homeassistant.components.bond.*
|
homeassistant.components.bond.*
|
||||||
homeassistant.components.braviatv.*
|
homeassistant.components.braviatv.*
|
||||||
homeassistant.components.brother.*
|
homeassistant.components.brother.*
|
||||||
|
homeassistant.components.button.*
|
||||||
homeassistant.components.calendar.*
|
homeassistant.components.calendar.*
|
||||||
homeassistant.components.camera.*
|
homeassistant.components.camera.*
|
||||||
homeassistant.components.canary.*
|
homeassistant.components.canary.*
|
||||||
|
|
|
@ -85,6 +85,7 @@ homeassistant/components/brunt/* @eavanvalkenburg
|
||||||
homeassistant/components/bsblan/* @liudger
|
homeassistant/components/bsblan/* @liudger
|
||||||
homeassistant/components/bt_smarthub/* @jxwolstenholme
|
homeassistant/components/bt_smarthub/* @jxwolstenholme
|
||||||
homeassistant/components/buienradar/* @mjj4791 @ties @Robbie1221
|
homeassistant/components/buienradar/* @mjj4791 @ties @Robbie1221
|
||||||
|
homeassistant/components/button/* @home-assistant/core
|
||||||
homeassistant/components/cast/* @emontnemery
|
homeassistant/components/cast/* @emontnemery
|
||||||
homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren
|
homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren
|
||||||
homeassistant/components/circuit/* @braam
|
homeassistant/components/circuit/* @braam
|
||||||
|
|
104
homeassistant/components/button/__init__.py
Normal file
104
homeassistant/components/button/__init__.py
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
"""Component to pressing a button as platforms."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
|
from typing import final
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||||
|
PLATFORM_SCHEMA,
|
||||||
|
PLATFORM_SCHEMA_BASE,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from .const import DOMAIN, SERVICE_PRESS
|
||||||
|
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
|
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up Button entities."""
|
||||||
|
component = hass.data[DOMAIN] = EntityComponent(
|
||||||
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
||||||
|
)
|
||||||
|
await component.async_setup(config)
|
||||||
|
|
||||||
|
component.async_register_entity_service(
|
||||||
|
SERVICE_PRESS,
|
||||||
|
{},
|
||||||
|
"_async_press_action",
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
component: EntityComponent = hass.data[DOMAIN]
|
||||||
|
return await component.async_setup_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
component: EntityComponent = hass.data[DOMAIN]
|
||||||
|
return await component.async_unload_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ButtonEntityDescription(EntityDescription):
|
||||||
|
"""A class that describes button entities."""
|
||||||
|
|
||||||
|
|
||||||
|
class ButtonEntity(RestoreEntity):
|
||||||
|
"""Representation of a Button entity."""
|
||||||
|
|
||||||
|
entity_description: ButtonEntityDescription
|
||||||
|
_attr_should_poll = False
|
||||||
|
_attr_device_class: None = None
|
||||||
|
_attr_state: None = None
|
||||||
|
__last_pressed: datetime | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
@final
|
||||||
|
def state(self) -> str | None:
|
||||||
|
"""Return the entity state."""
|
||||||
|
if self.__last_pressed is None:
|
||||||
|
return None
|
||||||
|
return self.__last_pressed.isoformat()
|
||||||
|
|
||||||
|
@final
|
||||||
|
async def _async_press_action(self) -> None:
|
||||||
|
"""Press the button (from e.g., service call).
|
||||||
|
|
||||||
|
Should not be overridden, handle setting last press timestamp.
|
||||||
|
"""
|
||||||
|
self.__last_pressed = dt_util.utcnow()
|
||||||
|
self.async_write_ha_state()
|
||||||
|
await self.async_press()
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Call when the button is added to hass."""
|
||||||
|
state = await self.async_get_last_state()
|
||||||
|
if state is not None and state.state is not None:
|
||||||
|
self.__last_pressed = dt_util.parse_datetime(state.state)
|
||||||
|
|
||||||
|
def press(self) -> None:
|
||||||
|
"""Press the button."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def async_press(self) -> None:
|
||||||
|
"""Press the button."""
|
||||||
|
await self.hass.async_add_executor_job(self.press)
|
4
homeassistant/components/button/const.py
Normal file
4
homeassistant/components/button/const.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
"""Provides the constants needed for the component."""
|
||||||
|
|
||||||
|
DOMAIN = "button"
|
||||||
|
SERVICE_PRESS = "press"
|
58
homeassistant/components/button/device_action.py
Normal file
58
homeassistant/components/button/device_action.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
"""Provides device actions for Button."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
CONF_DEVICE_ID,
|
||||||
|
CONF_DOMAIN,
|
||||||
|
CONF_ENTITY_ID,
|
||||||
|
CONF_TYPE,
|
||||||
|
)
|
||||||
|
from homeassistant.core import Context, HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
from .const import DOMAIN, SERVICE_PRESS
|
||||||
|
|
||||||
|
ACTION_TYPES = {"press"}
|
||||||
|
|
||||||
|
ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_TYPE): vol.In(ACTION_TYPES),
|
||||||
|
vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_actions(
|
||||||
|
hass: HomeAssistant, device_id: str
|
||||||
|
) -> list[dict[str, str]]:
|
||||||
|
"""List device actions for button devices."""
|
||||||
|
registry = entity_registry.async_get(hass)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
CONF_DEVICE_ID: device_id,
|
||||||
|
CONF_DOMAIN: DOMAIN,
|
||||||
|
CONF_ENTITY_ID: entry.entity_id,
|
||||||
|
CONF_TYPE: "press",
|
||||||
|
}
|
||||||
|
for entry in entity_registry.async_entries_for_device(registry, device_id)
|
||||||
|
if entry.domain == DOMAIN
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_call_action_from_config(
|
||||||
|
hass: HomeAssistant, config: dict, variables: dict, context: Context | None
|
||||||
|
) -> None:
|
||||||
|
"""Execute a device action."""
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_PRESS,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: config[CONF_ENTITY_ID],
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
context=context,
|
||||||
|
)
|
73
homeassistant/components/button/device_trigger.py
Normal file
73
homeassistant/components/button/device_trigger.py
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
"""Provides device triggers for Button."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.automation import (
|
||||||
|
AutomationActionType,
|
||||||
|
AutomationTriggerInfo,
|
||||||
|
)
|
||||||
|
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
|
||||||
|
from homeassistant.components.homeassistant.triggers.state import (
|
||||||
|
TRIGGER_SCHEMA as STATE_TRIGGER_SCHEMA,
|
||||||
|
async_attach_trigger as async_attach_state_trigger,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_DEVICE_ID,
|
||||||
|
CONF_DOMAIN,
|
||||||
|
CONF_ENTITY_ID,
|
||||||
|
CONF_PLATFORM,
|
||||||
|
CONF_TYPE,
|
||||||
|
)
|
||||||
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||||
|
from homeassistant.helpers import config_validation as cv, entity_registry
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from . import DOMAIN
|
||||||
|
|
||||||
|
TRIGGER_TYPES = {"pressed"}
|
||||||
|
|
||||||
|
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||||
|
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_triggers(
|
||||||
|
hass: HomeAssistant, device_id: str
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""List device triggers for button devices."""
|
||||||
|
registry = entity_registry.async_get(hass)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
CONF_PLATFORM: "device",
|
||||||
|
CONF_DEVICE_ID: device_id,
|
||||||
|
CONF_DOMAIN: DOMAIN,
|
||||||
|
CONF_ENTITY_ID: entry.entity_id,
|
||||||
|
CONF_TYPE: "pressed",
|
||||||
|
}
|
||||||
|
for entry in entity_registry.async_entries_for_device(registry, device_id)
|
||||||
|
if entry.domain == DOMAIN
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_attach_trigger(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: ConfigType,
|
||||||
|
action: AutomationActionType,
|
||||||
|
automation_info: AutomationTriggerInfo,
|
||||||
|
) -> CALLBACK_TYPE:
|
||||||
|
"""Attach a trigger."""
|
||||||
|
state_config = {
|
||||||
|
CONF_PLATFORM: "state",
|
||||||
|
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
|
||||||
|
}
|
||||||
|
|
||||||
|
state_config = STATE_TRIGGER_SCHEMA(state_config)
|
||||||
|
return await async_attach_state_trigger(
|
||||||
|
hass, state_config, action, automation_info, platform_type="device"
|
||||||
|
)
|
7
homeassistant/components/button/manifest.json
Normal file
7
homeassistant/components/button/manifest.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"domain": "button",
|
||||||
|
"name": "Button",
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/button",
|
||||||
|
"codeowners": ["@home-assistant/core"],
|
||||||
|
"quality_scale": "internal"
|
||||||
|
}
|
6
homeassistant/components/button/services.yaml
Normal file
6
homeassistant/components/button/services.yaml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
press:
|
||||||
|
name: Press
|
||||||
|
description: Press the button entity.
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: button
|
11
homeassistant/components/button/strings.json
Normal file
11
homeassistant/components/button/strings.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"title": "Button",
|
||||||
|
"device_automation": {
|
||||||
|
"trigger_type": {
|
||||||
|
"pressed": "{entity_name} has been pressed"
|
||||||
|
},
|
||||||
|
"action_type": {
|
||||||
|
"press": "Press {entity_name} button"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
homeassistant/components/button/translations/en.json
Normal file
11
homeassistant/components/button/translations/en.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"device_automation": {
|
||||||
|
"action_type": {
|
||||||
|
"press": "Press {entity_name} button"
|
||||||
|
},
|
||||||
|
"trigger_type": {
|
||||||
|
"pressed": "{entity_name} has been pressed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Button"
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [
|
||||||
"air_quality",
|
"air_quality",
|
||||||
"alarm_control_panel",
|
"alarm_control_panel",
|
||||||
"binary_sensor",
|
"binary_sensor",
|
||||||
|
"button",
|
||||||
"camera",
|
"camera",
|
||||||
"climate",
|
"climate",
|
||||||
"cover",
|
"cover",
|
||||||
|
|
65
homeassistant/components/demo/button.py
Normal file
65
homeassistant/components/demo/button.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
"""Demo platform that offers a fake button entity."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.components.button import ButtonEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
|
from . import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_platform(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: ConfigType,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
discovery_info: DiscoveryInfoType = None,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the demo Button entity."""
|
||||||
|
async_add_entities(
|
||||||
|
[
|
||||||
|
DemoButton(
|
||||||
|
unique_id="push",
|
||||||
|
name="Push",
|
||||||
|
icon="mdi:gesture-tap-button",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the Demo config entry."""
|
||||||
|
await async_setup_platform(hass, {}, async_add_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class DemoButton(ButtonEntity):
|
||||||
|
"""Representation of a demo button entity."""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
unique_id: str,
|
||||||
|
name: str,
|
||||||
|
icon: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Demo button entity."""
|
||||||
|
self._attr_unique_id = unique_id
|
||||||
|
self._attr_name = name or DEVICE_DEFAULT_NAME
|
||||||
|
self._attr_icon = icon
|
||||||
|
self._attr_device_info = {
|
||||||
|
"identifiers": {(DOMAIN, unique_id)},
|
||||||
|
"name": name,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def async_press(self) -> None:
|
||||||
|
"""Send out a persistent notification."""
|
||||||
|
self.hass.components.persistent_notification.async_create(
|
||||||
|
"Button pressed", title="Button"
|
||||||
|
)
|
11
mypy.ini
11
mypy.ini
|
@ -275,6 +275,17 @@ no_implicit_optional = true
|
||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.button.*]
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
disallow_subclassing_any = true
|
||||||
|
disallow_untyped_calls = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
no_implicit_optional = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unreachable = true
|
||||||
|
|
||||||
[mypy-homeassistant.components.calendar.*]
|
[mypy-homeassistant.components.calendar.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
|
|
@ -38,6 +38,7 @@ NO_IOT_CLASS = [
|
||||||
"automation",
|
"automation",
|
||||||
"binary_sensor",
|
"binary_sensor",
|
||||||
"blueprint",
|
"blueprint",
|
||||||
|
"button",
|
||||||
"calendar",
|
"calendar",
|
||||||
"camera",
|
"camera",
|
||||||
"climate",
|
"climate",
|
||||||
|
|
1
tests/components/button/__init__.py
Normal file
1
tests/components/button/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""The tests for the Button integration."""
|
87
tests/components/button/test_device_action.py
Normal file
87
tests/components/button/test_device_action.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
"""The tests for Button device actions."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components import automation
|
||||||
|
from homeassistant.components.button import DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry, entity_registry
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import (
|
||||||
|
MockConfigEntry,
|
||||||
|
assert_lists_same,
|
||||||
|
async_get_device_automations,
|
||||||
|
async_mock_service,
|
||||||
|
mock_device_registry,
|
||||||
|
mock_registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def device_reg(hass: HomeAssistant) -> device_registry.DeviceRegistry:
|
||||||
|
"""Return an empty, loaded, registry."""
|
||||||
|
return mock_device_registry(hass)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def entity_reg(hass: HomeAssistant) -> entity_registry.EntityRegistry:
|
||||||
|
"""Return an empty, loaded, registry."""
|
||||||
|
return mock_registry(hass)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_actions(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
device_reg: device_registry.DeviceRegistry,
|
||||||
|
entity_reg: entity_registry.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test we get the expected actions from a button."""
|
||||||
|
config_entry = MockConfigEntry(domain="test", data={})
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
device_entry = device_reg.async_get_or_create(
|
||||||
|
config_entry_id=config_entry.entry_id,
|
||||||
|
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||||
|
)
|
||||||
|
entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
|
||||||
|
expected_actions = [
|
||||||
|
{
|
||||||
|
"domain": DOMAIN,
|
||||||
|
"type": "press",
|
||||||
|
"device_id": device_entry.id,
|
||||||
|
"entity_id": "button.test_5678",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
actions = await async_get_device_automations(hass, "action", device_entry.id)
|
||||||
|
assert_lists_same(actions, expected_actions)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_action(hass: HomeAssistant) -> None:
|
||||||
|
"""Test for press action."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: [
|
||||||
|
{
|
||||||
|
"trigger": {
|
||||||
|
"platform": "event",
|
||||||
|
"event_type": "test_event",
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"domain": DOMAIN,
|
||||||
|
"device_id": "abcdefgh",
|
||||||
|
"entity_id": "button.entity",
|
||||||
|
"type": "press",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
press_calls = async_mock_service(hass, DOMAIN, "press")
|
||||||
|
|
||||||
|
hass.bus.async_fire("test_event")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(press_calls) == 1
|
||||||
|
assert press_calls[0].domain == DOMAIN
|
||||||
|
assert press_calls[0].service == "press"
|
||||||
|
assert press_calls[0].data == {"entity_id": "button.entity"}
|
108
tests/components/button/test_device_trigger.py
Normal file
108
tests/components/button/test_device_trigger.py
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
"""The tests for Button device triggers."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components import automation
|
||||||
|
from homeassistant.components.button import DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
|
from homeassistant.helpers import device_registry
|
||||||
|
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import (
|
||||||
|
MockConfigEntry,
|
||||||
|
assert_lists_same,
|
||||||
|
async_get_device_automations,
|
||||||
|
async_mock_service,
|
||||||
|
mock_device_registry,
|
||||||
|
mock_registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def device_reg(hass: HomeAssistant) -> device_registry.DeviceRegistry:
|
||||||
|
"""Return an empty, loaded, registry."""
|
||||||
|
return mock_device_registry(hass)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def entity_reg(hass: HomeAssistant) -> EntityRegistry:
|
||||||
|
"""Return an empty, loaded, registry."""
|
||||||
|
return mock_registry(hass)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def calls(hass: HomeAssistant) -> list[ServiceCall]:
|
||||||
|
"""Track calls to a mock service."""
|
||||||
|
return async_mock_service(hass, "test", "automation")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_triggers(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
device_reg: device_registry.DeviceRegistry,
|
||||||
|
entity_reg: EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test we get the expected triggers from a button."""
|
||||||
|
config_entry = MockConfigEntry(domain="test", data={})
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
device_entry = device_reg.async_get_or_create(
|
||||||
|
config_entry_id=config_entry.entry_id,
|
||||||
|
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||||
|
)
|
||||||
|
entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
|
||||||
|
expected_triggers = [
|
||||||
|
{
|
||||||
|
"platform": "device",
|
||||||
|
"domain": DOMAIN,
|
||||||
|
"type": "pressed",
|
||||||
|
"device_id": device_entry.id,
|
||||||
|
"entity_id": f"{DOMAIN}.test_5678",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
|
||||||
|
assert_lists_same(triggers, expected_triggers)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_if_fires_on_state_change(hass, calls):
|
||||||
|
"""Test for turn_on and turn_off triggers firing."""
|
||||||
|
hass.states.async_set("button.entity", "unknown")
|
||||||
|
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: [
|
||||||
|
{
|
||||||
|
"trigger": {
|
||||||
|
"platform": "device",
|
||||||
|
"domain": DOMAIN,
|
||||||
|
"device_id": "",
|
||||||
|
"entity_id": "button.entity",
|
||||||
|
"type": "pressed",
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
"data": {
|
||||||
|
"some": (
|
||||||
|
"to - {{ trigger.platform}} - "
|
||||||
|
"{{ trigger.entity_id}} - {{ trigger.from_state.state}} - "
|
||||||
|
"{{ trigger.to_state.state}} - {{ trigger.for }} - "
|
||||||
|
"{{ trigger.id}}"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test triggering device trigger with a to state
|
||||||
|
hass.states.async_set("button.entity", "2021-01-01T23:59:59+00:00")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0].data[
|
||||||
|
"some"
|
||||||
|
] == "to - device - {} - unknown - 2021-01-01T23:59:59+00:00 - None - 0".format(
|
||||||
|
"button.entity"
|
||||||
|
)
|
64
tests/components/button/test_init.py
Normal file
64
tests/components/button/test_init.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
"""The tests for the Button component."""
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.button import DOMAIN, SERVICE_PRESS, ButtonEntity
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, STATE_UNKNOWN
|
||||||
|
from homeassistant.core import HomeAssistant, State
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from tests.common import mock_restore_cache
|
||||||
|
|
||||||
|
|
||||||
|
async def test_button(hass: HomeAssistant) -> None:
|
||||||
|
"""Test getting data from the mocked button entity."""
|
||||||
|
button = ButtonEntity()
|
||||||
|
assert button.state is None
|
||||||
|
|
||||||
|
button.hass = hass
|
||||||
|
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
await button.async_press()
|
||||||
|
|
||||||
|
button.press = MagicMock()
|
||||||
|
await button.async_press()
|
||||||
|
|
||||||
|
assert button.press.called
|
||||||
|
|
||||||
|
|
||||||
|
async def test_custom_integration(hass, caplog, enable_custom_integrations):
|
||||||
|
"""Test we integration."""
|
||||||
|
platform = getattr(hass.components, f"test.{DOMAIN}")
|
||||||
|
platform.init()
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("button.button_1").state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
with patch("homeassistant.core.dt_util.utcnow", return_value=now):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_PRESS,
|
||||||
|
{ATTR_ENTITY_ID: "button.button_1"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hass.states.get("button.button_1").state == now.isoformat()
|
||||||
|
assert "The button has been pressed" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_restore_state(hass, enable_custom_integrations):
|
||||||
|
"""Test we restore state integration."""
|
||||||
|
mock_restore_cache(hass, (State("button.button_1", "2021-01-01T23:59:59+00:00"),))
|
||||||
|
|
||||||
|
platform = getattr(hass.components, f"test.{DOMAIN}")
|
||||||
|
platform.init()
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("button.button_1").state == "2021-01-01T23:59:59+00:00"
|
47
tests/components/demo/test_button.py
Normal file
47
tests/components/demo/test_button.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
"""The tests for the demo button component."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.button.const import DOMAIN, SERVICE_PRESS
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
ENTITY_PUSH = "button.push"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
async def setup_demo_button(hass: HomeAssistant) -> None:
|
||||||
|
"""Initialize setup demo button entity."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {"button": {"platform": "demo"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_params(hass: HomeAssistant) -> None:
|
||||||
|
"""Test the initial parameters."""
|
||||||
|
state = hass.states.get(ENTITY_PUSH)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
|
async def test_press(hass: HomeAssistant) -> None:
|
||||||
|
"""Test pressing the button."""
|
||||||
|
state = hass.states.get(ENTITY_PUSH)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
|
||||||
|
with patch("homeassistant.util.dt.utcnow", return_value=now):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_PRESS,
|
||||||
|
{ATTR_ENTITY_ID: ENTITY_PUSH},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get(ENTITY_PUSH)
|
||||||
|
assert state
|
||||||
|
assert state.state == now.isoformat()
|
47
tests/testing_config/custom_components/test/button.py
Normal file
47
tests/testing_config/custom_components/test/button.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
"""
|
||||||
|
Provide a mock button platform.
|
||||||
|
|
||||||
|
Call init before using it in your tests to ensure clean test data.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.button import ButtonEntity
|
||||||
|
|
||||||
|
from tests.common import MockEntity
|
||||||
|
|
||||||
|
UNIQUE_BUTTON_1 = "unique_button_1"
|
||||||
|
|
||||||
|
ENTITIES = []
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MockButtonEntity(MockEntity, ButtonEntity):
|
||||||
|
"""Mock Button class."""
|
||||||
|
|
||||||
|
def press(self) -> None:
|
||||||
|
"""Press the button."""
|
||||||
|
_LOGGER.info("The button has been pressed")
|
||||||
|
|
||||||
|
|
||||||
|
def init(empty=False):
|
||||||
|
"""Initialize the platform with entities."""
|
||||||
|
global ENTITIES
|
||||||
|
|
||||||
|
ENTITIES = (
|
||||||
|
[]
|
||||||
|
if empty
|
||||||
|
else [
|
||||||
|
MockButtonEntity(
|
||||||
|
name="button 1",
|
||||||
|
unique_id="unique_button_1",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_platform(
|
||||||
|
hass, config, async_add_entities_callback, discovery_info=None
|
||||||
|
):
|
||||||
|
"""Return mock entities."""
|
||||||
|
async_add_entities_callback(ENTITIES)
|
Loading…
Add table
Add a link
Reference in a new issue