diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index a16d891af54a481f258648fe950e176e50538297..61ec384ae500700027d9b56b32fc0fd9d19f171b 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -110,7 +110,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, Counter.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, Counter ) storage_collection = CounterStorageCollection( @@ -170,19 +170,26 @@ class CounterStorageCollection(collection.StorageCollection): return {**data, **update_data} -class Counter(RestoreEntity): +class Counter(collection.CollectionEntity, RestoreEntity): """Representation of a counter.""" _attr_should_poll: bool = False + editable: bool - def __init__(self, config: dict) -> None: + def __init__(self, config: ConfigType) -> None: """Initialize a counter.""" - self._config: dict = config + self._config: ConfigType = config self._state: int | None = config[CONF_INITIAL] - self.editable: bool = True @classmethod - def from_yaml(cls, config: dict) -> Counter: + def from_storage(cls, config: ConfigType) -> Counter: + """Create counter instance from storage.""" + counter = cls(config) + counter.editable = True + return counter + + @classmethod + def from_yaml(cls, config: ConfigType) -> Counter: """Create counter instance from yaml config.""" counter = cls(config) counter.editable = False @@ -273,7 +280,7 @@ class Counter(RestoreEntity): self._state = self.compute_next_state(new_state) self.async_write_ha_state() - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Change the counter's settings WS CRUD.""" self._config = config self._state = self.compute_next_state(self._state) diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 7dee3614ad50edc4af1204ac21abc07a9a8c3a35..e6e99037afade115f32c47d3ef306a26683e7eca 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -100,7 +100,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputBoolean.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputBoolean ) storage_collection = InputBooleanStorageCollection( @@ -150,21 +150,28 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -class InputBoolean(ToggleEntity, RestoreEntity): +class InputBoolean(collection.CollectionEntity, ToggleEntity, RestoreEntity): """Representation of a boolean input.""" _attr_should_poll = False + editable: bool def __init__(self, config: ConfigType) -> None: """Initialize a boolean input.""" self._config = config - self.editable = True self._attr_is_on = config.get(CONF_INITIAL, False) self._attr_unique_id = config[CONF_ID] + @classmethod + def from_storage(cls, config: ConfigType) -> InputBoolean: + """Return entity instance initialized from storage.""" + input_bool = cls(config) + input_bool.editable = True + return input_bool + @classmethod def from_yaml(cls, config: ConfigType) -> InputBoolean: - """Return entity instance initialized from yaml storage.""" + """Return entity instance initialized from yaml.""" input_bool = cls(config) input_bool.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_bool.editable = False diff --git a/homeassistant/components/input_button/__init__.py b/homeassistant/components/input_button/__init__.py index 3182e36d5fc7c38e4c85efaf5f2fc99cfed0bb94..d59142fb9155a6edd4cb0e098a2b625a4ff55ee9 100644 --- a/homeassistant/components/input_button/__init__.py +++ b/homeassistant/components/input_button/__init__.py @@ -85,7 +85,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputButton.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputButton ) storage_collection = InputButtonStorageCollection( @@ -131,20 +131,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -class InputButton(ButtonEntity, RestoreEntity): +class InputButton(collection.CollectionEntity, ButtonEntity, RestoreEntity): """Representation of a button.""" _attr_should_poll = False + editable: bool def __init__(self, config: ConfigType) -> None: """Initialize a button.""" self._config = config - self.editable = True self._attr_unique_id = config[CONF_ID] @classmethod - def from_yaml(cls, config: ConfigType) -> ButtonEntity: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> InputButton: + """Return entity instance initialized from storage.""" + button = cls(config) + button.editable = True + return button + + @classmethod + def from_yaml(cls, config: ConfigType) -> InputButton: + """Return entity instance initialized from yaml.""" button = cls(config) button.entity_id = f"{DOMAIN}.{config[CONF_ID]}" button.editable = False diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index bda5572081c20c20661d6f2c3fac20ef429c2d17..8b7e81f2c77cbd78330a6044e814669439b6fe6f 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -149,7 +149,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputDatetime.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputDatetime ) storage_collection = DateTimeStorageCollection( @@ -231,15 +231,15 @@ class DateTimeStorageCollection(collection.StorageCollection): return has_date_or_time({**data, **update_data}) -class InputDatetime(RestoreEntity): +class InputDatetime(collection.CollectionEntity, RestoreEntity): """Representation of a datetime input.""" _attr_should_poll = False + editable: bool - def __init__(self, config: dict) -> None: + def __init__(self, config: ConfigType) -> None: """Initialize a select input.""" self._config = config - self.editable = True self._current_datetime = None if not config.get(CONF_INITIAL): @@ -258,8 +258,15 @@ class InputDatetime(RestoreEntity): ) @classmethod - def from_yaml(cls, config: dict) -> InputDatetime: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> InputDatetime: + """Return entity instance initialized from storage.""" + input_dt = cls(config) + input_dt.editable = True + return input_dt + + @classmethod + def from_yaml(cls, config: ConfigType) -> InputDatetime: + """Return entity instance initialized from yaml.""" input_dt = cls(config) input_dt.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_dt.editable = False @@ -420,7 +427,7 @@ class InputDatetime(RestoreEntity): ) self.async_write_ha_state() - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" self._config = config self.async_write_ha_state() diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index ff01bd124b66e0acf8de296f2e395e6baac9afb9..d5fffeba3f9d6193c3dbcdfb920f8de47a35eec2 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -130,7 +130,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputNumber.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputNumber ) storage_collection = NumberStorageCollection( @@ -202,20 +202,27 @@ class NumberStorageCollection(collection.StorageCollection): return _cv_input_number({**data, **update_data}) -class InputNumber(RestoreEntity): +class InputNumber(collection.CollectionEntity, RestoreEntity): """Representation of a slider.""" _attr_should_poll = False + editable: bool - def __init__(self, config: dict) -> None: + def __init__(self, config: ConfigType) -> None: """Initialize an input number.""" self._config = config - self.editable = True self._current_value: float | None = config.get(CONF_INITIAL) @classmethod - def from_yaml(cls, config: dict) -> InputNumber: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> InputNumber: + """Return entity instance initialized from storage.""" + input_num = cls(config) + input_num.editable = True + return input_num + + @classmethod + def from_yaml(cls, config: ConfigType) -> InputNumber: + """Return entity instance initialized from yaml.""" input_num = cls(config) input_num.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_num.editable = False @@ -310,7 +317,7 @@ class InputNumber(RestoreEntity): """Decrement value.""" await self.async_set_value(max(self._current_value - self._step, self._minimum)) - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" self._config = config # just in case min/max values changed diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 83d6684a366acf06d9b21cc991aa25cd2b2e6196..fa582f22cd54bef85684c309c1b2d815a5591650 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -152,7 +152,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputSelect.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputSelect ) storage_collection = InputSelectStorageCollection( @@ -258,11 +258,11 @@ class InputSelectStorageCollection(collection.StorageCollection): return _cv_input_select({**data, **update_data}) -class InputSelect(SelectEntity, RestoreEntity): +class InputSelect(collection.CollectionEntity, SelectEntity, RestoreEntity): """Representation of a select input.""" _attr_should_poll = False - editable = True + editable: bool def __init__(self, config: ConfigType) -> None: """Initialize a select input.""" @@ -272,9 +272,16 @@ class InputSelect(SelectEntity, RestoreEntity): self._attr_options = config[CONF_OPTIONS] self._attr_unique_id = config[CONF_ID] + @classmethod + def from_storage(cls, config: ConfigType) -> InputSelect: + """Return entity instance initialized from storage.""" + input_select = cls(config) + input_select.editable = True + return input_select + @classmethod def from_yaml(cls, config: ConfigType) -> InputSelect: - """Return entity instance initialized from yaml storage.""" + """Return entity instance initialized from yaml.""" input_select = cls(config) input_select.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_select.editable = False diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 38d74f579317ca52a8395295bda6de6b77cb7146..ac6557dad91989668094d2e4cd563ddc4a6f150e 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -129,7 +129,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputText.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputText ) storage_collection = InputTextStorageCollection( @@ -195,20 +195,27 @@ class InputTextStorageCollection(collection.StorageCollection): return _cv_input_text({**data, **update_data}) -class InputText(RestoreEntity): +class InputText(collection.CollectionEntity, RestoreEntity): """Represent a text box.""" _attr_should_poll = False + editable: bool - def __init__(self, config: dict) -> None: + def __init__(self, config: ConfigType) -> None: """Initialize a text input.""" self._config = config - self.editable = True self._current_value = config.get(CONF_INITIAL) @classmethod - def from_yaml(cls, config: dict) -> InputText: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> InputText: + """Return entity instance initialized from storage.""" + input_text = cls(config) + input_text.editable = True + return input_text + + @classmethod + def from_yaml(cls, config: ConfigType) -> InputText: + """Return entity instance initialized from yaml.""" input_text = cls(config) input_text.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_text.editable = False @@ -286,7 +293,7 @@ class InputText(RestoreEntity): self._current_value = value self.async_write_ha_state() - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" self._config = config self.async_write_ha_state() diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 09851d70384187858b44dd092c2db63df6c3bfb8..c41be68d6ea971ff163d13b59e1cbbdedefd4783 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -342,7 +342,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, DOMAIN, DOMAIN, entity_component, yaml_collection, Person ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, entity_component, storage_collection, Person.from_yaml + hass, DOMAIN, DOMAIN, entity_component, storage_collection, Person ) await yaml_collection.async_load( @@ -385,15 +385,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -class Person(RestoreEntity): +class Person(collection.CollectionEntity, RestoreEntity): """Represent a tracked person.""" _attr_should_poll = False + editable: bool def __init__(self, config): """Set up person.""" self._config = config - self.editable = True self._latitude = None self._longitude = None self._gps_accuracy = None @@ -402,8 +402,15 @@ class Person(RestoreEntity): self._unsub_track_device = None @classmethod - def from_yaml(cls, config): - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType): + """Return entity instance initialized from storage.""" + person = cls(config) + person.editable = True + return person + + @classmethod + def from_yaml(cls, config: ConfigType): + """Return entity instance initialized from yaml.""" person = cls(config) person.editable = False return person @@ -468,7 +475,7 @@ class Person(RestoreEntity): EVENT_HOMEASSISTANT_START, person_start_hass ) - async def async_update_config(self, config): + async def async_update_config(self, config: ConfigType): """Handle when the config is updated.""" self._config = config diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py index f5519e93c3fad83fa2afb57c77524ac76fd9750c..394e2ae3c36eb49a842a57c795d21441d0160a7b 100644 --- a/homeassistant/components/schedule/__init__.py +++ b/homeassistant/components/schedule/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers.collection import ( + CollectionEntity, IDManager, StorageCollection, StorageCollectionWebsocket, @@ -27,7 +28,6 @@ from homeassistant.helpers.collection import ( sync_entity_lifecycle, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.integration_platform import ( @@ -163,9 +163,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: id_manager = IDManager() yaml_collection = YamlCollection(LOGGER, id_manager) - sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, Schedule.from_yaml - ) + sync_entity_lifecycle(hass, DOMAIN, DOMAIN, component, yaml_collection, Schedule) storage_collection = ScheduleStorageCollection( Store( @@ -239,7 +237,7 @@ class ScheduleStorageCollection(StorageCollection): return data -class Schedule(Entity): +class Schedule(CollectionEntity): """Schedule entity.""" _attr_has_entity_name = True @@ -249,7 +247,7 @@ class Schedule(Entity): _next: datetime _unsub_update: Callable[[], None] | None = None - def __init__(self, config: ConfigType, editable: bool = True) -> None: + def __init__(self, config: ConfigType, editable: bool) -> None: """Initialize a schedule.""" self._config = ENTITY_SCHEMA(config) self._attr_capability_attributes = {ATTR_EDITABLE: editable} @@ -257,9 +255,15 @@ class Schedule(Entity): self._attr_name = self._config[CONF_NAME] self._attr_unique_id = self._config[CONF_ID] + @classmethod + def from_storage(cls, config: ConfigType) -> Schedule: + """Return entity instance initialized from storage.""" + schedule = cls(config, editable=True) + return schedule + @classmethod def from_yaml(cls, config: ConfigType) -> Schedule: - """Return entity instance initialized from yaml storage.""" + """Return entity instance initialized from yaml.""" schedule = cls(config, editable=False) schedule.entity_id = f"{DOMAIN}.{config[CONF_ID]}" return schedule diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index e5564736c74842aa5c6c0a242e042589616335d1..53912a4dec8ff3958ba15cfce4efcc2b6ba0b595 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -119,7 +119,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, Timer.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, Timer ) storage_collection = TimerStorageCollection( @@ -195,13 +195,14 @@ class TimerStorageCollection(collection.StorageCollection): return data -class Timer(RestoreEntity): +class Timer(collection.CollectionEntity, RestoreEntity): """Representation of a timer.""" - def __init__(self, config: dict) -> None: + editable: bool + + def __init__(self, config: ConfigType) -> None: """Initialize a timer.""" self._config: dict = config - self.editable: bool = True self._state: str = STATUS_IDLE self._duration = cv.time_period_str(config[CONF_DURATION]) self._remaining: timedelta | None = None @@ -213,8 +214,15 @@ class Timer(RestoreEntity): self._attr_force_update = True @classmethod - def from_yaml(cls, config: dict) -> Timer: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> Timer: + """Return entity instance initialized from storage.""" + timer = cls(config) + timer.editable = True + return timer + + @classmethod + def from_yaml(cls, config: ConfigType) -> Timer: + """Return entity instance initialized from yaml.""" timer = cls(config) timer.entity_id = ENTITY_ID_FORMAT.format(config[CONF_ID]) timer.editable = False @@ -384,7 +392,7 @@ class Timer(RestoreEntity): ) self.async_write_ha_state() - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" self._config = config self._duration = cv.time_period_str(config[CONF_DURATION]) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 9dac47eaafb09bb07efa71d2fa36fd44521b4e14..aa910a7789e6f2a9fc6220aefe994e8b2545a7db 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -31,7 +31,6 @@ from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callbac from homeassistant.helpers import ( collection, config_validation as cv, - entity, entity_component, event, service, @@ -193,7 +192,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, Zone.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, Zone ) storage_collection = ZoneStorageCollection( @@ -284,21 +283,30 @@ async def async_unload_entry( return True -class Zone(entity.Entity): +class Zone(collection.CollectionEntity): """Representation of a Zone.""" - def __init__(self, config: dict) -> None: + editable: bool + + def __init__(self, config: ConfigType) -> None: """Initialize the zone.""" self._config = config self.editable = True self._attrs: dict | None = None self._remove_listener: Callable[[], None] | None = None self._persons_in_zone: set[str] = set() - self._generate_attrs() @classmethod - def from_yaml(cls, config: dict) -> Zone: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> Zone: + """Return entity instance initialized from storage.""" + zone = cls(config) + zone.editable = True + zone._generate_attrs() + return zone + + @classmethod + def from_yaml(cls, config: ConfigType) -> Zone: + """Return entity instance initialized from yaml.""" zone = cls(config) zone.editable = False zone._generate_attrs() @@ -329,7 +337,7 @@ class Zone(entity.Entity): """Zone does not poll.""" return False - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" if self._config == config: return diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 7a73f90539cdfbda509570d621e4e63963a9f6b3..e3c31ca0e337985257e94ff5f18d27495903cbfc 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -22,6 +22,7 @@ from . import entity_registry from .entity import Entity from .entity_component import EntityComponent from .storage import Store +from .typing import ConfigType STORAGE_VERSION = 1 SAVE_DELAY = 10 @@ -101,6 +102,24 @@ class IDManager: return proposal +class CollectionEntity(Entity): + """Mixin class for entities managed by an ObservableCollection.""" + + @classmethod + @abstractmethod + def from_storage(cls, config: ConfigType) -> CollectionEntity: + """Create instance from storage.""" + + @classmethod + @abstractmethod + def from_yaml(cls, config: ConfigType) -> CollectionEntity: + """Create instance from yaml config.""" + + @abstractmethod + async def async_update_config(self, config: ConfigType) -> None: + """Handle updated configuration.""" + + class ObservableCollection(ABC): """Base collection type that can be observed.""" @@ -155,6 +174,13 @@ class ObservableCollection(ABC): class YamlCollection(ObservableCollection): """Offer a collection based on static data.""" + @staticmethod + def create_entity( + entity_class: type[CollectionEntity], config: ConfigType + ) -> CollectionEntity: + """Create a CollectionEntity instance.""" + return entity_class.from_yaml(config) + async def async_load(self, data: list[dict]) -> None: """Load the YAML collection. Overrides existing data.""" old_ids = set(self.data) @@ -198,6 +224,13 @@ class StorageCollection(ObservableCollection): super().__init__(logger, id_manager) self.store = store + @staticmethod + def create_entity( + entity_class: type[CollectionEntity], config: ConfigType + ) -> CollectionEntity: + """Create a CollectionEntity instance.""" + return entity_class.from_storage(config) + @property def hass(self) -> HomeAssistant: """Home Assistant object.""" @@ -290,7 +323,7 @@ class StorageCollection(ObservableCollection): return {"items": list(self.data.values())} -class IDLessCollection(ObservableCollection): +class IDLessCollection(YamlCollection): """A collection without IDs.""" counter = 0 @@ -326,20 +359,22 @@ def sync_entity_lifecycle( domain: str, platform: str, entity_component: EntityComponent, - collection: ObservableCollection, - create_entity: Callable[[dict], Entity], + collection: StorageCollection | YamlCollection, + entity_class: type[CollectionEntity], ) -> None: """Map a collection to an entity component.""" - entities: dict[str, Entity] = {} + entities: dict[str, CollectionEntity] = {} ent_reg = entity_registry.async_get(hass) - async def _add_entity(change_set: CollectionChangeSet) -> Entity: + async def _add_entity(change_set: CollectionChangeSet) -> CollectionEntity: def entity_removed() -> None: """Remove entity from entities if it's removed or not added.""" if change_set.item_id in entities: entities.pop(change_set.item_id) - entities[change_set.item_id] = create_entity(change_set.item) + entities[change_set.item_id] = collection.create_entity( + entity_class, change_set.item + ) entities[change_set.item_id].async_on_remove(entity_removed) return entities[change_set.item_id] @@ -359,10 +394,11 @@ def sync_entity_lifecycle( async def _update_entity(change_set: CollectionChangeSet) -> None: if change_set.item_id not in entities: return - await entities[change_set.item_id].async_update_config(change_set.item) # type: ignore[attr-defined] + await entities[change_set.item_id].async_update_config(change_set.item) _func_map: dict[ - str, Callable[[CollectionChangeSet], Coroutine[Any, Any, Entity | None]] + str, + Callable[[CollectionChangeSet], Coroutine[Any, Any, CollectionEntity | None]], ] = { CHANGE_ADDED: _add_entity, CHANGE_REMOVED: _remove_entity, diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 8f605d2de9fb1e55d8339322ea5d652305d0c5ea..b51200c6ad0fe7f8c611a9e67f5fbdecc88923cb 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -672,7 +672,7 @@ async def test_restore_idle(hass): # Emulate a fresh load hass.data.pop(DATA_RESTORE_STATE_TASK) - entity = Timer( + entity = Timer.from_storage( { CONF_ID: "test", CONF_NAME: "test", @@ -712,7 +712,7 @@ async def test_restore_paused(hass): # Emulate a fresh load hass.data.pop(DATA_RESTORE_STATE_TASK) - entity = Timer( + entity = Timer.from_storage( { CONF_ID: "test", CONF_NAME: "test", @@ -756,7 +756,7 @@ async def test_restore_active_resume(hass): # Emulate a fresh load hass.data.pop(DATA_RESTORE_STATE_TASK) - entity = Timer( + entity = Timer.from_storage( { CONF_ID: "test", CONF_NAME: "test", @@ -807,7 +807,7 @@ async def test_restore_active_finished_outside_grace(hass): # Emulate a fresh load hass.data.pop(DATA_RESTORE_STATE_TASK) - entity = Timer( + entity = Timer.from_storage( { CONF_ID: "test", CONF_NAME: "test", diff --git a/tests/helpers/test_collection.py b/tests/helpers/test_collection.py index cd4bbba1a3ebf69634b7ea6af5299e2915f8a359..0a14118206534c3bfab6a2ca604ca157285ef0e6 100644 --- a/tests/helpers/test_collection.py +++ b/tests/helpers/test_collection.py @@ -1,4 +1,6 @@ """Tests for the collection helper.""" +from __future__ import annotations + import logging import pytest @@ -6,11 +8,11 @@ import voluptuous as vol from homeassistant.helpers import ( collection, - entity, entity_component, entity_registry as er, storage, ) +from homeassistant.helpers.typing import ConfigType from tests.common import flush_store @@ -29,13 +31,23 @@ def track_changes(coll: collection.ObservableCollection): return changes -class MockEntity(entity.Entity): +class MockEntity(collection.CollectionEntity): """Entity that is config based.""" def __init__(self, config): """Initialize entity.""" self._config = config + @classmethod + def from_storage(cls, config: ConfigType) -> MockEntity: + """Create instance from storage.""" + return cls(config) + + @classmethod + def from_yaml(cls, config: ConfigType) -> MockEntity: + """Create instance from storage.""" + raise NotImplementedError + @property def unique_id(self): """Return unique ID of entity.""" @@ -57,6 +69,17 @@ class MockEntity(entity.Entity): self.async_write_ha_state() +class MockObservableCollection(collection.ObservableCollection): + """Mock observable collection which can create entities.""" + + @staticmethod + def create_entity( + entity_class: type[collection.CollectionEntity], config: ConfigType + ) -> collection.CollectionEntity: + """Create a CollectionEntity instance.""" + return entity_class.from_storage(config) + + class MockStorageCollection(collection.StorageCollection): """Mock storage collection.""" @@ -231,7 +254,7 @@ async def test_storage_collection(hass): async def test_attach_entity_component_collection(hass): """Test attaching collection to entity component.""" ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass) - coll = collection.ObservableCollection(_LOGGER) + coll = MockObservableCollection(_LOGGER) collection.sync_entity_lifecycle(hass, "test", "test", ent_comp, coll, MockEntity) await coll.notify_changes( @@ -270,7 +293,7 @@ async def test_attach_entity_component_collection(hass): async def test_entity_component_collection_abort(hass): """Test aborted entity adding is handled.""" ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass) - coll = collection.ObservableCollection(_LOGGER) + coll = MockObservableCollection(_LOGGER) async_update_config_calls = [] async_remove_calls = [] @@ -336,7 +359,7 @@ async def test_entity_component_collection_abort(hass): async def test_entity_component_collection_entity_removed(hass): """Test entity removal is handled.""" ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass) - coll = collection.ObservableCollection(_LOGGER) + coll = MockObservableCollection(_LOGGER) async_update_config_calls = [] async_remove_calls = []