diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 81928a59a52bc..327b47bbea29f 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer-connected[china]==0.17.2"] + "requirements": ["bimmer-connected[china]==0.17.3"] } diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 4b056ead2c29b..938889955e960 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -35,7 +35,7 @@ ) from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity from hassil.trie import Trie -from hassil.util import merge_dict +from hassil.util import merge_dict, remove_punctuation from home_assistant_intents import ( ErrorKey, FuzzyConfig, @@ -327,12 +327,10 @@ async def async_recognize_intent( if self._exposed_names_trie is not None: # Filter by input string - text_lower = user_input.text.strip().lower() + text = remove_punctuation(user_input.text).strip().lower() slot_lists["name"] = TextSlotList( name="name", - values=[ - result[2] for result in self._exposed_names_trie.find(text_lower) - ], + values=[result[2] for result in self._exposed_names_trie.find(text)], ) start = time.monotonic() @@ -1263,7 +1261,7 @@ async def _make_slot_lists(self) -> dict[str, SlotList]: name_list = TextSlotList.from_tuples(exposed_entity_names, allow_template=False) for name_value in name_list.values: assert isinstance(name_value.text_in, TextChunk) - name_text = name_value.text_in.text.strip().lower() + name_text = remove_punctuation(name_value.text_in.text).strip().lower() self._exposed_names_trie.insert(name_text, name_value) self._slot_lists = { diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index c4951e88c91b2..485d6aa4b59ee 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -14,6 +14,9 @@ "toggle": "[%key:common::device_automation::action_type::toggle%]", "turn_on": "[%key:common::device_automation::action_type::turn_on%]", "turn_off": "[%key:common::device_automation::action_type::turn_off%]" + }, + "extra_fields": { + "for": "[%key:common::device_automation::extra_fields::for%]" } }, "entity_component": { diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 45d66e9621b65..5d5aba2af6036 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -183,8 +183,8 @@ "description": "Sets a new password for the guest Wi-Fi. The password must be between 8 and 63 characters long. If no additional parameter is set, the password will be auto-generated with a length of 12 characters.", "fields": { "device_id": { - "name": "Fritz!Box Device", - "description": "Select the Fritz!Box to configure." + "name": "FRITZ!Box Device", + "description": "Select the FRITZ!Box to configure." }, "password": { "name": "[%key:common::config_flow::data::password%]", diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index becab5a18c515..d74bf1f30b7f9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250903.2"] + "requirements": ["home-assistant-frontend==20250903.3"] } diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index e1f96b76bcb1a..2938de927210d 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -303,9 +303,9 @@ async def _websocket_forward( elif msg.type is aiohttp.WSMsgType.BINARY: await ws_to.send_bytes(msg.data) elif msg.type is aiohttp.WSMsgType.PING: - await ws_to.ping() + await ws_to.ping(msg.data) elif msg.type is aiohttp.WSMsgType.PONG: - await ws_to.pong() + await ws_to.pong(msg.data) elif ws_to.closed: await ws_to.close(code=ws_to.close_code, message=msg.extra) # type: ignore[arg-type] except RuntimeError: diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index bec443526138c..3328b5ab65941 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -9,7 +9,9 @@ import aiohttp from aiohue import LinkButtonNotPressed, create_app_key from aiohue.discovery import DiscoveredHueBridge, discover_bridge, discover_nupnp +from aiohue.errors import AiohueException from aiohue.util import normalize_bridge_id +from aiohue.v2 import HueBridgeV2 import slugify as unicode_slug import voluptuous as vol @@ -40,6 +42,9 @@ HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"] HUE_MANUAL_BRIDGE_ID = "manual" +BSB002_MODEL_ID = "BSB002" +BSB003_MODEL_ID = "BSB003" + class HueFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Hue config flow.""" @@ -74,7 +79,14 @@ async def _get_bridge( """Return a DiscoveredHueBridge object.""" try: bridge = await discover_bridge( - host, websession=aiohttp_client.async_get_clientsession(self.hass) + host, + websession=aiohttp_client.async_get_clientsession( + # NOTE: we disable SSL verification for now due to the fact that the (BSB003) + # Hue bridge uses a certificate from a on-bridge root authority. + # We need to specifically handle this case in a follow-up update. + self.hass, + verify_ssl=False, + ), ) except aiohttp.ClientError as err: LOGGER.warning( @@ -110,7 +122,9 @@ async def async_step_init( try: async with asyncio.timeout(5): bridges = await discover_nupnp( - websession=aiohttp_client.async_get_clientsession(self.hass) + websession=aiohttp_client.async_get_clientsession( + self.hass, verify_ssl=False + ) ) except TimeoutError: bridges = [] @@ -178,7 +192,9 @@ async def async_step_link( app_key = await create_app_key( bridge.host, f"home-assistant#{device_name}", - websession=aiohttp_client.async_get_clientsession(self.hass), + websession=aiohttp_client.async_get_clientsession( + self.hass, verify_ssl=False + ), ) except LinkButtonNotPressed: errors["base"] = "register_failed" @@ -228,7 +244,6 @@ async def async_step_zeroconf( self._abort_if_unique_id_configured( updates={CONF_HOST: discovery_info.host}, reload_on_update=True ) - # we need to query the other capabilities too bridge = await self._get_bridge( discovery_info.host, discovery_info.properties["bridgeid"] @@ -236,6 +251,14 @@ async def async_step_zeroconf( if bridge is None: return self.async_abort(reason="cannot_connect") self.bridge = bridge + if ( + bridge.supports_v2 + and discovery_info.properties.get("modelid") == BSB003_MODEL_ID + ): + # try to handle migration of BSB002 --> BSB003 + if await self._check_migrated_bridge(bridge): + return self.async_abort(reason="migrated_bridge") + return await self.async_step_link() async def async_step_homekit( @@ -272,6 +295,55 @@ async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResu self.bridge = bridge return await self.async_step_link() + async def _check_migrated_bridge(self, bridge: DiscoveredHueBridge) -> bool: + """Check if the discovered bridge is a migrated bridge.""" + # Try to handle migration of BSB002 --> BSB003. + # Once we detect a BSB003 bridge on the network which has not yet been + # configured in HA (otherwise we would have had a unique id match), + # we check if we have any existing (BSB002) entries and if we can connect to the + # new bridge with our previously stored api key. + # If that succeeds, we migrate the entry to the new bridge. + for conf_entry in self.hass.config_entries.async_entries( + DOMAIN, include_ignore=False, include_disabled=False + ): + if conf_entry.data[CONF_API_VERSION] != 2: + continue + if conf_entry.data[CONF_HOST] == bridge.host: + continue + # found an existing (BSB002) bridge entry, + # check if we can connect to the new BSB003 bridge using the old credentials + api = HueBridgeV2(bridge.host, conf_entry.data[CONF_API_KEY]) + try: + await api.fetch_full_state() + except (AiohueException, aiohttp.ClientError): + continue + old_bridge_id = conf_entry.unique_id + assert old_bridge_id is not None + # found a matching entry, migrate it + self.hass.config_entries.async_update_entry( + conf_entry, + data={ + **conf_entry.data, + CONF_HOST: bridge.host, + }, + unique_id=bridge.id, + ) + # also update the bridge device + dev_reg = dr.async_get(self.hass) + if bridge_device := dev_reg.async_get_device( + identifiers={(DOMAIN, old_bridge_id)} + ): + dev_reg.async_update_device( + bridge_device.id, + # overwrite identifiers with new bridge id + new_identifiers={(DOMAIN, bridge.id)}, + # overwrite mac addresses with empty set to drop the old (incorrect) addresses + # this will be auto corrected once the integration is loaded + new_connections=set(), + ) + return True + return False + class HueV1OptionsFlowHandler(OptionsFlow): """Handle Hue options for V1 implementation.""" diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 8bc3d84bd5039..e6f431727d019 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -10,6 +10,6 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["aiohue"], - "requirements": ["aiohue==4.7.4"], + "requirements": ["aiohue==4.7.5"], "zeroconf": ["_hue._tcp.local."] } diff --git a/homeassistant/components/imeon_inverter/manifest.json b/homeassistant/components/imeon_inverter/manifest.json index a9a37f3fd9c7c..837b735124175 100644 --- a/homeassistant/components/imeon_inverter/manifest.json +++ b/homeassistant/components/imeon_inverter/manifest.json @@ -7,7 +7,7 @@ "integration_type": "device", "iot_class": "local_polling", "quality_scale": "bronze", - "requirements": ["imeon_inverter_api==0.3.14"], + "requirements": ["imeon_inverter_api==0.3.16"], "ssdp": [ { "manufacturer": "IMEON", diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index 72853276ab3aa..17ec8602d9834 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -615,7 +615,7 @@ async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response intent_result = await intent.async_handle( hass, DOMAIN, intent_name, slots, "", self.context(request) ) - except intent.IntentHandleError as err: + except (intent.IntentHandleError, intent.MatchFailedError) as err: intent_result = intent.IntentResponse(language=language) intent_result.async_set_speech(str(err)) diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 7a53f2569e781..a17d6793b83e2 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -57,7 +57,8 @@ }, "extra_fields": { "brightness_pct": "Brightness", - "flash": "Flash" + "flash": "Flash", + "for": "[%key:common::device_automation::extra_fields::for%]" } }, "entity_component": { diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index c5cc94ead3055..4ae2ac8bbbfb6 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/mill", "iot_class": "local_polling", "loggers": ["mill", "mill_local"], - "requirements": ["millheater==0.12.5", "mill-local==0.3.0"] + "requirements": ["millheater==0.13.1", "mill-local==0.3.0"] } diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index 38622c4c19744..8667bc17a796a 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -62,7 +62,6 @@ CONF_VIRTUAL_COUNT, CONF_WRITE_TYPE, CONF_ZERO_SUPPRESS, - SIGNAL_START_ENTITY, SIGNAL_STOP_ENTITY, DataType, ) @@ -143,7 +142,6 @@ def async_disable(self) -> None: self._cancel_call() self._cancel_call = None self._attr_available = False - self.async_write_ha_state() async def async_await_connection(self, _now: Any) -> None: """Wait for first connect.""" @@ -162,11 +160,6 @@ async def async_base_added_to_hass(self) -> None: self.async_on_remove( async_dispatcher_connect(self.hass, SIGNAL_STOP_ENTITY, self.async_disable) ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_START_ENTITY, self.async_local_update - ) - ) class BaseStructPlatform(BasePlatform, RestoreEntity): diff --git a/homeassistant/components/ohme/coordinator.py b/homeassistant/components/ohme/coordinator.py index 864b03e9a7c10..d9e009ed1f10d 100644 --- a/homeassistant/components/ohme/coordinator.py +++ b/homeassistant/components/ohme/coordinator.py @@ -10,7 +10,7 @@ from ohme import ApiException, OhmeApiClient from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -83,6 +83,21 @@ class OhmeAdvancedSettingsCoordinator(OhmeBaseCoordinator): coordinator_name = "Advanced Settings" + def __init__( + self, hass: HomeAssistant, config_entry: OhmeConfigEntry, client: OhmeApiClient + ) -> None: + """Initialise coordinator.""" + super().__init__(hass, config_entry, client) + + @callback + def _dummy_listener() -> None: + pass + + # This coordinator is used by the API library to determine whether the + # charger is online and available. It is therefore required even if no + # entities are using it. + self.async_add_listener(_dummy_listener) + async def _internal_update_data(self) -> None: """Fetch data from API endpoint.""" await self.client.async_get_advanced_settings() diff --git a/homeassistant/components/ohme/manifest.json b/homeassistant/components/ohme/manifest.json index 786c615d68a3c..14612fff6eb89 100644 --- a/homeassistant/components/ohme/manifest.json +++ b/homeassistant/components/ohme/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["ohme==1.5.1"] + "requirements": ["ohme==1.5.2"] } diff --git a/homeassistant/components/remote/strings.json b/homeassistant/components/remote/strings.json index 09b270b968766..0c6cf98de7f3f 100644 --- a/homeassistant/components/remote/strings.json +++ b/homeassistant/components/remote/strings.json @@ -14,6 +14,9 @@ "changed_states": "[%key:common::device_automation::trigger_type::changed_states%]", "turned_on": "[%key:common::device_automation::trigger_type::turned_on%]", "turned_off": "[%key:common::device_automation::trigger_type::turned_off%]" + }, + "extra_fields": { + "for": "[%key:common::device_automation::extra_fields::for%]" } }, "entity_component": { diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json index b71afe01e5609..eadf5585f3074 100644 --- a/homeassistant/components/schlage/manifest.json +++ b/homeassistant/components/schlage/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/schlage", "iot_class": "cloud_polling", - "requirements": ["pyschlage==2025.7.3"] + "requirements": ["pyschlage==2025.9.0"] } diff --git a/homeassistant/components/sonos/select.py b/homeassistant/components/sonos/select.py index 052a1d8796763..0a56e37e75c0f 100644 --- a/homeassistant/components/sonos/select.py +++ b/homeassistant/components/sonos/select.py @@ -61,8 +61,15 @@ def available_soco_attributes( if ( state := getattr(speaker.soco, select_data.soco_attribute, None) ) is not None: - setattr(speaker, select_data.speaker_attribute, state) - features.append(select_data) + try: + setattr(speaker, select_data.speaker_attribute, int(state)) + features.append(select_data) + except ValueError: + _LOGGER.error( + "Invalid value for %s %s", + select_data.speaker_attribute, + state, + ) return features async def _async_create_entities(speaker: SonosSpeaker) -> None: diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 427f02f047989..acf1b08cd36ed 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -599,7 +599,12 @@ def async_update_volume(self, event: SonosEvent) -> None: for enum_var in (ATTR_DIALOG_LEVEL,): if enum_var in variables: - setattr(self, f"{enum_var}_enum", variables[enum_var]) + try: + setattr(self, f"{enum_var}_enum", int(variables[enum_var])) + except ValueError: + _LOGGER.error( + "Invalid value for %s %s", enum_var, variables[enum_var] + ) self.async_write_entity_states() diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json index b73cf8f849dcc..be5aa09cf342b 100644 --- a/homeassistant/components/switch/strings.json +++ b/homeassistant/components/switch/strings.json @@ -14,6 +14,9 @@ "changed_states": "[%key:common::device_automation::trigger_type::changed_states%]", "turned_on": "[%key:common::device_automation::trigger_type::turned_on%]", "turned_off": "[%key:common::device_automation::trigger_type::turned_off%]" + }, + "extra_fields": { + "for": "[%key:common::device_automation::extra_fields::for%]" } }, "entity_component": { diff --git a/homeassistant/components/update/strings.json b/homeassistant/components/update/strings.json index 5194965cf69f7..a90f5c8a998b1 100644 --- a/homeassistant/components/update/strings.json +++ b/homeassistant/components/update/strings.json @@ -5,6 +5,9 @@ "changed_states": "{entity_name} update availability changed", "turned_on": "{entity_name} got an update available", "turned_off": "{entity_name} became up-to-date" + }, + "extra_fields": { + "for": "[%key:common::device_automation::extra_fields::for%]" } }, "entity_component": { diff --git a/homeassistant/const.py b/homeassistant/const.py index d46b4cd7717f5..318594196e27a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 50acadce8088a..b43da76a609fe 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==5.3.0 hass-nabucasa==1.1.0 hassil==3.2.0 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250903.2 +home-assistant-frontend==20250903.3 home-assistant-intents==2025.9.3 httpx==0.28.1 ifaddr==0.2.0 diff --git a/pyproject.toml b/pyproject.toml index 45751ec957d31..ee06c96403bb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.9.0" +version = "2025.9.1" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." diff --git a/requirements_all.txt b/requirements_all.txt index 37e728130d76e..88442d3864a47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -277,7 +277,7 @@ aiohomekit==3.2.15 aiohttp_sse==2.2.0 # homeassistant.components.hue -aiohue==4.7.4 +aiohue==4.7.5 # homeassistant.components.imap aioimaplib==2.0.1 @@ -618,7 +618,7 @@ beautifulsoup4==4.13.3 # beewi-smartclim==0.0.10 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.17.2 +bimmer-connected[china]==0.17.3 # homeassistant.components.bizkaibus bizkaibus==0.1.1 @@ -1178,7 +1178,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250903.2 +home-assistant-frontend==20250903.3 # homeassistant.components.conversation home-assistant-intents==2025.9.3 @@ -1241,7 +1241,7 @@ igloohome-api==0.1.1 ihcsdk==2.8.5 # homeassistant.components.imeon_inverter -imeon_inverter_api==0.3.14 +imeon_inverter_api==0.3.16 # homeassistant.components.imgw_pib imgw_pib==1.5.4 @@ -1441,7 +1441,7 @@ microBeesPy==0.3.5 mill-local==0.3.0 # homeassistant.components.mill -millheater==0.12.5 +millheater==0.13.1 # homeassistant.components.minio minio==7.1.12 @@ -1580,7 +1580,7 @@ odp-amsterdam==6.1.2 oemthermostat==1.1.1 # homeassistant.components.ohme -ohme==1.5.1 +ohme==1.5.2 # homeassistant.components.ollama ollama==0.5.1 @@ -2312,7 +2312,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2025.7.3 +pyschlage==2025.9.0 # homeassistant.components.sensibo pysensibo==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a294ba2d468ef..046d9b3bfcb0a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -262,7 +262,7 @@ aiohomekit==3.2.15 aiohttp_sse==2.2.0 # homeassistant.components.hue -aiohue==4.7.4 +aiohue==4.7.5 # homeassistant.components.imap aioimaplib==2.0.1 @@ -555,7 +555,7 @@ base36==0.1.1 beautifulsoup4==4.13.3 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.17.2 +bimmer-connected[china]==0.17.3 # homeassistant.components.eq3btsmart # homeassistant.components.esphome @@ -1027,7 +1027,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250903.2 +home-assistant-frontend==20250903.3 # homeassistant.components.conversation home-assistant-intents==2025.9.3 @@ -1075,7 +1075,7 @@ ifaddr==0.2.0 igloohome-api==0.1.1 # homeassistant.components.imeon_inverter -imeon_inverter_api==0.3.14 +imeon_inverter_api==0.3.16 # homeassistant.components.imgw_pib imgw_pib==1.5.4 @@ -1233,7 +1233,7 @@ microBeesPy==0.3.5 mill-local==0.3.0 # homeassistant.components.mill -millheater==0.12.5 +millheater==0.13.1 # homeassistant.components.minio minio==7.1.12 @@ -1348,7 +1348,7 @@ objgraph==3.5.0 odp-amsterdam==6.1.2 # homeassistant.components.ohme -ohme==1.5.1 +ohme==1.5.2 # homeassistant.components.ollama ollama==0.5.1 @@ -1924,7 +1924,7 @@ pyrympro==0.0.9 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2025.7.3 +pyschlage==2025.9.0 # homeassistant.components.sensibo pysensibo==1.2.1 diff --git a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr index b87da22a332ee..06e90c878af21 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr @@ -138,6 +138,7 @@ 'state': 'LOW', }), ]), + 'urgent_check_control_messages': None, }), 'climate': dict({ 'activity': 'INACTIVE', @@ -193,6 +194,24 @@ 'state': 'OK', }), ]), + 'next_service_by_distance': dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), + 'next_service_by_time': dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), }), 'data': dict({ 'attributes': dict({ @@ -1053,6 +1072,7 @@ 'state': 'LOW', }), ]), + 'urgent_check_control_messages': None, }), 'climate': dict({ 'activity': 'HEATING', @@ -1108,6 +1128,24 @@ 'state': 'OK', }), ]), + 'next_service_by_distance': dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), + 'next_service_by_time': dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), }), 'data': dict({ 'attributes': dict({ @@ -1858,6 +1896,7 @@ 'state': 'LOW', }), ]), + 'urgent_check_control_messages': None, }), 'climate': dict({ 'activity': 'INACTIVE', @@ -1922,6 +1961,24 @@ 'state': 'OK', }), ]), + 'next_service_by_distance': dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), + 'next_service_by_time': dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), }), 'data': dict({ 'attributes': dict({ @@ -2621,6 +2678,7 @@ 'has_check_control_messages': False, 'messages': list([ ]), + 'urgent_check_control_messages': None, }), 'climate': dict({ 'activity': 'UNKNOWN', @@ -2658,6 +2716,16 @@ 'state': 'OK', }), ]), + 'next_service_by_distance': None, + 'next_service_by_time': dict({ + 'due_date': '2022-10-01T00:00:00+00:00', + 'due_distance': list([ + None, + None, + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), }), 'data': dict({ 'attributes': dict({ @@ -4991,6 +5059,7 @@ 'has_check_control_messages': False, 'messages': list([ ]), + 'urgent_check_control_messages': None, }), 'climate': dict({ 'activity': 'UNKNOWN', @@ -5028,6 +5097,16 @@ 'state': 'OK', }), ]), + 'next_service_by_distance': None, + 'next_service_by_time': dict({ + 'due_date': '2022-10-01T00:00:00+00:00', + 'due_distance': list([ + None, + None, + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), }), 'data': dict({ 'attributes': dict({ diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 7c5e897d86c67..a90cd1b55c188 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -231,6 +231,29 @@ async def test_conversation_agent(hass: HomeAssistant) -> None: ) +@pytest.mark.usefixtures("init_components") +async def test_punctuation(hass: HomeAssistant) -> None: + """Test punctuation is handled properly.""" + hass.states.async_set( + "light.test_light", + "off", + attributes={ATTR_FRIENDLY_NAME: "Test light"}, + ) + expose_entity(hass, "light.test_light", True) + + calls = async_mock_service(hass, "light", "turn_on") + result = await conversation.async_converse( + hass, "Turn?? on,, test;; light!!!", None, Context(), None + ) + + assert len(calls) == 1 + assert calls[0].data["entity_id"][0] == "light.test_light" + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.intent is not None + assert result.response.intent.slots["name"]["value"] == "test light" + assert result.response.intent.slots["name"]["text"] == "test light" + + async def test_expose_flag_automatically_set( hass: HomeAssistant, entity_registry: er.EntityRegistry, diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index e4bdda422d1c8..bc63343f9bef9 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, patch from aiohue.discovery import URL_NUPNP -from aiohue.errors import LinkButtonNotPressed +from aiohue.errors import AiohueException, LinkButtonNotPressed import pytest import voluptuous as vol @@ -732,3 +732,216 @@ async def test_bridge_connection_failed( ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" + + +async def test_bsb003_bridge_discovery( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + device_registry: dr.DeviceRegistry, +) -> None: + """Test a bridge being discovered.""" + entry = MockConfigEntry( + domain=const.DOMAIN, + data={"host": "192.168.1.217", "api_version": 2, "api_key": "abc"}, + unique_id="bsb002_00000", + ) + entry.add_to_hass(hass) + device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(const.DOMAIN, "bsb002_00000")}, + connections={(dr.CONNECTION_NETWORK_MAC, "AA:BB:CC:DD:EE:FF")}, + ) + create_mock_api_discovery( + aioclient_mock, + [("192.168.1.217", "bsb002_00000"), ("192.168.1.218", "bsb003_00000")], + ) + disc_bridge = get_discovered_bridge( + bridge_id="bsb003_00000", host="192.168.1.218", supports_v2=True + ) + + with ( + patch( + "homeassistant.components.hue.config_flow.discover_bridge", + return_value=disc_bridge, + ), + patch( + "homeassistant.components.hue.config_flow.HueBridgeV2", + autospec=True, + ) as mock_bridge, + ): + mock_bridge.return_value.fetch_full_state.return_value = {} + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("192.168.1.218"), + ip_addresses=[ip_address("192.168.1.218")], + port=443, + hostname="Philips-hue.local", + type="_hue._tcp.local.", + name="Philips Hue - ABCABC._hue._tcp.local.", + properties={ + "bridgeid": "bsb003_00000", + "modelid": "BSB003", + }, + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "migrated_bridge" + + migrated_device = device_registry.async_get(device.id) + + assert migrated_device is not None + assert len(migrated_device.identifiers) == 1 + assert list(migrated_device.identifiers)[0] == (const.DOMAIN, "bsb003_00000") + # The tests don't add new connection, but that will happen + # outside of the config flow + assert len(migrated_device.connections) == 0 + assert entry.data["host"] == "192.168.1.218" + + +async def test_bsb003_bridge_discovery_old_version( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + device_registry: dr.DeviceRegistry, +) -> None: + """Test a bridge being discovered.""" + entry = MockConfigEntry( + domain=const.DOMAIN, + data={"host": "192.168.1.217", "api_version": 1, "api_key": "abc"}, + unique_id="bsb002_00000", + ) + entry.add_to_hass(hass) + disc_bridge = get_discovered_bridge( + bridge_id="bsb003_00000", host="192.168.1.218", supports_v2=True + ) + + with patch( + "homeassistant.components.hue.config_flow.discover_bridge", + return_value=disc_bridge, + ): + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("192.168.1.218"), + ip_addresses=[ip_address("192.168.1.218")], + port=443, + hostname="Philips-hue.local", + type="_hue._tcp.local.", + name="Philips Hue - ABCABC._hue._tcp.local.", + properties={ + "bridgeid": "bsb003_00000", + "modelid": "BSB003", + }, + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "link" + + +async def test_bsb003_bridge_discovery_same_host( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + device_registry: dr.DeviceRegistry, +) -> None: + """Test a bridge being discovered.""" + entry = MockConfigEntry( + domain=const.DOMAIN, + data={"host": "192.168.1.217", "api_version": 2, "api_key": "abc"}, + unique_id="bsb002_00000", + ) + entry.add_to_hass(hass) + create_mock_api_discovery( + aioclient_mock, + [("192.168.1.217", "bsb003_00000")], + ) + disc_bridge = get_discovered_bridge( + bridge_id="bsb003_00000", host="192.168.1.217", supports_v2=True + ) + + with ( + patch( + "homeassistant.components.hue.config_flow.discover_bridge", + return_value=disc_bridge, + ), + patch( + "homeassistant.components.hue.config_flow.HueBridgeV2", + autospec=True, + ), + ): + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("192.168.1.217"), + ip_addresses=[ip_address("192.168.1.217")], + port=443, + hostname="Philips-hue.local", + type="_hue._tcp.local.", + name="Philips Hue - ABCABC._hue._tcp.local.", + properties={ + "bridgeid": "bsb003_00000", + "modelid": "BSB003", + }, + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "link" + + +@pytest.mark.parametrize("exception", [AiohueException, ClientError]) +async def test_bsb003_bridge_discovery_cannot_connect( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + device_registry: dr.DeviceRegistry, + exception: Exception, +) -> None: + """Test a bridge being discovered.""" + entry = MockConfigEntry( + domain=const.DOMAIN, + data={"host": "192.168.1.217", "api_version": 2, "api_key": "abc"}, + unique_id="bsb002_00000", + ) + entry.add_to_hass(hass) + create_mock_api_discovery( + aioclient_mock, + [("192.168.1.217", "bsb003_00000")], + ) + disc_bridge = get_discovered_bridge( + bridge_id="bsb003_00000", host="192.168.1.217", supports_v2=True + ) + + with ( + patch( + "homeassistant.components.hue.config_flow.discover_bridge", + return_value=disc_bridge, + ), + patch( + "homeassistant.components.hue.config_flow.HueBridgeV2", + autospec=True, + ) as mock_bridge, + ): + mock_bridge.return_value.fetch_full_state.side_effect = exception + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("192.168.1.217"), + ip_addresses=[ip_address("192.168.1.217")], + port=443, + hostname="Philips-hue.local", + type="_hue._tcp.local.", + name="Philips Hue - ABCABC._hue._tcp.local.", + properties={ + "bridgeid": "bsb003_00000", + "modelid": "BSB003", + }, + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "link" diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index 3779930e360a8..1993ebe46e40b 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -73,6 +73,32 @@ async def async_handle(self, intent_obj): } +async def test_http_handle_intent_match_failure( + hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_admin_user: MockUser +) -> None: + """Test handle intent match failure via HTTP API.""" + + assert await async_setup_component(hass, "intent", {}) + + hass.states.async_set( + "cover.garage_door_1", "closed", {ATTR_FRIENDLY_NAME: "Garage Door"} + ) + hass.states.async_set( + "cover.garage_door_2", "closed", {ATTR_FRIENDLY_NAME: "Garage Door"} + ) + async_mock_service(hass, "cover", SERVICE_OPEN_COVER) + + client = await hass_client() + resp = await client.post( + "/api/intent/handle", + json={"name": "HassTurnOn", "data": {"name": "Garage Door"}}, + ) + assert resp.status == 200 + data = await resp.json() + + assert "DUPLICATE_NAME" in data["speech"]["plain"]["speech"] + + async def test_cover_intents_loading(hass: HomeAssistant) -> None: """Test Cover Intents Loading.""" assert await async_setup_component(hass, "intent", {}) diff --git a/tests/components/sonos/test_select.py b/tests/components/sonos/test_select.py index ada48de21f396..dbbf28a52d74a 100644 --- a/tests/components/sonos/test_select.py +++ b/tests/components/sonos/test_select.py @@ -38,9 +38,9 @@ async def platform_binary_sensor_fixture(): [ (0, "off"), (1, "low"), - (2, "medium"), - (3, "high"), - (4, "max"), + ("2", "medium"), + ("3", "high"), + ("4", "max"), ], ) async def test_select_dialog_level( @@ -49,7 +49,7 @@ async def test_select_dialog_level( soco, entity_registry: er.EntityRegistry, speaker_info: dict[str, str], - level: int, + level: int | str, result: str, ) -> None: """Test dialog level select entity."""