diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 213e948bafb91..f2b88c6c59833 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -460,7 +460,6 @@ def rename_old_gas_to_mbus( ent_reg.async_update_entity( entity.entity_id, new_unique_id=mbus_device_id, - device_id=mbus_device_id, ) except ValueError: LOGGER.debug( diff --git a/homeassistant/components/heos/quality_scale.yaml b/homeassistant/components/heos/quality_scale.yaml index 4b657f359f725..4cd3943452147 100644 --- a/homeassistant/components/heos/quality_scale.yaml +++ b/homeassistant/components/heos/quality_scale.yaml @@ -14,7 +14,7 @@ rules: docs-actions: done docs-high-level-description: done docs-installation-instructions: done - docs-removal-instructions: todo + docs-removal-instructions: done entity-event-setup: done entity-unique-id: done has-entity-name: done @@ -60,14 +60,12 @@ rules: status: todo comment: Explore if this is possible. discovery: done - docs-data-update: todo - docs-examples: todo - docs-known-limitations: todo - docs-supported-devices: todo + docs-data-update: done + docs-examples: done + docs-known-limitations: done + docs-supported-devices: done docs-supported-functions: done - docs-troubleshooting: - status: todo - comment: Has some troublehsooting setps, but needs to be improved + docs-troubleshooting: done docs-use-cases: done dynamic-devices: todo entity-category: done diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index 3c4aac2cd7df3..b144e12795e11 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -4,16 +4,14 @@ from pyownet import protocol -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from .const import DOMAIN, PLATFORMS -from .onewirehub import CannotConnect, OneWireHub +from .onewirehub import CannotConnect, OneWireConfigEntry, OneWireHub _LOGGER = logging.getLogger(__name__) -type OneWireConfigEntry = ConfigEntry[OneWireHub] async def async_setup_entry(hass: HomeAssistant, entry: OneWireConfigEntry) -> bool: diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 5607fd7ed1d00..5d3c71b5eae17 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import timedelta import os from homeassistant.components.binary_sensor import ( @@ -14,10 +15,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import OneWireConfigEntry from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_BOOL from .entity import OneWireEntity, OneWireEntityDescription -from .onewirehub import OneWireHub +from .onewirehub import OneWireConfigEntry, OneWireHub + +PARALLEL_UPDATES = 1 +SCAN_INTERVAL = timedelta(seconds=30) @dataclass(frozen=True) diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index 3889db2a06935..31c0d35ee4b5f 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -7,12 +7,7 @@ import voluptuous as vol -from homeassistant.config_entries import ( - ConfigEntry, - ConfigFlow, - ConfigFlowResult, - OptionsFlow, -) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr @@ -29,7 +24,7 @@ OPTION_ENTRY_SENSOR_PRECISION, PRECISION_MAPPING_FAMILY_28, ) -from .onewirehub import CannotConnect, OneWireHub +from .onewirehub import CannotConnect, OneWireConfigEntry, OneWireHub DATA_SCHEMA = vol.Schema( { @@ -39,21 +34,16 @@ ) -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: - """Validate the user input allows us to connect. - - Data has the keys from DATA_SCHEMA with values provided by the user. - """ +async def validate_input( + hass: HomeAssistant, data: dict[str, Any], errors: dict[str, str] +) -> None: + """Validate the user input allows us to connect.""" hub = OneWireHub(hass) - - host = data[CONF_HOST] - port = data[CONF_PORT] - # Raises CannotConnect exception on failure - await hub.connect(host, port) - - # Return info that you want to store in the config entry. - return {"title": host} + try: + await hub.connect(data[CONF_HOST], data[CONF_PORT]) + except CannotConnect: + errors["base"] = "cannot_connect" class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): @@ -61,48 +51,58 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self) -> None: - """Initialize 1-Wire config flow.""" - self.onewire_config: dict[str, Any] = {} - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Handle 1-Wire config flow start. - - Let user manually input configuration. - """ + """Handle 1-Wire config flow start.""" errors: dict[str, str] = {} if user_input: - # Prevent duplicate entries self._async_abort_entries_match( - { - CONF_HOST: user_input[CONF_HOST], - CONF_PORT: user_input[CONF_PORT], - } + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} ) - self.onewire_config.update(user_input) - - try: - info = await validate_input(self.hass, user_input) - except CannotConnect: - errors["base"] = "cannot_connect" - else: + await validate_input(self.hass, user_input, errors) + if not errors: return self.async_create_entry( - title=info["title"], data=self.onewire_config + title=user_input[CONF_HOST], data=user_input ) return self.async_show_form( step_id="user", - data_schema=DATA_SCHEMA, + data_schema=self.add_suggested_values_to_schema(DATA_SCHEMA, user_input), + errors=errors, + ) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle 1-Wire reconfiguration.""" + errors: dict[str, str] = {} + reconfigure_entry = self._get_reconfigure_entry() + if user_input: + self._async_abort_entries_match( + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} + ) + + await validate_input(self.hass, user_input, errors) + if not errors: + return self.async_update_reload_and_abort( + reconfigure_entry, data_updates=user_input + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + DATA_SCHEMA, reconfigure_entry.data | (user_input or {}) + ), + description_placeholders={"name": reconfigure_entry.title}, errors=errors, ) @staticmethod @callback def async_get_options_flow( - config_entry: ConfigEntry, + config_entry: OneWireConfigEntry, ) -> OnewireOptionsFlowHandler: """Get the options flow for this handler.""" return OnewireOptionsFlowHandler(config_entry) @@ -126,7 +126,7 @@ class OnewireOptionsFlowHandler(OptionsFlow): current_device: str """Friendly name of the currently selected device.""" - def __init__(self, config_entry: ConfigEntry) -> None: + def __init__(self, config_entry: OneWireConfigEntry) -> None: """Initialize options flow.""" self.options = deepcopy(dict(config_entry.options)) diff --git a/homeassistant/components/onewire/diagnostics.py b/homeassistant/components/onewire/diagnostics.py index 523bb4e258006..48426cf3b5bd9 100644 --- a/homeassistant/components/onewire/diagnostics.py +++ b/homeassistant/components/onewire/diagnostics.py @@ -9,7 +9,7 @@ from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from . import OneWireConfigEntry +from .onewirehub import OneWireConfigEntry TO_REDACT = {CONF_HOST} diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index 2dc617ba039f1..3bf4de006f54b 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -44,6 +44,8 @@ _LOGGER = logging.getLogger(__name__) +type OneWireConfigEntry = ConfigEntry[OneWireHub] + def _is_known_device(device_family: str, device_type: str | None) -> bool: """Check if device family/type is known to the library.""" @@ -70,7 +72,7 @@ async def connect(self, host: str, port: int) -> None: except protocol.ConnError as exc: raise CannotConnect from exc - async def initialize(self, config_entry: ConfigEntry) -> None: + async def initialize(self, config_entry: OneWireConfigEntry) -> None: """Initialize a config entry.""" host = config_entry.data[CONF_HOST] port = config_entry.data[CONF_PORT] diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 2dca53af1cfc9..e345550c2652e 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -4,6 +4,7 @@ from collections.abc import Callable, Mapping import dataclasses +from datetime import timedelta import logging import os from types import MappingProxyType @@ -28,7 +29,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import OneWireConfigEntry from .const import ( DEVICE_KEYS_0_3, DEVICE_KEYS_A_B, @@ -39,7 +39,10 @@ READ_MODE_INT, ) from .entity import OneWireEntity, OneWireEntityDescription -from .onewirehub import OneWireHub +from .onewirehub import OneWireConfigEntry, OneWireHub + +PARALLEL_UPDATES = 1 +SCAN_INTERVAL = timedelta(seconds=30) @dataclasses.dataclass(frozen=True) diff --git a/homeassistant/components/onewire/strings.json b/homeassistant/components/onewire/strings.json index 68585c3203f7c..cd8615dc5aa56 100644 --- a/homeassistant/components/onewire/strings.json +++ b/homeassistant/components/onewire/strings.json @@ -1,21 +1,34 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "step": { + "reconfigure": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "host": "[%key:component::onewire::config::step::user::data_description::host%]", + "port": "[%key:component::onewire::config::step::user::data_description::port%]" + }, + "description": "Update OWServer configuration for {name}" + }, "user": { "data": { "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]" }, "data_description": { - "host": "The hostname or IP address of your 1-Wire device." + "host": "The hostname or IP address of your OWServer instance.", + "port": "The port of your OWServer instance (default is 4304)." }, - "title": "Set server details" + "title": "Set OWServer instance details" } } }, diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index ec0bc44e03fee..57f4f41924e31 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import timedelta import os from typing import Any @@ -11,10 +12,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import OneWireConfigEntry from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_BOOL from .entity import OneWireEntity, OneWireEntityDescription -from .onewirehub import OneWireHub +from .onewirehub import OneWireConfigEntry, OneWireHub + +PARALLEL_UPDATES = 1 +SCAN_INTERVAL = timedelta(seconds=30) @dataclass(frozen=True) diff --git a/homeassistant/components/overkiz/__init__.py b/homeassistant/components/overkiz/__init__.py index 2b4a0367bf766..51efb52e55db4 100644 --- a/homeassistant/components/overkiz/__init__.py +++ b/homeassistant/components/overkiz/__init__.py @@ -41,6 +41,7 @@ PLATFORMS, UPDATE_INTERVAL, UPDATE_INTERVAL_ALL_ASSUMED_STATE, + UPDATE_INTERVAL_LOCAL, ) from .coordinator import OverkizDataUpdateCoordinator @@ -116,13 +117,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: OverkizDataConfigEntry) if coordinator.is_stateless: LOGGER.debug( - ( - "All devices have an assumed state. Update interval has been reduced" - " to: %s" - ), + "All devices have an assumed state. Update interval has been reduced to: %s", UPDATE_INTERVAL_ALL_ASSUMED_STATE, ) - coordinator.update_interval = UPDATE_INTERVAL_ALL_ASSUMED_STATE + coordinator.set_update_interval(UPDATE_INTERVAL_ALL_ASSUMED_STATE) + + if api_type == APIType.LOCAL: + LOGGER.debug( + "Devices connect via Local API. Update interval has been reduced to: %s", + UPDATE_INTERVAL_LOCAL, + ) + coordinator.set_update_interval(UPDATE_INTERVAL_LOCAL) platforms: defaultdict[Platform, list[Device]] = defaultdict(list) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 1a89fecf9c0a1..41b567500a9b6 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -44,6 +44,7 @@ DEFAULT_HOST: Final = "gateway-xxxx-xxxx-xxxx.local:8443" UPDATE_INTERVAL: Final = timedelta(seconds=30) +UPDATE_INTERVAL_LOCAL: Final = timedelta(seconds=5) UPDATE_INTERVAL_ALL_ASSUMED_STATE: Final = timedelta(minutes=60) PLATFORMS: list[Platform] = [ diff --git a/homeassistant/components/overkiz/coordinator.py b/homeassistant/components/overkiz/coordinator.py index 17068d26b7cc2..484ef138cf7ec 100644 --- a/homeassistant/components/overkiz/coordinator.py +++ b/homeassistant/components/overkiz/coordinator.py @@ -26,7 +26,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.decorator import Registry -from .const import DOMAIN, LOGGER, UPDATE_INTERVAL +from .const import DOMAIN, IGNORED_OVERKIZ_DEVICES, LOGGER EVENT_HANDLERS: Registry[ str, Callable[[OverkizDataUpdateCoordinator, Event], Coroutine[Any, Any, None]] @@ -36,6 +36,8 @@ class OverkizDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Device]]): """Class to manage fetching data from Overkiz platform.""" + _default_update_interval: timedelta + def __init__( self, hass: HomeAssistant, @@ -45,7 +47,7 @@ def __init__( client: OverkizClient, devices: list[Device], places: Place | None, - update_interval: timedelta | None = None, + update_interval: timedelta, config_entry_id: str, ) -> None: """Initialize global data updater.""" @@ -59,12 +61,17 @@ def __init__( self.data = {} self.client = client self.devices: dict[str, Device] = {d.device_url: d for d in devices} - self.is_stateless = all( - device.protocol in (Protocol.RTS, Protocol.INTERNAL) for device in devices - ) self.executions: dict[str, dict[str, str]] = {} self.areas = self._places_to_area(places) if places else None self.config_entry_id = config_entry_id + self._default_update_interval = update_interval + + self.is_stateless = all( + device.protocol in (Protocol.RTS, Protocol.INTERNAL) + for device in devices + if device.widget not in IGNORED_OVERKIZ_DEVICES + and device.ui_class not in IGNORED_OVERKIZ_DEVICES + ) async def _async_update_data(self) -> dict[str, Device]: """Fetch Overkiz data via event listener.""" @@ -102,8 +109,9 @@ async def _async_update_data(self) -> dict[str, Device]: if event_handler := EVENT_HANDLERS.get(event.name): await event_handler(self, event) + # Restore the default update interval if no executions are pending if not self.executions: - self.update_interval = UPDATE_INTERVAL + self.update_interval = self._default_update_interval return self.devices @@ -124,6 +132,11 @@ def _places_to_area(self, place: Place) -> dict[str, str]: return areas + def set_update_interval(self, update_interval: timedelta) -> None: + """Set the update interval and store this value.""" + self.update_interval = update_interval + self._default_update_interval = update_interval + @EVENT_HANDLERS.register(EventName.DEVICE_AVAILABLE) async def on_device_available( diff --git a/homeassistant/components/overkiz/executor.py b/homeassistant/components/overkiz/executor.py index 02829eaf1a378..220c6fe7cb224 100644 --- a/homeassistant/components/overkiz/executor.py +++ b/homeassistant/components/overkiz/executor.py @@ -6,7 +6,7 @@ from urllib.parse import urlparse from pyoverkiz.enums import OverkizCommand, Protocol -from pyoverkiz.exceptions import OverkizException +from pyoverkiz.exceptions import BaseOverkizException from pyoverkiz.models import Command, Device, StateDefinition from pyoverkiz.types import StateType as OverkizStateType @@ -105,7 +105,7 @@ async def async_execute_command( "Home Assistant", ) # Catch Overkiz exceptions to support `continue_on_error` functionality - except OverkizException as exception: + except BaseOverkizException as exception: raise HomeAssistantError(exception) from exception # ExecutionRegisteredEvent doesn't contain the device_url, thus we need to register it here diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index bc82aadffed12..9ab9226c9a574 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -205,14 +205,6 @@ async def setup_device_v1( coordinator = RoborockDataUpdateCoordinator( hass, device, networking, product_info, mqtt_client, home_data_rooms ) - # Verify we can communicate locally - if we can't, switch to cloud api - await coordinator.verify_api() - coordinator.api.is_available = True - try: - await coordinator.get_maps() - except RoborockException as err: - _LOGGER.warning("Failed to get map data") - _LOGGER.debug(err) try: await coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady as ex: diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 23267a3aa2d7a..443e50642f248 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -73,7 +73,27 @@ def __init__( self.maps: dict[int, RoborockMapInfo] = {} self._home_data_rooms = {str(room.id): room.name for room in home_data_rooms} - async def verify_api(self) -> None: + async def _async_setup(self) -> None: + """Set up the coordinator.""" + # Verify we can communicate locally - if we can't, switch to cloud api + await self._verify_api() + self.api.is_available = True + + try: + maps = await self.api.get_multi_maps_list() + except RoborockException as err: + raise UpdateFailed("Failed to get map data: {err}") from err + # Rooms names populated later with calls to `set_current_map_rooms` for each map + self.maps = { + roborock_map.mapFlag: RoborockMapInfo( + flag=roborock_map.mapFlag, + name=roborock_map.name or f"Map {roborock_map.mapFlag}", + rooms={}, + ) + for roborock_map in (maps.map_info if (maps and maps.map_info) else ()) + } + + async def _verify_api(self) -> None: """Verify that the api is reachable. If it is not, switch clients.""" if isinstance(self.api, RoborockLocalClientV1): try: @@ -96,12 +116,8 @@ async def release(self) -> None: async def _update_device_prop(self) -> None: """Update device properties.""" - device_prop = await self.api.get_prop() - if device_prop: - if self.roborock_device_info.props: - self.roborock_device_info.props.update(device_prop) - else: - self.roborock_device_info.props = device_prop + if (device_prop := await self.api.get_prop()) is not None: + self.roborock_device_info.props.update(device_prop) async def _async_update_data(self) -> DeviceProp: """Update data via library.""" @@ -111,7 +127,7 @@ async def _async_update_data(self) -> DeviceProp: # Set the new map id from the updated device props self._set_current_map() # Get the rooms for that map id. - await self.get_rooms() + await self.set_current_map_rooms() except RoborockException as ex: raise UpdateFailed(ex) from ex return self.roborock_device_info.props @@ -127,29 +143,18 @@ def _set_current_map(self) -> None: self.roborock_device_info.props.status.map_status - 3 ) // 4 - async def get_maps(self) -> None: - """Add a map to the coordinators mapping.""" - maps = await self.api.get_multi_maps_list() - if maps and maps.map_info: - for roborock_map in maps.map_info: - self.maps[roborock_map.mapFlag] = RoborockMapInfo( - flag=roborock_map.mapFlag, - name=roborock_map.name or f"Map {roborock_map.mapFlag}", - rooms={}, - ) - - async def get_rooms(self) -> None: - """Get all of the rooms for the current map.""" + async def set_current_map_rooms(self) -> None: + """Fetch all of the rooms for the current map and set on RoborockMapInfo.""" # The api is only able to access rooms for the currently selected map # So it is important this is only called when you have the map you care # about selected. - if self.current_map in self.maps: - iot_rooms = await self.api.get_room_mapping() - if iot_rooms is not None: - for room in iot_rooms: - self.maps[self.current_map].rooms[room.segment_id] = ( - self._home_data_rooms.get(room.iot_id, "Unknown") - ) + if self.current_map is None or self.current_map not in self.maps: + return + room_mapping = await self.api.get_room_mapping() + self.maps[self.current_map].rooms = { + room.segment_id: self._home_data_rooms.get(room.iot_id, "Unknown") + for room in room_mapping or () + } @cached_property def duid(self) -> str: diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index 15414cfc2d9d8..6a96b04e12e84 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -61,6 +61,9 @@ "total_cleaning_area": { "default": "mdi:texture-box" }, + "total_cleaning_count": { + "default": "mdi:counter" + }, "vacuum_error": { "default": "mdi:alert-circle" }, diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index ee48656290ff7..8717920b9078f 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -121,7 +121,10 @@ async def async_image(self) -> bytes | None: """Update the image if it is not cached.""" if self.is_map_valid(): response = await asyncio.gather( - *(self.cloud_api.get_map_v1(), self.coordinator.get_rooms()), + *( + self.cloud_api.get_map_v1(), + self.coordinator.set_current_map_rooms(), + ), return_exceptions=True, ) if not isinstance(response[0], bytes): @@ -174,7 +177,8 @@ async def create_coordinator_maps( await asyncio.sleep(MAP_SLEEP) # Get the map data map_update = await asyncio.gather( - *[coord.cloud_api.get_map_v1(), coord.get_rooms()], return_exceptions=True + *[coord.cloud_api.get_map_v1(), coord.set_current_map_rooms()], + return_exceptions=True, ) # If we fail to get the map, we should set it to empty byte, # still create it, and set it as unavailable. diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 47849ed5cc53c..e01a03d7720dc 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -24,6 +24,7 @@ SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfArea, UnitOfTime from homeassistant.core import HomeAssistant @@ -112,6 +113,13 @@ def _dock_error_value_fn(properties: DeviceProp) -> str | None: value_fn=lambda data: data.clean_summary.clean_time, entity_category=EntityCategory.DIAGNOSTIC, ), + RoborockSensorDescription( + key="total_cleaning_count", + translation_key="total_cleaning_count", + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.clean_summary.clean_count, + entity_category=EntityCategory.DIAGNOSTIC, + ), RoborockSensorDescription( key="status", device_class=SensorDeviceClass.ENUM, diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 6e68ab804534b..7005344614c04 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -228,6 +228,9 @@ "total_cleaning_area": { "name": "Total cleaning area" }, + "total_cleaning_count": { + "name": "Total cleaning count" + }, "vacuum_error": { "name": "Vacuum error", "state": { diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index df6c5ef7acbc2..7a5e910923c2e 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -9,7 +9,7 @@ from screenlogicpy.const.msg import CODE from screenlogicpy.device_const.chemistry import DOSE_STATE from screenlogicpy.device_const.pump import PUMP_TYPE -from screenlogicpy.device_const.system import EQUIPMENT_FLAG +from screenlogicpy.device_const.system import CONTROLLER_STATE, EQUIPMENT_FLAG from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, @@ -41,7 +41,7 @@ class ScreenLogicSensorDescription( ): """Describes a ScreenLogic sensor.""" - value_mod: Callable[[int | str], int | str] | None = None + value_mod: Callable[[int | str], int | str | None] | None = None @dataclasses.dataclass(frozen=True, kw_only=True) @@ -60,6 +60,18 @@ class ScreenLogicPushSensorDescription( state_class=SensorStateClass.MEASUREMENT, translation_key="air_temperature", ), + ScreenLogicPushSensorDescription( + subscription_code=CODE.STATUS_CHANGED, + data_root=(DEVICE.CONTROLLER, GROUP.SENSOR), + key=VALUE.STATE, + device_class=SensorDeviceClass.ENUM, + options=["ready", "sync", "service"], + value_mod=lambda val: ( + CONTROLLER_STATE(val).name.lower() if val in [1, 2, 3] else None + ), + entity_category=EntityCategory.DIAGNOSTIC, + translation_key="controller_state", + ), ] SUPPORTED_PUMP_SENSORS = [ @@ -344,7 +356,7 @@ def __init__( ) @property - def native_value(self) -> str | int | float: + def native_value(self) -> str | int | float | None: """State of the sensor.""" val = self.entity_data[ATTR.VALUE] value_mod = self.entity_description.value_mod diff --git a/homeassistant/components/screenlogic/strings.json b/homeassistant/components/screenlogic/strings.json index 1f5695d661314..09e64808dfe4b 100644 --- a/homeassistant/components/screenlogic/strings.json +++ b/homeassistant/components/screenlogic/strings.json @@ -184,6 +184,14 @@ "air_temperature": { "name": "Air temperature" }, + "controller_state": { + "name": "Controller state", + "state": { + "ready": "Ready", + "sync": "Sync", + "service": "Service" + } + }, "chem_now": { "name": "{chem} now" }, diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json index c0aef09e8c324..185705ff16350 100644 --- a/homeassistant/components/tplink/strings.json +++ b/homeassistant/components/tplink/strings.json @@ -14,6 +14,9 @@ "pick_device": { "data": { "device": "[%key:common::config_flow::data::device%]" + }, + "data_description": { + "device": "Pick the TP-Link device to add." } }, "discovery_confirm": { @@ -25,6 +28,10 @@ "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "username": "Your TP-Link cloud username which is the full email and is case sensitive.", + "password": "Your TP-Link cloud password which is case sensitive." } }, "discovery_auth_confirm": { @@ -33,6 +40,10 @@ "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "username": "[%key:component::tplink::config::step::user_auth_confirm::data_description::username%]", + "password": "[%key:component::tplink::config::step::user_auth_confirm::data_description::password%]" } }, "reauth_confirm": { @@ -41,6 +52,10 @@ "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "username": "[%key:component::tplink::config::step::user_auth_confirm::data_description::username%]", + "password": "[%key:component::tplink::config::step::user_auth_confirm::data_description::password%]" } }, "reconfigure": { @@ -48,15 +63,23 @@ "description": "Update your configuration for device {mac}", "data": { "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "[%key:component::tplink::config::step::user::data_description::host%]" } }, "camera_auth_confirm": { "title": "Set camera account credentials", - "description": "Input device camera account credentials. Leave blank if they are the same as your TPLink cloud credentials.", + "description": "Input device camera account credentials.", "data": { "live_view": "Enable camera live view", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "live_view": "Enabling live view will create the live view camera entity and requires your camera account credentials.", + "username": "Your camera account username configured for the device in the Tapo app.", + "password": "Your camera account password configured for the device in the Tapo app." } } }, diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 9d50b7ae83b9e..6b6becd4dd3f5 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -648,6 +648,7 @@ def _validate_item( domain: str, platform: str, *, + device_id: str | None | UndefinedType = None, disabled_by: RegistryEntryDisabler | None | UndefinedType = None, entity_category: EntityCategory | None | UndefinedType = None, hidden_by: RegistryEntryHider | None | UndefinedType = None, @@ -671,6 +672,10 @@ def _validate_item( unique_id, report_issue, ) + if device_id and device_id is not UNDEFINED: + device_registry = dr.async_get(hass) + if not device_registry.async_get(device_id): + raise ValueError(f"Device {device_id} does not exist") if ( disabled_by and disabled_by is not UNDEFINED @@ -859,6 +864,7 @@ def async_get_or_create( self.hass, domain, platform, + device_id=device_id, disabled_by=disabled_by, entity_category=entity_category, hidden_by=hidden_by, @@ -1090,6 +1096,7 @@ def _async_update_entity( self.hass, old.domain, old.platform, + device_id=device_id, disabled_by=disabled_by, entity_category=entity_category, hidden_by=hidden_by, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a9a74fddc0664..35a603415f9e2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -68,7 +68,7 @@ typing-extensions>=4.12.2,<5.0 ulid-transform==1.0.2 urllib3>=1.26.5,<2 uv==0.5.8 -voluptuous-openapi==0.0.5 +voluptuous-openapi==0.0.6 voluptuous-serialize==2.6.0 voluptuous==0.15.2 webrtc-models==0.3.0 diff --git a/pyproject.toml b/pyproject.toml index 3a8db6a981cf5..3889dadff7414 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ dependencies = [ "uv==0.5.8", "voluptuous==0.15.2", "voluptuous-serialize==2.6.0", - "voluptuous-openapi==0.0.5", + "voluptuous-openapi==0.0.6", "yarl==1.18.3", "webrtc-models==0.3.0", "zeroconf==0.137.2" diff --git a/requirements.txt b/requirements.txt index e728049fe51fc..9aaef2a6b7941 100644 --- a/requirements.txt +++ b/requirements.txt @@ -48,7 +48,7 @@ urllib3>=1.26.5,<2 uv==0.5.8 voluptuous==0.15.2 voluptuous-serialize==2.6.0 -voluptuous-openapi==0.0.5 +voluptuous-openapi==0.0.6 yarl==1.18.3 webrtc-models==0.3.0 zeroconf==0.137.2 diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index b152309b24a68..cb456be503669 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -32,7 +32,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture @@ -264,6 +264,7 @@ async def test_google_entity_registry_sync( @pytest.mark.usefixtures("mock_cloud_login") async def test_google_device_registry_sync( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, cloud_prefs: CloudPreferences, ) -> None: @@ -275,8 +276,14 @@ async def test_google_device_registry_sync( # Enable exposing new entities to Google expose_new(hass, True) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) entity_entry = entity_registry.async_get_or_create( - "light", "hue", "1234", device_id="1234" + "light", "hue", "1234", device_id=device_entry.id ) entity_entry = entity_registry.async_update_entity( entity_entry.entity_id, area_id="ABCD" @@ -294,7 +301,7 @@ async def test_google_device_registry_sync( dr.EVENT_DEVICE_REGISTRY_UPDATED, { "action": "update", - "device_id": "1234", + "device_id": device_entry.id, "changes": ["manufacturer"], }, ) @@ -308,7 +315,7 @@ async def test_google_device_registry_sync( dr.EVENT_DEVICE_REGISTRY_UPDATED, { "action": "update", - "device_id": "1234", + "device_id": device_entry.id, "changes": ["area_id"], }, ) @@ -324,7 +331,7 @@ async def test_google_device_registry_sync( dr.EVENT_DEVICE_REGISTRY_UPDATED, { "action": "update", - "device_id": "1234", + "device_id": device_entry.id, "changes": ["area_id"], }, ) diff --git a/tests/components/freebox/conftest.py b/tests/components/freebox/conftest.py index 2fe4e1b77de42..e6adae572f301 100644 --- a/tests/components/freebox/conftest.py +++ b/tests/components/freebox/conftest.py @@ -1,7 +1,7 @@ """Test helpers for Freebox.""" import json -from unittest.mock import AsyncMock, PropertyMock, patch +from unittest.mock import AsyncMock, patch from freebox_api.exceptions import HttpRequestError import pytest @@ -36,16 +36,6 @@ def mock_path(): yield -@pytest.fixture(autouse=True) -def enable_all_entities(): - """Make sure all entities are enabled.""" - with patch( - "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", - PropertyMock(return_value=True), - ): - yield - - @pytest.fixture def mock_device_registry_devices( hass: HomeAssistant, device_registry: dr.DeviceRegistry diff --git a/tests/components/freebox/test_binary_sensor.py b/tests/components/freebox/test_binary_sensor.py index 4950ef27e5f98..a56f3ed078903 100644 --- a/tests/components/freebox/test_binary_sensor.py +++ b/tests/components/freebox/test_binary_sensor.py @@ -4,6 +4,7 @@ from unittest.mock import Mock from freezegun.api import FrozenDateTimeFactory +import pytest from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_DOMAIN, @@ -45,6 +46,7 @@ async def test_raid_array_degraded( ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_home( hass: HomeAssistant, freezer: FrozenDateTimeFactory, router: Mock ) -> None: diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index d6d0c7118dbb9..c1b4ecfed707d 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -1406,7 +1406,6 @@ async def test_options_flow_exclude_mode_skips_category_entities( "switch", "sonos", "config", - device_id="1234", entity_category=EntityCategory.CONFIG, ) hass.states.async_set(sonos_config_switch.entity_id, "off") @@ -1415,7 +1414,6 @@ async def test_options_flow_exclude_mode_skips_category_entities( "switch", "sonos", "notconfig", - device_id="1234", entity_category=None, ) hass.states.async_set(sonos_notconfig_switch.entity_id, "off") @@ -1510,7 +1508,6 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( "switch", "sonos", "config", - device_id="1234", hidden_by=er.RegistryEntryHider.INTEGRATION, ) hass.states.async_set(sonos_hidden_switch.entity_id, "off") @@ -1594,7 +1591,6 @@ async def test_options_flow_include_mode_allows_hidden_entities( "switch", "sonos", "config", - device_id="1234", hidden_by=er.RegistryEntryHider.INTEGRATION, ) hass.states.async_set(sonos_hidden_switch.entity_id, "off") diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 4000c61e422b5..0829c96ce1d6f 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -615,7 +615,6 @@ async def test_homekit_entity_glob_filter_with_config_entities( "select", "any", "any", - device_id="1234", entity_category=EntityCategory.CONFIG, ) hass.states.async_set(select_config_entity.entity_id, "off") @@ -624,7 +623,6 @@ async def test_homekit_entity_glob_filter_with_config_entities( "switch", "any", "any", - device_id="1234", entity_category=EntityCategory.CONFIG, ) hass.states.async_set(switch_config_entity.entity_id, "off") @@ -669,7 +667,6 @@ async def test_homekit_entity_glob_filter_with_hidden_entities( "select", "any", "any", - device_id="1234", hidden_by=er.RegistryEntryHider.INTEGRATION, ) hass.states.async_set(select_config_entity.entity_id, "off") @@ -678,7 +675,6 @@ async def test_homekit_entity_glob_filter_with_hidden_entities( "switch", "any", "any", - device_id="1234", hidden_by=er.RegistryEntryHider.INTEGRATION, ) hass.states.async_set(switch_config_entity.entity_id, "off") @@ -1867,7 +1863,11 @@ async def test_homekit_ignored_missing_devices( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: - """Test HomeKit handles a device in the entity registry but missing from the device registry.""" + """Test HomeKit handles a device in the entity registry but missing from the device registry. + + If the entity registry is updated to remove entities linked to non-existent devices, + or set the link to None, this test can be removed. + """ entry = await async_init_integration(hass) homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) @@ -1885,47 +1885,37 @@ async def test_homekit_ignored_missing_devices( connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_registry.async_get_or_create( + binary_sensor_entity = entity_registry.async_get_or_create( "binary_sensor", "powerwall", "battery_charging", device_id=device_entry.id, original_device_class=BinarySensorDeviceClass.BATTERY_CHARGING, ) - entity_registry.async_get_or_create( + sensor_entity = entity_registry.async_get_or_create( "sensor", "powerwall", "battery", device_id=device_entry.id, original_device_class=SensorDeviceClass.BATTERY, ) - light = entity_registry.async_get_or_create( + light_entity = light = entity_registry.async_get_or_create( "light", "powerwall", "demo", device_id=device_entry.id ) # Delete the device to make sure we fallback # to using the platform - device_registry.async_remove_device(device_entry.id) - # Wait for the entities to be removed - await asyncio.sleep(0) - await asyncio.sleep(0) - # Restore the registry - entity_registry.async_get_or_create( - "binary_sensor", - "powerwall", - "battery_charging", - device_id=device_entry.id, - original_device_class=BinarySensorDeviceClass.BATTERY_CHARGING, - ) - entity_registry.async_get_or_create( - "sensor", - "powerwall", - "battery", - device_id=device_entry.id, - original_device_class=SensorDeviceClass.BATTERY, - ) - light = entity_registry.async_get_or_create( - "light", "powerwall", "demo", device_id=device_entry.id - ) + with patch( + "homeassistant.helpers.entity_registry.async_entries_for_device", + return_value=[], + ): + device_registry.async_remove_device(device_entry.id) + # Wait for the device registry event handlers to execute + await asyncio.sleep(0) + await asyncio.sleep(0) + # Check the entities were not removed + assert binary_sensor_entity.entity_id in entity_registry.entities + assert sensor_entity.entity_id in entity_registry.entities + assert light_entity.entity_id in entity_registry.entities hass.states.async_set(light.entity_id, STATE_ON) hass.states.async_set("light.two", STATE_ON) diff --git a/tests/components/number/test_device_action.py b/tests/components/number/test_device_action.py index ffebd62fcbfde..6b24c15f18a54 100644 --- a/tests/components/number/test_device_action.py +++ b/tests/components/number/test_device_action.py @@ -243,18 +243,26 @@ async def test_action_legacy( async def test_capabilities( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test getting capabilities.""" - entry = entity_registry.async_get_or_create( - DOMAIN, "test", "5678", device_id="abcdefgh" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_entry = entity_registry.async_get_or_create( + DOMAIN, "test", "5678", device_id=device_entry.id ) capabilities = await device_action.async_get_action_capabilities( hass, { "domain": DOMAIN, "device_id": "abcdefgh", - "entity_id": entry.id, + "entity_id": entity_entry.id, "type": "set_value", }, ) @@ -267,18 +275,26 @@ async def test_capabilities( async def test_capabilities_legacy( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test getting capabilities.""" - entry = entity_registry.async_get_or_create( - DOMAIN, "test", "5678", device_id="abcdefgh" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_entry = entity_registry.async_get_or_create( + DOMAIN, "test", "5678", device_id=device_entry.id ) capabilities = await device_action.async_get_action_capabilities( hass, { "domain": DOMAIN, "device_id": "abcdefgh", - "entity_id": entry.entity_id, + "entity_id": entity_entry.entity_id, "type": "set_value", }, ) diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index 029e1278c868e..cf61ab190db25 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -17,6 +17,8 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr +from tests.common import MockConfigEntry + pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -102,6 +104,78 @@ async def test_user_duplicate( assert result["reason"] == "already_configured" +async def test_reconfigure_flow( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_setup_entry: AsyncMock +) -> None: + """Test reconfigure flow.""" + result = await config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + assert not result["errors"] + + # Invalid server + with patch( + "homeassistant.components.onewire.onewirehub.protocol.proxy", + side_effect=protocol.ConnError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "2.3.4.5", CONF_PORT: 2345}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + assert result["errors"] == {"base": "cannot_connect"} + + # Valid server + with patch( + "homeassistant.components.onewire.onewirehub.protocol.proxy", + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "2.3.4.5", CONF_PORT: 2345}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert config_entry.data == {CONF_HOST: "2.3.4.5", CONF_PORT: 2345} + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reconfigure_duplicate( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_setup_entry: AsyncMock +) -> None: + """Test reconfigure duplicate flow.""" + other_config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_HOST: "2.3.4.5", + CONF_PORT: 2345, + }, + entry_id="other", + ) + other_config_entry.add_to_hass(hass) + + result = await config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + assert not result["errors"] + + # Duplicate server + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "2.3.4.5", CONF_PORT: 2345}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert len(mock_setup_entry.mock_calls) == 0 + assert config_entry.data == {CONF_HOST: "1.2.3.4", CONF_PORT: 1234} + assert other_config_entry.data == {CONF_HOST: "2.3.4.5", CONF_PORT: 2345} + + @pytest.mark.usefixtures("filled_device_registry") async def test_user_options_clear( hass: HomeAssistant, config_entry: ConfigEntry diff --git a/tests/components/prusalink/test_binary_sensor.py b/tests/components/prusalink/test_binary_sensor.py index c39b15471c6d6..474a4e265d150 100644 --- a/tests/components/prusalink/test_binary_sensor.py +++ b/tests/components/prusalink/test_binary_sensor.py @@ -1,6 +1,6 @@ """Test Prusalink sensors.""" -from unittest.mock import PropertyMock, patch +from unittest.mock import patch import pytest @@ -12,16 +12,13 @@ @pytest.fixture(autouse=True) def setup_binary_sensor_platform_only(): """Only setup sensor platform.""" - with ( - patch("homeassistant.components.prusalink.PLATFORMS", [Platform.BINARY_SENSOR]), - patch( - "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", - PropertyMock(return_value=True), - ), + with patch( + "homeassistant.components.prusalink.PLATFORMS", [Platform.BINARY_SENSOR] ): yield +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_binary_sensors_no_job( hass: HomeAssistant, mock_config_entry, mock_api ) -> None: diff --git a/tests/components/prusalink/test_sensor.py b/tests/components/prusalink/test_sensor.py index c069362660002..ead56f6493dac 100644 --- a/tests/components/prusalink/test_sensor.py +++ b/tests/components/prusalink/test_sensor.py @@ -1,7 +1,7 @@ """Test Prusalink sensors.""" from datetime import UTC, datetime -from unittest.mock import PropertyMock, patch +from unittest.mock import patch import pytest @@ -27,16 +27,11 @@ @pytest.fixture(autouse=True) def setup_sensor_platform_only(): """Only setup sensor platform.""" - with ( - patch("homeassistant.components.prusalink.PLATFORMS", [Platform.SENSOR]), - patch( - "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", - PropertyMock(return_value=True), - ), - ): + with patch("homeassistant.components.prusalink.PLATFORMS", [Platform.SENSOR]): yield +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors_no_job(hass: HomeAssistant, mock_config_entry, mock_api) -> None: """Test sensors while no job active.""" assert await async_setup_component(hass, "prusalink", {}) @@ -140,6 +135,7 @@ async def test_sensors_no_job(hass: HomeAssistant, mock_config_entry, mock_api) assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == REVOLUTIONS_PER_MINUTE +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors_idle_job_mk3( hass: HomeAssistant, mock_config_entry, @@ -248,6 +244,7 @@ async def test_sensors_idle_job_mk3( assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == REVOLUTIONS_PER_MINUTE +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors_active_job( hass: HomeAssistant, mock_config_entry, diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index d14f367b2bd6a..12edb7a9c6e9a 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -29,7 +29,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import location @@ -80,8 +80,6 @@ MOCK_DATA_VERSION_1 = {CONF_TOKEN: MOCK_CREDS, "devices": [MOCK_DEVICE_VERSION_1]} -MOCK_DEVICE_ID = "somedeviceid" - MOCK_ENTRY_VERSION_1 = MockConfigEntry( domain=DOMAIN, data=MOCK_DATA_VERSION_1, entry_id=MOCK_ENTRY_ID, version=1 ) @@ -141,20 +139,26 @@ async def test_creating_entry_sets_up_media_player(hass: HomeAssistant) -> None: async def test_config_flow_entry_migrate( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test that config flow entry is migrated correctly.""" # Start with the config entry at Version 1. manager = hass.config_entries mock_entry = MOCK_ENTRY_VERSION_1 mock_entry.add_to_manager(manager) + mock_device_entry = device_registry.async_get_or_create( + config_entry_id=mock_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) mock_entity_id = f"media_player.ps4_{MOCK_UNIQUE_ID}" mock_e_entry = entity_registry.async_get_or_create( "media_player", "ps4", MOCK_UNIQUE_ID, config_entry=mock_entry, - device_id=MOCK_DEVICE_ID, + device_id=mock_device_entry.id, ) assert len(entity_registry.entities) == 1 assert mock_e_entry.entity_id == mock_entity_id @@ -180,7 +184,7 @@ async def test_config_flow_entry_migrate( # Test that entity_id remains the same. assert mock_entity.entity_id == mock_entity_id - assert mock_entity.device_id == MOCK_DEVICE_ID + assert mock_entity.device_id == mock_device_entry.id # Test that last four of credentials is appended to the unique_id. assert mock_entity.unique_id == f"{MOCK_UNIQUE_ID}_{MOCK_CREDS[-4:]}" diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 44084574e01ce..d65bf7c61d72c 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -1,5 +1,6 @@ """Global fixtures for Roborock integration.""" +from collections.abc import Generator from copy import deepcopy from unittest.mock import patch @@ -14,7 +15,7 @@ CONF_USER_DATA, DOMAIN, ) -from homeassistant.const import CONF_USERNAME +from homeassistant.const import CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -167,13 +168,21 @@ def mock_roborock_entry(hass: HomeAssistant) -> MockConfigEntry: return mock_entry +@pytest.fixture(name="platforms") +def mock_platforms() -> list[Platform]: + """Fixture to specify platforms to test.""" + return [] + + @pytest.fixture async def setup_entry( hass: HomeAssistant, bypass_api_fixture, mock_roborock_entry: MockConfigEntry, -) -> MockConfigEntry: + platforms: list[Platform], +) -> Generator[MockConfigEntry]: """Set up the Roborock platform.""" - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - return mock_roborock_entry + with patch("homeassistant.components.roborock.PLATFORMS", platforms): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + yield mock_roborock_entry diff --git a/tests/components/roborock/test_binary_sensor.py b/tests/components/roborock/test_binary_sensor.py index e70dac5ffc918..0e4b338f469d9 100644 --- a/tests/components/roborock/test_binary_sensor.py +++ b/tests/components/roborock/test_binary_sensor.py @@ -1,10 +1,19 @@ """Test Roborock Binary Sensor.""" +import pytest + +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.BINARY_SENSOR] + + async def test_binary_sensors( hass: HomeAssistant, setup_entry: MockConfigEntry ) -> None: diff --git a/tests/components/roborock/test_button.py b/tests/components/roborock/test_button.py index 43ef043f79c9f..0a7efe83513eb 100644 --- a/tests/components/roborock/test_button.py +++ b/tests/components/roborock/test_button.py @@ -6,12 +6,19 @@ import roborock from homeassistant.components.button import SERVICE_PRESS +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.BUTTON] + + @pytest.mark.parametrize( ("entity_id"), [ diff --git a/tests/components/roborock/test_image.py b/tests/components/roborock/test_image.py index c884baef12334..e240dccf7ebde 100644 --- a/tests/components/roborock/test_image.py +++ b/tests/components/roborock/test_image.py @@ -5,10 +5,11 @@ from http import HTTPStatus from unittest.mock import patch +import pytest from roborock import RoborockException from homeassistant.components.roborock import DOMAIN -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -19,6 +20,12 @@ from tests.typing import ClientSessionGenerator +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.IMAGE] + + async def test_floorplan_image( hass: HomeAssistant, setup_entry: MockConfigEntry, diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index 4cd2a37effcf3..f4f490e68d9dd 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -133,20 +133,18 @@ async def test_local_client_fails_props( assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY -async def test_fails_maps_continue( +async def test_fail_maps( hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture_v1_only, ) -> None: - """Test that if we fail to get the maps, we still setup.""" + """Test that the integration fails to load if we fail to get the maps.""" with patch( "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_multi_maps_list", side_effect=RoborockException(), ): await async_setup_component(hass, DOMAIN, {}) - assert mock_roborock_entry.state is ConfigEntryState.LOADED - # No map data means no images - assert len(hass.states.async_all("image")) == 0 + assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY async def test_reauth_started( diff --git a/tests/components/roborock/test_number.py b/tests/components/roborock/test_number.py index 7e87b49253e68..bfd8cc6da2bed 100644 --- a/tests/components/roborock/test_number.py +++ b/tests/components/roborock/test_number.py @@ -6,12 +6,19 @@ import roborock from homeassistant.components.number import ATTR_VALUE, SERVICE_SET_VALUE +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.NUMBER] + + @pytest.mark.parametrize( ("entity_id", "value"), [ diff --git a/tests/components/roborock/test_select.py b/tests/components/roborock/test_select.py index 4859af0f79034..7f25141306b8e 100644 --- a/tests/components/roborock/test_select.py +++ b/tests/components/roborock/test_select.py @@ -7,7 +7,7 @@ from roborock.exceptions import RoborockException from homeassistant.components.roborock import DOMAIN -from homeassistant.const import SERVICE_SELECT_OPTION, STATE_UNKNOWN +from homeassistant.const import SERVICE_SELECT_OPTION, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -17,6 +17,12 @@ from tests.common import MockConfigEntry +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.SELECT] + + @pytest.mark.parametrize( ("entity_id", "value"), [ diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index 908754f3b921b..e33d3aa78d547 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -2,6 +2,7 @@ from unittest.mock import patch +import pytest from roborock import DeviceData, HomeDataDevice from roborock.const import ( FILTER_REPLACE_TIME, @@ -12,6 +13,7 @@ from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol from roborock.version_1_apis import RoborockMqttClientV1 +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .mock_data import CONSUMABLE, STATUS, USER_DATA @@ -19,9 +21,15 @@ from tests.common import MockConfigEntry +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.SENSOR] + + async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> None: """Test sensors and check test values are correctly set.""" - assert len(hass.states.async_all("sensor")) == 38 + assert len(hass.states.async_all("sensor")) == 40 assert hass.states.get("sensor.roborock_s7_maxv_main_brush_time_left").state == str( MAIN_BRUSH_REPLACE_TIME - 74382 ) @@ -46,6 +54,7 @@ async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> Non assert hass.states.get("sensor.roborock_s7_maxv_vacuum_error").state == "none" assert hass.states.get("sensor.roborock_s7_maxv_battery").state == "100" assert hass.states.get("sensor.roborock_s7_maxv_dock_error").state == "ok" + assert hass.states.get("sensor.roborock_s7_maxv_total_cleaning_count").state == "31" assert ( hass.states.get("sensor.roborock_s7_maxv_last_clean_begin").state == "2023-01-01T03:22:10+00:00" diff --git a/tests/components/roborock/test_switch.py b/tests/components/roborock/test_switch.py index 5de3c208c1e9d..2476bfe497c8b 100644 --- a/tests/components/roborock/test_switch.py +++ b/tests/components/roborock/test_switch.py @@ -6,12 +6,19 @@ import roborock from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.SWITCH] + + @pytest.mark.parametrize( ("entity_id"), [ diff --git a/tests/components/roborock/test_time.py b/tests/components/roborock/test_time.py index 836a86bd1147f..eb48e8e537f12 100644 --- a/tests/components/roborock/test_time.py +++ b/tests/components/roborock/test_time.py @@ -7,12 +7,19 @@ import roborock from homeassistant.components.time import SERVICE_SET_VALUE +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.TIME] + + @pytest.mark.parametrize( ("entity_id"), [ diff --git a/tests/components/roborock/test_vacuum.py b/tests/components/roborock/test_vacuum.py index 449ba7bca954a..d9d4340ec837c 100644 --- a/tests/components/roborock/test_vacuum.py +++ b/tests/components/roborock/test_vacuum.py @@ -40,6 +40,15 @@ DEVICE_ID = "abc123" +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + # Note: Currently the Image platform is required to make these tests pass since + # some initialization of the coordinator happens as a side effect of loading + # image platform. Fix that and remove IMAGE here. + return [Platform.VACUUM, Platform.IMAGE] + + async def test_registry_entries( hass: HomeAssistant, entity_registry: er.EntityRegistry, diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index 4bee5c0e58916..5576128eae5cc 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -7,7 +7,7 @@ from datetime import datetime import socket from typing import Any -from unittest.mock import AsyncMock, MagicMock, PropertyMock, create_autospec, patch +from unittest.mock import AsyncMock, MagicMock, create_autospec, patch from urllib.parse import urlparse from async_upnp_client.aiohttp import AiohttpNotifyServer @@ -286,11 +286,8 @@ async def mock_config_entry( # Load config_entry. entry.add_to_hass(hass) - with patch( - "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", - PropertyMock(return_value=True), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() return entry diff --git a/tests/components/upnp/test_sensor.py b/tests/components/upnp/test_sensor.py index 67a64b265d9ff..e9d8a9cce8f53 100644 --- a/tests/components/upnp/test_sensor.py +++ b/tests/components/upnp/test_sensor.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from async_upnp_client.profiles.igd import IgdDevice, IgdState +import pytest from homeassistant.components.upnp.const import DEFAULT_SCAN_INTERVAL from homeassistant.core import HomeAssistant @@ -11,6 +12,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_upnp_sensors( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index cf7bbe7d1e25d..08b984a047747 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -2276,7 +2276,9 @@ async def test_cleanup_startup(hass: HomeAssistant) -> None: @pytest.mark.parametrize("load_registries", [False]) -async def test_cleanup_entity_registry_change(hass: HomeAssistant) -> None: +async def test_cleanup_entity_registry_change( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test we run a cleanup when entity registry changes. Don't pre-load the registries as the debouncer will then not be waiting for @@ -2284,8 +2286,14 @@ async def test_cleanup_entity_registry_change(hass: HomeAssistant) -> None: """ await dr.async_load(hass) await er.async_load(hass) + dev_reg = dr.async_get(hass) ent_reg = er.async_get(hass) + entry = dev_reg.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + with patch( "homeassistant.helpers.device_registry.Debouncer.async_schedule_call" ) as mock_call: @@ -2299,7 +2307,7 @@ async def test_cleanup_entity_registry_change(hass: HomeAssistant) -> None: assert len(mock_call.mock_calls) == 0 # Device ID update triggers - ent_reg.async_get_or_create("light", "hue", "e1", device_id="bla") + ent_reg.async_get_or_create("light", "hue", "e1", device_id=entry.id) await hass.async_block_till_done() assert len(mock_call.mock_calls) == 1 diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 97f7e1dcc561f..682f78434530e 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -72,11 +72,18 @@ def test_get_or_create_suggested_object_id(entity_registry: er.EntityRegistry) - def test_get_or_create_updates_data( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, ) -> None: """Test that we update data in get_or_create.""" orig_config_entry = MockConfigEntry(domain="light") + orig_config_entry.add_to_hass(hass) + orig_device_entry = device_registry.async_get_or_create( + config_entry_id=orig_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) created = datetime.fromisoformat("2024-02-14T12:00:00.0+00:00") freezer.move_to(created) @@ -86,7 +93,7 @@ def test_get_or_create_updates_data( "5678", capabilities={"max": 100}, config_entry=orig_config_entry, - device_id="mock-dev-id", + device_id=orig_device_entry.id, disabled_by=er.RegistryEntryDisabler.HASS, entity_category=EntityCategory.CONFIG, has_entity_name=True, @@ -99,7 +106,7 @@ def test_get_or_create_updates_data( unit_of_measurement="initial-unit_of_measurement", ) - assert set(entity_registry.async_device_ids()) == {"mock-dev-id"} + assert set(entity_registry.async_device_ids()) == {orig_device_entry.id} assert orig_entry == er.RegistryEntry( "light.hue_5678", @@ -109,7 +116,7 @@ def test_get_or_create_updates_data( config_entry_id=orig_config_entry.entry_id, created_at=created, device_class=None, - device_id="mock-dev-id", + device_id=orig_device_entry.id, disabled_by=er.RegistryEntryDisabler.HASS, entity_category=EntityCategory.CONFIG, has_entity_name=True, @@ -127,6 +134,11 @@ def test_get_or_create_updates_data( ) new_config_entry = MockConfigEntry(domain="light") + new_config_entry.add_to_hass(hass) + new_device_entry = device_registry.async_get_or_create( + config_entry_id=new_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "34:56:AB:CD:EF:12")}, + ) modified = created + timedelta(minutes=5) freezer.move_to(modified) @@ -136,7 +148,7 @@ def test_get_or_create_updates_data( "5678", capabilities={"new-max": 150}, config_entry=new_config_entry, - device_id="new-mock-dev-id", + device_id=new_device_entry.id, disabled_by=er.RegistryEntryDisabler.USER, entity_category=EntityCategory.DIAGNOSTIC, has_entity_name=False, @@ -159,7 +171,7 @@ def test_get_or_create_updates_data( config_entry_id=new_config_entry.entry_id, created_at=created, device_class=None, - device_id="new-mock-dev-id", + device_id=new_device_entry.id, disabled_by=er.RegistryEntryDisabler.HASS, # Should not be updated entity_category=EntityCategory.DIAGNOSTIC, has_entity_name=False, @@ -176,7 +188,7 @@ def test_get_or_create_updates_data( unit_of_measurement="updated-unit_of_measurement", ) - assert set(entity_registry.async_device_ids()) == {"new-mock-dev-id"} + assert set(entity_registry.async_device_ids()) == {new_device_entry.id} modified = created + timedelta(minutes=5) freezer.move_to(modified) @@ -262,10 +274,18 @@ def test_create_triggers_save(entity_registry: er.EntityRegistry) -> None: async def test_loading_saving_data( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test that we load/save data correctly.""" mock_config = MockConfigEntry(domain="light") + mock_config.add_to_hass(hass) + + device_entry = device_registry.async_get_or_create( + config_entry_id=mock_config.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) orig_entry1 = entity_registry.async_get_or_create("light", "hue", "1234") orig_entry2 = entity_registry.async_get_or_create( @@ -274,7 +294,7 @@ async def test_loading_saving_data( "5678", capabilities={"max": 100}, config_entry=mock_config, - device_id="mock-dev-id", + device_id=device_entry.id, disabled_by=er.RegistryEntryDisabler.HASS, entity_category=EntityCategory.CONFIG, hidden_by=er.RegistryEntryHider.INTEGRATION, @@ -338,7 +358,7 @@ async def test_loading_saving_data( assert new_entry2.capabilities == {"max": 100} assert new_entry2.config_entry_id == mock_config.entry_id assert new_entry2.device_class == "user-class" - assert new_entry2.device_id == "mock-dev-id" + assert new_entry2.device_id == device_entry.id assert new_entry2.disabled_by is er.RegistryEntryDisabler.HASS assert new_entry2.entity_category == "config" assert new_entry2.icon == "hass:user-icon" @@ -1741,6 +1761,16 @@ def test_entity_registry_items() -> None: assert entities.get_entry(entry2.id) is None +async def test_device_does_not_exist(entity_registry: er.EntityRegistry) -> None: + """Test adding an entity linked to an unknown device.""" + with pytest.raises(ValueError): + entity_registry.async_get_or_create("light", "hue", "1234", device_id="blah") + + entity_id = entity_registry.async_get_or_create("light", "hue", "1234").entity_id + with pytest.raises(ValueError): + entity_registry.async_update_entity(entity_id, device_id="blah") + + async def test_disabled_by_str_not_allowed(entity_registry: er.EntityRegistry) -> None: """Test we need to pass disabled by type.""" with pytest.raises(ValueError):