diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index a3aeab9deb913d..2e7e977cf3d76d 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -336,7 +336,7 @@ class AemetSensorEntityDescription(SensorEntityDescription): keys=[AOD_WEATHER, AOD_WIND_DIRECTION], name="Wind bearing", native_unit_of_measurement=DEGREE, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT_ANGLE, device_class=SensorDeviceClass.WIND_DIRECTION, ), AemetSensorEntityDescription( diff --git a/homeassistant/components/alexa_devices/strings.json b/homeassistant/components/alexa_devices/strings.json index eb279e28d35400..b3bb699d003603 100644 --- a/homeassistant/components/alexa_devices/strings.json +++ b/homeassistant/components/alexa_devices/strings.json @@ -1,8 +1,7 @@ { "common": { - "data_country": "Country code", "data_code": "One-time password (OTP code)", - "data_description_country": "The country of your Amazon account.", + "data_description_country": "The country where your Amazon account is registered.", "data_description_username": "The email address of your Amazon account.", "data_description_password": "The password of your Amazon account.", "data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported." @@ -12,10 +11,10 @@ "step": { "user": { "data": { - "country": "[%key:component::alexa_devices::common::data_country%]", + "country": "[%key:common::config_flow::data::country%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "code": "[%key:component::alexa_devices::common::data_description_code%]" + "code": "[%key:component::alexa_devices::common::data_code%]" }, "data_description": { "country": "[%key:component::alexa_devices::common::data_description_country%]", diff --git a/homeassistant/components/google_generative_ai_conversation/diagnostics.py b/homeassistant/components/google_generative_ai_conversation/diagnostics.py index 13643da7e000ff..34b9f7623553c6 100644 --- a/homeassistant/components/google_generative_ai_conversation/diagnostics.py +++ b/homeassistant/components/google_generative_ai_conversation/diagnostics.py @@ -21,6 +21,7 @@ async def async_get_config_entry_diagnostics( "title": entry.title, "data": entry.data, "options": entry.options, + "subentries": dict(entry.subentries), }, TO_REDACT, ) diff --git a/homeassistant/components/lametric/update.py b/homeassistant/components/lametric/update.py index d486d9d27ba38f..3d93f919c58a55 100644 --- a/homeassistant/components/lametric/update.py +++ b/homeassistant/components/lametric/update.py @@ -42,5 +42,5 @@ def installed_version(self) -> str: def latest_version(self) -> str | None: """Return the latest version of the entity.""" if not self.coordinator.data.update: - return None + return self.coordinator.data.os_version return self.coordinator.data.update.version diff --git a/homeassistant/components/matter/vacuum.py b/homeassistant/components/matter/vacuum.py index 96c6ba212de592..141400c384b620 100644 --- a/homeassistant/components/matter/vacuum.py +++ b/homeassistant/components/matter/vacuum.py @@ -62,14 +62,25 @@ class MatterVacuum(MatterEntity, StateVacuumEntity): _last_accepted_commands: list[int] | None = None _supported_run_modes: ( - dict[int, clusters.RvcCleanMode.Structs.ModeOptionStruct] | None + dict[int, clusters.RvcRunMode.Structs.ModeOptionStruct] | None ) = None entity_description: StateVacuumEntityDescription _platform_translation_key = "vacuum" async def async_stop(self, **kwargs: Any) -> None: """Stop the vacuum cleaner.""" - await self.send_device_command(clusters.OperationalState.Commands.Stop()) + # We simply set the RvcRunMode to the first runmode + # that has the idle tag to stop the vacuum cleaner. + # this is compatible with both Matter 1.2 and 1.3+ devices. + supported_run_modes = self._supported_run_modes or {} + for mode in supported_run_modes.values(): + for tag in mode.modeTags: + if tag.value == ModeTag.IDLE: + # stop the vacuum by changing the run mode to idle + await self.send_device_command( + clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode) + ) + return async def async_return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" @@ -83,15 +94,30 @@ async def async_start(self) -> None: """Start or resume the cleaning task.""" if TYPE_CHECKING: assert self._last_accepted_commands is not None + + accepted_operational_commands = self._last_accepted_commands if ( clusters.RvcOperationalState.Commands.Resume.command_id - in self._last_accepted_commands + in accepted_operational_commands + and self.state == VacuumActivity.PAUSED ): + # vacuum is paused and supports resume command await self.send_device_command( clusters.RvcOperationalState.Commands.Resume() ) - else: - await self.send_device_command(clusters.OperationalState.Commands.Start()) + return + + # We simply set the RvcRunMode to the first runmode + # that has the cleaning tag to start the vacuum cleaner. + # this is compatible with both Matter 1.2 and 1.3+ devices. + supported_run_modes = self._supported_run_modes or {} + for mode in supported_run_modes.values(): + for tag in mode.modeTags: + if tag.value == ModeTag.CLEANING: + await self.send_device_command( + clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode) + ) + return async def async_pause(self) -> None: """Pause the cleaning task.""" @@ -130,6 +156,8 @@ def _update_from_device(self) -> None: state = VacuumActivity.CLEANING elif ModeTag.IDLE in tags: state = VacuumActivity.IDLE + elif ModeTag.MAPPING in tags: + state = VacuumActivity.CLEANING self._attr_activity = state @callback @@ -143,7 +171,10 @@ def _calculate_features(self) -> None: return self._last_accepted_commands = accepted_operational_commands supported_features: VacuumEntityFeature = VacuumEntityFeature(0) + supported_features |= VacuumEntityFeature.START supported_features |= VacuumEntityFeature.STATE + supported_features |= VacuumEntityFeature.STOP + # optional battery attribute = battery feature if self.get_matter_attribute_value( clusters.PowerSource.Attributes.BatPercentRemaining @@ -153,7 +184,7 @@ def _calculate_features(self) -> None: if self.get_matter_attribute_value(clusters.Identify.Attributes.IdentifyType): supported_features |= VacuumEntityFeature.LOCATE # create a map of supported run modes - run_modes: list[clusters.RvcCleanMode.Structs.ModeOptionStruct] = ( + run_modes: list[clusters.RvcRunMode.Structs.ModeOptionStruct] = ( self.get_matter_attribute_value( clusters.RvcRunMode.Attributes.SupportedModes ) @@ -165,22 +196,6 @@ def _calculate_features(self) -> None: in accepted_operational_commands ): supported_features |= VacuumEntityFeature.PAUSE - if ( - clusters.OperationalState.Commands.Stop.command_id - in accepted_operational_commands - ): - supported_features |= VacuumEntityFeature.STOP - if ( - clusters.OperationalState.Commands.Start.command_id - in accepted_operational_commands - ): - # note that start has been replaced by resume in rev2 of the spec - supported_features |= VacuumEntityFeature.START - if ( - clusters.RvcOperationalState.Commands.Resume.command_id - in accepted_operational_commands - ): - supported_features |= VacuumEntityFeature.START if ( clusters.RvcOperationalState.Commands.GoHome.command_id in accepted_operational_commands @@ -202,10 +217,7 @@ def _calculate_features(self) -> None: clusters.RvcRunMode.Attributes.CurrentMode, clusters.RvcOperationalState.Attributes.OperationalState, ), - optional_attributes=( - clusters.RvcCleanMode.Attributes.CurrentMode, - clusters.PowerSource.Attributes.BatPercentRemaining, - ), + optional_attributes=(clusters.PowerSource.Attributes.BatPercentRemaining,), device_type=(device_types.RoboticVacuumCleaner,), allow_none_value=True, ), diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 168df5edcaa767..a17f13e0d07e54 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -3105,7 +3105,7 @@ def set_driver_ready(event: dict) -> None: driver.once("driver ready", set_driver_ready), ] - await controller.async_restore_nvm_base64(msg["data"]) + await controller.async_restore_nvm_base64(msg["data"], {"preserveRoutes": False}) with suppress(TimeoutError): async with asyncio.timeout(DRIVER_READY_TIMEOUT): diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 5e8e702283976f..35b54aa2e496a7 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -1400,7 +1400,9 @@ def set_driver_ready(event: dict) -> None: driver.once("driver ready", set_driver_ready), ] try: - await controller.async_restore_nvm(self.backup_data) + await controller.async_restore_nvm( + self.backup_data, {"preserveRoutes": False} + ) except FailedCommand as err: raise AbortFlow(f"Failed to restore network: {err}") from err else: diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 082a3dd9f959f5..93d585d72a2550 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["zwave_js_server"], - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.64.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.65.0"], "usb": [ { "vid": "0658", diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index ca3a78f804616a..e76b7ae099f4e5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1646,6 +1646,7 @@ async def async_finish_flow( report_usage( "creates a config entry when another entry with the same unique ID " "exists", + breaks_in_ha_version="2026.3", core_behavior=ReportBehavior.LOG, core_integration_behavior=ReportBehavior.LOG, custom_integration_behavior=ReportBehavior.LOG, diff --git a/requirements_all.txt b/requirements_all.txt index 76ef5d07d10454..eb8de18f20c273 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3205,7 +3205,7 @@ ziggo-mediabox-xl==1.1.0 zm-py==0.5.4 # homeassistant.components.zwave_js -zwave-js-server-python==0.64.0 +zwave-js-server-python==0.65.0 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9557e405e986f1..f059b073f0f80a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2637,7 +2637,7 @@ zeversolar==0.3.2 zha==0.0.61 # homeassistant.components.zwave_js -zwave-js-server-python==0.64.0 +zwave-js-server-python==0.65.0 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/tests/components/google_generative_ai_conversation/conftest.py b/tests/components/google_generative_ai_conversation/conftest.py index afea41bbb26049..331afc723aecac 100644 --- a/tests/components/google_generative_ai_conversation/conftest.py +++ b/tests/components/google_generative_ai_conversation/conftest.py @@ -34,12 +34,14 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: "data": {}, "subentry_type": "conversation", "title": DEFAULT_CONVERSATION_NAME, + "subentry_id": "ulid-conversation", "unique_id": None, }, { "data": {}, "subentry_type": "tts", "title": DEFAULT_TTS_NAME, + "subentry_id": "ulid-tts", "unique_id": None, }, ], diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_diagnostics.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_diagnostics.ambr index a31827c7acc06f..48091d83a009b7 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_diagnostics.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_diagnostics.ambr @@ -5,17 +5,35 @@ 'api_key': '**REDACTED**', }), 'options': dict({ - 'chat_model': 'models/gemini-2.5-flash', - 'dangerous_block_threshold': 'BLOCK_MEDIUM_AND_ABOVE', - 'harassment_block_threshold': 'BLOCK_MEDIUM_AND_ABOVE', - 'hate_block_threshold': 'BLOCK_MEDIUM_AND_ABOVE', - 'max_tokens': 1500, - 'prompt': 'Speak like a pirate', - 'recommended': False, - 'sexual_block_threshold': 'BLOCK_MEDIUM_AND_ABOVE', - 'temperature': 1.0, - 'top_k': 64, - 'top_p': 0.95, + }), + 'subentries': dict({ + 'ulid-conversation': dict({ + 'data': dict({ + 'chat_model': 'models/gemini-2.5-flash', + 'dangerous_block_threshold': 'BLOCK_MEDIUM_AND_ABOVE', + 'harassment_block_threshold': 'BLOCK_MEDIUM_AND_ABOVE', + 'hate_block_threshold': 'BLOCK_MEDIUM_AND_ABOVE', + 'max_tokens': 1500, + 'prompt': 'Speak like a pirate', + 'recommended': False, + 'sexual_block_threshold': 'BLOCK_MEDIUM_AND_ABOVE', + 'temperature': 1.0, + 'top_k': 64, + 'top_p': 0.95, + }), + 'subentry_id': 'ulid-conversation', + 'subentry_type': 'conversation', + 'title': 'Google AI Conversation', + 'unique_id': None, + }), + 'ulid-tts': dict({ + 'data': dict({ + }), + 'subentry_id': 'ulid-tts', + 'subentry_type': 'tts', + 'title': 'Google AI TTS', + 'unique_id': None, + }), }), 'title': 'Google Generative AI Conversation', }) diff --git a/tests/components/google_generative_ai_conversation/test_diagnostics.py b/tests/components/google_generative_ai_conversation/test_diagnostics.py index ebc1b5e52a5e50..0f1932386691a3 100644 --- a/tests/components/google_generative_ai_conversation/test_diagnostics.py +++ b/tests/components/google_generative_ai_conversation/test_diagnostics.py @@ -35,10 +35,10 @@ async def test_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" - mock_config_entry.add_to_hass(hass) - hass.config_entries.async_update_entry( + hass.config_entries.async_update_subentry( mock_config_entry, - options={ + next(iter(mock_config_entry.subentries.values())), + data={ CONF_RECOMMENDED: False, CONF_PROMPT: "Speak like a pirate", CONF_TEMPERATURE: RECOMMENDED_TEMPERATURE, diff --git a/tests/components/matter/snapshots/test_vacuum.ambr b/tests/components/matter/snapshots/test_vacuum.ambr index cb859147d7587c..71e0f75614ddd8 100644 --- a/tests/components/matter/snapshots/test_vacuum.ambr +++ b/tests/components/matter/snapshots/test_vacuum.ambr @@ -28,7 +28,7 @@ 'platform': 'matter', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000042-MatterNodeDevice-1-MatterVacuumCleaner-84-1', 'unit_of_measurement': None, @@ -38,7 +38,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Mock Vacuum', - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'vacuum.mock_vacuum', diff --git a/tests/components/matter/test_vacuum.py b/tests/components/matter/test_vacuum.py index 2642ff39ef8db3..b464e9f1cd392f 100644 --- a/tests/components/matter/test_vacuum.py +++ b/tests/components/matter/test_vacuum.py @@ -9,7 +9,6 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ServiceNotSupported from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -61,7 +60,29 @@ async def test_vacuum_actions( ) matter_client.send_device_command.reset_mock() - # test start/resume action + # test start action (from idle state) + await hass.services.async_call( + "vacuum", + "start", + { + "entity_id": entity_id, + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=matter_node.node_id, + endpoint_id=1, + command=clusters.RvcRunMode.Commands.ChangeToMode(newMode=1), + ) + matter_client.send_device_command.reset_mock() + + # test resume action (from paused state) + # first set the operational state to paused + set_node_attribute(matter_node, 1, 97, 4, 0x02) + await trigger_subscription_callback(hass, matter_client) + await hass.services.async_call( "vacuum", "start", @@ -98,25 +119,6 @@ async def test_vacuum_actions( matter_client.send_device_command.reset_mock() # test stop action - # stop command is not supported by the vacuum fixture - with pytest.raises( - ServiceNotSupported, - match="Entity vacuum.mock_vacuum does not support action vacuum.stop", - ): - await hass.services.async_call( - "vacuum", - "stop", - { - "entity_id": entity_id, - }, - blocking=True, - ) - - # update accepted command list to add support for stop command - set_node_attribute( - matter_node, 1, 97, 65529, [clusters.OperationalState.Commands.Stop.command_id] - ) - await trigger_subscription_callback(hass, matter_client) await hass.services.async_call( "vacuum", "stop", @@ -129,7 +131,7 @@ async def test_vacuum_actions( assert matter_client.send_device_command.call_args == call( node_id=matter_node.node_id, endpoint_id=1, - command=clusters.OperationalState.Commands.Stop(), + command=clusters.RvcRunMode.Commands.ChangeToMode(newMode=0), ) matter_client.send_device_command.reset_mock() @@ -209,11 +211,21 @@ async def test_vacuum_updates( assert state assert state.state == "idle" + # confirm state is 'cleaning' by setting; + # - the operational state to 0x00 + # - the run mode is set to a mode which has mapping tag + set_node_attribute(matter_node, 1, 97, 4, 0) + set_node_attribute(matter_node, 1, 84, 1, 2) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get(entity_id) + assert state + assert state.state == "cleaning" + # confirm state is 'unknown' by setting; # - the operational state to 0x00 # - the run mode is set to a mode which has neither cleaning or idle tag set_node_attribute(matter_node, 1, 97, 4, 0) - set_node_attribute(matter_node, 1, 84, 1, 2) + set_node_attribute(matter_node, 1, 84, 1, 5) await trigger_subscription_callback(hass, matter_client) state = hass.states.get(entity_id) assert state diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 3f1f9b737bdaf8..bac0162ba7467e 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -5649,8 +5649,9 @@ async def async_send_command_driver_ready( { "command": "controller.restore_nvm", "nvmData": "dGVzdA==", + "migrateOptions": {"preserveRoutes": False}, }, - require_schema=14, + require_schema=42, ) assert entry.unique_id == "1234" @@ -5684,8 +5685,9 @@ async def async_send_command_driver_ready( { "command": "controller.restore_nvm", "nvmData": "dGVzdA==", + "migrateOptions": {"preserveRoutes": False}, }, - require_schema=14, + require_schema=42, ) assert ( "Failed to get server version, cannot update config entry" @@ -5738,8 +5740,9 @@ async def async_send_command_no_driver_ready( { "command": "controller.restore_nvm", "nvmData": "dGVzdA==", + "migrateOptions": {"preserveRoutes": False}, }, - require_schema=14, + require_schema=42, ) client.async_send_command.reset_mock() diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index dd8838e0775d09..a7bb02d5920fbb 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -900,7 +900,7 @@ async def mock_reset_controller(): client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) - async def mock_restore_nvm(data: bytes): + async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None): client.driver.controller.emit( "nvm convert progress", {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, @@ -1031,7 +1031,7 @@ async def mock_reset_controller(): client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) - async def mock_restore_nvm(data: bytes): + async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None): client.driver.controller.emit( "nvm convert progress", {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, @@ -3501,7 +3501,7 @@ async def mock_reset_controller(): client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) - async def mock_restore_nvm(data: bytes): + async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None): client.driver.controller.emit( "nvm convert progress", {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, @@ -3686,7 +3686,7 @@ async def mock_reset_controller(): client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) - async def mock_restore_nvm(data: bytes): + async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None): client.driver.controller.emit( "nvm convert progress", {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, @@ -3835,7 +3835,7 @@ async def mock_reset_controller(): client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) - async def mock_restore_nvm(data: bytes): + async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None): client.driver.controller.emit( "nvm convert progress", {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 45bb956b7a1a82..dc893e4c5fd309 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -8823,7 +8823,7 @@ async def async_step_user(self, user_input=None): log_text = ( f"Detected that integration '{domain}' creates a config entry " - "when another entry with the same unique ID exists. Please " - "create a bug report at https:" + "when another entry with the same unique ID exists. This will stop " + "working in Home Assistant 2026.3, please create a bug report at https:" ) assert (log_text in caplog.text) == expected_log