def test_valid_entity_id(): """Test valid entity ID.""" for invalid in [ "_light.kitchen", ".kitchen", ".light.kitchen", "light_.kitchen", "light._kitchen", "light.", "light.kitchen__ceiling", "light.kitchen_yo_", "light.kitchen.", "Light.kitchen", "light.Kitchen", "lightkitchen", ]: assert not ha.valid_entity_id(invalid), invalid for valid in [ "1.a", "1light.kitchen", "a.1", "a.a", "input_boolean.hello_world_0123", "light.1kitchen", "light.kitchen", "light.something_yoo", ]: assert ha.valid_entity_id(valid), valid
def __getattr__(self, name): """Return the domain state.""" if "." in name: if not valid_entity_id(name): raise TemplateError(f"Invalid entity ID '{name}'") return _get_state(self._opp, name) if not valid_entity_id(f"{name}.entity"): raise TemplateError(f"Invalid domain name '{name}'") return DomainStates(self._opp, name)
def entity_id(value: Any) -> str: """Validate Entity ID.""" str_value = string(value).lower() if valid_entity_id(str_value): return str_value raise vol.Invalid(f"Entity ID {value} is an invalid entity ID")
async def load_instance(opp: OpenPeerPower) -> "RestoreStateData": """Set up the restore state helper.""" data = cls(opp) try: stored_states = await data.store.async_load() except OpenPeerPowerError as exc: _LOGGER.error("Error loading last states", exc_info=exc) stored_states = None if stored_states is None: _LOGGER.debug("Not creating cache - no saved states found") data.last_states = {} else: data.last_states = { item["state"]["entity_id"]: StoredState.from_dict(item) for item in stored_states if valid_entity_id(item["state"]["entity_id"]) } _LOGGER.debug("Created cache with %s", list(data.last_states)) if opp.state == CoreState.running: data.async_setup_dump() else: opp.bus.async_listen_once(EVENT_OPENPEERPOWER_START, data.async_setup_dump) return data
def extract_entities( template: Optional[str], variables: Optional[Dict[str, Any]] = None) -> Union[str, List[str]]: """Extract all entities for state_changed listener from template string.""" if template is None or _RE_JINJA_DELIMITERS.search(template) is None: return [] if _RE_NONE_ENTITIES.search(template): return MATCH_ALL extraction = _RE_GET_ENTITIES.findall(template) extraction_final = [] for result in extraction: if (result[0] == "trigger.entity_id" and variables and "trigger" in variables and "entity_id" in variables["trigger"]): extraction_final.append(variables["trigger"]["entity_id"]) elif result[0]: extraction_final.append(result[0]) if (variables and result[1] in variables and isinstance(variables[result[1]], str) and valid_entity_id(variables[result[1]])): extraction_final.append(variables[result[1]]) if extraction_final: return list(set(extraction_final)) return MATCH_ALL
def service(value: Any) -> str: """Validate service.""" # Services use same format as entities so we can use same helper. str_value = string(value).lower() if valid_entity_id(str_value): return str_value raise vol.Invalid(f"Service {value} does not match format <domain>.<name>")
async def test_loading_invalid_entity_id(opp, opp_storage): """Test we autofix invalid entity IDs.""" opp_storage[er.STORAGE_KEY] = { "version": er.STORAGE_VERSION, "data": { "entities": [ { "entity_id": "test.invalid__middle", "platform": "super_platform", "unique_id": "id-invalid-middle", "name": "registry override", }, { "entity_id": "test.invalid_end_", "platform": "super_platform", "unique_id": "id-invalid-end", }, { "entity_id": "test._invalid_start", "platform": "super_platform", "unique_id": "id-invalid-start", }, ] }, } registry = er.async_get(opp) entity_invalid_middle = registry.async_get_or_create( "test", "super_platform", "id-invalid-middle") assert valid_entity_id(entity_invalid_middle.entity_id) entity_invalid_end = registry.async_get_or_create("test", "super_platform", "id-invalid-end") assert valid_entity_id(entity_invalid_end.entity_id) entity_invalid_start = registry.async_get_or_create( "test", "super_platform", "id-invalid-start") assert valid_entity_id(entity_invalid_start.entity_id)
def distance(opp, *args): """Calculate distance. Will calculate distance from home to a point or between points. Points can be passed in using state objects or lat/lng coordinates. """ locations = [] to_process = list(args) while to_process: value = to_process.pop(0) if isinstance(value, str) and not valid_entity_id(value): point_state = None else: point_state = _resolve_state(opp, value) if point_state is None: # We expect this and next value to be lat&lng if not to_process: _LOGGER.warning( "Distance:Expected latitude and longitude, got %s", value) return None value_2 = to_process.pop(0) latitude = convert(value, float) longitude = convert(value_2, float) if latitude is None or longitude is None: _LOGGER.warning( "Distance:Unable to process latitude and longitude: %s, %s", value, value_2, ) return None else: if not loc_helper.has_location(point_state): _LOGGER.warning( "Distance:State does not contain valid location: %s", point_state) return None latitude = point_state.attributes.get(ATTR_LATITUDE) longitude = point_state.attributes.get(ATTR_LONGITUDE) locations.append((latitude, longitude)) if len(locations) == 1: return opp.config.distance(*locations[0]) return opp.config.units.length( loc_util.distance(*locations[0] + locations[1]), LENGTH_METERS)
def __getattr__(self, name): """Return the domain state.""" if "." in name: return _get_state_if_valid(self._opp, name) if name in _RESERVED_NAMES: return None if not valid_entity_id(f"{name}.entity"): raise TemplateError(f"Invalid domain name '{name}'") return DomainStates(self._opp, name)
async def async_load(self) -> None: """Load the entity registry.""" async_setup_entity_restore(self.opp, self) data = await self.opp.helpers.storage.async_migrator( self.opp.config.path(PATH_REGISTRY), self._store, old_conf_load_func=load_yaml, old_conf_migrate_func=_async_migrate, ) entities: dict[str, RegistryEntry] = OrderedDict() if data is not None: for entity in data["entities"]: # Some old installations can have some bad entities. # Filter them out as they cause errors down the line. # Can be removed in Jan 2021 if not valid_entity_id(entity["entity_id"]): continue entities[entity["entity_id"]] = RegistryEntry( entity_id=entity["entity_id"], config_entry_id=entity.get("config_entry_id"), device_id=entity.get("device_id"), area_id=entity.get("area_id"), unique_id=entity["unique_id"], platform=entity["platform"], name=entity.get("name"), icon=entity.get("icon"), disabled_by=entity.get("disabled_by"), capabilities=entity.get("capabilities") or {}, supported_features=entity.get("supported_features", 0), device_class=entity.get("device_class"), unit_of_measurement=entity.get("unit_of_measurement"), original_name=entity.get("original_name"), original_icon=entity.get("original_icon"), ) self.entities = entities self._rebuild_index()
def _get_state_if_valid(opp: OpenPeerPower, entity_id: str) -> TemplateState | None: state = opp.states.get(entity_id) if state is None and not valid_entity_id(entity_id): raise TemplateError(f"Invalid entity ID '{entity_id}'") # type: ignore return _get_template_state_from_state(opp, entity_id, state)
def _async_update_entity( self, entity_id: str, *, name: str | None | UndefinedType = UNDEFINED, icon: str | None | UndefinedType = UNDEFINED, config_entry_id: str | None | UndefinedType = UNDEFINED, new_entity_id: str | UndefinedType = UNDEFINED, device_id: str | None | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, new_unique_id: str | UndefinedType = UNDEFINED, disabled_by: str | None | UndefinedType = UNDEFINED, capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED, supported_features: int | UndefinedType = UNDEFINED, device_class: str | None | UndefinedType = UNDEFINED, unit_of_measurement: str | None | UndefinedType = UNDEFINED, original_name: str | None | UndefinedType = UNDEFINED, original_icon: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: """Private facing update properties method.""" old = self.entities[entity_id] new_values = {} # Dict with new key/value pairs old_values = {} # Dict with old key/value pairs for attr_name, value in ( ("name", name), ("icon", icon), ("config_entry_id", config_entry_id), ("device_id", device_id), ("area_id", area_id), ("disabled_by", disabled_by), ("capabilities", capabilities), ("supported_features", supported_features), ("device_class", device_class), ("unit_of_measurement", unit_of_measurement), ("original_name", original_name), ("original_icon", original_icon), ): if value is not UNDEFINED and value != getattr(old, attr_name): new_values[attr_name] = value old_values[attr_name] = getattr(old, attr_name) if new_entity_id is not UNDEFINED and new_entity_id != old.entity_id: if self.async_is_registered(new_entity_id): raise ValueError("Entity with this ID is already registered") if not valid_entity_id(new_entity_id): raise ValueError("Invalid entity ID") if split_entity_id(new_entity_id)[0] != split_entity_id(entity_id)[0]: raise ValueError("New entity ID should be same domain") self.entities.pop(entity_id) entity_id = new_values["entity_id"] = new_entity_id old_values["entity_id"] = old.entity_id if new_unique_id is not UNDEFINED: conflict_entity_id = self.async_get_entity_id( old.domain, old.platform, new_unique_id ) if conflict_entity_id: raise ValueError( f"Unique id '{new_unique_id}' is already in use by " f"'{conflict_entity_id}'" ) new_values["unique_id"] = new_unique_id old_values["unique_id"] = old.unique_id if not new_values: return old self._remove_index(old) new = attr.evolve(old, **new_values) self._register_entry(new) self.async_schedule_save() data = {"action": "update", "entity_id": entity_id, "changes": old_values} if old.entity_id != entity_id: data["old_entity_id"] = old.entity_id self.opp.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, data) return new
def __getattr__(self, name): """Return the states.""" entity_id = f"{self._domain}.{name}" if not valid_entity_id(entity_id): raise TemplateError(f"Invalid entity ID '{entity_id}'") return _get_state(self._opp, entity_id)
def _async_update_entity( self, entity_id, *, name=_UNDEF, icon=_UNDEF, config_entry_id=_UNDEF, new_entity_id=_UNDEF, device_id=_UNDEF, new_unique_id=_UNDEF, disabled_by=_UNDEF, capabilities=_UNDEF, supported_features=_UNDEF, device_class=_UNDEF, unit_of_measurement=_UNDEF, original_name=_UNDEF, original_icon=_UNDEF, ): """Private facing update properties method.""" old = self.entities[entity_id] changes = {} for attr_name, value in ( ("name", name), ("icon", icon), ("config_entry_id", config_entry_id), ("device_id", device_id), ("disabled_by", disabled_by), ("capabilities", capabilities), ("supported_features", supported_features), ("device_class", device_class), ("unit_of_measurement", unit_of_measurement), ("original_name", original_name), ("original_icon", original_icon), ): if value is not _UNDEF and value != getattr(old, attr_name): changes[attr_name] = value if new_entity_id is not _UNDEF and new_entity_id != old.entity_id: if self.async_is_registered(new_entity_id): raise ValueError("Entity is already registered") if not valid_entity_id(new_entity_id): raise ValueError("Invalid entity ID") if split_entity_id(new_entity_id)[0] != split_entity_id( entity_id)[0]: raise ValueError("New entity ID should be same domain") self.entities.pop(entity_id) entity_id = changes["entity_id"] = new_entity_id if new_unique_id is not _UNDEF: conflict = next( (entity for entity in self.entities.values() if entity.unique_id == new_unique_id and entity.domain == old.domain and entity.platform == old.platform), None, ) if conflict: raise ValueError( f"Unique id '{new_unique_id}' is already in use by " f"'{conflict.entity_id}'") changes["unique_id"] = new_unique_id if not changes: return old new = self.entities[entity_id] = attr.evolve(old, **changes) self.async_schedule_save() data = { "action": "update", "entity_id": entity_id, "changes": list(changes) } if old.entity_id != entity_id: data["old_entity_id"] = old.entity_id self.opp.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, data) return new
async def valid_entity_id(opp): """Run valid entity ID a million times.""" start = timer() for _ in range(10 ** 6): core.valid_entity_id("light.kitchen") return timer() - start
async def _async_add_entity( # noqa: C901 self, entity: Entity, update_before_add: bool, entity_registry: EntityRegistry, device_registry: DeviceRegistry, ) -> None: """Add an entity to the platform.""" if entity is None: raise ValueError("Entity cannot be None") entity.add_to_platform_start( self.opp, self, self._get_parallel_updates_semaphore( hasattr(entity, "async_update")), ) # Update properties before we generate the entity_id if update_before_add: try: await entity.async_device_update(warning=False) except Exception: # pylint: disable=broad-except self.logger.exception("%s: Error on device update!", self.platform_name) entity.add_to_platform_abort() return requested_entity_id = None suggested_object_id: str | None = None generate_new_entity_id = False # Get entity_id from unique ID registration if entity.unique_id is not None: if entity.entity_id is not None: requested_entity_id = entity.entity_id suggested_object_id = split_entity_id(entity.entity_id)[1] else: suggested_object_id = entity.name # type: ignore[unreachable] if self.entity_namespace is not None: suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" if self.config_entry is not None: config_entry_id: str | None = self.config_entry.entry_id else: config_entry_id = None device_info = entity.device_info device_id = None if config_entry_id is not None and device_info is not None: processed_dev_info = {"config_entry_id": config_entry_id} for key in ( "connections", "identifiers", "manufacturer", "model", "name", "default_manufacturer", "default_model", "default_name", "sw_version", "entry_type", "via_device", "suggested_area", ): if key in device_info: processed_dev_info[key] = device_info[ key] # type: ignore[misc] try: device = device_registry.async_get_or_create( **processed_dev_info) # type: ignore[arg-type] device_id = device.id except RequiredParameterMissing: pass disabled_by: str | None = None if not entity.entity_registry_enabled_default: disabled_by = DISABLED_INTEGRATION entry = entity_registry.async_get_or_create( self.domain, self.platform_name, entity.unique_id, suggested_object_id=suggested_object_id, config_entry=self.config_entry, device_id=device_id, known_object_ids=self.entities.keys(), disabled_by=disabled_by, capabilities=entity.capability_attributes, supported_features=entity.supported_features, device_class=entity.device_class, unit_of_measurement=entity.unit_of_measurement, original_name=entity.name, original_icon=entity.icon, ) entity.registry_entry = entry entity.entity_id = entry.entity_id if entry.disabled: self.logger.info( "Not adding entity %s because it's disabled", entry.name or entity.name or f'"{self.platform_name} {entity.unique_id}"', ) entity.add_to_platform_abort() return # We won't generate an entity ID if the platform has already set one # We will however make sure that platform cannot pick a registered ID elif entity.entity_id is not None and entity_registry.async_is_registered( entity.entity_id): # If entity already registered, convert entity id to suggestion suggested_object_id = split_entity_id(entity.entity_id)[1] generate_new_entity_id = True # Generate entity ID if entity.entity_id is None or generate_new_entity_id: suggested_object_id = (suggested_object_id or entity.name or DEVICE_DEFAULT_NAME) if self.entity_namespace is not None: suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" entity.entity_id = entity_registry.async_generate_entity_id( self.domain, suggested_object_id, self.entities.keys()) # Make sure it is valid in case an entity set the value themselves if not valid_entity_id(entity.entity_id): entity.add_to_platform_abort() raise OpenPeerPowerError(f"Invalid entity ID: {entity.entity_id}") already_exists = entity.entity_id in self.entities restored = False if not already_exists and not self.opp.states.async_available( entity.entity_id): existing = self.opp.states.get(entity.entity_id) if existing is not None and ATTR_RESTORED in existing.attributes: restored = True else: already_exists = True if already_exists: if entity.unique_id is not None: msg = f"Platform {self.platform_name} does not generate unique IDs. " if requested_entity_id: msg += f"ID {entity.unique_id} is already used by {entity.entity_id} - ignoring {requested_entity_id}" else: msg += f"ID {entity.unique_id} already exists - ignoring {entity.entity_id}" else: msg = f"Entity id already exists - ignoring: {entity.entity_id}" self.logger.error(msg) entity.add_to_platform_abort() return entity_id = entity.entity_id self.entities[entity_id] = entity if not restored: # Reserve the state in the state machine # because as soon as we return control to the event # loop below, another entity could be added # with the same id before `entity.add_to_platform_finish()` # has a chance to finish. self.opp.states.async_reserve(entity.entity_id) def remove_entity_cb() -> None: """Remove entity from entities list.""" self.entities.pop(entity_id) entity.async_on_remove(remove_entity_cb) await entity.add_to_platform_finish()
async def _async_add_entity(self, entity, update_before_add, entity_registry, device_registry): """Add an entity to the platform.""" if entity is None: raise ValueError("Entity cannot be None") entity.opp = self.opp entity.platform = self entity.parallel_updates = self._get_parallel_updates_semaphore( hasattr(entity, "async_update")) # Update properties before we generate the entity_id if update_before_add: try: await entity.async_device_update(warning=False) except Exception: # pylint: disable=broad-except self.logger.exception("%s: Error on device update!", self.platform_name) return suggested_object_id = None # Get entity_id from unique ID registration if entity.unique_id is not None: if entity.entity_id is not None: suggested_object_id = split_entity_id(entity.entity_id)[1] else: suggested_object_id = entity.name if self.entity_namespace is not None: suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" if self.config_entry is not None: config_entry_id = self.config_entry.entry_id else: config_entry_id = None device_info = entity.device_info device_id = None if config_entry_id is not None and device_info is not None: processed_dev_info = {"config_entry_id": config_entry_id} for key in ( "connections", "identifiers", "manufacturer", "model", "name", "sw_version", "via_device", ): if key in device_info: processed_dev_info[key] = device_info[key] device = device_registry.async_get_or_create( **processed_dev_info) if device: device_id = device.id disabled_by: Optional[str] = None if not entity.entity_registry_enabled_default: disabled_by = DISABLED_INTEGRATION entry = entity_registry.async_get_or_create( self.domain, self.platform_name, entity.unique_id, suggested_object_id=suggested_object_id, config_entry=self.config_entry, device_id=device_id, known_object_ids=self.entities.keys(), disabled_by=disabled_by, capabilities=entity.capability_attributes, supported_features=entity.supported_features, device_class=entity.device_class, unit_of_measurement=entity.unit_of_measurement, original_name=entity.name, original_icon=entity.icon, ) entity.registry_entry = entry entity.entity_id = entry.entity_id if entry.disabled: self.logger.info( "Not adding entity %s because it's disabled", entry.name or entity.name or f'"{self.platform_name} {entity.unique_id}"', ) return # We won't generate an entity ID if the platform has already set one # We will however make sure that platform cannot pick a registered ID elif entity.entity_id is not None and entity_registry.async_is_registered( entity.entity_id): # If entity already registered, convert entity id to suggestion suggested_object_id = split_entity_id(entity.entity_id)[1] entity.entity_id = None # Generate entity ID if entity.entity_id is None: suggested_object_id = (suggested_object_id or entity.name or DEVICE_DEFAULT_NAME) if self.entity_namespace is not None: suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" entity.entity_id = entity_registry.async_generate_entity_id( self.domain, suggested_object_id, self.entities.keys()) # Make sure it is valid in case an entity set the value themselves if not valid_entity_id(entity.entity_id): raise OpenPeerPowerError(f"Invalid entity id: {entity.entity_id}") already_exists = entity.entity_id in self.entities if not already_exists: existing = self.opp.states.get(entity.entity_id) if existing and not existing.attributes.get("restored"): already_exists = True if already_exists: msg = f"Entity id already exists: {entity.entity_id}" if entity.unique_id is not None: msg += f". Platform {self.platform_name} does not generate unique IDs" raise OpenPeerPowerError(msg) entity_id = entity.entity_id self.entities[entity_id] = entity entity.async_on_remove(lambda: self.entities.pop(entity_id)) await entity.async_internal_added_to_opp() await entity.async_added_to_opp() await entity.async_update_op_state()