2016-03-09 22:49:54 +00:00
|
|
|
"""An abstract class for entities."""
|
2021-03-17 17:34:19 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2019-11-19 19:42:09 +00:00
|
|
|
from abc import ABC
|
2019-09-29 17:07:49 +00:00
|
|
|
import asyncio
|
2022-04-28 08:52:42 +00:00
|
|
|
from collections.abc import Coroutine, Iterable, Mapping, MutableMapping
|
2021-07-26 20:00:43 +00:00
|
|
|
from dataclasses import dataclass
|
2023-09-06 09:04:49 +00:00
|
|
|
from datetime import timedelta
|
2022-03-08 04:42:16 +00:00
|
|
|
from enum import Enum, auto
|
2016-11-16 05:06:50 +00:00
|
|
|
import functools as ft
|
2019-12-09 15:42:10 +00:00
|
|
|
import logging
|
2021-04-27 19:48:24 +00:00
|
|
|
import math
|
|
|
|
import sys
|
2016-11-04 04:58:25 +00:00
|
|
|
from timeit import default_timer as timer
|
2023-09-12 18:41:26 +00:00
|
|
|
from typing import (
|
|
|
|
TYPE_CHECKING,
|
|
|
|
Any,
|
|
|
|
Final,
|
|
|
|
Literal,
|
|
|
|
NotRequired,
|
|
|
|
TypedDict,
|
|
|
|
TypeVar,
|
|
|
|
final,
|
|
|
|
)
|
2021-10-15 12:28:30 +00:00
|
|
|
|
|
|
|
import voluptuous as vol
|
2016-07-28 03:33:49 +00:00
|
|
|
|
2023-06-27 00:18:46 +00:00
|
|
|
from homeassistant.backports.functools import cached_property
|
2019-12-09 15:42:10 +00:00
|
|
|
from homeassistant.config import DATA_CUSTOMIZE
|
2016-02-19 05:27:50 +00:00
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_ASSUMED_STATE,
|
2021-10-11 21:15:32 +00:00
|
|
|
ATTR_ATTRIBUTION,
|
2019-12-09 15:42:10 +00:00
|
|
|
ATTR_DEVICE_CLASS,
|
|
|
|
ATTR_ENTITY_PICTURE,
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_FRIENDLY_NAME,
|
|
|
|
ATTR_ICON,
|
2019-12-09 15:42:10 +00:00
|
|
|
ATTR_SUPPORTED_FEATURES,
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_UNIT_OF_MEASUREMENT,
|
|
|
|
DEVICE_DEFAULT_NAME,
|
|
|
|
STATE_OFF,
|
|
|
|
STATE_ON,
|
|
|
|
STATE_UNAVAILABLE,
|
|
|
|
STATE_UNKNOWN,
|
2023-02-09 19:15:37 +00:00
|
|
|
EntityCategory,
|
2022-03-30 17:15:00 +00:00
|
|
|
)
|
2023-07-24 08:34:16 +00:00
|
|
|
from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback
|
2023-09-04 12:09:51 +00:00
|
|
|
from homeassistant.exceptions import (
|
|
|
|
HomeAssistantError,
|
|
|
|
InvalidStateError,
|
|
|
|
NoEntitySpecifiedError,
|
|
|
|
)
|
2023-10-04 11:40:33 +00:00
|
|
|
from homeassistant.loader import async_suggest_report_issue, bind_hass
|
2023-09-06 09:04:49 +00:00
|
|
|
from homeassistant.util import ensure_unique_string, slugify
|
2019-07-21 16:59:02 +00:00
|
|
|
|
2022-06-28 16:38:05 +00:00
|
|
|
from . import device_registry as dr, entity_registry as er
|
2023-08-11 02:04:26 +00:00
|
|
|
from .device_registry import DeviceInfo, EventDeviceRegistryUpdatedData
|
2023-05-31 09:01:55 +00:00
|
|
|
from .event import (
|
|
|
|
async_track_device_registry_updated_event,
|
|
|
|
async_track_entity_registry_updated_event,
|
|
|
|
)
|
2023-07-24 07:14:10 +00:00
|
|
|
from .typing import UNDEFINED, EventType, StateType, UndefinedType
|
2021-12-23 19:14:47 +00:00
|
|
|
|
2023-02-09 18:15:53 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
from .entity_platform import EntityPlatform
|
|
|
|
|
2023-06-24 03:34:34 +00:00
|
|
|
|
|
|
|
_T = TypeVar("_T")
|
|
|
|
|
2016-06-22 16:13:18 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2017-03-14 16:26:55 +00:00
|
|
|
SLOW_UPDATE_WARNING = 10
|
2020-08-19 12:57:38 +00:00
|
|
|
DATA_ENTITY_SOURCE = "entity_info"
|
|
|
|
|
2021-04-27 19:48:24 +00:00
|
|
|
# Used when converting float states to string: limit precision according to machine
|
|
|
|
# epsilon to make the string representation readable
|
|
|
|
FLOAT_PRECISION = abs(int(math.floor(math.log10(abs(sys.float_info.epsilon))))) - 1
|
|
|
|
|
2020-08-19 12:57:38 +00:00
|
|
|
|
2023-02-18 13:21:41 +00:00
|
|
|
@callback
|
|
|
|
def async_setup(hass: HomeAssistant) -> None:
|
|
|
|
"""Set up entity sources."""
|
|
|
|
hass.data[DATA_ENTITY_SOURCE] = {}
|
|
|
|
|
|
|
|
|
2020-08-19 12:57:38 +00:00
|
|
|
@callback
|
|
|
|
@bind_hass
|
2023-09-12 18:41:26 +00:00
|
|
|
def entity_sources(hass: HomeAssistant) -> dict[str, EntityInfo]:
|
2020-08-19 12:57:38 +00:00
|
|
|
"""Get the entity sources."""
|
2023-09-12 18:41:26 +00:00
|
|
|
_entity_sources: dict[str, EntityInfo] = hass.data[DATA_ENTITY_SOURCE]
|
2023-02-18 13:21:41 +00:00
|
|
|
return _entity_sources
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2016-01-24 06:37:15 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def generate_entity_id(
|
|
|
|
entity_id_format: str,
|
2021-03-17 17:34:19 +00:00
|
|
|
name: str | None,
|
|
|
|
current_ids: list[str] | None = None,
|
|
|
|
hass: HomeAssistant | None = None,
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> str:
|
2016-03-25 19:35:38 +00:00
|
|
|
"""Generate a unique entity ID based on given entity IDs or used IDs."""
|
2020-07-31 06:50:42 +00:00
|
|
|
return async_generate_entity_id(entity_id_format, name, current_ids, hass)
|
2016-10-01 08:22:13 +00:00
|
|
|
|
|
|
|
|
2017-11-17 05:03:05 +00:00
|
|
|
@callback
|
2019-07-31 19:25:30 +00:00
|
|
|
def async_generate_entity_id(
|
|
|
|
entity_id_format: str,
|
2021-03-17 17:34:19 +00:00
|
|
|
name: str | None,
|
|
|
|
current_ids: Iterable[str] | None = None,
|
|
|
|
hass: HomeAssistant | None = None,
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> str:
|
2016-10-01 08:22:13 +00:00
|
|
|
"""Generate a unique entity ID based on given entity IDs or used IDs."""
|
|
|
|
name = (name or DEVICE_DEFAULT_NAME).lower()
|
2020-07-31 06:50:42 +00:00
|
|
|
preferred_string = entity_id_format.format(slugify(name))
|
|
|
|
|
|
|
|
if current_ids is not None:
|
|
|
|
return ensure_unique_string(preferred_string, current_ids)
|
|
|
|
|
|
|
|
if hass is None:
|
|
|
|
raise ValueError("Missing required parameter current_ids or hass")
|
|
|
|
|
|
|
|
test_string = preferred_string
|
|
|
|
tries = 1
|
2020-10-21 15:01:51 +00:00
|
|
|
while not hass.states.async_available(test_string):
|
2020-07-31 06:50:42 +00:00
|
|
|
tries += 1
|
|
|
|
test_string = f"{preferred_string}_{tries}"
|
2016-10-01 08:22:13 +00:00
|
|
|
|
2020-07-31 06:50:42 +00:00
|
|
|
return test_string
|
2016-01-24 07:00:46 +00:00
|
|
|
|
|
|
|
|
2021-06-21 12:49:51 +00:00
|
|
|
def get_capability(hass: HomeAssistant, entity_id: str, capability: str) -> Any | None:
|
2021-06-11 09:13:55 +00:00
|
|
|
"""Get a capability attribute of an entity.
|
|
|
|
|
|
|
|
First try the statemachine, then entity registry.
|
|
|
|
"""
|
2021-09-18 23:31:35 +00:00
|
|
|
if state := hass.states.get(entity_id):
|
2021-06-11 09:13:55 +00:00
|
|
|
return state.attributes.get(capability)
|
|
|
|
|
|
|
|
entity_registry = er.async_get(hass)
|
2021-09-18 23:31:35 +00:00
|
|
|
if not (entry := entity_registry.async_get(entity_id)):
|
2021-06-11 09:13:55 +00:00
|
|
|
raise HomeAssistantError(f"Unknown entity {entity_id}")
|
|
|
|
|
|
|
|
return entry.capabilities.get(capability) if entry.capabilities else None
|
|
|
|
|
|
|
|
|
2021-06-09 06:28:08 +00:00
|
|
|
def get_device_class(hass: HomeAssistant, entity_id: str) -> str | None:
|
|
|
|
"""Get device class of an entity.
|
|
|
|
|
|
|
|
First try the statemachine, then entity registry.
|
|
|
|
"""
|
2021-09-18 23:31:35 +00:00
|
|
|
if state := hass.states.get(entity_id):
|
2021-06-09 06:28:08 +00:00
|
|
|
return state.attributes.get(ATTR_DEVICE_CLASS)
|
|
|
|
|
|
|
|
entity_registry = er.async_get(hass)
|
2021-09-18 23:31:35 +00:00
|
|
|
if not (entry := entity_registry.async_get(entity_id)):
|
2021-06-09 06:28:08 +00:00
|
|
|
raise HomeAssistantError(f"Unknown entity {entity_id}")
|
|
|
|
|
2021-11-22 16:38:06 +00:00
|
|
|
return entry.device_class or entry.original_device_class
|
2021-06-09 06:28:08 +00:00
|
|
|
|
|
|
|
|
2021-04-10 06:19:16 +00:00
|
|
|
def get_supported_features(hass: HomeAssistant, entity_id: str) -> int:
|
|
|
|
"""Get supported features for an entity.
|
|
|
|
|
|
|
|
First try the statemachine, then entity registry.
|
|
|
|
"""
|
2021-09-18 23:31:35 +00:00
|
|
|
if state := hass.states.get(entity_id):
|
2023-10-11 11:25:11 +00:00
|
|
|
return state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) # type: ignore[no-any-return]
|
2021-04-10 06:19:16 +00:00
|
|
|
|
|
|
|
entity_registry = er.async_get(hass)
|
2021-09-18 23:31:35 +00:00
|
|
|
if not (entry := entity_registry.async_get(entity_id)):
|
2021-04-10 06:19:16 +00:00
|
|
|
raise HomeAssistantError(f"Unknown entity {entity_id}")
|
|
|
|
|
|
|
|
return entry.supported_features or 0
|
|
|
|
|
|
|
|
|
2021-06-09 06:28:08 +00:00
|
|
|
def get_unit_of_measurement(hass: HomeAssistant, entity_id: str) -> str | None:
|
2022-11-25 17:20:33 +00:00
|
|
|
"""Get unit of measurement of an entity.
|
2021-06-09 06:28:08 +00:00
|
|
|
|
|
|
|
First try the statemachine, then entity registry.
|
|
|
|
"""
|
2021-09-18 23:31:35 +00:00
|
|
|
if state := hass.states.get(entity_id):
|
2021-06-09 06:28:08 +00:00
|
|
|
return state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
|
|
|
|
|
|
|
entity_registry = er.async_get(hass)
|
2021-09-18 23:31:35 +00:00
|
|
|
if not (entry := entity_registry.async_get(entity_id)):
|
2021-06-09 06:28:08 +00:00
|
|
|
raise HomeAssistantError(f"Unknown entity {entity_id}")
|
|
|
|
|
|
|
|
return entry.unit_of_measurement
|
|
|
|
|
|
|
|
|
2022-04-01 16:40:43 +00:00
|
|
|
ENTITY_CATEGORIES_SCHEMA: Final = vol.Coerce(EntityCategory)
|
|
|
|
|
|
|
|
|
2023-09-12 18:41:26 +00:00
|
|
|
class EntityInfo(TypedDict):
|
|
|
|
"""Entity info."""
|
|
|
|
|
|
|
|
domain: str
|
|
|
|
custom_component: bool
|
|
|
|
config_entry: NotRequired[str]
|
|
|
|
|
|
|
|
|
2023-09-20 16:09:12 +00:00
|
|
|
class StateInfo(TypedDict):
|
|
|
|
"""State info."""
|
|
|
|
|
|
|
|
unrecorded_attributes: frozenset[str]
|
|
|
|
|
|
|
|
|
2022-03-08 04:42:16 +00:00
|
|
|
class EntityPlatformState(Enum):
|
|
|
|
"""The platform state of an entity."""
|
|
|
|
|
2023-01-08 23:44:09 +00:00
|
|
|
# Not Added: Not yet added to a platform, polling updates
|
|
|
|
# are written to the state machine.
|
2022-03-08 04:42:16 +00:00
|
|
|
NOT_ADDED = auto()
|
|
|
|
|
2023-01-08 23:44:09 +00:00
|
|
|
# Added: Added to a platform, polling updates
|
|
|
|
# are written to the state machine.
|
2022-03-08 04:42:16 +00:00
|
|
|
ADDED = auto()
|
|
|
|
|
2023-01-08 23:44:09 +00:00
|
|
|
# Removed: Removed from a platform, polling updates
|
|
|
|
# are not written to the state machine.
|
2022-03-08 04:42:16 +00:00
|
|
|
REMOVED = auto()
|
|
|
|
|
|
|
|
|
2023-04-11 17:58:28 +00:00
|
|
|
@dataclass(slots=True)
|
2021-07-26 20:00:43 +00:00
|
|
|
class EntityDescription:
|
2021-07-26 22:22:21 +00:00
|
|
|
"""A class that describes Home Assistant entities."""
|
2021-07-26 20:00:43 +00:00
|
|
|
|
|
|
|
# This is the key identifier for this entity
|
|
|
|
key: str
|
|
|
|
|
|
|
|
device_class: str | None = None
|
2022-03-31 22:04:33 +00:00
|
|
|
entity_category: EntityCategory | None = None
|
2021-07-26 20:00:43 +00:00
|
|
|
entity_registry_enabled_default: bool = True
|
2022-04-25 17:10:42 +00:00
|
|
|
entity_registry_visible_default: bool = True
|
2021-07-26 20:00:43 +00:00
|
|
|
force_update: bool = False
|
|
|
|
icon: str | None = None
|
2022-06-28 16:38:05 +00:00
|
|
|
has_entity_name: bool = False
|
2023-06-13 17:48:54 +00:00
|
|
|
name: str | UndefinedType | None = UNDEFINED
|
2022-12-01 08:34:09 +00:00
|
|
|
translation_key: str | None = None
|
2021-07-26 20:00:43 +00:00
|
|
|
unit_of_measurement: str | None = None
|
|
|
|
|
|
|
|
|
2019-11-19 19:42:09 +00:00
|
|
|
class Entity(ABC):
|
2016-03-25 19:35:38 +00:00
|
|
|
"""An abstract class for Home Assistant entities."""
|
2016-01-31 02:03:51 +00:00
|
|
|
|
2015-04-15 02:57:32 +00:00
|
|
|
# SAFE TO OVERWRITE
|
2016-03-25 19:35:38 +00:00
|
|
|
# The properties and methods here are safe to overwrite when inheriting
|
|
|
|
# this class. These may be used to customize the behavior of the entity.
|
2022-02-18 10:31:37 +00:00
|
|
|
entity_id: str = None # type: ignore[assignment]
|
2016-10-01 04:38:39 +00:00
|
|
|
|
2023-06-09 08:56:04 +00:00
|
|
|
# Owning hass instance. Set by EntityPlatform by calling add_to_platform_start
|
2021-03-15 14:11:41 +00:00
|
|
|
# While not purely typed, it makes typehinting more useful for us
|
|
|
|
# and removes the need for constant None checks or asserts.
|
2022-02-18 10:31:37 +00:00
|
|
|
hass: HomeAssistant = None # type: ignore[assignment]
|
2016-10-01 04:38:39 +00:00
|
|
|
|
2023-06-09 08:56:04 +00:00
|
|
|
# Owning platform instance. Set by EntityPlatform by calling add_to_platform_start
|
|
|
|
# While not purely typed, it makes typehinting more useful for us
|
|
|
|
# and removes the need for constant None checks or asserts.
|
|
|
|
platform: EntityPlatform = None # type: ignore[assignment]
|
2018-01-23 06:54:41 +00:00
|
|
|
|
2021-07-26 20:00:43 +00:00
|
|
|
# Entity description instance for this Entity
|
|
|
|
entity_description: EntityDescription
|
|
|
|
|
2016-12-17 21:00:08 +00:00
|
|
|
# If we reported if this entity was slow
|
|
|
|
_slow_reported = False
|
|
|
|
|
2019-08-22 21:12:24 +00:00
|
|
|
# If we reported this entity is updated while disabled
|
|
|
|
_disabled_reported = False
|
|
|
|
|
2023-04-13 17:49:56 +00:00
|
|
|
# If we reported this entity is using async_update_ha_state, while
|
|
|
|
# it should be using async_write_ha_state.
|
|
|
|
_async_update_ha_state_reported = False
|
|
|
|
|
2023-06-15 09:09:53 +00:00
|
|
|
# If we reported this entity is implicitly using device name
|
|
|
|
_implicit_device_name_reported = False
|
|
|
|
|
2023-06-09 13:17:41 +00:00
|
|
|
# If we reported this entity was added without its platform set
|
|
|
|
_no_platform_reported = False
|
|
|
|
|
2017-10-19 08:56:25 +00:00
|
|
|
# Protect for multiple updates
|
|
|
|
_update_staged = False
|
|
|
|
|
2018-01-29 22:37:19 +00:00
|
|
|
# Process updates in parallel
|
2021-03-17 17:34:19 +00:00
|
|
|
parallel_updates: asyncio.Semaphore | None = None
|
2017-03-14 16:26:55 +00:00
|
|
|
|
2019-06-26 16:22:51 +00:00
|
|
|
# Entry in the entity registry
|
2021-11-01 03:21:46 +00:00
|
|
|
registry_entry: er.RegistryEntry | None = None
|
2018-02-11 17:16:01 +00:00
|
|
|
|
2023-07-10 13:17:35 +00:00
|
|
|
# The device entry for this entity
|
|
|
|
device_entry: dr.DeviceEntry | None = None
|
|
|
|
|
2018-07-24 12:12:53 +00:00
|
|
|
# Hold list for functions to call on remove.
|
2021-03-17 17:34:19 +00:00
|
|
|
_on_remove: list[CALLBACK_TYPE] | None = None
|
2018-07-24 12:12:53 +00:00
|
|
|
|
2023-05-31 09:01:55 +00:00
|
|
|
_unsub_device_updates: CALLBACK_TYPE | None = None
|
|
|
|
|
2018-08-20 15:39:53 +00:00
|
|
|
# Context
|
2021-03-17 17:34:19 +00:00
|
|
|
_context: Context | None = None
|
2023-09-06 09:04:49 +00:00
|
|
|
_context_set: float | None = None
|
2018-08-20 15:39:53 +00:00
|
|
|
|
2020-08-19 12:57:38 +00:00
|
|
|
# If entity is added to an entity platform
|
2022-03-08 04:42:16 +00:00
|
|
|
_platform_state = EntityPlatformState.NOT_ADDED
|
2020-08-19 12:57:38 +00:00
|
|
|
|
2023-09-20 16:09:12 +00:00
|
|
|
# Attributes to exclude from recording, only set by base components, e.g. light
|
|
|
|
_entity_component_unrecorded_attributes: frozenset[str] = frozenset()
|
|
|
|
# Additional integration specific attributes to exclude from recording, set by
|
|
|
|
# platforms, e.g. a derived class in hue.light
|
|
|
|
_unrecorded_attributes: frozenset[str] = frozenset()
|
|
|
|
# Union of _entity_component_unrecorded_attributes and _unrecorded_attributes,
|
|
|
|
# set automatically by __init_subclass__
|
|
|
|
__combined_unrecorded_attributes: frozenset[str] = (
|
|
|
|
_entity_component_unrecorded_attributes | _unrecorded_attributes
|
|
|
|
)
|
|
|
|
|
|
|
|
# StateInfo. Set by EntityPlatform by calling async_internal_added_to_hass
|
|
|
|
# While not purely typed, it makes typehinting more useful for us
|
|
|
|
# and removes the need for constant None checks or asserts.
|
|
|
|
_state_info: StateInfo = None # type: ignore[assignment]
|
|
|
|
|
2021-05-22 16:13:50 +00:00
|
|
|
# Entity Properties
|
|
|
|
_attr_assumed_state: bool = False
|
2021-10-11 21:15:32 +00:00
|
|
|
_attr_attribution: str | None = None
|
2021-05-22 16:13:50 +00:00
|
|
|
_attr_available: bool = True
|
2022-08-11 08:34:58 +00:00
|
|
|
_attr_capability_attributes: Mapping[str, Any] | None = None
|
2021-05-22 16:13:50 +00:00
|
|
|
_attr_context_recent_time: timedelta = timedelta(seconds=5)
|
2021-07-26 20:00:43 +00:00
|
|
|
_attr_device_class: str | None
|
2021-05-22 16:13:50 +00:00
|
|
|
_attr_device_info: DeviceInfo | None = None
|
2022-01-24 11:51:11 +00:00
|
|
|
_attr_entity_category: EntityCategory | None
|
2022-06-28 16:38:05 +00:00
|
|
|
_attr_has_entity_name: bool
|
2021-05-22 16:13:50 +00:00
|
|
|
_attr_entity_picture: str | None = None
|
2021-07-26 20:00:43 +00:00
|
|
|
_attr_entity_registry_enabled_default: bool
|
2022-04-22 05:06:34 +00:00
|
|
|
_attr_entity_registry_visible_default: bool
|
2021-07-26 23:25:22 +00:00
|
|
|
_attr_extra_state_attributes: MutableMapping[str, Any]
|
2021-07-26 20:00:43 +00:00
|
|
|
_attr_force_update: bool
|
|
|
|
_attr_icon: str | None
|
2023-05-25 15:20:54 +00:00
|
|
|
_attr_name: str | None
|
2021-05-22 16:13:50 +00:00
|
|
|
_attr_should_poll: bool = True
|
|
|
|
_attr_state: StateType = STATE_UNKNOWN
|
|
|
|
_attr_supported_features: int | None = None
|
2022-12-01 08:34:09 +00:00
|
|
|
_attr_translation_key: str | None
|
2021-05-22 16:13:50 +00:00
|
|
|
_attr_unique_id: str | None = None
|
2021-07-26 20:00:43 +00:00
|
|
|
_attr_unit_of_measurement: str | None
|
2021-05-22 16:13:50 +00:00
|
|
|
|
2023-09-20 16:09:12 +00:00
|
|
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
|
|
"""Initialize an Entity subclass."""
|
|
|
|
super().__init_subclass__(**kwargs)
|
|
|
|
cls.__combined_unrecorded_attributes = (
|
|
|
|
cls._entity_component_unrecorded_attributes | cls._unrecorded_attributes
|
|
|
|
)
|
|
|
|
|
2015-03-22 01:49:30 +00:00
|
|
|
@property
|
2016-07-28 03:33:49 +00:00
|
|
|
def should_poll(self) -> bool:
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Return True if entity has to be polled for state.
|
2016-01-31 02:03:51 +00:00
|
|
|
|
2015-03-22 01:49:30 +00:00
|
|
|
False if entity pushes its state to HA.
|
|
|
|
"""
|
2021-05-22 16:13:50 +00:00
|
|
|
return self._attr_should_poll
|
2015-03-22 01:49:30 +00:00
|
|
|
|
|
|
|
@property
|
2021-03-17 17:34:19 +00:00
|
|
|
def unique_id(self) -> str | None:
|
2018-03-03 18:23:55 +00:00
|
|
|
"""Return a unique ID."""
|
2021-05-22 16:13:50 +00:00
|
|
|
return self._attr_unique_id
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2023-06-24 19:16:28 +00:00
|
|
|
def _report_implicit_device_name(self) -> None:
|
|
|
|
"""Report entities which use implicit device name."""
|
|
|
|
if self._implicit_device_name_reported:
|
|
|
|
return
|
|
|
|
report_issue = self._suggest_report_issue()
|
|
|
|
_LOGGER.warning(
|
|
|
|
(
|
|
|
|
"Entity %s (%s) is implicitly using device name by not setting its "
|
|
|
|
"name. Instead, the name should be set to None, please %s"
|
|
|
|
),
|
|
|
|
self.entity_id,
|
|
|
|
type(self),
|
|
|
|
report_issue,
|
|
|
|
)
|
|
|
|
self._implicit_device_name_reported = True
|
|
|
|
|
2023-06-15 09:09:53 +00:00
|
|
|
@property
|
|
|
|
def use_device_name(self) -> bool:
|
|
|
|
"""Return if this entity does not have its own name.
|
|
|
|
|
|
|
|
Should be True if the entity represents the single main feature of a device.
|
|
|
|
"""
|
|
|
|
if hasattr(self, "_attr_name"):
|
|
|
|
return not self._attr_name
|
|
|
|
|
2023-06-27 00:18:46 +00:00
|
|
|
if name_translation_key := self._name_translation_key:
|
2023-06-15 09:09:53 +00:00
|
|
|
if name_translation_key in self.platform.platform_translations:
|
|
|
|
return False
|
|
|
|
|
|
|
|
if hasattr(self, "entity_description"):
|
|
|
|
if not (name := self.entity_description.name):
|
|
|
|
return True
|
2023-06-22 09:30:19 +00:00
|
|
|
if name is UNDEFINED and not self._default_to_device_class_name():
|
2023-06-15 09:09:53 +00:00
|
|
|
# Backwards compatibility with leaving EntityDescription.name unassigned
|
|
|
|
# for device name.
|
|
|
|
# Deprecated in HA Core 2023.6, remove in HA Core 2023.9
|
2023-06-24 19:16:28 +00:00
|
|
|
self._report_implicit_device_name()
|
2023-06-15 09:09:53 +00:00
|
|
|
return True
|
|
|
|
return False
|
2023-06-22 09:30:19 +00:00
|
|
|
if self.name is UNDEFINED and not self._default_to_device_class_name():
|
2023-06-15 09:09:53 +00:00
|
|
|
# Backwards compatibility with not overriding name property for device name.
|
|
|
|
# Deprecated in HA Core 2023.6, remove in HA Core 2023.9
|
2023-06-24 19:16:28 +00:00
|
|
|
self._report_implicit_device_name()
|
2023-06-15 09:09:53 +00:00
|
|
|
return True
|
|
|
|
return not self.name
|
|
|
|
|
2022-06-28 16:38:05 +00:00
|
|
|
@property
|
|
|
|
def has_entity_name(self) -> bool:
|
|
|
|
"""Return if the name of the entity is describing only the entity itself."""
|
|
|
|
if hasattr(self, "_attr_has_entity_name"):
|
|
|
|
return self._attr_has_entity_name
|
|
|
|
if hasattr(self, "entity_description"):
|
|
|
|
return self.entity_description.has_entity_name
|
|
|
|
return False
|
|
|
|
|
2023-06-27 12:37:50 +00:00
|
|
|
def _device_class_name_helper(
|
|
|
|
self,
|
|
|
|
component_translations: dict[str, Any],
|
|
|
|
) -> str | None:
|
2023-06-13 17:48:54 +00:00
|
|
|
"""Return a translated name of the entity based on its device class."""
|
|
|
|
if not self.has_entity_name:
|
|
|
|
return None
|
|
|
|
device_class_key = self.device_class or "_"
|
2023-06-25 13:50:48 +00:00
|
|
|
platform = self.platform
|
2023-06-13 17:48:54 +00:00
|
|
|
name_translation_key = (
|
2023-06-27 12:37:50 +00:00
|
|
|
f"component.{platform.domain}.entity_component.{device_class_key}.name"
|
|
|
|
)
|
|
|
|
return component_translations.get(name_translation_key)
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def _object_id_device_class_name(self) -> str | None:
|
|
|
|
"""Return a translated name of the entity based on its device class."""
|
|
|
|
return self._device_class_name_helper(
|
|
|
|
self.platform.object_id_component_translations
|
2023-06-25 13:50:48 +00:00
|
|
|
)
|
2023-06-27 12:37:50 +00:00
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def _device_class_name(self) -> str | None:
|
|
|
|
"""Return a translated name of the entity based on its device class."""
|
|
|
|
return self._device_class_name_helper(self.platform.component_translations)
|
2023-06-13 17:48:54 +00:00
|
|
|
|
|
|
|
def _default_to_device_class_name(self) -> bool:
|
|
|
|
"""Return True if an unnamed entity should be named by its device class."""
|
|
|
|
return False
|
|
|
|
|
2023-06-27 00:18:46 +00:00
|
|
|
@cached_property
|
2023-06-15 09:09:53 +00:00
|
|
|
def _name_translation_key(self) -> str | None:
|
|
|
|
"""Return translation key for entity name."""
|
|
|
|
if self.translation_key is None:
|
|
|
|
return None
|
2023-06-25 13:50:48 +00:00
|
|
|
platform = self.platform
|
2023-06-27 00:18:46 +00:00
|
|
|
return (
|
2023-06-25 13:50:48 +00:00
|
|
|
f"component.{platform.platform_name}.entity.{platform.domain}"
|
2023-06-15 09:09:53 +00:00
|
|
|
f".{self.translation_key}.name"
|
|
|
|
)
|
|
|
|
|
2023-06-27 12:37:50 +00:00
|
|
|
def _name_internal(
|
|
|
|
self,
|
|
|
|
device_class_name: str | None,
|
|
|
|
platform_translations: dict[str, Any],
|
|
|
|
) -> str | UndefinedType | None:
|
2016-01-31 02:03:51 +00:00
|
|
|
"""Return the name of the entity."""
|
2021-07-26 20:00:43 +00:00
|
|
|
if hasattr(self, "_attr_name"):
|
|
|
|
return self._attr_name
|
2023-06-25 13:50:48 +00:00
|
|
|
if (
|
|
|
|
self.has_entity_name
|
2023-06-27 00:18:46 +00:00
|
|
|
and (name_translation_key := self._name_translation_key)
|
2023-06-27 12:37:50 +00:00
|
|
|
and (name := platform_translations.get(name_translation_key))
|
2023-06-15 09:09:53 +00:00
|
|
|
):
|
2023-06-25 13:50:48 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
assert isinstance(name, str)
|
|
|
|
return name
|
2021-07-26 20:00:43 +00:00
|
|
|
if hasattr(self, "entity_description"):
|
2023-06-13 17:48:54 +00:00
|
|
|
description_name = self.entity_description.name
|
|
|
|
if description_name is UNDEFINED and self._default_to_device_class_name():
|
2023-06-27 12:37:50 +00:00
|
|
|
return device_class_name
|
2023-06-15 09:09:53 +00:00
|
|
|
return description_name
|
2023-06-13 17:48:54 +00:00
|
|
|
|
|
|
|
# The entity has no name set by _attr_name, translation_key or entity_description
|
|
|
|
# Check if the entity should be named by its device class
|
|
|
|
if self._default_to_device_class_name():
|
2023-06-27 12:37:50 +00:00
|
|
|
return device_class_name
|
2023-06-15 09:09:53 +00:00
|
|
|
return UNDEFINED
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2023-06-27 12:37:50 +00:00
|
|
|
@property
|
|
|
|
def suggested_object_id(self) -> str | None:
|
|
|
|
"""Return input for object id."""
|
|
|
|
# The check for self.platform guards against integrations not using an
|
|
|
|
# EntityComponent and can be removed in HA Core 2024.1
|
|
|
|
# mypy doesn't know about fget: https://github.com/python/mypy/issues/6185
|
|
|
|
if self.__class__.name.fget is Entity.name.fget and self.platform: # type: ignore[attr-defined]
|
|
|
|
name = self._name_internal(
|
|
|
|
self._object_id_device_class_name,
|
|
|
|
self.platform.object_id_platform_translations,
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
name = self.name
|
|
|
|
return None if name is UNDEFINED else name
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self) -> str | UndefinedType | None:
|
|
|
|
"""Return the name of the entity."""
|
|
|
|
# The check for self.platform guards against integrations not using an
|
|
|
|
# EntityComponent and can be removed in HA Core 2024.1
|
|
|
|
if not self.platform:
|
|
|
|
return self._name_internal(None, {})
|
|
|
|
return self._name_internal(
|
|
|
|
self._device_class_name,
|
|
|
|
self.platform.platform_translations,
|
|
|
|
)
|
|
|
|
|
2015-03-22 01:49:30 +00:00
|
|
|
@property
|
2020-08-17 19:02:43 +00:00
|
|
|
def state(self) -> StateType:
|
2016-01-31 02:03:51 +00:00
|
|
|
"""Return the state of the entity."""
|
2021-05-22 16:13:50 +00:00
|
|
|
return self._attr_state
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2019-12-02 19:15:50 +00:00
|
|
|
@property
|
2021-03-31 17:34:00 +00:00
|
|
|
def capability_attributes(self) -> Mapping[str, Any] | None:
|
2019-12-02 19:15:50 +00:00
|
|
|
"""Return the capability attributes.
|
|
|
|
|
|
|
|
Attributes that explain the capabilities of an entity.
|
|
|
|
|
|
|
|
Implemented by component base class. Convention for attribute names
|
|
|
|
is lowercase snake_case.
|
|
|
|
"""
|
2022-08-11 08:34:58 +00:00
|
|
|
return self._attr_capability_attributes
|
2019-12-02 19:15:50 +00:00
|
|
|
|
2022-10-24 14:08:02 +00:00
|
|
|
def get_initial_entity_options(self) -> er.EntityOptionsType | None:
|
|
|
|
"""Return initial entity options.
|
|
|
|
|
|
|
|
These will be stored in the entity registry the first time the entity is seen,
|
|
|
|
and then never updated.
|
|
|
|
|
|
|
|
Implemented by component base class, should not be extended by integrations.
|
|
|
|
|
|
|
|
Note: Not a property to avoid calculating unless needed.
|
|
|
|
"""
|
|
|
|
return None
|
|
|
|
|
2015-03-22 01:49:30 +00:00
|
|
|
@property
|
2021-03-17 17:34:19 +00:00
|
|
|
def state_attributes(self) -> dict[str, Any] | None:
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Return the state attributes.
|
2016-02-07 06:28:29 +00:00
|
|
|
|
2021-03-09 12:58:43 +00:00
|
|
|
Implemented by component base class, should not be extended by integrations.
|
|
|
|
Convention for attribute names is lowercase snake_case.
|
2016-02-07 06:28:29 +00:00
|
|
|
"""
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
2021-03-31 17:34:00 +00:00
|
|
|
def device_state_attributes(self) -> Mapping[str, Any] | None:
|
2021-03-09 12:58:43 +00:00
|
|
|
"""Return entity specific state attributes.
|
|
|
|
|
|
|
|
This method is deprecated, platform classes should implement
|
|
|
|
extra_state_attributes instead.
|
|
|
|
"""
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
2021-03-31 17:34:00 +00:00
|
|
|
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
2021-03-09 12:58:43 +00:00
|
|
|
"""Return entity specific state attributes.
|
2016-02-07 06:28:29 +00:00
|
|
|
|
2019-10-07 15:16:26 +00:00
|
|
|
Implemented by platform classes. Convention for attribute names
|
|
|
|
is lowercase snake_case.
|
2016-02-07 06:28:29 +00:00
|
|
|
"""
|
2021-07-26 23:25:22 +00:00
|
|
|
if hasattr(self, "_attr_extra_state_attributes"):
|
|
|
|
return self._attr_extra_state_attributes
|
|
|
|
return None
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2018-08-22 08:46:37 +00:00
|
|
|
@property
|
2021-04-30 21:21:39 +00:00
|
|
|
def device_info(self) -> DeviceInfo | None:
|
2018-08-22 08:46:37 +00:00
|
|
|
"""Return device specific attributes.
|
|
|
|
|
|
|
|
Implemented by platform classes.
|
|
|
|
"""
|
2021-05-22 16:13:50 +00:00
|
|
|
return self._attr_device_info
|
2018-08-22 08:46:37 +00:00
|
|
|
|
2023-09-15 09:25:24 +00:00
|
|
|
@property
|
2021-03-17 17:34:19 +00:00
|
|
|
def device_class(self) -> str | None:
|
2017-02-11 04:46:15 +00:00
|
|
|
"""Return the class of this device, from component DEVICE_CLASSES."""
|
2021-07-26 20:00:43 +00:00
|
|
|
if hasattr(self, "_attr_device_class"):
|
|
|
|
return self._attr_device_class
|
|
|
|
if hasattr(self, "entity_description"):
|
|
|
|
return self.entity_description.device_class
|
|
|
|
return None
|
2017-02-11 04:46:15 +00:00
|
|
|
|
2015-03-22 01:49:30 +00:00
|
|
|
@property
|
2021-03-17 17:34:19 +00:00
|
|
|
def unit_of_measurement(self) -> str | None:
|
2016-01-31 02:03:51 +00:00
|
|
|
"""Return the unit of measurement of this entity, if any."""
|
2021-07-26 20:00:43 +00:00
|
|
|
if hasattr(self, "_attr_unit_of_measurement"):
|
|
|
|
return self._attr_unit_of_measurement
|
|
|
|
if hasattr(self, "entity_description"):
|
|
|
|
return self.entity_description.unit_of_measurement
|
|
|
|
return None
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2015-11-03 08:20:48 +00:00
|
|
|
@property
|
2021-03-17 17:34:19 +00:00
|
|
|
def icon(self) -> str | None:
|
2016-01-31 02:03:51 +00:00
|
|
|
"""Return the icon to use in the frontend, if any."""
|
2021-07-26 20:00:43 +00:00
|
|
|
if hasattr(self, "_attr_icon"):
|
|
|
|
return self._attr_icon
|
|
|
|
if hasattr(self, "entity_description"):
|
|
|
|
return self.entity_description.icon
|
|
|
|
return None
|
2015-11-03 08:20:48 +00:00
|
|
|
|
2016-02-24 06:41:24 +00:00
|
|
|
@property
|
2021-03-17 17:34:19 +00:00
|
|
|
def entity_picture(self) -> str | None:
|
2016-02-24 06:41:24 +00:00
|
|
|
"""Return the entity picture to use in the frontend, if any."""
|
2021-05-22 16:13:50 +00:00
|
|
|
return self._attr_entity_picture
|
2016-02-24 06:41:24 +00:00
|
|
|
|
2016-01-31 02:03:51 +00:00
|
|
|
@property
|
2016-07-28 03:33:49 +00:00
|
|
|
def available(self) -> bool:
|
2016-01-31 02:03:51 +00:00
|
|
|
"""Return True if entity is available."""
|
2021-05-22 16:13:50 +00:00
|
|
|
return self._attr_available
|
2016-01-31 02:03:51 +00:00
|
|
|
|
2016-02-14 07:42:11 +00:00
|
|
|
@property
|
2016-07-28 03:33:49 +00:00
|
|
|
def assumed_state(self) -> bool:
|
2016-03-25 19:35:38 +00:00
|
|
|
"""Return True if unable to access real state of the entity."""
|
2021-05-22 16:13:50 +00:00
|
|
|
return self._attr_assumed_state
|
2016-02-14 07:42:11 +00:00
|
|
|
|
2016-06-26 07:33:23 +00:00
|
|
|
@property
|
2016-07-28 03:33:49 +00:00
|
|
|
def force_update(self) -> bool:
|
2016-06-26 07:33:23 +00:00
|
|
|
"""Return True if state updates should be forced.
|
|
|
|
|
|
|
|
If True, a state change will be triggered anytime the state property is
|
|
|
|
updated, not just when the value changes.
|
|
|
|
"""
|
2021-07-26 20:00:43 +00:00
|
|
|
if hasattr(self, "_attr_force_update"):
|
|
|
|
return self._attr_force_update
|
|
|
|
if hasattr(self, "entity_description"):
|
|
|
|
return self.entity_description.force_update
|
|
|
|
return False
|
2016-06-26 07:33:23 +00:00
|
|
|
|
2017-02-08 04:42:45 +00:00
|
|
|
@property
|
2021-03-17 17:34:19 +00:00
|
|
|
def supported_features(self) -> int | None:
|
2017-02-08 04:42:45 +00:00
|
|
|
"""Flag supported features."""
|
2021-05-22 16:13:50 +00:00
|
|
|
return self._attr_supported_features
|
2017-02-08 04:42:45 +00:00
|
|
|
|
2018-08-20 15:39:53 +00:00
|
|
|
@property
|
2019-09-24 21:20:04 +00:00
|
|
|
def context_recent_time(self) -> timedelta:
|
2018-08-20 15:39:53 +00:00
|
|
|
"""Time that a context is considered recent."""
|
2021-05-22 16:13:50 +00:00
|
|
|
return self._attr_context_recent_time
|
2018-08-20 15:39:53 +00:00
|
|
|
|
2019-08-16 23:17:16 +00:00
|
|
|
@property
|
2019-09-24 21:20:04 +00:00
|
|
|
def entity_registry_enabled_default(self) -> bool:
|
2023-01-08 23:44:09 +00:00
|
|
|
"""Return if the entity should be enabled when first added.
|
|
|
|
|
|
|
|
This only applies when fist added to the entity registry.
|
|
|
|
"""
|
2021-07-26 20:00:43 +00:00
|
|
|
if hasattr(self, "_attr_entity_registry_enabled_default"):
|
|
|
|
return self._attr_entity_registry_enabled_default
|
|
|
|
if hasattr(self, "entity_description"):
|
|
|
|
return self.entity_description.entity_registry_enabled_default
|
|
|
|
return True
|
2022-04-22 05:06:34 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def entity_registry_visible_default(self) -> bool:
|
2023-01-08 23:44:09 +00:00
|
|
|
"""Return if the entity should be visible when first added.
|
|
|
|
|
|
|
|
This only applies when fist added to the entity registry.
|
|
|
|
"""
|
2022-04-22 05:06:34 +00:00
|
|
|
if hasattr(self, "_attr_entity_registry_visible_default"):
|
|
|
|
return self._attr_entity_registry_visible_default
|
|
|
|
if hasattr(self, "entity_description"):
|
|
|
|
return self.entity_description.entity_registry_visible_default
|
|
|
|
return True
|
2019-08-16 23:17:16 +00:00
|
|
|
|
2023-09-15 09:25:24 +00:00
|
|
|
@property
|
2021-10-11 21:15:32 +00:00
|
|
|
def attribution(self) -> str | None:
|
|
|
|
"""Return the attribution."""
|
|
|
|
return self._attr_attribution
|
|
|
|
|
2021-10-14 08:04:26 +00:00
|
|
|
@property
|
2022-03-31 22:04:33 +00:00
|
|
|
def entity_category(self) -> EntityCategory | None:
|
2021-10-14 08:04:26 +00:00
|
|
|
"""Return the category of the entity, if any."""
|
|
|
|
if hasattr(self, "_attr_entity_category"):
|
|
|
|
return self._attr_entity_category
|
|
|
|
if hasattr(self, "entity_description"):
|
|
|
|
return self.entity_description.entity_category
|
|
|
|
return None
|
|
|
|
|
2023-09-15 09:25:24 +00:00
|
|
|
@property
|
2022-12-01 08:34:09 +00:00
|
|
|
def translation_key(self) -> str | None:
|
|
|
|
"""Return the translation key to translate the entity's states."""
|
|
|
|
if hasattr(self, "_attr_translation_key"):
|
|
|
|
return self._attr_translation_key
|
|
|
|
if hasattr(self, "entity_description"):
|
|
|
|
return self.entity_description.translation_key
|
|
|
|
return None
|
|
|
|
|
2015-04-15 02:57:32 +00:00
|
|
|
# DO NOT OVERWRITE
|
|
|
|
# These properties and methods are either managed by Home Assistant or they
|
|
|
|
# are used to perform a very specific function. Overwriting these may
|
|
|
|
# produce undesirable effects in the entity's operation.
|
|
|
|
|
2019-08-28 20:38:20 +00:00
|
|
|
@property
|
2019-09-24 21:20:04 +00:00
|
|
|
def enabled(self) -> bool:
|
2019-09-24 21:21:00 +00:00
|
|
|
"""Return if the entity is enabled in the entity registry.
|
|
|
|
|
|
|
|
If an entity is not part of the registry, it cannot be disabled
|
|
|
|
and will therefore always be enabled.
|
|
|
|
"""
|
2019-08-28 20:38:20 +00:00
|
|
|
return self.registry_entry is None or not self.registry_entry.disabled
|
|
|
|
|
2018-08-20 15:39:53 +00:00
|
|
|
@callback
|
2019-09-24 21:20:04 +00:00
|
|
|
def async_set_context(self, context: Context) -> None:
|
2018-08-20 15:39:53 +00:00
|
|
|
"""Set the context the entity currently operates under."""
|
|
|
|
self._context = context
|
2023-09-06 09:04:49 +00:00
|
|
|
self._context_set = self.hass.loop.time()
|
2018-08-20 15:39:53 +00:00
|
|
|
|
2020-06-06 18:34:56 +00:00
|
|
|
async def async_update_ha_state(self, force_refresh: bool = False) -> None:
|
2016-09-30 19:57:24 +00:00
|
|
|
"""Update Home Assistant with current state of entity.
|
|
|
|
|
|
|
|
If force_refresh == True will update entity before setting state.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2015-03-22 01:49:30 +00:00
|
|
|
if self.hass is None:
|
2019-08-23 16:53:33 +00:00
|
|
|
raise RuntimeError(f"Attribute hass is None for {self}")
|
2015-03-22 01:49:30 +00:00
|
|
|
|
|
|
|
if self.entity_id is None:
|
|
|
|
raise NoEntitySpecifiedError(
|
2019-08-23 16:53:33 +00:00
|
|
|
f"No entity id specified for entity {self.name}"
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2017-03-14 16:26:55 +00:00
|
|
|
# update entity data
|
2015-03-22 01:49:30 +00:00
|
|
|
if force_refresh:
|
2017-03-14 16:26:55 +00:00
|
|
|
try:
|
2018-10-01 06:56:50 +00:00
|
|
|
await self.async_device_update()
|
2017-03-14 16:26:55 +00:00
|
|
|
except Exception: # pylint: disable=broad-except
|
2017-05-02 16:18:47 +00:00
|
|
|
_LOGGER.exception("Update for %s fails", self.entity_id)
|
2017-03-14 16:26:55 +00:00
|
|
|
return
|
2023-04-13 20:39:03 +00:00
|
|
|
elif not self._async_update_ha_state_reported:
|
2023-04-13 17:49:56 +00:00
|
|
|
report_issue = self._suggest_report_issue()
|
|
|
|
_LOGGER.warning(
|
|
|
|
(
|
|
|
|
"Entity %s (%s) is using self.async_update_ha_state(), without"
|
|
|
|
" enabling force_update. Instead it should use"
|
|
|
|
" self.async_write_ha_state(), please %s"
|
|
|
|
),
|
|
|
|
self.entity_id,
|
|
|
|
type(self),
|
|
|
|
report_issue,
|
|
|
|
)
|
|
|
|
self._async_update_ha_state_reported = True
|
|
|
|
|
2019-03-09 17:52:22 +00:00
|
|
|
self._async_write_ha_state()
|
|
|
|
|
|
|
|
@callback
|
2020-01-22 20:36:25 +00:00
|
|
|
def async_write_ha_state(self) -> None:
|
2019-03-09 17:52:22 +00:00
|
|
|
"""Write the state to the state machine."""
|
|
|
|
if self.hass is None:
|
2019-08-23 16:53:33 +00:00
|
|
|
raise RuntimeError(f"Attribute hass is None for {self}")
|
2019-03-09 17:52:22 +00:00
|
|
|
|
2023-06-09 13:17:41 +00:00
|
|
|
# The check for self.platform guards against integrations not using an
|
|
|
|
# EntityComponent and can be removed in HA Core 2024.1
|
|
|
|
if self.platform is None and not self._no_platform_reported: # type: ignore[unreachable]
|
|
|
|
report_issue = self._suggest_report_issue() # type: ignore[unreachable]
|
|
|
|
_LOGGER.warning(
|
|
|
|
(
|
|
|
|
"Entity %s (%s) does not have a platform, this may be caused by "
|
|
|
|
"adding it manually instead of with an EntityComponent helper"
|
|
|
|
", please %s"
|
|
|
|
),
|
|
|
|
self.entity_id,
|
|
|
|
type(self),
|
|
|
|
report_issue,
|
|
|
|
)
|
|
|
|
self._no_platform_reported = True
|
|
|
|
|
2019-03-09 17:52:22 +00:00
|
|
|
if self.entity_id is None:
|
|
|
|
raise NoEntitySpecifiedError(
|
2019-08-23 16:53:33 +00:00
|
|
|
f"No entity id specified for entity {self.name}"
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-03-09 17:52:22 +00:00
|
|
|
|
2020-06-06 18:34:56 +00:00
|
|
|
self._async_write_ha_state()
|
2019-03-09 17:52:22 +00:00
|
|
|
|
2022-03-09 20:39:43 +00:00
|
|
|
def _stringify_state(self, available: bool) -> str:
|
2021-04-27 19:48:24 +00:00
|
|
|
"""Convert state to string."""
|
2022-03-09 20:39:43 +00:00
|
|
|
if not available:
|
2021-04-27 19:48:24 +00:00
|
|
|
return STATE_UNAVAILABLE
|
2021-09-18 23:31:35 +00:00
|
|
|
if (state := self.state) is None:
|
2021-04-27 19:48:24 +00:00
|
|
|
return STATE_UNKNOWN
|
|
|
|
if isinstance(state, float):
|
|
|
|
# If the entity's state is a float, limit precision according to machine
|
|
|
|
# epsilon to make the string representation readable
|
|
|
|
return f"{state:.{FLOAT_PRECISION}}"
|
|
|
|
return str(state)
|
|
|
|
|
2023-03-18 00:32:24 +00:00
|
|
|
def _friendly_name_internal(self) -> str | None:
|
|
|
|
"""Return the friendly name.
|
|
|
|
|
|
|
|
If has_entity_name is False, this returns self.name
|
|
|
|
If has_entity_name is True, this returns device.name + self.name
|
|
|
|
"""
|
2023-06-15 09:09:53 +00:00
|
|
|
name = self.name
|
|
|
|
if name is UNDEFINED:
|
|
|
|
name = None
|
|
|
|
|
2023-07-10 13:17:35 +00:00
|
|
|
if not self.has_entity_name or not (device_entry := self.device_entry):
|
2023-06-15 09:09:53 +00:00
|
|
|
return name
|
2023-03-18 00:32:24 +00:00
|
|
|
|
2023-07-06 15:14:09 +00:00
|
|
|
device_name = device_entry.name_by_user or device_entry.name
|
2023-06-15 09:09:53 +00:00
|
|
|
if self.use_device_name:
|
2023-07-06 15:14:09 +00:00
|
|
|
return device_name
|
|
|
|
return f"{device_name} {name}" if device_name else name
|
2023-03-18 00:32:24 +00:00
|
|
|
|
2019-03-09 17:52:22 +00:00
|
|
|
@callback
|
2023-08-22 08:29:16 +00:00
|
|
|
def _async_generate_attributes(self) -> tuple[str, dict[str, Any]]:
|
|
|
|
"""Calculate state string and attribute mapping."""
|
2023-03-18 00:32:24 +00:00
|
|
|
entry = self.registry_entry
|
|
|
|
|
2019-12-31 13:29:43 +00:00
|
|
|
attr = self.capability_attributes
|
|
|
|
attr = dict(attr) if attr else {}
|
|
|
|
|
2022-03-09 20:39:43 +00:00
|
|
|
available = self.available # only call self.available once per update cycle
|
|
|
|
state = self._stringify_state(available)
|
|
|
|
if available:
|
2019-04-17 16:52:08 +00:00
|
|
|
attr.update(self.state_attributes or {})
|
2022-03-09 20:39:43 +00:00
|
|
|
attr.update(self.extra_state_attributes or {})
|
2016-02-07 06:28:29 +00:00
|
|
|
|
2022-03-09 20:39:43 +00:00
|
|
|
if (unit_of_measurement := self.unit_of_measurement) is not None:
|
2018-02-08 11:16:51 +00:00
|
|
|
attr[ATTR_UNIT_OF_MEASUREMENT] = unit_of_measurement
|
|
|
|
|
2021-11-18 10:51:32 +00:00
|
|
|
if assumed_state := self.assumed_state:
|
|
|
|
attr[ATTR_ASSUMED_STATE] = assumed_state
|
2018-02-08 11:16:51 +00:00
|
|
|
|
2021-11-18 10:51:32 +00:00
|
|
|
if (attribution := self.attribution) is not None:
|
|
|
|
attr[ATTR_ATTRIBUTION] = attribution
|
|
|
|
|
2021-11-22 16:38:06 +00:00
|
|
|
if (
|
|
|
|
device_class := (entry and entry.device_class) or self.device_class
|
|
|
|
) is not None:
|
2021-11-18 10:51:32 +00:00
|
|
|
attr[ATTR_DEVICE_CLASS] = str(device_class)
|
2018-02-08 11:16:51 +00:00
|
|
|
|
2021-09-18 23:31:35 +00:00
|
|
|
if (entity_picture := self.entity_picture) is not None:
|
2018-02-08 11:16:51 +00:00
|
|
|
attr[ATTR_ENTITY_PICTURE] = entity_picture
|
2016-02-01 01:11:16 +00:00
|
|
|
|
2021-11-18 10:51:32 +00:00
|
|
|
if (icon := (entry and entry.icon) or self.icon) is not None:
|
|
|
|
attr[ATTR_ICON] = icon
|
|
|
|
|
2023-03-18 00:32:24 +00:00
|
|
|
if (
|
|
|
|
name := (entry and entry.name) or self._friendly_name_internal()
|
|
|
|
) is not None:
|
2021-11-18 10:51:32 +00:00
|
|
|
attr[ATTR_FRIENDLY_NAME] = name
|
2018-02-08 11:16:51 +00:00
|
|
|
|
2021-09-18 23:31:35 +00:00
|
|
|
if (supported_features := self.supported_features) is not None:
|
2018-02-08 11:16:51 +00:00
|
|
|
attr[ATTR_SUPPORTED_FEATURES] = supported_features
|
|
|
|
|
2023-08-22 08:29:16 +00:00
|
|
|
return (state, attr)
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_write_ha_state(self) -> None:
|
|
|
|
"""Write the state to the state machine."""
|
|
|
|
if self._platform_state == EntityPlatformState.REMOVED:
|
|
|
|
# Polling returned after the entity has already been removed
|
|
|
|
return
|
|
|
|
|
|
|
|
hass = self.hass
|
|
|
|
entity_id = self.entity_id
|
|
|
|
|
|
|
|
if (entry := self.registry_entry) and entry.disabled_by:
|
|
|
|
if not self._disabled_reported:
|
|
|
|
self._disabled_reported = True
|
|
|
|
_LOGGER.warning(
|
|
|
|
(
|
|
|
|
"Entity %s is incorrectly being triggered for updates while it"
|
|
|
|
" is disabled. This is a bug in the %s integration"
|
|
|
|
),
|
|
|
|
entity_id,
|
|
|
|
self.platform.platform_name,
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
|
|
|
start = timer()
|
|
|
|
state, attr = self._async_generate_attributes()
|
2016-11-04 04:58:25 +00:00
|
|
|
end = timer()
|
|
|
|
|
2018-02-08 11:16:51 +00:00
|
|
|
if end - start > 0.4 and not self._slow_reported:
|
2016-12-17 21:00:08 +00:00
|
|
|
self._slow_reported = True
|
2021-08-11 08:45:05 +00:00
|
|
|
report_issue = self._suggest_report_issue()
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.warning(
|
2021-08-11 08:45:05 +00:00
|
|
|
"Updating state for %s (%s) took %.3f seconds. Please %s",
|
2023-03-18 00:32:24 +00:00
|
|
|
entity_id,
|
2019-07-31 19:25:30 +00:00
|
|
|
type(self),
|
|
|
|
end - start,
|
2021-08-11 08:45:05 +00:00
|
|
|
report_issue,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2016-11-04 04:58:25 +00:00
|
|
|
|
2016-03-07 22:39:52 +00:00
|
|
|
# Overwrite properties that have been set in the config file.
|
2023-03-18 00:32:24 +00:00
|
|
|
if customize := hass.data.get(DATA_CUSTOMIZE):
|
|
|
|
attr.update(customize.get(entity_id))
|
2015-04-15 02:57:32 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if (
|
2020-06-06 18:34:56 +00:00
|
|
|
self._context_set is not None
|
2023-09-06 09:04:49 +00:00
|
|
|
and hass.loop.time() - self._context_set
|
|
|
|
> self.context_recent_time.total_seconds()
|
2019-07-31 19:25:30 +00:00
|
|
|
):
|
2018-08-20 15:39:53 +00:00
|
|
|
self._context = None
|
|
|
|
self._context_set = None
|
|
|
|
|
2023-09-04 12:09:51 +00:00
|
|
|
try:
|
|
|
|
hass.states.async_set(
|
2023-09-20 16:09:12 +00:00
|
|
|
entity_id,
|
|
|
|
state,
|
|
|
|
attr,
|
|
|
|
self.force_update,
|
|
|
|
self._context,
|
|
|
|
self._state_info,
|
2023-09-04 12:09:51 +00:00
|
|
|
)
|
|
|
|
except InvalidStateError:
|
2023-10-08 17:40:42 +00:00
|
|
|
_LOGGER.exception(
|
|
|
|
"Failed to set state for %s, fall back to %s", entity_id, STATE_UNKNOWN
|
|
|
|
)
|
2023-09-04 12:09:51 +00:00
|
|
|
hass.states.async_set(
|
|
|
|
entity_id, STATE_UNKNOWN, {}, self.force_update, self._context
|
|
|
|
)
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2020-06-06 18:34:56 +00:00
|
|
|
def schedule_update_ha_state(self, force_refresh: bool = False) -> None:
|
2018-01-27 19:58:27 +00:00
|
|
|
"""Schedule an update ha state change task.
|
2016-11-16 05:06:50 +00:00
|
|
|
|
2019-03-09 17:52:22 +00:00
|
|
|
Scheduling the update avoids executor deadlocks.
|
|
|
|
|
|
|
|
Entity state and attributes are read when the update ha state change
|
|
|
|
task is executed.
|
|
|
|
If state is changed more than once before the ha state change task has
|
|
|
|
been executed, the intermediate state transitions will be missed.
|
2016-11-16 05:06:50 +00:00
|
|
|
"""
|
2023-04-13 16:39:03 +00:00
|
|
|
if force_refresh:
|
|
|
|
self.hass.create_task(
|
|
|
|
self.async_update_ha_state(force_refresh),
|
|
|
|
f"Entity {self.entity_id} schedule update ha state",
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
self.hass.loop.call_soon_threadsafe(self.async_write_ha_state)
|
2016-11-16 05:06:50 +00:00
|
|
|
|
2017-11-17 05:03:05 +00:00
|
|
|
@callback
|
2020-06-06 18:34:56 +00:00
|
|
|
def async_schedule_update_ha_state(self, force_refresh: bool = False) -> None:
|
2019-03-09 17:52:22 +00:00
|
|
|
"""Schedule an update ha state change task.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
Scheduling the update avoids executor deadlocks.
|
|
|
|
|
|
|
|
Entity state and attributes are read when the update ha state change
|
|
|
|
task is executed.
|
|
|
|
If state is changed more than once before the ha state change task has
|
|
|
|
been executed, the intermediate state transitions will be missed.
|
|
|
|
"""
|
2020-02-13 18:22:06 +00:00
|
|
|
if force_refresh:
|
2023-03-05 11:46:02 +00:00
|
|
|
self.hass.async_create_task(
|
|
|
|
self.async_update_ha_state(force_refresh),
|
|
|
|
f"Entity schedule update ha state {self.entity_id}",
|
|
|
|
)
|
2020-02-13 18:22:06 +00:00
|
|
|
else:
|
|
|
|
self.async_write_ha_state()
|
2017-09-12 08:01:03 +00:00
|
|
|
|
2023-04-09 02:22:56 +00:00
|
|
|
@callback
|
|
|
|
def _async_slow_update_warning(self) -> None:
|
|
|
|
"""Log a warning if update is taking too long."""
|
|
|
|
_LOGGER.warning(
|
|
|
|
"Update of %s is taking over %s seconds",
|
|
|
|
self.entity_id,
|
|
|
|
SLOW_UPDATE_WARNING,
|
|
|
|
)
|
|
|
|
|
2020-06-06 18:34:56 +00:00
|
|
|
async def async_device_update(self, warning: bool = True) -> None:
|
2017-10-22 15:40:00 +00:00
|
|
|
"""Process 'update' or 'async_update' from entity.
|
|
|
|
|
|
|
|
This method is a coroutine.
|
|
|
|
"""
|
|
|
|
if self._update_staged:
|
|
|
|
return
|
2023-04-09 02:22:56 +00:00
|
|
|
|
|
|
|
hass = self.hass
|
|
|
|
assert hass is not None
|
|
|
|
|
2017-10-22 15:40:00 +00:00
|
|
|
self._update_staged = True
|
|
|
|
|
|
|
|
# Process update sequential
|
|
|
|
if self.parallel_updates:
|
2018-10-01 06:56:50 +00:00
|
|
|
await self.parallel_updates.acquire()
|
2017-10-22 15:40:00 +00:00
|
|
|
|
2023-04-09 02:22:56 +00:00
|
|
|
if warning:
|
2023-05-27 23:45:35 +00:00
|
|
|
update_warn = hass.loop.call_at(
|
|
|
|
hass.loop.time() + SLOW_UPDATE_WARNING, self._async_slow_update_warning
|
2020-10-05 13:28:15 +00:00
|
|
|
)
|
2023-05-06 14:46:00 +00:00
|
|
|
|
2023-04-09 02:22:56 +00:00
|
|
|
try:
|
2023-05-06 14:46:00 +00:00
|
|
|
if hasattr(self, "async_update"):
|
|
|
|
await self.async_update()
|
|
|
|
elif hasattr(self, "update"):
|
|
|
|
await hass.async_add_executor_job(self.update)
|
|
|
|
else:
|
|
|
|
return
|
2017-10-22 15:40:00 +00:00
|
|
|
finally:
|
|
|
|
self._update_staged = False
|
2023-04-09 02:22:56 +00:00
|
|
|
if warning:
|
|
|
|
update_warn.cancel()
|
2017-10-22 15:40:00 +00:00
|
|
|
if self.parallel_updates:
|
|
|
|
self.parallel_updates.release()
|
|
|
|
|
2018-07-24 12:12:53 +00:00
|
|
|
@callback
|
2019-07-21 16:59:02 +00:00
|
|
|
def async_on_remove(self, func: CALLBACK_TYPE) -> None:
|
2022-04-27 15:05:00 +00:00
|
|
|
"""Add a function to call when entity is removed or not added."""
|
2018-07-24 12:12:53 +00:00
|
|
|
if self._on_remove is None:
|
|
|
|
self._on_remove = []
|
|
|
|
self._on_remove.append(func)
|
|
|
|
|
2020-02-25 04:46:02 +00:00
|
|
|
async def async_removed_from_registry(self) -> None:
|
|
|
|
"""Run when entity has been removed from entity registry.
|
|
|
|
|
|
|
|
To be extended by integrations.
|
|
|
|
"""
|
|
|
|
|
2020-08-19 12:57:38 +00:00
|
|
|
@callback
|
|
|
|
def add_to_platform_start(
|
|
|
|
self,
|
|
|
|
hass: HomeAssistant,
|
|
|
|
platform: EntityPlatform,
|
2021-03-17 17:34:19 +00:00
|
|
|
parallel_updates: asyncio.Semaphore | None,
|
2020-08-19 12:57:38 +00:00
|
|
|
) -> None:
|
|
|
|
"""Start adding an entity to a platform."""
|
2022-03-08 04:42:16 +00:00
|
|
|
if self._platform_state == EntityPlatformState.ADDED:
|
2020-08-19 12:57:38 +00:00
|
|
|
raise HomeAssistantError(
|
2022-12-27 10:18:56 +00:00
|
|
|
f"Entity {self.entity_id} cannot be added a second time to an entity"
|
|
|
|
" platform"
|
2020-08-19 12:57:38 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
self.hass = hass
|
|
|
|
self.platform = platform
|
|
|
|
self.parallel_updates = parallel_updates
|
2022-03-08 04:42:16 +00:00
|
|
|
self._platform_state = EntityPlatformState.ADDED
|
2020-08-19 12:57:38 +00:00
|
|
|
|
2022-04-27 15:05:00 +00:00
|
|
|
def _call_on_remove_callbacks(self) -> None:
|
|
|
|
"""Call callbacks registered by async_on_remove."""
|
|
|
|
if self._on_remove is None:
|
|
|
|
return
|
|
|
|
while self._on_remove:
|
|
|
|
self._on_remove.pop()()
|
|
|
|
|
2020-08-19 12:57:38 +00:00
|
|
|
@callback
|
|
|
|
def add_to_platform_abort(self) -> None:
|
|
|
|
"""Abort adding an entity to a platform."""
|
2022-04-27 15:05:00 +00:00
|
|
|
|
|
|
|
self._platform_state = EntityPlatformState.NOT_ADDED
|
|
|
|
self._call_on_remove_callbacks()
|
|
|
|
|
2022-02-18 10:31:37 +00:00
|
|
|
self.hass = None # type: ignore[assignment]
|
2023-06-09 08:56:04 +00:00
|
|
|
self.platform = None # type: ignore[assignment]
|
2020-08-19 12:57:38 +00:00
|
|
|
self.parallel_updates = None
|
|
|
|
|
|
|
|
async def add_to_platform_finish(self) -> None:
|
|
|
|
"""Finish adding an entity to a platform."""
|
|
|
|
await self.async_internal_added_to_hass()
|
|
|
|
await self.async_added_to_hass()
|
|
|
|
self.async_write_ha_state()
|
|
|
|
|
2021-02-08 09:45:46 +00:00
|
|
|
async def async_remove(self, *, force_remove: bool = False) -> None:
|
|
|
|
"""Remove entity from Home Assistant.
|
|
|
|
|
|
|
|
If the entity has a non disabled entry in the entity registry,
|
|
|
|
the entity's state will be set to unavailable, in the same way
|
|
|
|
as when the entity registry is loaded.
|
|
|
|
|
|
|
|
If the entity doesn't have a non disabled entry in the entity registry,
|
|
|
|
or if force_remove=True, its state will be removed.
|
|
|
|
"""
|
2023-06-09 13:17:41 +00:00
|
|
|
# The check for self.platform guards against integrations not using an
|
|
|
|
# EntityComponent and can be removed in HA Core 2024.1
|
2022-03-08 04:42:16 +00:00
|
|
|
if self.platform and self._platform_state != EntityPlatformState.ADDED:
|
2020-08-19 12:57:38 +00:00
|
|
|
raise HomeAssistantError(
|
|
|
|
f"Entity {self.entity_id} async_remove called twice"
|
|
|
|
)
|
|
|
|
|
2022-03-08 04:42:16 +00:00
|
|
|
self._platform_state = EntityPlatformState.REMOVED
|
2020-08-19 12:57:38 +00:00
|
|
|
|
2022-04-27 15:05:00 +00:00
|
|
|
self._call_on_remove_callbacks()
|
2018-07-24 12:12:53 +00:00
|
|
|
|
2020-04-29 05:58:55 +00:00
|
|
|
await self.async_internal_will_remove_from_hass()
|
|
|
|
await self.async_will_remove_from_hass()
|
|
|
|
|
2021-02-08 09:45:46 +00:00
|
|
|
# Check if entry still exists in entity registry (e.g. unloading config entry)
|
|
|
|
if (
|
|
|
|
not force_remove
|
|
|
|
and self.registry_entry
|
|
|
|
and not self.registry_entry.disabled
|
|
|
|
):
|
|
|
|
# Set the entity's state will to unavailable + ATTR_RESTORED: True
|
|
|
|
self.registry_entry.write_unavailable_state(self.hass)
|
|
|
|
else:
|
|
|
|
self.hass.states.async_remove(self.entity_id, context=self._context)
|
2016-09-04 15:15:52 +00:00
|
|
|
|
2019-06-26 16:22:51 +00:00
|
|
|
async def async_added_to_hass(self) -> None:
|
|
|
|
"""Run when entity about to be added to hass.
|
|
|
|
|
|
|
|
To be extended by integrations.
|
|
|
|
"""
|
|
|
|
|
|
|
|
async def async_will_remove_from_hass(self) -> None:
|
|
|
|
"""Run when entity will be removed from hass.
|
|
|
|
|
|
|
|
To be extended by integrations.
|
|
|
|
"""
|
|
|
|
|
2022-03-30 13:43:04 +00:00
|
|
|
@callback
|
|
|
|
def async_registry_entry_updated(self) -> None:
|
|
|
|
"""Run when the entity registry entry has been updated.
|
|
|
|
|
|
|
|
To be extended by integrations.
|
|
|
|
"""
|
|
|
|
|
2019-06-26 16:22:51 +00:00
|
|
|
async def async_internal_added_to_hass(self) -> None:
|
|
|
|
"""Run when entity about to be added to hass.
|
2018-07-24 12:12:53 +00:00
|
|
|
|
2019-06-26 16:22:51 +00:00
|
|
|
Not to be extended by integrations.
|
|
|
|
"""
|
2023-09-20 16:09:12 +00:00
|
|
|
entity_info: EntityInfo = {
|
2023-06-09 13:17:41 +00:00
|
|
|
"domain": self.platform.platform_name,
|
|
|
|
"custom_component": "custom_components" in type(self).__module__,
|
|
|
|
}
|
|
|
|
|
|
|
|
if self.platform.config_entry:
|
2023-09-20 16:09:12 +00:00
|
|
|
entity_info["config_entry"] = self.platform.config_entry.entry_id
|
2020-08-19 12:57:38 +00:00
|
|
|
|
2023-09-20 16:09:12 +00:00
|
|
|
entity_sources(self.hass)[self.entity_id] = entity_info
|
|
|
|
|
|
|
|
self._state_info = {
|
|
|
|
"unrecorded_attributes": self.__combined_unrecorded_attributes
|
|
|
|
}
|
2020-08-19 12:57:38 +00:00
|
|
|
|
2019-06-26 16:22:51 +00:00
|
|
|
if self.registry_entry is not None:
|
2020-08-19 12:57:38 +00:00
|
|
|
# This is an assert as it should never happen, but helps in tests
|
|
|
|
assert (
|
|
|
|
not self.registry_entry.disabled_by
|
|
|
|
), f"Entity {self.entity_id} is being added while it's disabled"
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
self.async_on_remove(
|
2020-07-18 04:59:18 +00:00
|
|
|
async_track_entity_registry_updated_event(
|
|
|
|
self.hass, self.entity_id, self._async_registry_updated
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
|
|
|
)
|
2023-05-31 09:01:55 +00:00
|
|
|
self._async_subscribe_device_updates()
|
2019-06-26 16:22:51 +00:00
|
|
|
|
|
|
|
async def async_internal_will_remove_from_hass(self) -> None:
|
|
|
|
"""Run when entity will be removed from hass.
|
|
|
|
|
|
|
|
Not to be extended by integrations.
|
|
|
|
"""
|
2023-06-09 13:17:41 +00:00
|
|
|
# The check for self.platform guards against integrations not using an
|
|
|
|
# EntityComponent and can be removed in HA Core 2024.1
|
2020-08-19 12:57:38 +00:00
|
|
|
if self.platform:
|
|
|
|
self.hass.data[DATA_ENTITY_SOURCE].pop(self.entity_id)
|
2019-06-26 16:22:51 +00:00
|
|
|
|
2023-07-24 07:14:10 +00:00
|
|
|
async def _async_registry_updated(
|
|
|
|
self, event: EventType[er.EventEntityRegistryUpdatedData]
|
|
|
|
) -> None:
|
2019-06-26 16:22:51 +00:00
|
|
|
"""Handle entity registry update."""
|
|
|
|
data = event.data
|
2020-07-18 04:59:18 +00:00
|
|
|
if data["action"] == "remove":
|
2020-02-25 04:46:02 +00:00
|
|
|
await self.async_removed_from_registry()
|
2021-02-08 09:45:46 +00:00
|
|
|
self.registry_entry = None
|
2020-03-26 23:33:50 +00:00
|
|
|
await self.async_remove()
|
2020-02-25 04:46:02 +00:00
|
|
|
|
2020-07-18 04:59:18 +00:00
|
|
|
if data["action"] != "update":
|
2018-07-24 12:12:53 +00:00
|
|
|
return
|
|
|
|
|
2023-05-31 09:01:55 +00:00
|
|
|
if "device_id" in data["changes"]:
|
|
|
|
self._async_subscribe_device_updates()
|
|
|
|
|
2021-11-01 03:21:46 +00:00
|
|
|
ent_reg = er.async_get(self.hass)
|
2019-06-26 16:22:51 +00:00
|
|
|
old = self.registry_entry
|
2023-07-10 13:17:35 +00:00
|
|
|
registry_entry = ent_reg.async_get(data["entity_id"])
|
|
|
|
assert registry_entry is not None
|
|
|
|
self.registry_entry = registry_entry
|
|
|
|
|
|
|
|
if device_id := registry_entry.device_id:
|
|
|
|
self.device_entry = dr.async_get(self.hass).async_get(device_id)
|
2018-07-24 12:12:53 +00:00
|
|
|
|
2023-07-10 13:17:35 +00:00
|
|
|
if registry_entry.disabled:
|
2019-08-23 00:32:43 +00:00
|
|
|
await self.async_remove()
|
|
|
|
return
|
|
|
|
|
2020-06-06 18:34:56 +00:00
|
|
|
assert old is not None
|
2023-07-10 13:17:35 +00:00
|
|
|
if registry_entry.entity_id == old.entity_id:
|
2022-03-30 13:43:04 +00:00
|
|
|
self.async_registry_entry_updated()
|
2019-06-26 16:22:51 +00:00
|
|
|
self.async_write_ha_state()
|
|
|
|
return
|
2018-02-24 18:53:59 +00:00
|
|
|
|
2021-02-08 09:45:46 +00:00
|
|
|
await self.async_remove(force_remove=True)
|
2018-11-28 12:16:43 +00:00
|
|
|
|
2023-07-10 13:17:35 +00:00
|
|
|
self.entity_id = registry_entry.entity_id
|
2019-06-26 16:22:51 +00:00
|
|
|
await self.platform.async_add_entities([self])
|
2018-11-28 12:16:43 +00:00
|
|
|
|
2023-05-31 09:01:55 +00:00
|
|
|
@callback
|
|
|
|
def _async_unsubscribe_device_updates(self) -> None:
|
|
|
|
"""Unsubscribe from device registry updates."""
|
|
|
|
if not self._unsub_device_updates:
|
|
|
|
return
|
|
|
|
self._unsub_device_updates()
|
|
|
|
self._unsub_device_updates = None
|
|
|
|
|
2023-06-24 19:16:28 +00:00
|
|
|
@callback
|
2023-07-24 07:42:01 +00:00
|
|
|
def _async_device_registry_updated(
|
|
|
|
self, event: EventType[EventDeviceRegistryUpdatedData]
|
|
|
|
) -> None:
|
2023-06-24 19:16:28 +00:00
|
|
|
"""Handle device registry update."""
|
|
|
|
data = event.data
|
|
|
|
|
|
|
|
if data["action"] != "update":
|
|
|
|
return
|
|
|
|
|
|
|
|
if "name" not in data["changes"] and "name_by_user" not in data["changes"]:
|
|
|
|
return
|
|
|
|
|
2023-07-10 13:17:35 +00:00
|
|
|
self.device_entry = dr.async_get(self.hass).async_get(data["device_id"])
|
2023-06-24 19:16:28 +00:00
|
|
|
self.async_write_ha_state()
|
|
|
|
|
2023-05-31 09:01:55 +00:00
|
|
|
@callback
|
|
|
|
def _async_subscribe_device_updates(self) -> None:
|
|
|
|
"""Subscribe to device registry updates."""
|
|
|
|
assert self.registry_entry
|
|
|
|
|
|
|
|
self._async_unsubscribe_device_updates()
|
|
|
|
|
|
|
|
if (device_id := self.registry_entry.device_id) is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
if not self.has_entity_name:
|
|
|
|
return
|
|
|
|
|
|
|
|
self._unsub_device_updates = async_track_device_registry_updated_event(
|
|
|
|
self.hass,
|
|
|
|
device_id,
|
2023-06-24 19:16:28 +00:00
|
|
|
self._async_device_registry_updated,
|
2023-05-31 09:01:55 +00:00
|
|
|
)
|
|
|
|
if (
|
|
|
|
not self._on_remove
|
|
|
|
or self._async_unsubscribe_device_updates not in self._on_remove
|
|
|
|
):
|
|
|
|
self.async_on_remove(self._async_unsubscribe_device_updates)
|
|
|
|
|
2019-09-24 21:20:04 +00:00
|
|
|
def __repr__(self) -> str:
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Return the representation."""
|
2023-02-06 18:35:36 +00:00
|
|
|
return f"<entity {self.entity_id}={self._stringify_state(self.available)}>"
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2023-06-24 03:34:34 +00:00
|
|
|
async def async_request_call(self, coro: Coroutine[Any, Any, _T]) -> _T:
|
Switch on/off all lights, and wait for the result (#27078)
* Switch on/off all lights, and wait for the result
Reuses the parallel_updates semaphore.
This is a small crutch which serializes platforms which already do tis
for updates. Platforms which can parallelize everything, this makes it
go faster
* Fix broken unittest
With manual validation, with help from @frenck, we found out that the
assertions are wrong and the test should be failing.
The sequence requested is
OFF
ON
without cancelation, this code should result in:
off,off,off,on,on,on
testable, by adding a `await hass.async_block_till_done()` between the
off and on call.
with cancelation. there should be less off call's so
off,on,on,on
* Adding tests for async_request_call
* Process review feedback
* Switch gather with wait
* :shirt: running black
2019-10-06 15:23:12 +00:00
|
|
|
"""Process request batched."""
|
|
|
|
if self.parallel_updates:
|
|
|
|
await self.parallel_updates.acquire()
|
|
|
|
|
|
|
|
try:
|
2023-06-24 03:34:34 +00:00
|
|
|
return await coro
|
Switch on/off all lights, and wait for the result (#27078)
* Switch on/off all lights, and wait for the result
Reuses the parallel_updates semaphore.
This is a small crutch which serializes platforms which already do tis
for updates. Platforms which can parallelize everything, this makes it
go faster
* Fix broken unittest
With manual validation, with help from @frenck, we found out that the
assertions are wrong and the test should be failing.
The sequence requested is
OFF
ON
without cancelation, this code should result in:
off,off,off,on,on,on
testable, by adding a `await hass.async_block_till_done()` between the
off and on call.
with cancelation. there should be less off call's so
off,on,on,on
* Adding tests for async_request_call
* Process review feedback
* Switch gather with wait
* :shirt: running black
2019-10-06 15:23:12 +00:00
|
|
|
finally:
|
|
|
|
if self.parallel_updates:
|
|
|
|
self.parallel_updates.release()
|
|
|
|
|
2021-08-11 08:45:05 +00:00
|
|
|
def _suggest_report_issue(self) -> str:
|
|
|
|
"""Suggest to report an issue."""
|
2023-09-12 19:07:32 +00:00
|
|
|
# The check for self.platform guards against integrations not using an
|
|
|
|
# EntityComponent and can be removed in HA Core 2024.1
|
2023-10-04 11:40:33 +00:00
|
|
|
platform_name = self.platform.platform_name if self.platform else None
|
|
|
|
return async_suggest_report_issue(
|
|
|
|
self.hass, integration_domain=platform_name, module=type(self).__module__
|
|
|
|
)
|
2021-08-11 08:45:05 +00:00
|
|
|
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2023-04-11 17:58:28 +00:00
|
|
|
@dataclass(slots=True)
|
2021-07-26 22:22:21 +00:00
|
|
|
class ToggleEntityDescription(EntityDescription):
|
|
|
|
"""A class that describes toggle entities."""
|
|
|
|
|
|
|
|
|
2015-03-22 01:49:30 +00:00
|
|
|
class ToggleEntity(Entity):
|
2016-03-25 19:35:38 +00:00
|
|
|
"""An abstract class for entities that can be turned on and off."""
|
2016-01-31 02:03:51 +00:00
|
|
|
|
2021-07-26 22:22:21 +00:00
|
|
|
entity_description: ToggleEntityDescription
|
2022-01-23 10:31:01 +00:00
|
|
|
_attr_is_on: bool | None = None
|
2021-06-02 06:53:55 +00:00
|
|
|
_attr_state: None = None
|
|
|
|
|
2015-03-22 01:49:30 +00:00
|
|
|
@property
|
2021-06-02 06:53:55 +00:00
|
|
|
@final
|
2022-01-23 10:31:01 +00:00
|
|
|
def state(self) -> Literal["on", "off"] | None:
|
2016-01-31 02:03:51 +00:00
|
|
|
"""Return the state."""
|
2022-01-23 10:31:01 +00:00
|
|
|
if (is_on := self.is_on) is None:
|
|
|
|
return None
|
|
|
|
return STATE_ON if is_on else STATE_OFF
|
2015-03-22 01:49:30 +00:00
|
|
|
|
|
|
|
@property
|
2022-01-23 10:31:01 +00:00
|
|
|
def is_on(self) -> bool | None:
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Return True if entity is on."""
|
2021-06-02 06:53:55 +00:00
|
|
|
return self._attr_is_on
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2019-08-15 15:53:25 +00:00
|
|
|
def turn_on(self, **kwargs: Any) -> None:
|
2016-11-04 01:32:14 +00:00
|
|
|
"""Turn the entity on."""
|
2016-11-16 05:06:50 +00:00
|
|
|
raise NotImplementedError()
|
2016-11-04 01:32:14 +00:00
|
|
|
|
2020-06-06 18:34:56 +00:00
|
|
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
2020-01-29 21:59:45 +00:00
|
|
|
"""Turn the entity on."""
|
2020-04-13 13:33:04 +00:00
|
|
|
await self.hass.async_add_executor_job(ft.partial(self.turn_on, **kwargs))
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2019-08-15 15:53:25 +00:00
|
|
|
def turn_off(self, **kwargs: Any) -> None:
|
2016-11-04 01:32:14 +00:00
|
|
|
"""Turn the entity off."""
|
2016-11-16 05:06:50 +00:00
|
|
|
raise NotImplementedError()
|
2016-11-04 01:32:14 +00:00
|
|
|
|
2020-06-06 18:34:56 +00:00
|
|
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
2020-01-29 21:59:45 +00:00
|
|
|
"""Turn the entity off."""
|
2020-04-13 13:33:04 +00:00
|
|
|
await self.hass.async_add_executor_job(ft.partial(self.turn_off, **kwargs))
|
2016-01-16 15:45:05 +00:00
|
|
|
|
2022-05-23 15:52:21 +00:00
|
|
|
@final
|
2019-08-15 15:53:25 +00:00
|
|
|
def toggle(self, **kwargs: Any) -> None:
|
2022-05-23 15:52:21 +00:00
|
|
|
"""Toggle the entity.
|
|
|
|
|
|
|
|
This method will never be called by Home Assistant and should not be implemented
|
|
|
|
by integrations.
|
|
|
|
"""
|
2016-11-04 01:32:14 +00:00
|
|
|
|
2020-06-06 18:34:56 +00:00
|
|
|
async def async_toggle(self, **kwargs: Any) -> None:
|
2022-05-23 15:52:21 +00:00
|
|
|
"""Toggle the entity.
|
|
|
|
|
|
|
|
This method should typically not be implemented by integrations, it's enough to
|
|
|
|
implement async_turn_on + async_turn_off or turn_on + turn_off.
|
|
|
|
"""
|
2016-01-17 21:59:22 +00:00
|
|
|
if self.is_on:
|
2020-01-29 21:59:45 +00:00
|
|
|
await self.async_turn_off(**kwargs)
|
|
|
|
else:
|
|
|
|
await self.async_turn_on(**kwargs)
|