diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index a487e95582cd26..a7083c4ae0f361 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -71,4 +71,4 @@ } UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=10) UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6) -UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(hours=30) +UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(minutes=30) diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index ee94052c28663f..49ca7970ae3730 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -16,10 +16,12 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, + LIGHT_LUX, PERCENTAGE, EntityCategory, Platform, UnitOfPressure, + UnitOfSoundPressure, UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback @@ -112,6 +114,21 @@ state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=0, ), + "lux": SensorEntityDescription( + key="lux", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + ), + "noise": SensorEntityDescription( + key="noise", + translation_key="ambient_noise", + device_class=SensorDeviceClass.SOUND_PRESSURE, + native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + ), } PARALLEL_UPDATES = 0 diff --git a/homeassistant/components/airthings_ble/strings.json b/homeassistant/components/airthings_ble/strings.json index 6799aa20ba7076..2755866cdb6a9c 100644 --- a/homeassistant/components/airthings_ble/strings.json +++ b/homeassistant/components/airthings_ble/strings.json @@ -41,6 +41,9 @@ }, "illuminance": { "name": "[%key:component::sensor::entity_component::illuminance::name%]" + }, + "ambient_noise": { + "name": "Ambient noise" } } } diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 82e83275b6b67a..e0aee9b5c2a00f 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.81", "babel==2.15.0"] + "requirements": ["holidays==0.82", "babel==2.15.0"] } diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 3bf47df83a4086..b8121286310a32 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.1.1"] + "requirements": ["pylamarzocco==2.1.2"] } diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 04adc9f2d608b2..be1f3e9f8f31c2 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -174,7 +174,7 @@ class MotionBaseDevice(MotionCoordinatorEntity, CoverEntity): _restore_tilt = False - def __init__(self, coordinator, blind, device_class): + def __init__(self, coordinator, blind, device_class) -> None: """Initialize the blind.""" super().__init__(coordinator, blind) @@ -275,7 +275,7 @@ def current_cover_tilt_position(self) -> int | None: """ if self._blind.angle is None: return None - return self._blind.angle * 100 / 180 + return 100 - (self._blind.angle * 100 / 180) @property def is_closed(self) -> bool | None: @@ -287,14 +287,14 @@ def is_closed(self) -> bool | None: async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" async with self._api_lock: - await self.hass.async_add_executor_job(self._blind.Set_angle, 180) + await self.hass.async_add_executor_job(self._blind.Set_angle, 0) await self.async_request_position_till_stop() async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" async with self._api_lock: - await self.hass.async_add_executor_job(self._blind.Set_angle, 0) + await self.hass.async_add_executor_job(self._blind.Set_angle, 180) await self.async_request_position_till_stop() @@ -302,7 +302,7 @@ async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" angle = kwargs[ATTR_TILT_POSITION] * 180 / 100 async with self._api_lock: - await self.hass.async_add_executor_job(self._blind.Set_angle, angle) + await self.hass.async_add_executor_job(self._blind.Set_angle, 180 - angle) await self.async_request_position_till_stop() @@ -347,9 +347,9 @@ def current_cover_tilt_position(self) -> int | None: if self._blind.position is None: if self._blind.angle is None: return None - return self._blind.angle * 100 / 180 + return 100 - (self._blind.angle * 100 / 180) - return self._blind.position + return 100 - self._blind.position @property def is_closed(self) -> bool | None: @@ -357,9 +357,9 @@ def is_closed(self) -> bool | None: if self._blind.position is None: if self._blind.angle is None: return None - return self._blind.angle == 0 + return self._blind.angle == 180 - return self._blind.position == 0 + return self._blind.position == 100 async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" @@ -381,10 +381,14 @@ async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: if self._blind.position is None: angle = angle * 180 / 100 async with self._api_lock: - await self.hass.async_add_executor_job(self._blind.Set_angle, angle) + await self.hass.async_add_executor_job( + self._blind.Set_angle, 180 - angle + ) else: async with self._api_lock: - await self.hass.async_add_executor_job(self._blind.Set_position, angle) + await self.hass.async_add_executor_job( + self._blind.Set_position, 100 - angle + ) await self.async_request_position_till_stop() @@ -397,10 +401,14 @@ async def async_set_absolute_position(self, **kwargs): if self._blind.position is None: angle = angle * 180 / 100 async with self._api_lock: - await self.hass.async_add_executor_job(self._blind.Set_angle, angle) + await self.hass.async_add_executor_job( + self._blind.Set_angle, 180 - angle + ) else: async with self._api_lock: - await self.hass.async_add_executor_job(self._blind.Set_position, angle) + await self.hass.async_add_executor_job( + self._blind.Set_position, 100 - angle + ) await self.async_request_position_till_stop() @@ -408,7 +416,7 @@ async def async_set_absolute_position(self, **kwargs): class MotionTDBUDevice(MotionBaseDevice): """Representation of a Motion Top Down Bottom Up blind Device.""" - def __init__(self, coordinator, blind, device_class, motor): + def __init__(self, coordinator, blind, device_class, motor) -> None: """Initialize the blind.""" super().__init__(coordinator, blind, device_class) self._motor = motor diff --git a/homeassistant/components/open_router/manifest.json b/homeassistant/components/open_router/manifest.json index 4a406e0613960b..5c3334193abf7a 100644 --- a/homeassistant/components/open_router/manifest.json +++ b/homeassistant/components/open_router/manifest.json @@ -9,5 +9,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "bronze", - "requirements": ["openai==1.99.5", "python-open-router==0.3.1"] + "requirements": ["openai==2.2.0", "python-open-router==0.3.1"] } diff --git a/homeassistant/components/openai_conversation/entity.py b/homeassistant/components/openai_conversation/entity.py index 4d2c62a7a8c200..e876c50481d927 100644 --- a/homeassistant/components/openai_conversation/entity.py +++ b/homeassistant/components/openai_conversation/entity.py @@ -487,7 +487,7 @@ async def _async_handle_chat_log( if options.get(CONF_WEB_SEARCH): web_search = WebSearchToolParam( - type="web_search_preview", + type="web_search", search_context_size=options.get( CONF_WEB_SEARCH_CONTEXT_SIZE, RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE ), diff --git a/homeassistant/components/openai_conversation/manifest.json b/homeassistant/components/openai_conversation/manifest.json index a96efbf1ce815b..f733b7a2af62a1 100644 --- a/homeassistant/components/openai_conversation/manifest.json +++ b/homeassistant/components/openai_conversation/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/openai_conversation", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["openai==1.99.5"] + "requirements": ["openai==2.2.0"] } diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index fbc46160f1ce95..4c6d5695f33578 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -9,7 +9,6 @@ from aioshelly.const import BLU_TRV_IDENTIFIER, MODEL_BLU_GATEWAY_G3, RPC_GENERATIONS from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError -from aioshelly.rpc_device import RpcDevice from homeassistant.components.button import ( DOMAIN as BUTTON_PLATFORM, @@ -24,16 +23,24 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, LOGGER, SHELLY_GAS_MODELS +from .const import DOMAIN, LOGGER, MODEL_FRANKEVER_WATER_VALVE, SHELLY_GAS_MODELS from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator -from .entity import get_entity_block_device_info, get_entity_rpc_device_info +from .entity import ( + RpcEntityDescription, + ShellyRpcAttributeEntity, + async_setup_entry_rpc, + get_entity_block_device_info, + get_entity_rpc_device_info, + rpc_call, +) from .utils import ( async_remove_orphaned_entities, format_ble_addr, get_blu_trv_device_info, get_device_entry_gen, - get_rpc_entity_name, get_rpc_key_ids, + get_rpc_key_instances, + get_rpc_role_by_key, get_virtual_component_ids, ) @@ -51,6 +58,11 @@ class ShellyButtonDescription[ supported: Callable[[_ShellyCoordinatorT], bool] = lambda _: True +@dataclass(frozen=True, kw_only=True) +class RpcButtonDescription(RpcEntityDescription, ButtonEntityDescription): + """Class to describe a RPC button.""" + + BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [ ShellyButtonDescription[ShellyBlockCoordinator | ShellyRpcCoordinator]( key="reboot", @@ -96,12 +108,24 @@ class ShellyButtonDescription[ ), ] -VIRTUAL_BUTTONS: Final[list[ShellyButtonDescription]] = [ - ShellyButtonDescription[ShellyRpcCoordinator]( +RPC_VIRTUAL_BUTTONS = { + "button_generic": RpcButtonDescription( key="button", - press_action="single_push", - ) -] + role="generic", + ), + "button_open": RpcButtonDescription( + key="button", + entity_registry_enabled_default=False, + role="open", + models={MODEL_FRANKEVER_WATER_VALVE}, + ), + "button_close": RpcButtonDescription( + key="button", + entity_registry_enabled_default=False, + role="close", + models={MODEL_FRANKEVER_WATER_VALVE}, + ), +} @callback @@ -129,8 +153,10 @@ def async_migrate_unique_ids( ) } + if not isinstance(coordinator, ShellyRpcCoordinator): + return None + if blutrv_key_ids := get_rpc_key_ids(coordinator.device.status, BLU_TRV_IDENTIFIER): - assert isinstance(coordinator.device, RpcDevice) for _id in blutrv_key_ids: key = f"{BLU_TRV_IDENTIFIER}:{_id}" ble_addr: str = coordinator.device.config[key]["addr"] @@ -149,6 +175,26 @@ def async_migrate_unique_ids( ) } + if virtual_button_keys := get_rpc_key_instances( + coordinator.device.config, "button" + ): + for key in virtual_button_keys: + old_unique_id = f"{coordinator.mac}-{key}" + if entity_entry.unique_id == old_unique_id: + role = get_rpc_role_by_key(coordinator.device.config, key) + new_unique_id = f"{coordinator.mac}-{key}-button_{role}" + LOGGER.debug( + "Migrating unique_id for %s entity from [%s] to [%s]", + entity_entry.entity_id, + old_unique_id, + new_unique_id, + ) + return { + "new_unique_id": entity_entry.unique_id.replace( + old_unique_id, new_unique_id + ) + } + return None @@ -172,7 +218,7 @@ async def async_setup_entry( hass, config_entry.entry_id, partial(async_migrate_unique_ids, coordinator) ) - entities: list[ShellyButton | ShellyBluTrvButton | ShellyVirtualButton] = [] + entities: list[ShellyButton | ShellyBluTrvButton] = [] entities.extend( ShellyButton(coordinator, button) @@ -185,12 +231,9 @@ async def async_setup_entry( return # add virtual buttons - if virtual_button_ids := get_rpc_key_ids(coordinator.device.status, "button"): - entities.extend( - ShellyVirtualButton(coordinator, button, id_) - for id_ in virtual_button_ids - for button in VIRTUAL_BUTTONS - ) + async_setup_entry_rpc( + hass, config_entry, async_add_entities, RPC_VIRTUAL_BUTTONS, RpcVirtualButton + ) # add BLU TRV buttons if blutrv_key_ids := get_rpc_key_ids(coordinator.device.status, BLU_TRV_IDENTIFIER): @@ -332,30 +375,16 @@ async def _press_method(self) -> None: await method(self._id) -class ShellyVirtualButton(ShellyBaseButton): - """Defines a Shelly virtual component button.""" - - def __init__( - self, - coordinator: ShellyRpcCoordinator, - description: ShellyButtonDescription, - _id: int, - ) -> None: - """Initialize Shelly virtual component button.""" - super().__init__(coordinator, description) +class RpcVirtualButton(ShellyRpcAttributeEntity, ButtonEntity): + """Defines a Shelly RPC virtual component button.""" - self._attr_unique_id = f"{coordinator.mac}-{description.key}:{_id}" - self._attr_device_info = get_entity_rpc_device_info(coordinator) - self._attr_name = get_rpc_entity_name( - coordinator.device, f"{description.key}:{_id}" - ) - self._id = _id + entity_description: RpcButtonDescription + _id: int - async def _press_method(self) -> None: - """Press method.""" + @rpc_call + async def async_press(self) -> None: + """Triggers the Shelly button press service.""" if TYPE_CHECKING: assert isinstance(self.coordinator, ShellyRpcCoordinator) - await self.coordinator.device.button_trigger( - self._id, self.entity_description.press_action - ) + await self.coordinator.device.button_trigger(self._id, "single_push") diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 327d40df421395..44a75e64b3170f 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -195,9 +195,11 @@ def async_setup_rpc_attribute_entities( ): continue - if description.sub_key not in coordinator.device.status[ - key - ] and not description.supported(coordinator.device.status[key]): + if ( + description.sub_key + and description.sub_key not in coordinator.device.status[key] + and not description.supported(coordinator.device.status[key]) + ): continue # Filter and remove entities that according to settings/status @@ -309,7 +311,7 @@ class RpcEntityDescription(EntityDescription): # restrict the type to str. name: str = "" - sub_key: str + sub_key: str | None = None value: Callable[[Any, Any], Any] | None = None available: Callable[[dict], bool] | None = None diff --git a/homeassistant/components/smartthings/icons.json b/homeassistant/components/smartthings/icons.json index aad9182576d413..123668612ef697 100644 --- a/homeassistant/components/smartthings/icons.json +++ b/homeassistant/components/smartthings/icons.json @@ -34,6 +34,17 @@ "climate": { "air_conditioner": { "state_attributes": { + "preset_mode": { + "state": { + "wind_free": "mdi:weather-dust", + "wind_free_sleep": "mdi:sleep", + "quiet": "mdi:volume-off", + "long_wind": "mdi:weather-windy", + "smart": "mdi:leaf", + "motion_direct": "mdi:account-arrow-left", + "motion_indirect": "mdi:account-arrow-right" + } + }, "fan_mode": { "state": { "turbo": "mdi:wind-power" diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index c7a97ffb392df8..d0b420fe5c63c3 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.81"] + "requirements": ["holidays==0.82"] } diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index bece865bef21ed..59f8d134f27a5b 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -744,8 +744,11 @@ async def async_step_confirm( # Without confirmation, discovery can automatically progress into parts of the # config flow logic that interacts with hardware. + # Ignore Zeroconf discoveries during onboarding, as they may be in use already. if user_input is not None or ( - not onboarding.async_is_onboarded(self.hass) and not zha_config_entries + not onboarding.async_is_onboarded(self.hass) + and not zha_config_entries + and self.source != SOURCE_ZEROCONF ): # Probe the radio type if we don't have one yet if self._radio_mgr.radio_type is None: diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b879a9aac2048d..4c6d8c2d579c74 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -130,7 +130,7 @@ multidict>=6.0.2 backoff>=2.0 # ensure pydantic version does not float since it might have breaking changes -pydantic==2.11.9 +pydantic==2.12.0 # Required for Python 3.12.4 compatibility (#119223). mashumaro>=3.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index e1e8cb043c3083..f0736dc1429cc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1183,7 +1183,7 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.81 +holidays==0.82 # homeassistant.components.frontend home-assistant-frontend==20251001.0 @@ -1628,7 +1628,7 @@ open-meteo==0.3.2 # homeassistant.components.open_router # homeassistant.components.openai_conversation -openai==1.99.5 +openai==2.2.0 # homeassistant.components.openerz openerz-api==0.3.0 @@ -2135,7 +2135,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.1.1 +pylamarzocco==2.1.2 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test.txt b/requirements_test.txt index 78750341109fb2..a7edadc0112826 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -15,7 +15,7 @@ license-expression==30.4.3 mock-open==1.4.0 mypy-dev==1.19.0a2 pre-commit==4.2.0 -pydantic==2.11.9 +pydantic==2.12.0 pylint==3.3.8 pylint-per-file-ignores==1.4.0 pipdeptree==2.26.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a447316f2a70da..2b81bb34f56d57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1032,7 +1032,7 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.81 +holidays==0.82 # homeassistant.components.frontend home-assistant-frontend==20251001.0 @@ -1399,7 +1399,7 @@ open-meteo==0.3.2 # homeassistant.components.open_router # homeassistant.components.openai_conversation -openai==1.99.5 +openai==2.2.0 # homeassistant.components.openerz openerz-api==0.3.0 @@ -1783,7 +1783,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.1.1 +pylamarzocco==2.1.2 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index bdd8ed2cda15b9..6b96e3b5e53da0 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -155,7 +155,7 @@ backoff>=2.0 # ensure pydantic version does not float since it might have breaking changes -pydantic==2.11.9 +pydantic==2.12.0 # Required for Python 3.12.4 compatibility (#119223). mashumaro>=3.13.1 diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py index d2dfd6bbf1267b..cf91634f71f764 100644 --- a/tests/components/airthings_ble/__init__.py +++ b/tests/components/airthings_ble/__init__.py @@ -9,12 +9,17 @@ AirthingsDevice, AirthingsDeviceType, ) +from bleak.backends.device import BLEDevice from homeassistant.components.airthings_ble.const import DOMAIN from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceRegistry +from homeassistant.helpers.device_registry import ( + CONNECTION_BLUETOOTH, + DeviceEntry, + DeviceRegistry, +) from tests.common import MockConfigEntry, MockEntity from tests.components.bluetooth import generate_advertisement_data, generate_ble_device @@ -28,7 +33,15 @@ def patch_async_setup_entry(return_value=True): ) -def patch_async_ble_device_from_address(return_value: BluetoothServiceInfoBleak | None): +def patch_async_discovered_service_info(return_value: list[BluetoothServiceInfoBleak]): + """Patch async_discovered_service_info to return given list.""" + return patch( + "homeassistant.components.bluetooth.async_discovered_service_info", + return_value=return_value, + ) + + +def patch_async_ble_device_from_address(return_value: BLEDevice | None): """Patch async ble device from address to return a given value.""" return patch( "homeassistant.components.bluetooth.async_ble_device_from_address", @@ -101,6 +114,27 @@ def patch_airthings_device_update(): tx_power=0, ) +WAVE_ENHANCE_SERVICE_INFO = BluetoothServiceInfoBleak( + name="cc-cc-cc-cc-cc-cc", + address="cc:cc:cc:cc:cc:cc", + device=generate_ble_device( + address="cc:cc:cc:cc:cc:cc", + name="Airthings Wave Enhance", + ), + rssi=-61, + manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"}, + service_data={}, + service_uuids=[], + source="local", + advertisement=generate_advertisement_data( + manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"}, + service_uuids=[], + ), + connectable=True, + time=0, + tx_power=0, +) + VIEW_PLUS_SERVICE_INFO = BluetoothServiceInfoBleak( name="cc-cc-cc-cc-cc-cc", address="cc:cc:cc:cc:cc:cc", @@ -211,6 +245,26 @@ def patch_airthings_device_update(): address="cc:cc:cc:cc:cc:cc", ) +WAVE_ENHANCE_DEVICE_INFO = AirthingsDevice( + manufacturer="Airthings AS", + hw_version="REV X", + sw_version="T-SUB-2.6.2-master+0", + model=AirthingsDeviceType.WAVE_ENHANCE_EU, + name="Airthings Wave Enhance", + identifier="123456", + sensors={ + "lux": 25, + "battery": 85, + "humidity": 60.0, + "temperature": 21.0, + "co2": 500.0, + "voc": 155.0, + "pressure": 1020, + "noise": 40, + }, + address="cc:cc:cc:cc:cc:cc", +) + TEMPERATURE_V1 = MockEntity( unique_id="Airthings Wave Plus 123456_temperature", name="Airthings Wave Plus 123456 Temperature", @@ -247,23 +301,32 @@ def patch_airthings_device_update(): ) -def create_entry(hass: HomeAssistant) -> MockConfigEntry: +def create_entry( + hass: HomeAssistant, + service_info: BluetoothServiceInfoBleak, + device_info: AirthingsDevice, +) -> MockConfigEntry: """Create a config entry.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id=WAVE_SERVICE_INFO.address, - title="Airthings Wave Plus (123456)", + unique_id=service_info.address, + title=f"{device_info.name} ({device_info.identifier})", ) entry.add_to_hass(hass) return entry -def create_device(entry: ConfigEntry, device_registry: DeviceRegistry): +def create_device( + entry: ConfigEntry, + device_registry: DeviceRegistry, + service_info: BluetoothServiceInfoBleak, + device_info: AirthingsDevice, +) -> DeviceEntry: """Create a device for the given entry.""" return device_registry.async_get_or_create( config_entry_id=entry.entry_id, - connections={(CONNECTION_BLUETOOTH, WAVE_SERVICE_INFO.address)}, + connections={(CONNECTION_BLUETOOTH, service_info.address)}, manufacturer="Airthings AS", - name="Airthings Wave Plus (123456)", - model="Wave Plus", + name=f"{device_info.name} ({device_info.identifier})", + model=device_info.model.product_name, ) diff --git a/tests/components/airthings_ble/test_sensor.py b/tests/components/airthings_ble/test_sensor.py index a8acdf7ec7be53..988dc313dab171 100644 --- a/tests/components/airthings_ble/test_sensor.py +++ b/tests/components/airthings_ble/test_sensor.py @@ -2,6 +2,8 @@ import logging +import pytest + from homeassistant.components.airthings_ble.const import DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -16,10 +18,15 @@ VOC_V2, VOC_V3, WAVE_DEVICE_INFO, + WAVE_ENHANCE_DEVICE_INFO, + WAVE_ENHANCE_SERVICE_INFO, WAVE_SERVICE_INFO, create_device, create_entry, + patch_airthings_ble, patch_airthings_device_update, + patch_async_ble_device_from_address, + patch_async_discovered_service_info, ) from tests.components.bluetooth import inject_bluetooth_service_info @@ -33,8 +40,8 @@ async def test_migration_from_v1_to_v3_unique_id( device_registry: dr.DeviceRegistry, ) -> None: """Verify that we can migrate from v1 (pre 2023.9.0) to the latest unique id format.""" - entry = create_entry(hass) - device = create_device(entry, device_registry) + entry = create_entry(hass, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO) + device = create_device(entry, device_registry, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO) assert entry is not None assert device is not None @@ -74,8 +81,8 @@ async def test_migration_from_v2_to_v3_unique_id( device_registry: dr.DeviceRegistry, ) -> None: """Verify that we can migrate from v2 (introduced in 2023.9.0) to the latest unique id format.""" - entry = create_entry(hass) - device = create_device(entry, device_registry) + entry = create_entry(hass, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO) + device = create_device(entry, device_registry, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO) assert entry is not None assert device is not None @@ -115,8 +122,8 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id( device_registry: dr.DeviceRegistry, ) -> None: """Test if migration works when we have both v1 (pre 2023.9.0) and v2 (introduced in 2023.9.0) unique ids.""" - entry = create_entry(hass) - device = create_device(entry, device_registry) + entry = create_entry(hass, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO) + device = create_device(entry, device_registry, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO) assert entry is not None assert device is not None @@ -165,8 +172,8 @@ async def test_migration_with_all_unique_ids( device_registry: dr.DeviceRegistry, ) -> None: """Test if migration works when we have all unique ids.""" - entry = create_entry(hass) - device = create_device(entry, device_registry) + entry = create_entry(hass, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO) + device = create_device(entry, device_registry, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO) assert entry is not None assert device is not None @@ -215,3 +222,48 @@ async def test_migration_with_all_unique_ids( assert entity_registry.async_get(v1.entity_id).unique_id == VOC_V1.unique_id assert entity_registry.async_get(v2.entity_id).unique_id == VOC_V2.unique_id assert entity_registry.async_get(v3.entity_id).unique_id == VOC_V3.unique_id + + +@pytest.mark.parametrize( + ("unique_suffix", "expected_sensor_name"), + [ + ("lux", "Illuminance"), + ("noise", "Ambient noise"), + ], +) +async def test_translation_keys( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + unique_suffix: str, + expected_sensor_name: str, +) -> None: + """Test that translated sensor names are correct.""" + entry = create_entry(hass, WAVE_ENHANCE_SERVICE_INFO, WAVE_DEVICE_INFO) + device = create_device( + entry, device_registry, WAVE_ENHANCE_SERVICE_INFO, WAVE_ENHANCE_DEVICE_INFO + ) + + with ( + patch_async_ble_device_from_address(WAVE_ENHANCE_SERVICE_INFO.device), + patch_async_discovered_service_info([WAVE_ENHANCE_SERVICE_INFO]), + patch_airthings_ble(WAVE_ENHANCE_DEVICE_INFO), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert device is not None + assert device.name == "Airthings Wave Enhance (123456)" + + unique_id = f"{WAVE_ENHANCE_DEVICE_INFO.address}_{unique_suffix}" + entity_id = entity_registry.async_get_entity_id(Platform.SENSOR, DOMAIN, unique_id) + assert entity_id is not None + + state = hass.states.get(entity_id) + assert state is not None + + expected_value = WAVE_ENHANCE_DEVICE_INFO.sensors[unique_suffix] + assert state.state == str(expected_value) + + expected_name = f"Airthings Wave Enhance (123456) {expected_sensor_name}" + assert state.attributes.get("friendly_name") == expected_name diff --git a/tests/components/openai_conversation/__init__.py b/tests/components/openai_conversation/__init__.py index fb19236034f267..ccd41dffbdadf5 100644 --- a/tests/components/openai_conversation/__init__.py +++ b/tests/components/openai_conversation/__init__.py @@ -157,6 +157,7 @@ def create_function_tool_call_item( ResponseFunctionCallArgumentsDoneEvent( arguments="".join(arguments), item_id=id, + name=name, output_index=output_index, sequence_number=0, type="response.function_call_arguments.done", diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index 452404f65acd32..a53644d6f5b44a 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -474,7 +474,7 @@ async def test_web_search( assert mock_create_stream.mock_calls[0][2]["tools"] == [ { - "type": "web_search_preview", + "type": "web_search", "search_context_size": "low", "user_location": { "type": "approximate", diff --git a/tests/components/shelly/snapshots/test_button.ambr b/tests/components/shelly/snapshots/test_button.ambr index af19860f546616..7ec15e7b1db02b 100644 --- a/tests/components/shelly/snapshots/test_button.ambr +++ b/tests/components/shelly/snapshots/test_button.ambr @@ -127,7 +127,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456789ABC-button:200', + 'unique_id': '123456789ABC-button:200-button_generic', 'unit_of_measurement': None, }) # --- @@ -175,7 +175,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456789ABC-button:200', + 'unique_id': '123456789ABC-button:200-button_generic', 'unit_of_measurement': None, }) # --- diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py index f6a3df0bb4860c..dd1f56872e108a 100644 --- a/tests/components/shelly/test_button.py +++ b/tests/components/shelly/test_button.py @@ -9,7 +9,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.components.shelly.const import DOMAIN +from homeassistant.components.shelly.const import DOMAIN, MODEL_FRANKEVER_WATER_VALVE from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant @@ -17,7 +17,13 @@ from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity_registry import EntityRegistry -from . import init_integration, patch_platforms, register_device, register_entity +from . import ( + MOCK_MAC, + init_integration, + patch_platforms, + register_device, + register_entity, +) @pytest.fixture(autouse=True) @@ -417,3 +423,56 @@ async def test_migrate_unique_id_blu_trv( assert entity_entry.unique_id == "F8447725F0DD-blutrv:200-calibrate" assert "Migrating unique_id for button.trv_name_calibrate" in caplog.text + + +@pytest.mark.parametrize( + ("old_id", "new_id", "role"), + [ + ("button", "button_generic", None), + ("button", "button_open", "open"), + ("button", "button_close", "close"), + ], +) +async def test_migrate_unique_id_virtual_components_roles( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, + old_id: str, + new_id: str, + role: str | None, +) -> None: + """Test migration of unique_id for virtual components to include role.""" + entry = await init_integration( + hass, 3, model=MODEL_FRANKEVER_WATER_VALVE, skip_setup=True + ) + old_unique_id = f"{MOCK_MAC}-{old_id}:200" + new_unique_id = f"{old_unique_id}-{new_id}" + config = deepcopy(mock_rpc_device.config) + if role: + config[f"{old_id}:200"] = { + "role": role, + } + else: + config[f"{old_id}:200"] = {} + monkeypatch.setattr(mock_rpc_device, "config", config) + + entity = entity_registry.async_get_or_create( + suggested_object_id="test_name_test_button", + disabled_by=None, + domain=BUTTON_DOMAIN, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=entry, + ) + assert entity.unique_id == old_unique_id + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_entry = entity_registry.async_get("button.test_name_test_button") + assert entity_entry + assert entity_entry.unique_id == new_unique_id + + assert "Migrating unique_id for button.test_name_test_button" in caplog.text diff --git a/tests/components/youtube/snapshots/test_sensor.ambr b/tests/components/youtube/snapshots/test_sensor.ambr index feddd644cee984..01720efc978833 100644 --- a/tests/components/youtube/snapshots/test_sensor.ambr +++ b/tests/components/youtube/snapshots/test_sensor.ambr @@ -4,7 +4,7 @@ 'attributes': ReadOnlyDict({ 'entity_picture': 'https://i.ytimg.com/vi/wysukDrMdqU/maxresdefault.jpg', 'friendly_name': 'Google for Developers Latest upload', - 'published_at': datetime.datetime(2023, 5, 11, 0, 20, 46, tzinfo=TzInfo(UTC)), + 'published_at': datetime.datetime(2023, 5, 11, 0, 20, 46, tzinfo=TzInfo(0)), 'video_id': 'wysukDrMdqU', }), 'context': , diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index aae16dbccfba81..341357662f193e 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -952,6 +952,33 @@ async def test_zeroconf_discovery_via_socket_already_setup_with_ip_match( assert result["reason"] == "single_instance_allowed" +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_zeroconf_not_onboarded(hass: HomeAssistant) -> None: + """Test zeroconf discovery needing confirmation when not onboarded.""" + service_info = ZeroconfServiceInfo( + ip_address=ip_address("192.168.1.100"), + ip_addresses=[ip_address("192.168.1.100")], + hostname="tube-zigbee-gw.local.", + name="mock_name", + port=6638, + properties={"name": "tube_123456"}, + type="mock_type", + ) + with patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ): + result_create = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=service_info, + ) + await hass.async_block_till_done() + + # not automatically confirmed + assert result_create["type"] is FlowResultType.FORM + assert result_create["step_id"] == "confirm" + + @patch( "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", mock_detect_radio_type(radio_type=RadioType.deconz),