From c11dd58c1d32f5767d0e124d2f5ac81d5194d532 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Thu, 12 Oct 2023 11:52:01 +0100 Subject: [PATCH 01/37] Improve handling of roon media players with fixed and incremental volume (#99819) --- homeassistant/components/roon/media_player.py | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index d56bacd67c4..d6128d26723 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -92,7 +92,6 @@ class RoonDevice(MediaPlayerEntity): MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.GROUPING | MediaPlayerEntityFeature.PAUSE - | MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.STOP | MediaPlayerEntityFeature.PREVIOUS_TRACK | MediaPlayerEntityFeature.NEXT_TRACK @@ -104,7 +103,6 @@ class RoonDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PLAY_MEDIA - | MediaPlayerEntityFeature.VOLUME_STEP ) def __init__(self, server, player_data): @@ -124,6 +122,8 @@ class RoonDevice(MediaPlayerEntity): self._attr_shuffle = False self._attr_media_image_url = None self._attr_volume_level = 0 + self._volume_fixed = True + self._volume_incremental = False self.update_data(player_data) async def async_added_to_hass(self) -> None: @@ -190,12 +190,21 @@ class RoonDevice(MediaPlayerEntity): "level": 0, "step": 0, "muted": False, + "fixed": True, + "incremental": False, } try: volume_data = player_data["volume"] - volume_muted = volume_data["is_muted"] - volume_step = convert(volume_data["step"], int, 0) + except KeyError: + return volume + + volume["fixed"] = False + volume["incremental"] = volume_data["type"] == "incremental" + volume["muted"] = volume_data.get("is_muted", False) + volume["step"] = convert(volume_data.get("step"), int, 0) + + try: volume_max = volume_data["max"] volume_min = volume_data["min"] raw_level = convert(volume_data["value"], float, 0) @@ -204,15 +213,9 @@ class RoonDevice(MediaPlayerEntity): volume_percentage_factor = volume_range / 100 level = (raw_level - volume_min) / volume_percentage_factor - volume_level = convert(level, int, 0) / 100 - + volume["level"] = convert(level, int, 0) / 100 except KeyError: - # catch KeyError pass - else: - volume["muted"] = volume_muted - volume["step"] = volume_step - volume["level"] = volume_level return volume @@ -288,6 +291,16 @@ class RoonDevice(MediaPlayerEntity): self._attr_is_volume_muted = volume["muted"] self._attr_volume_step = volume["step"] self._attr_volume_level = volume["level"] + self._volume_fixed = volume["fixed"] + self._volume_incremental = volume["incremental"] + if not self._volume_fixed: + self._attr_supported_features = ( + self._attr_supported_features | MediaPlayerEntityFeature.VOLUME_STEP + ) + if not self._volume_incremental: + self._attr_supported_features = ( + self._attr_supported_features | MediaPlayerEntityFeature.VOLUME_SET + ) now_playing = self._parse_now_playing(self.player_data) self._attr_media_title = now_playing["title"] @@ -359,11 +372,17 @@ class RoonDevice(MediaPlayerEntity): def volume_up(self) -> None: """Send new volume_level to device.""" - self._server.roonapi.change_volume_percent(self.output_id, 3) + if self._volume_incremental: + self._server.roonapi.change_volume_raw(self.output_id, 1, "relative_step") + else: + self._server.roonapi.change_volume_percent(self.output_id, 3) def volume_down(self) -> None: """Send new volume_level to device.""" - self._server.roonapi.change_volume_percent(self.output_id, -3) + if self._volume_incremental: + self._server.roonapi.change_volume_raw(self.output_id, -1, "relative_step") + else: + self._server.roonapi.change_volume_percent(self.output_id, -3) def turn_on(self) -> None: """Turn on device (if supported).""" From db91e9a7204f2d4c0fe620cf2aba502e5af01cee Mon Sep 17 00:00:00 2001 From: TJ Horner Date: Fri, 6 Oct 2023 11:00:04 -0700 Subject: [PATCH 02/37] Auto-fix common key entry issues during WeatherKit config flow (#101504) --- .../components/weatherkit/config_flow.py | 20 ++++++++ .../components/weatherkit/test_config_flow.py | 51 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/homeassistant/components/weatherkit/config_flow.py b/homeassistant/components/weatherkit/config_flow.py index 5762c4ae9b2..657a80547ab 100644 --- a/homeassistant/components/weatherkit/config_flow.py +++ b/homeassistant/components/weatherkit/config_flow.py @@ -66,6 +66,7 @@ class WeatherKitFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: try: + user_input[CONF_KEY_PEM] = self._fix_key_input(user_input[CONF_KEY_PEM]) await self._test_config(user_input) except WeatherKitUnsupportedLocationError as exception: LOGGER.error(exception) @@ -104,6 +105,25 @@ class WeatherKitFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + def _fix_key_input(self, key_input: str) -> str: + """Fix common user errors with the key input.""" + # OSes may sometimes turn two hyphens (--) into an em dash (—) + key_input = key_input.replace("—", "--") + + # Trim whitespace and line breaks + key_input = key_input.strip() + + # Make sure header and footer are present + header = "-----BEGIN PRIVATE KEY-----" + if not key_input.startswith(header): + key_input = f"{header}\n{key_input}" + + footer = "-----END PRIVATE KEY-----" + if not key_input.endswith(footer): + key_input += f"\n{footer}" + + return key_input + async def _test_config(self, user_input: dict[str, Any]) -> None: """Validate credentials.""" client = WeatherKitApiClient( diff --git a/tests/components/weatherkit/test_config_flow.py b/tests/components/weatherkit/test_config_flow.py index 3b6cf76a3d5..9e4d03cbad4 100644 --- a/tests/components/weatherkit/test_config_flow.py +++ b/tests/components/weatherkit/test_config_flow.py @@ -126,3 +126,54 @@ async def test_form_unsupported_location(hass: HomeAssistant) -> None: ) assert result["type"] == FlowResultType.CREATE_ENTRY + + +@pytest.mark.parametrize( + ("input_header"), + [ + "-----BEGIN PRIVATE KEY-----\n", + "", + " \n\n-----BEGIN PRIVATE KEY-----\n", + "—---BEGIN PRIVATE KEY-----\n", + ], + ids=["Correct header", "No header", "Leading characters", "Em dash in header"], +) +@pytest.mark.parametrize( + ("input_footer"), + [ + "\n-----END PRIVATE KEY-----", + "", + "\n-----END PRIVATE KEY-----\n\n ", + "\n—---END PRIVATE KEY-----", + ], + ids=["Correct footer", "No footer", "Trailing characters", "Em dash in footer"], +) +async def test_auto_fix_key_input( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + input_header: str, + input_footer: str, +) -> None: + """Test that we fix common user errors in key input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.weatherkit.WeatherKitApiClient.get_availability", + return_value=[DataSetType.CURRENT_WEATHER], + ): + user_input = EXAMPLE_USER_INPUT.copy() + user_input[CONF_KEY_PEM] = f"{input_header}whateverkey{input_footer}" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + + assert result["data"][CONF_KEY_PEM] == EXAMPLE_CONFIG_DATA[CONF_KEY_PEM] + assert len(mock_setup_entry.mock_calls) == 1 From ede7d13c1eeb84d3fcf2c8c00cac376c28a5080f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Sat, 7 Oct 2023 17:52:31 +0100 Subject: [PATCH 03/37] Improve Ikea Idasen config flow error messages (#101567) --- .../components/idasen_desk/config_flow.py | 7 ++- .../components/idasen_desk/manifest.json | 2 +- .../components/idasen_desk/strings.json | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../idasen_desk/test_config_flow.py | 55 ++++++++++++++++++- 6 files changed, 64 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/idasen_desk/config_flow.py b/homeassistant/components/idasen_desk/config_flow.py index f56446396d2..92f5a836751 100644 --- a/homeassistant/components/idasen_desk/config_flow.py +++ b/homeassistant/components/idasen_desk/config_flow.py @@ -4,9 +4,9 @@ from __future__ import annotations import logging from typing import Any -from bleak import BleakError +from bleak.exc import BleakError from bluetooth_data_tools import human_readable_name -from idasen_ha import Desk +from idasen_ha import AuthFailedError, Desk import voluptuous as vol from homeassistant import config_entries @@ -64,6 +64,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): desk = Desk(None) try: await desk.connect(discovery_info.device, monitor_height=False) + except AuthFailedError as err: + _LOGGER.exception("AuthFailedError", exc_info=err) + errors["base"] = "auth_failed" except TimeoutError as err: _LOGGER.exception("TimeoutError", exc_info=err) errors["base"] = "cannot_connect" diff --git a/homeassistant/components/idasen_desk/manifest.json b/homeassistant/components/idasen_desk/manifest.json index f77e0c22373..cdb06cf907d 100644 --- a/homeassistant/components/idasen_desk/manifest.json +++ b/homeassistant/components/idasen_desk/manifest.json @@ -11,5 +11,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/idasen_desk", "iot_class": "local_push", - "requirements": ["idasen-ha==1.4"] + "requirements": ["idasen-ha==1.4.1"] } diff --git a/homeassistant/components/idasen_desk/strings.json b/homeassistant/components/idasen_desk/strings.json index f7459906ac8..6b9bf80edfc 100644 --- a/homeassistant/components/idasen_desk/strings.json +++ b/homeassistant/components/idasen_desk/strings.json @@ -9,7 +9,8 @@ } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "auth_failed": "Unable to authenticate with the desk. This is usually solved by using an ESPHome Bluetooth Proxy. Please check the integration documentation for alternative workarounds.", + "cannot_connect": "Cannot connect. Make sure that the desk is in Bluetooth pairing mode. If not already, you can also use an ESPHome Bluetooth Proxy, as it provides a better connection.", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/requirements_all.txt b/requirements_all.txt index e1c8091b2d0..3b9562913de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1042,7 +1042,7 @@ ical==5.0.1 icmplib==3.0 # homeassistant.components.idasen_desk -idasen-ha==1.4 +idasen-ha==1.4.1 # homeassistant.components.network ifaddr==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 952cb0f6fa2..0aa120304e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -822,7 +822,7 @@ ical==5.0.1 icmplib==3.0 # homeassistant.components.idasen_desk -idasen-ha==1.4 +idasen-ha==1.4.1 # homeassistant.components.network ifaddr==0.2.0 diff --git a/tests/components/idasen_desk/test_config_flow.py b/tests/components/idasen_desk/test_config_flow.py index 8635e5bfddc..223ecc55e28 100644 --- a/tests/components/idasen_desk/test_config_flow.py +++ b/tests/components/idasen_desk/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import patch from bleak import BleakError +from idasen_ha import AuthFailedError import pytest from homeassistant import config_entries @@ -89,7 +90,7 @@ async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: async def test_user_step_cannot_connect( hass: HomeAssistant, exception: Exception ) -> None: - """Test user step and we cannot connect.""" + """Test user step with a cannot connect error.""" with patch( "homeassistant.components.idasen_desk.config_flow.async_discovered_service_info", return_value=[IDASEN_DISCOVERY_INFO], @@ -140,6 +141,58 @@ async def test_user_step_cannot_connect( assert len(mock_setup_entry.mock_calls) == 1 +async def test_user_step_auth_failed(hass: HomeAssistant) -> None: + """Test user step with an auth failed error.""" + with patch( + "homeassistant.components.idasen_desk.config_flow.async_discovered_service_info", + return_value=[IDASEN_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.idasen_desk.config_flow.Desk.connect", + side_effect=AuthFailedError, + ), patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "auth_failed"} + + with patch("homeassistant.components.idasen_desk.config_flow.Desk.connect"), patch( + "homeassistant.components.idasen_desk.config_flow.Desk.disconnect" + ), patch( + "homeassistant.components.idasen_desk.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == IDASEN_DISCOVERY_INFO.name + assert result3["data"] == { + CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, + } + assert result3["result"].unique_id == IDASEN_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: """Test user step with an unknown exception.""" with patch( From bab524f264a860b1ed825bde9eafab30ad1e02a7 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 7 Oct 2023 10:10:07 +0200 Subject: [PATCH 04/37] Update pyfronius to 0.7.2 (#101571) --- homeassistant/components/fronius/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index ecf3f81b380..bbe0f452bea 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -12,5 +12,5 @@ "iot_class": "local_polling", "loggers": ["pyfronius"], "quality_scale": "platinum", - "requirements": ["PyFronius==0.7.1"] + "requirements": ["PyFronius==0.7.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3b9562913de..bda5437263e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -64,7 +64,7 @@ PyFlick==0.0.2 PyFlume==0.6.5 # homeassistant.components.fronius -PyFronius==0.7.1 +PyFronius==0.7.2 # homeassistant.components.mvglive PyMVGLive==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0aa120304e1..e682554c582 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -57,7 +57,7 @@ PyFlick==0.0.2 PyFlume==0.6.5 # homeassistant.components.fronius -PyFronius==0.7.1 +PyFronius==0.7.2 # homeassistant.components.met_eireann PyMetEireann==2021.8.0 From 5f0bf4e2a3b2f0256eda39af21892d7289975d4b Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 7 Oct 2023 11:05:48 +0200 Subject: [PATCH 05/37] Update ha-philipsjs to 3.1.1 (#101574) Update philips to 3.1.1 --- homeassistant/components/philips_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 46b1340a28d..4751e85d378 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/philips_js", "iot_class": "local_polling", "loggers": ["haphilipsjs"], - "requirements": ["ha-philipsjs==3.1.0"] + "requirements": ["ha-philipsjs==3.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index bda5437263e..6ca8ecfa0bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -955,7 +955,7 @@ ha-ffmpeg==3.1.0 ha-iotawattpy==0.1.1 # homeassistant.components.philips_js -ha-philipsjs==3.1.0 +ha-philipsjs==3.1.1 # homeassistant.components.habitica habitipy==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e682554c582..d32e52587e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -756,7 +756,7 @@ ha-ffmpeg==3.1.0 ha-iotawattpy==0.1.1 # homeassistant.components.philips_js -ha-philipsjs==3.1.0 +ha-philipsjs==3.1.1 # homeassistant.components.habitica habitipy==0.2.0 From f24843f2116defbf10601905ed9677490e9ac2ad Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 7 Oct 2023 18:14:08 +0200 Subject: [PATCH 06/37] Update aiohttp to 3.8.6 (#101590) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 005a6735e03..12fbc6f9f9a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,5 +1,5 @@ aiodiscover==1.5.1 -aiohttp==3.8.5 +aiohttp==3.8.6 aiohttp_cors==0.7.0 astral==2.2 async-upnp-client==0.36.1 diff --git a/pyproject.toml b/pyproject.toml index 86e3d7a5e63..c16c0c70476 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ ] requires-python = ">=3.11.0" dependencies = [ - "aiohttp==3.8.5", + "aiohttp==3.8.6", "astral==2.2", "attrs==23.1.0", "atomicwrites-homeassistant==1.4.1", diff --git a/requirements.txt b/requirements.txt index 60eb2359ba5..a7ede68c9ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiohttp==3.8.5 +aiohttp==3.8.6 astral==2.2 attrs==23.1.0 atomicwrites-homeassistant==1.4.1 From d5c26beb910d3a9752c81ff88d64538434a6fd31 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 7 Oct 2023 10:17:08 -0700 Subject: [PATCH 07/37] Additional fix for rainbird unique id (#101599) Additiona fix for rainbird unique id --- .../components/rainbird/binary_sensor.py | 2 +- .../components/rainbird/coordinator.py | 2 +- .../components/rainbird/test_binary_sensor.py | 31 +++++++++++++++++-- tests/components/rainbird/test_number.py | 30 ++++++++++++++++++ 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rainbird/binary_sensor.py b/homeassistant/components/rainbird/binary_sensor.py index 3333d8bc4cb..142e8ecc4b8 100644 --- a/homeassistant/components/rainbird/binary_sensor.py +++ b/homeassistant/components/rainbird/binary_sensor.py @@ -48,7 +48,7 @@ class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], BinarySensorE """Initialize the Rain Bird sensor.""" super().__init__(coordinator) self.entity_description = description - if coordinator.unique_id: + if coordinator.unique_id is not None: self._attr_unique_id = f"{coordinator.unique_id}-{description.key}" self._attr_device_info = coordinator.device_info else: diff --git a/homeassistant/components/rainbird/coordinator.py b/homeassistant/components/rainbird/coordinator.py index 763e50fe5d9..9f1ea95b333 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -84,7 +84,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): @property def device_info(self) -> DeviceInfo | None: """Return information about the device.""" - if not self._unique_id: + if self._unique_id is None: return None return DeviceInfo( name=self.device_name, diff --git a/tests/components/rainbird/test_binary_sensor.py b/tests/components/rainbird/test_binary_sensor.py index e372a10ae23..24cd1750ed4 100644 --- a/tests/components/rainbird/test_binary_sensor.py +++ b/tests/components/rainbird/test_binary_sensor.py @@ -7,7 +7,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import RAIN_SENSOR_OFF, RAIN_SENSOR_ON, ComponentSetup +from .conftest import RAIN_SENSOR_OFF, RAIN_SENSOR_ON, SERIAL_NUMBER, ComponentSetup from tests.test_util.aiohttp import AiohttpClientMockResponse @@ -41,11 +41,38 @@ async def test_rainsensor( "icon": "mdi:water", } + +@pytest.mark.parametrize( + ("config_entry_unique_id", "entity_unique_id"), + [ + (SERIAL_NUMBER, "1263613994342-rainsensor"), + # Some existing config entries may have a "0" serial number but preserve + # their unique id + (0, "0-rainsensor"), + ], +) +async def test_unique_id( + hass: HomeAssistant, + setup_integration: ComponentSetup, + entity_registry: er.EntityRegistry, + entity_unique_id: str, +) -> None: + """Test rainsensor binary sensor.""" + + assert await setup_integration() + + rainsensor = hass.states.get("binary_sensor.rain_bird_controller_rainsensor") + assert rainsensor is not None + assert rainsensor.attributes == { + "friendly_name": "Rain Bird Controller Rainsensor", + "icon": "mdi:water", + } + entity_entry = entity_registry.async_get( "binary_sensor.rain_bird_controller_rainsensor" ) assert entity_entry - assert entity_entry.unique_id == "1263613994342-rainsensor" + assert entity_entry.unique_id == entity_unique_id @pytest.mark.parametrize( diff --git a/tests/components/rainbird/test_number.py b/tests/components/rainbird/test_number.py index 5d208f08a25..b3cfd56832d 100644 --- a/tests/components/rainbird/test_number.py +++ b/tests/components/rainbird/test_number.py @@ -63,6 +63,36 @@ async def test_number_values( assert entity_entry.unique_id == "1263613994342-rain-delay" +@pytest.mark.parametrize( + ("config_entry_unique_id", "entity_unique_id"), + [ + (SERIAL_NUMBER, "1263613994342-rain-delay"), + # Some existing config entries may have a "0" serial number but preserve + # their unique id + (0, "0-rain-delay"), + ], +) +async def test_unique_id( + hass: HomeAssistant, + setup_integration: ComponentSetup, + entity_registry: er.EntityRegistry, + entity_unique_id: str, +) -> None: + """Test number platform.""" + + assert await setup_integration() + + raindelay = hass.states.get("number.rain_bird_controller_rain_delay") + assert raindelay is not None + assert ( + raindelay.attributes.get("friendly_name") == "Rain Bird Controller Rain delay" + ) + + entity_entry = entity_registry.async_get("number.rain_bird_controller_rain_delay") + assert entity_entry + assert entity_entry.unique_id == entity_unique_id + + async def test_set_value( hass: HomeAssistant, setup_integration: ComponentSetup, From 2639602f5b6c53faae69106e5a5eb2d0cdd891d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Oct 2023 07:43:00 -1000 Subject: [PATCH 08/37] Fix compiling missing statistics losing rows (#101616) --- .../components/recorder/statistics.py | 133 ++++++++---------- homeassistant/components/sensor/recorder.py | 27 +--- tests/components/energy/test_sensor.py | 18 ++- tests/components/recorder/test_statistics.py | 70 +++++---- .../components/recorder/test_websocket_api.py | 13 +- .../sensor/test_recorder_missing_stats.py | 124 ++++++++++++++++ 6 files changed, 252 insertions(+), 133 deletions(-) create mode 100644 tests/components/sensor/test_recorder_missing_stats.py diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 0ea16e09df4..a6fe7ddb22f 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -526,7 +526,7 @@ def _compile_statistics( ): continue compiled: PlatformCompiledStatistics = platform_compile_statistics( - instance.hass, start, end + instance.hass, session, start, end ) _LOGGER.debug( "Statistics for %s during %s-%s: %s", @@ -1871,7 +1871,7 @@ def get_latest_short_term_statistics_by_ids( return list( cast( Sequence[Row], - execute_stmt_lambda_element(session, stmt, orm_rows=False), + execute_stmt_lambda_element(session, stmt), ) ) @@ -1887,75 +1887,69 @@ def _latest_short_term_statistics_by_ids_stmt( ) -def get_latest_short_term_statistics( +def get_latest_short_term_statistics_with_session( hass: HomeAssistant, + session: Session, statistic_ids: set[str], types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]], metadata: dict[str, tuple[int, StatisticMetaData]] | None = None, ) -> dict[str, list[StatisticsRow]]: - """Return the latest short term statistics for a list of statistic_ids.""" - with session_scope(hass=hass, read_only=True) as session: - # Fetch metadata for the given statistic_ids - if not metadata: - metadata = get_instance(hass).statistics_meta_manager.get_many( - session, statistic_ids=statistic_ids - ) - if not metadata: - return {} - metadata_ids = set( - _extract_metadata_and_discard_impossible_columns(metadata, types) + """Return the latest short term statistics for a list of statistic_ids with a session.""" + # Fetch metadata for the given statistic_ids + if not metadata: + metadata = get_instance(hass).statistics_meta_manager.get_many( + session, statistic_ids=statistic_ids ) - run_cache = get_short_term_statistics_run_cache(hass) - # Try to find the latest short term statistics ids for the metadata_ids - # from the run cache first if we have it. If the run cache references - # a non-existent id because of a purge, we will detect it missing in the - # next step and run a query to re-populate the cache. - stats: list[Row] = [] - if metadata_id_to_id := run_cache.get_latest_ids(metadata_ids): - stats = get_latest_short_term_statistics_by_ids( - session, metadata_id_to_id.values() - ) - # If we are missing some metadata_ids in the run cache, we need run a query - # to populate the cache for each metadata_id, and then run another query - # to get the latest short term statistics for the missing metadata_ids. - if (missing_metadata_ids := metadata_ids - set(metadata_id_to_id)) and ( - found_latest_ids := { - latest_id - for metadata_id in missing_metadata_ids - if ( - latest_id := cache_latest_short_term_statistic_id_for_metadata_id( - # orm_rows=False is used here because we are in - # a read-only session, and there will never be - # any pending inserts in the session. - run_cache, - session, - metadata_id, - orm_rows=False, - ) + if not metadata: + return {} + metadata_ids = set( + _extract_metadata_and_discard_impossible_columns(metadata, types) + ) + run_cache = get_short_term_statistics_run_cache(hass) + # Try to find the latest short term statistics ids for the metadata_ids + # from the run cache first if we have it. If the run cache references + # a non-existent id because of a purge, we will detect it missing in the + # next step and run a query to re-populate the cache. + stats: list[Row] = [] + if metadata_id_to_id := run_cache.get_latest_ids(metadata_ids): + stats = get_latest_short_term_statistics_by_ids( + session, metadata_id_to_id.values() + ) + # If we are missing some metadata_ids in the run cache, we need run a query + # to populate the cache for each metadata_id, and then run another query + # to get the latest short term statistics for the missing metadata_ids. + if (missing_metadata_ids := metadata_ids - set(metadata_id_to_id)) and ( + found_latest_ids := { + latest_id + for metadata_id in missing_metadata_ids + if ( + latest_id := cache_latest_short_term_statistic_id_for_metadata_id( + run_cache, + session, + metadata_id, ) - is not None - } - ): - stats.extend( - get_latest_short_term_statistics_by_ids(session, found_latest_ids) ) + is not None + } + ): + stats.extend(get_latest_short_term_statistics_by_ids(session, found_latest_ids)) - if not stats: - return {} + if not stats: + return {} - # Return statistics combined with metadata - return _sorted_statistics_to_dict( - hass, - session, - stats, - statistic_ids, - metadata, - False, - StatisticsShortTerm, - None, - None, - types, - ) + # Return statistics combined with metadata + return _sorted_statistics_to_dict( + hass, + session, + stats, + statistic_ids, + metadata, + False, + StatisticsShortTerm, + None, + None, + types, + ) def _generate_statistics_at_time_stmt( @@ -2316,14 +2310,8 @@ def _import_statistics_with_session( # We just inserted new short term statistics, so we need to update the # ShortTermStatisticsRunCache with the latest id for the metadata_id run_cache = get_short_term_statistics_run_cache(instance.hass) - # - # Because we are in the same session and we want to read rows - # that have not been flushed yet, we need to pass orm_rows=True - # to cache_latest_short_term_statistic_id_for_metadata_id - # to ensure that it gets the rows that were just inserted - # cache_latest_short_term_statistic_id_for_metadata_id( - run_cache, session, metadata_id, orm_rows=True + run_cache, session, metadata_id ) return True @@ -2341,7 +2329,6 @@ def cache_latest_short_term_statistic_id_for_metadata_id( run_cache: ShortTermStatisticsRunCache, session: Session, metadata_id: int, - orm_rows: bool, ) -> int | None: """Cache the latest short term statistic for a given metadata_id. @@ -2352,13 +2339,7 @@ def cache_latest_short_term_statistic_id_for_metadata_id( if latest := cast( Sequence[Row], execute_stmt_lambda_element( - session, - _find_latest_short_term_statistic_for_metadata_id_stmt(metadata_id), - orm_rows=orm_rows - # _import_statistics_with_session needs to be able - # to read back the rows it just inserted without - # a flush so we have to pass orm_rows so we get - # back the latest data. + session, _find_latest_short_term_statistic_for_metadata_id_stmt(metadata_id) ), ): id_: int = latest[0].id diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 2ef1b6854fc..b13d7cd0d1f 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -16,7 +16,6 @@ from homeassistant.components.recorder import ( get_instance, history, statistics, - util as recorder_util, ) from homeassistant.components.recorder.models import ( StatisticData, @@ -383,27 +382,7 @@ def _timestamp_to_isoformat_or_none(timestamp: float | None) -> str | None: return dt_util.utc_from_timestamp(timestamp).isoformat() -def compile_statistics( - hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime -) -> statistics.PlatformCompiledStatistics: - """Compile statistics for all entities during start-end. - - Note: This will query the database and must not be run in the event loop - """ - # There is already an active session when this code is called since - # it is called from the recorder statistics. We need to make sure - # this session never gets committed since it would be out of sync - # with the recorder statistics session so we mark it as read only. - # - # If we ever need to write to the database from this function we - # will need to refactor the recorder statistics to use a single - # session. - with recorder_util.session_scope(hass=hass, read_only=True) as session: - compiled = _compile_statistics(hass, session, start, end) - return compiled - - -def _compile_statistics( # noqa: C901 +def compile_statistics( # noqa: C901 hass: HomeAssistant, session: Session, start: datetime.datetime, @@ -480,8 +459,8 @@ def _compile_statistics( # noqa: C901 if "sum" in wanted_statistics[entity_id]: to_query.add(entity_id) - last_stats = statistics.get_latest_short_term_statistics( - hass, to_query, {"last_reset", "state", "sum"}, metadata=old_metadatas + last_stats = statistics.get_latest_short_term_statistics_with_session( + hass, session, to_query, {"last_reset", "state", "sum"}, metadata=old_metadatas ) for ( # pylint: disable=too-many-nested-blocks entity_id, diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index f5fea153380..bf1513507f8 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -6,6 +6,7 @@ from typing import Any import pytest from homeassistant.components.energy import data +from homeassistant.components.recorder.util import session_scope from homeassistant.components.sensor import ( ATTR_LAST_RESET, ATTR_STATE_CLASS, @@ -155,7 +156,10 @@ async def test_cost_sensor_price_entity_total_increasing( """Test energy cost price from total_increasing type sensor entity.""" def _compile_statistics(_): - return compile_statistics(hass, now, now + timedelta(seconds=1)).platform_stats + with session_scope(hass=hass) as session: + return compile_statistics( + hass, session, now, now + timedelta(seconds=1) + ).platform_stats energy_attributes = { ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, @@ -365,9 +369,10 @@ async def test_cost_sensor_price_entity_total( """Test energy cost price from total type sensor entity.""" def _compile_statistics(_): - return compile_statistics( - hass, now, now + timedelta(seconds=0.17) - ).platform_stats + with session_scope(hass=hass) as session: + return compile_statistics( + hass, session, now, now + timedelta(seconds=0.17) + ).platform_stats energy_attributes = { ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, @@ -579,7 +584,10 @@ async def test_cost_sensor_price_entity_total_no_reset( """Test energy cost price from total type sensor entity with no last_reset.""" def _compile_statistics(_): - return compile_statistics(hass, now, now + timedelta(seconds=1)).platform_stats + with session_scope(hass=hass) as session: + return compile_statistics( + hass, session, now, now + timedelta(seconds=1) + ).platform_stats energy_attributes = { ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index e56b2b83274..03dc7b84caa 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -22,7 +22,7 @@ from homeassistant.components.recorder.statistics import ( async_import_statistics, get_last_short_term_statistics, get_last_statistics, - get_latest_short_term_statistics, + get_latest_short_term_statistics_with_session, get_metadata, get_short_term_statistics_run_cache, list_statistic_ids, @@ -71,9 +71,13 @@ def test_compile_hourly_statistics(hass_recorder: Callable[..., HomeAssistant]) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) # Should not fail if there is nothing there yet - stats = get_latest_short_term_statistics( - hass, {"sensor.test1"}, {"last_reset", "max", "mean", "min", "state", "sum"} - ) + with session_scope(hass=hass, read_only=True) as session: + stats = get_latest_short_term_statistics_with_session( + hass, + session, + {"sensor.test1"}, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) assert stats == {} for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): @@ -172,28 +176,38 @@ def test_compile_hourly_statistics(hass_recorder: Callable[..., HomeAssistant]) ) assert stats == {"sensor.test1": [expected_2]} - stats = get_latest_short_term_statistics( - hass, {"sensor.test1"}, {"last_reset", "max", "mean", "min", "state", "sum"} - ) + with session_scope(hass=hass, read_only=True) as session: + stats = get_latest_short_term_statistics_with_session( + hass, + session, + {"sensor.test1"}, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) assert stats == {"sensor.test1": [expected_2]} # Now wipe the latest_short_term_statistics_ids table and test again # to make sure we can rebuild the missing data run_cache = get_short_term_statistics_run_cache(instance.hass) run_cache._latest_id_by_metadata_id = {} - stats = get_latest_short_term_statistics( - hass, {"sensor.test1"}, {"last_reset", "max", "mean", "min", "state", "sum"} - ) + with session_scope(hass=hass, read_only=True) as session: + stats = get_latest_short_term_statistics_with_session( + hass, + session, + {"sensor.test1"}, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) assert stats == {"sensor.test1": [expected_2]} metadata = get_metadata(hass, statistic_ids={"sensor.test1"}) - stats = get_latest_short_term_statistics( - hass, - {"sensor.test1"}, - {"last_reset", "max", "mean", "min", "state", "sum"}, - metadata=metadata, - ) + with session_scope(hass=hass, read_only=True) as session: + stats = get_latest_short_term_statistics_with_session( + hass, + session, + {"sensor.test1"}, + {"last_reset", "max", "mean", "min", "state", "sum"}, + metadata=metadata, + ) assert stats == {"sensor.test1": [expected_2]} stats = get_last_short_term_statistics( @@ -225,10 +239,14 @@ def test_compile_hourly_statistics(hass_recorder: Callable[..., HomeAssistant]) instance.get_session().query(StatisticsShortTerm).delete() # Should not fail there is nothing in the table - stats = get_latest_short_term_statistics( - hass, {"sensor.test1"}, {"last_reset", "max", "mean", "min", "state", "sum"} - ) - assert stats == {} + with session_scope(hass=hass, read_only=True) as session: + stats = get_latest_short_term_statistics_with_session( + hass, + session, + {"sensor.test1"}, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) + assert stats == {} # Delete again, and manually wipe the cache since we deleted all the data instance.get_session().query(StatisticsShortTerm).delete() @@ -236,9 +254,13 @@ def test_compile_hourly_statistics(hass_recorder: Callable[..., HomeAssistant]) run_cache._latest_id_by_metadata_id = {} # And test again to make sure there is no data - stats = get_latest_short_term_statistics( - hass, {"sensor.test1"}, {"last_reset", "max", "mean", "min", "state", "sum"} - ) + with session_scope(hass=hass, read_only=True) as session: + stats = get_latest_short_term_statistics_with_session( + hass, + session, + {"sensor.test1"}, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) assert stats == {} @@ -259,7 +281,7 @@ def mock_sensor_statistics(): "stat": {"start": start}, } - def get_fake_stats(_hass, start, _end): + def get_fake_stats(_hass, session, start, _end): return statistics.PlatformCompiledStatistics( [ sensor_stats("sensor.test1", start), diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 969fdd63ae5..b371d69fe5f 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -14,11 +14,12 @@ from homeassistant.components.recorder.db_schema import Statistics, StatisticsSh from homeassistant.components.recorder.statistics import ( async_add_external_statistics, get_last_statistics, - get_latest_short_term_statistics, + get_latest_short_term_statistics_with_session, get_metadata, get_short_term_statistics_run_cache, list_statistic_ids, ) +from homeassistant.components.recorder.util import session_scope from homeassistant.components.recorder.websocket_api import UNIT_SCHEMA from homeassistant.components.sensor import UNIT_CONVERTERS from homeassistant.core import HomeAssistant @@ -636,9 +637,13 @@ async def test_statistic_during_period( "change": (imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"]) * 1000, } - stats = get_latest_short_term_statistics( - hass, {"sensor.test"}, {"last_reset", "max", "mean", "min", "state", "sum"} - ) + with session_scope(hass=hass, read_only=True) as session: + stats = get_latest_short_term_statistics_with_session( + hass, + session, + {"sensor.test"}, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) start = imported_stats_5min[-1]["start"].timestamp() end = start + (5 * 60) assert stats == { diff --git a/tests/components/sensor/test_recorder_missing_stats.py b/tests/components/sensor/test_recorder_missing_stats.py new file mode 100644 index 00000000000..f6f6445a0fb --- /dev/null +++ b/tests/components/sensor/test_recorder_missing_stats.py @@ -0,0 +1,124 @@ +"""The tests for sensor recorder platform can catch up.""" +from datetime import datetime, timedelta +from pathlib import Path +from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.components.recorder.history import get_significant_states +from homeassistant.components.recorder.statistics import ( + get_latest_short_term_statistics_with_session, + statistics_during_period, +) +from homeassistant.components.recorder.util import session_scope +from homeassistant.core import CoreState, HomeAssistant +from homeassistant.helpers import recorder as recorder_helper +from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + +from tests.common import get_test_home_assistant +from tests.components.recorder.common import do_adhoc_statistics, wait_recording_done + +POWER_SENSOR_ATTRIBUTES = { + "device_class": "energy", + "state_class": "measurement", + "unit_of_measurement": "kWh", +} + + +@pytest.fixture(autouse=True) +def disable_db_issue_creation(): + """Disable the creation of the database issue.""" + with patch( + "homeassistant.components.recorder.util._async_create_mariadb_range_index_regression_issue" + ): + yield + + +@pytest.mark.timeout(25) +def test_compile_missing_statistics( + freezer: FrozenDateTimeFactory, recorder_db_url: str, tmp_path: Path +) -> None: + """Test compile missing statistics.""" + if recorder_db_url == "sqlite://": + # On-disk database because we need to stop and start hass + # and have it persist. + recorder_db_url = "sqlite:///" + str(tmp_path / "pytest.db") + config = { + "db_url": recorder_db_url, + } + three_days_ago = datetime(2021, 1, 1, 0, 0, 0, tzinfo=dt_util.UTC) + start_time = three_days_ago + timedelta(days=3) + freezer.move_to(three_days_ago) + hass: HomeAssistant = get_test_home_assistant() + hass.state = CoreState.not_running + recorder_helper.async_initialize_recorder(hass) + setup_component(hass, "sensor", {}) + setup_component(hass, "recorder", {"recorder": config}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + + hass.states.set("sensor.test1", "0", POWER_SENSOR_ATTRIBUTES) + wait_recording_done(hass) + + two_days_ago = three_days_ago + timedelta(days=1) + freezer.move_to(two_days_ago) + do_adhoc_statistics(hass, start=two_days_ago) + wait_recording_done(hass) + with session_scope(hass=hass, read_only=True) as session: + latest = get_latest_short_term_statistics_with_session( + hass, session, {"sensor.test1"}, {"state", "sum"} + ) + latest_stat = latest["sensor.test1"][0] + assert latest_stat["start"] == 1609545600.0 + assert latest_stat["end"] == 1609545600.0 + 300 + count = 1 + past_time = two_days_ago + while past_time <= start_time: + freezer.move_to(past_time) + hass.states.set("sensor.test1", str(count), POWER_SENSOR_ATTRIBUTES) + past_time += timedelta(minutes=5) + count += 1 + + wait_recording_done(hass) + + states = get_significant_states(hass, three_days_ago, past_time, ["sensor.test1"]) + assert len(states["sensor.test1"]) == 577 + + hass.stop() + freezer.move_to(start_time) + hass: HomeAssistant = get_test_home_assistant() + hass.state = CoreState.not_running + recorder_helper.async_initialize_recorder(hass) + setup_component(hass, "sensor", {}) + hass.states.set("sensor.test1", "0", POWER_SENSOR_ATTRIBUTES) + setup_component(hass, "recorder", {"recorder": config}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + with session_scope(hass=hass, read_only=True) as session: + latest = get_latest_short_term_statistics_with_session( + hass, session, {"sensor.test1"}, {"state", "sum", "max", "mean", "min"} + ) + latest_stat = latest["sensor.test1"][0] + assert latest_stat["start"] == 1609718100.0 + assert latest_stat["end"] == 1609718100.0 + 300 + assert latest_stat["mean"] == 576.0 + assert latest_stat["min"] == 575.0 + assert latest_stat["max"] == 576.0 + stats = statistics_during_period( + hass, + two_days_ago, + start_time, + units={"energy": "kWh"}, + statistic_ids={"sensor.test1"}, + period="hour", + types={"mean"}, + ) + # Make sure we have 48 hours of statistics + assert len(stats["sensor.test1"]) == 48 + # Make sure the last mean is 570.5 + assert stats["sensor.test1"][-1]["mean"] == 570.5 + hass.stop() From 8109c77f6a3ad351301dd7af5d4be45f69f3ca57 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 8 Oct 2023 09:10:20 +0100 Subject: [PATCH 09/37] Bump systembridgeconnector to 3.8.4 (#101621) Update systembridgeconnector to 3.8.4 --- homeassistant/components/system_bridge/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index bcc6189c8ef..64590ecb96f 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -10,6 +10,6 @@ "iot_class": "local_push", "loggers": ["systembridgeconnector"], "quality_scale": "silver", - "requirements": ["systembridgeconnector==3.8.2"], + "requirements": ["systembridgeconnector==3.8.4"], "zeroconf": ["_system-bridge._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 6ca8ecfa0bd..9d2b0c98fa4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2523,7 +2523,7 @@ switchbot-api==1.1.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridgeconnector==3.8.2 +systembridgeconnector==3.8.4 # homeassistant.components.tailscale tailscale==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d32e52587e2..12777785660 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1877,7 +1877,7 @@ surepy==0.8.0 switchbot-api==1.1.0 # homeassistant.components.system_bridge -systembridgeconnector==3.8.2 +systembridgeconnector==3.8.4 # homeassistant.components.tailscale tailscale==0.2.0 From a042703dd723dcaf1a21ddd1370f00af2ebc00a2 Mon Sep 17 00:00:00 2001 From: Matthew Donoughe Date: Sun, 8 Oct 2023 02:24:32 -0400 Subject: [PATCH 10/37] Update pylutron-caseta to 0.18.3 (#101630) --- homeassistant/components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index bf6ed32c668..ff2831950c6 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -9,7 +9,7 @@ }, "iot_class": "local_push", "loggers": ["pylutron_caseta"], - "requirements": ["pylutron-caseta==0.18.2"], + "requirements": ["pylutron-caseta==0.18.3"], "zeroconf": [ { "type": "_lutron._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 9d2b0c98fa4..3b26e70c9f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1833,7 +1833,7 @@ pylitejet==0.5.0 pylitterbot==2023.4.9 # homeassistant.components.lutron_caseta -pylutron-caseta==0.18.2 +pylutron-caseta==0.18.3 # homeassistant.components.lutron pylutron==0.2.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12777785660..76a9cfbb8f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1379,7 +1379,7 @@ pylitejet==0.5.0 pylitterbot==2023.4.9 # homeassistant.components.lutron_caseta -pylutron-caseta==0.18.2 +pylutron-caseta==0.18.3 # homeassistant.components.mailgun pymailgunner==1.4 From dbc3382dfbf55e8dd47575c0d84f4c5c469b9cd2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 8 Oct 2023 05:20:35 -0700 Subject: [PATCH 11/37] Add additional calendar state alarm debugging (#101631) --- homeassistant/components/calendar/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 5f6b54824fd..f868f951646 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -528,7 +528,9 @@ class CalendarEntity(Entity): the current or upcoming event. """ super().async_write_ha_state() - + _LOGGER.debug( + "Clearing %s alarms (%s)", self.entity_id, len(self._alarm_unsubs) + ) for unsub in self._alarm_unsubs: unsub() self._alarm_unsubs.clear() @@ -536,6 +538,7 @@ class CalendarEntity(Entity): now = dt_util.now() event = self.event if event is None or now >= event.end_datetime_local: + _LOGGER.debug("No alarms needed for %s (event=%s)", self.entity_id, event) return @callback From e32044f8845da0811652e14ac5a1ea42564f8dd7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 8 Oct 2023 13:32:35 +0200 Subject: [PATCH 12/37] Abort config flow when invalid token is received (#101642) --- .../components/withings/strings.json | 3 +- .../helpers/config_entry_oauth2_flow.py | 4 ++ tests/components/withings/test_config_flow.py | 51 +++++++++++++++++++ .../helpers/test_config_entry_oauth2_flow.py | 6 ++- 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index df948a2b593..a9ba69ad045 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -16,7 +16,8 @@ "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "already_configured": "Configuration updated for profile.", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]" }, "create_entry": { "default": "Successfully authenticated with Withings." diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 4fd8948843e..16a4fad5d0c 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -318,6 +318,10 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): _LOGGER.error("Timeout resolving OAuth token: %s", err) return self.async_abort(reason="oauth2_timeout") + if "expires_in" not in token: + _LOGGER.warning("Invalid token: %s", token) + return self.async_abort(reason="oauth_error") + # Force int for non-compliant oauth2 providers try: token["expires_in"] = int(token["expires_in"]) diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 36edffcc346..f8f8f62becf 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -253,3 +253,54 @@ async def test_config_reauth_wrong_account( assert result assert result["type"] == FlowResultType.ABORT assert result["reason"] == "wrong_account" + + +async def test_config_flow_with_invalid_credentials( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + polling_config_entry: MockConfigEntry, + withings: AsyncMock, + current_request_with_host, +) -> None: + """Test flow with invalid credentials.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["url"] == ( + "https://account.withings.com/oauth2_user/authorize2?" + f"response_type=code&client_id={CLIENT_ID}&" + "redirect_uri=https://example.com/auth/external/callback&" + f"state={state}" + "&scope=user.info,user.metrics,user.activity,user.sleepevents" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.clear_requests() + aioclient_mock.post( + "https://wbsapi.withings.net/v2/oauth2", + json={ + "body": { + "status": 503, + "error": "Invalid Params: invalid client id/secret", + }, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "oauth_error" diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 94cdf34cba3..c36b62f66c0 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -167,6 +167,7 @@ async def test_abort_if_no_url_available( assert result["reason"] == "no_url_available" +@pytest.mark.parametrize("expires_in_dict", [{}, {"expires_in": "badnumber"}]) async def test_abort_if_oauth_error( hass: HomeAssistant, flow_handler, @@ -174,6 +175,7 @@ async def test_abort_if_oauth_error( hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, current_request_with_host: None, + expires_in_dict: dict[str, str], ) -> None: """Check bad oauth token.""" flow_handler.async_register_implementation(hass, local_impl) @@ -219,8 +221,8 @@ async def test_abort_if_oauth_error( "refresh_token": REFRESH_TOKEN, "access_token": ACCESS_TOKEN_1, "type": "bearer", - "expires_in": "badnumber", - }, + } + | expires_in_dict, ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) From 327e6d2362fce009c83941e651ffcead389c9bdd Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 8 Oct 2023 20:57:14 +0200 Subject: [PATCH 13/37] Fix mqtt sensor or binary_sensor state not saved after expiry (#101670) Fix mqtt sensor state not saved after expire --- homeassistant/components/mqtt/binary_sensor.py | 2 +- homeassistant/components/mqtt/sensor.py | 4 +++- tests/components/mqtt/test_binary_sensor.py | 13 ++++++++++++- tests/components/mqtt/test_sensor.py | 13 ++++++++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index c0f4cc7786e..7eb444b046a 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -180,7 +180,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): @callback @log_messages(self.hass, self.entity_id) - @write_state_on_attr_change(self, {"_attr_is_on"}) + @write_state_on_attr_change(self, {"_attr_is_on", "_expired"}) def state_message_received(msg: ReceiveMessage) -> None: """Handle a new received MQTT state message.""" # auto-expire enabled? diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 05db22a8e62..0f73b93f1de 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -277,7 +277,9 @@ class MqttSensor(MqttEntity, RestoreSensor): ) @callback - @write_state_on_attr_change(self, {"_attr_native_value", "_attr_last_reset"}) + @write_state_on_attr_change( + self, {"_attr_native_value", "_attr_last_reset", "_expired"} + ) @log_messages(self.hass, self.entity_id) def message_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index ea9c8072290..e7a4c9ab1aa 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -123,7 +123,6 @@ async def test_setting_sensor_value_expires_availability_topic( "name": "test", "state_topic": "test-topic", "expire_after": 4, - "force_update": True, } } } @@ -200,6 +199,18 @@ async def expires_helper(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNAVAILABLE + # Send the last message again + # Time jump 0.5s + now += timedelta(seconds=0.5) + freezer.move_to(now) + async_fire_time_changed(hass, now) + async_fire_mqtt_message(hass, "test-topic", "OFF") + await hass.async_block_till_done() + + # Value was updated correctly. + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_OFF + async def test_expiration_on_discovery_and_discovery_update_of_binary_sensor( hass: HomeAssistant, diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index bc75492a03e..06967b7f8a8 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -339,7 +339,6 @@ async def test_setting_sensor_value_expires_availability_topic( "state_topic": "test-topic", "unit_of_measurement": "fav unit", "expire_after": "4", - "force_update": True, } } } @@ -413,6 +412,18 @@ async def expires_helper(hass: HomeAssistant) -> None: state = hass.states.get("sensor.test") assert state.state == STATE_UNAVAILABLE + # Send the last message again + # Time jump 0.5s + now += timedelta(seconds=0.5) + freezer.move_to(now) + async_fire_time_changed(hass, now) + async_fire_mqtt_message(hass, "test-topic", "101") + await hass.async_block_till_done() + + # Value was updated correctly. + state = hass.states.get("sensor.test") + assert state.state == "101" + @pytest.mark.parametrize( "hass_config", From c4737e442352e58924b562b700e2a64251647510 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 9 Oct 2023 23:35:29 -0400 Subject: [PATCH 14/37] Fix Slack type error for file upload (#101720) Fix regression --- homeassistant/components/slack/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index deba0796750..aae2846503d 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -166,7 +166,7 @@ class SlackNotificationService(BaseNotificationService): filename=filename, initial_comment=message, title=title or filename, - thread_ts=thread_ts, + thread_ts=thread_ts or "", ) except (SlackApiError, ClientError) as err: _LOGGER.error("Error while uploading file-based message: %r", err) From 887263d80ed05ce9028c247e1df916d56d1c9542 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler <2292715+bdr99@users.noreply.github.com> Date: Mon, 9 Oct 2023 20:28:39 -0400 Subject: [PATCH 15/37] Update eufylife-ble-client to 0.1.8 (#101727) --- homeassistant/components/eufylife_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/eufylife_ble/manifest.json b/homeassistant/components/eufylife_ble/manifest.json index c3a2357ebca..efafaa971e8 100644 --- a/homeassistant/components/eufylife_ble/manifest.json +++ b/homeassistant/components/eufylife_ble/manifest.json @@ -24,5 +24,5 @@ "documentation": "https://www.home-assistant.io/integrations/eufylife_ble", "integration_type": "device", "iot_class": "local_push", - "requirements": ["eufylife-ble-client==0.1.7"] + "requirements": ["eufylife-ble-client==0.1.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3b26e70c9f1..de838e7de09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -767,7 +767,7 @@ esphome-dashboard-api==1.2.3 eternalegypt==0.0.16 # homeassistant.components.eufylife_ble -eufylife-ble-client==0.1.7 +eufylife-ble-client==0.1.8 # homeassistant.components.keyboard_remote # evdev==1.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 76a9cfbb8f3..8ebad1e2ba0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -617,7 +617,7 @@ epson-projector==0.5.1 esphome-dashboard-api==1.2.3 # homeassistant.components.eufylife_ble -eufylife-ble-client==0.1.7 +eufylife-ble-client==0.1.8 # homeassistant.components.faa_delays faadelays==2023.9.1 From d7a36cb6a4dcfdd9f0b9d32c99a25bdb0ef6c641 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 10 Oct 2023 20:48:46 -0700 Subject: [PATCH 16/37] Add google calendar required feature for create event service (#101741) * Add google calendar required feature for create event service * Update docstring --- homeassistant/components/google/calendar.py | 1 + tests/components/google/test_init.py | 53 +++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 9559a06d49c..bd0fe18912e 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -240,6 +240,7 @@ async def async_setup_entry( SERVICE_CREATE_EVENT, CREATE_EVENT_SCHEMA, async_create_event, + required_features=CalendarEntityFeature.CREATE_EVENT, ) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 233635510e0..9ede0573922 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -576,6 +576,59 @@ async def test_add_event_date_time( } +@pytest.mark.parametrize( + "calendars_config", + [ + [ + { + "cal_id": CALENDAR_ID, + "entities": [ + { + "device_id": "backyard_light", + "name": "Backyard Light", + "search": "#Backyard", + }, + ], + } + ], + ], +) +async def test_unsupported_create_event( + hass: HomeAssistant, + mock_calendars_yaml: Mock, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + mock_insert_event: Callable[[str, dict[str, Any]], None], + test_api_calendar: dict[str, Any], + mock_events_list: ApiResult, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test create event service call is unsupported for virtual calendars.""" + + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) + assert await component_setup() + + start_datetime = datetime.datetime.now(tz=zoneinfo.ZoneInfo("America/Regina")) + delta = datetime.timedelta(days=3, hours=3) + end_datetime = start_datetime + delta + + with pytest.raises(HomeAssistantError, match="does not support this service"): + await hass.services.async_call( + DOMAIN, + "create_event", + { + # **data, + "start_date_time": start_datetime.isoformat(), + "end_date_time": end_datetime.isoformat(), + "summary": TEST_EVENT_SUMMARY, + "description": TEST_EVENT_DESCRIPTION, + }, + target={"entity_id": "calendar.backyard_light"}, + blocking=True, + ) + + async def test_add_event_failure( hass: HomeAssistant, component_setup: ComponentSetup, From ed57d0beac6e1022ed1e0a0659df10276d26539c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 10 Oct 2023 10:18:52 +0200 Subject: [PATCH 17/37] Fix Airzone climate double setpoint (#101744) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Ɓlvaro FernĆ”ndez Rojas --- homeassistant/components/airzone/climate.py | 10 +++++----- tests/components/airzone/test_climate.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index 74a564fa2de..c3ba74236bd 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -217,8 +217,8 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): if ATTR_TEMPERATURE in kwargs: params[API_SET_POINT] = kwargs[ATTR_TEMPERATURE] if ATTR_TARGET_TEMP_LOW in kwargs and ATTR_TARGET_TEMP_HIGH in kwargs: - params[API_COOL_SET_POINT] = kwargs[ATTR_TARGET_TEMP_LOW] - params[API_HEAT_SET_POINT] = kwargs[ATTR_TARGET_TEMP_HIGH] + params[API_COOL_SET_POINT] = kwargs[ATTR_TARGET_TEMP_HIGH] + params[API_HEAT_SET_POINT] = kwargs[ATTR_TARGET_TEMP_LOW] await self._async_update_hvac_params(params) @callback @@ -248,8 +248,8 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED)) if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE: self._attr_target_temperature_high = self.get_airzone_value( - AZD_HEAT_TEMP_SET - ) - self._attr_target_temperature_low = self.get_airzone_value( AZD_COOL_TEMP_SET ) + self._attr_target_temperature_low = self.get_airzone_value( + AZD_HEAT_TEMP_SET + ) diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py index 591584da10b..94bea0a5e07 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -615,5 +615,5 @@ async def test_airzone_climate_set_temp_range(hass: HomeAssistant) -> None: ) state = hass.states.get("climate.dkn_plus") - assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25.0 - assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 20.0 + assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 20.0 + assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 25.0 From 49f060d95b6f52ea8016270d534a2ff55100e988 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Tue, 10 Oct 2023 01:15:24 -0700 Subject: [PATCH 18/37] Bump screenlogicpy to 0.9.2 (#101746) --- homeassistant/components/screenlogic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index 4d9bbacf3a8..a57ad0026e6 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -15,5 +15,5 @@ "documentation": "https://www.home-assistant.io/integrations/screenlogic", "iot_class": "local_push", "loggers": ["screenlogicpy"], - "requirements": ["screenlogicpy==0.9.1"] + "requirements": ["screenlogicpy==0.9.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index de838e7de09..15478ba8a15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2375,7 +2375,7 @@ satel-integra==0.3.7 scapy==2.5.0 # homeassistant.components.screenlogic -screenlogicpy==0.9.1 +screenlogicpy==0.9.2 # homeassistant.components.scsgate scsgate==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ebad1e2ba0..baca04344b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1762,7 +1762,7 @@ samsungtvws[async,encrypted]==2.6.0 scapy==2.5.0 # homeassistant.components.screenlogic -screenlogicpy==0.9.1 +screenlogicpy==0.9.2 # homeassistant.components.backup securetar==2023.3.0 From 417ba3644bb8b3850adf1d63baf813c733b77233 Mon Sep 17 00:00:00 2001 From: Betacart Date: Tue, 10 Oct 2023 09:50:17 +0200 Subject: [PATCH 19/37] Fix typo in Ombi translation strings (#101747) Update strings.json Fixed typo ("Sumbit" -> "Submit") --- homeassistant/components/ombi/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ombi/strings.json b/homeassistant/components/ombi/strings.json index 2cf18248ab8..764eb5ff1b5 100644 --- a/homeassistant/components/ombi/strings.json +++ b/homeassistant/components/ombi/strings.json @@ -1,7 +1,7 @@ { "services": { "submit_movie_request": { - "name": "Sumbit movie request", + "name": "Submit movie request", "description": "Searches for a movie and requests the first result.", "fields": { "name": { From 8b3fc107df350a8dc143519bba708abcde3445b0 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Tue, 10 Oct 2023 17:37:02 +0200 Subject: [PATCH 20/37] Bump pyDuotecno to 2023.10.0 (#101754) --- homeassistant/components/duotecno/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json index d04e883f867..c7885496af8 100644 --- a/homeassistant/components/duotecno/manifest.json +++ b/homeassistant/components/duotecno/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/duotecno", "iot_class": "local_push", - "requirements": ["pyDuotecno==2023.9.0"] + "requirements": ["pyDuotecno==2023.10.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 15478ba8a15..0dfc8a1395a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1538,7 +1538,7 @@ pyCEC==0.5.2 pyControl4==1.1.0 # homeassistant.components.duotecno -pyDuotecno==2023.9.0 +pyDuotecno==2023.10.0 # homeassistant.components.eight_sleep pyEight==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index baca04344b1..5b1a5927a17 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1171,7 +1171,7 @@ pyCEC==0.5.2 pyControl4==1.1.0 # homeassistant.components.duotecno -pyDuotecno==2023.9.0 +pyDuotecno==2023.10.0 # homeassistant.components.eight_sleep pyEight==0.3.2 From f0a1977d2ed3725c697d1745ced2960ef33b0d53 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 11 Oct 2023 00:06:42 +0200 Subject: [PATCH 21/37] Subscribe to Withings webhooks outside of coordinator (#101759) * Subscribe to Withings webhooks outside of coordinator * Subscribe to Withings webhooks outside of coordinator * Update homeassistant/components/withings/__init__.py Co-authored-by: J. Nick Koston * Update homeassistant/components/withings/__init__.py Co-authored-by: J. Nick Koston --------- Co-authored-by: J. Nick Koston --- homeassistant/components/withings/__init__.py | 57 +++++++++++++++++- .../components/withings/coordinator.py | 59 ++----------------- tests/components/withings/conftest.py | 4 +- tests/components/withings/test_init.py | 2 +- 4 files changed, 64 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 810ad49171c..16606a40645 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -4,8 +4,10 @@ For more details about this platform, please refer to the documentation at """ from __future__ import annotations +import asyncio from collections.abc import Awaitable, Callable import contextlib +from datetime import timedelta from typing import Any from aiohttp.hdrs import METH_HEAD, METH_POST @@ -78,6 +80,8 @@ CONFIG_SCHEMA = vol.Schema( }, extra=vol.ALLOW_EXTRA, ) +SUBSCRIBE_DELAY = timedelta(seconds=5) +UNSUBSCRIBE_DELAY = timedelta(seconds=1) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -141,7 +145,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) -> None: LOGGER.debug("Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID]) webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) - await hass.data[DOMAIN][entry.entry_id].async_unsubscribe_webhooks() + await async_unsubscribe_webhooks(client) + coordinator.webhook_subscription_listener(False) async def register_webhook( _: Any, @@ -170,7 +175,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: get_webhook_handler(coordinator), ) - await hass.data[DOMAIN][entry.entry_id].async_subscribe_webhooks(webhook_url) + await async_subscribe_webhooks(client, webhook_url) + coordinator.webhook_subscription_listener(True) LOGGER.debug("Register Withings webhook: %s", webhook_url) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) @@ -213,6 +219,53 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: await hass.config_entries.async_reload(entry.entry_id) +async def async_subscribe_webhooks( + client: ConfigEntryWithingsApi, webhook_url: str +) -> None: + """Subscribe to Withings webhooks.""" + await async_unsubscribe_webhooks(client) + + notification_to_subscribe = { + NotifyAppli.WEIGHT, + NotifyAppli.CIRCULATORY, + NotifyAppli.ACTIVITY, + NotifyAppli.SLEEP, + NotifyAppli.BED_IN, + NotifyAppli.BED_OUT, + } + + for notification in notification_to_subscribe: + LOGGER.debug( + "Subscribing %s for %s in %s seconds", + webhook_url, + notification, + SUBSCRIBE_DELAY.total_seconds(), + ) + # Withings will HTTP HEAD the callback_url and needs some downtime + # between each call or there is a higher chance of failure. + await asyncio.sleep(SUBSCRIBE_DELAY.total_seconds()) + await client.async_notify_subscribe(webhook_url, notification) + + +async def async_unsubscribe_webhooks(client: ConfigEntryWithingsApi) -> None: + """Unsubscribe to all Withings webhooks.""" + current_webhooks = await client.async_notify_list() + + for webhook_configuration in current_webhooks.profiles: + LOGGER.debug( + "Unsubscribing %s for %s in %s seconds", + webhook_configuration.callbackurl, + webhook_configuration.appli, + UNSUBSCRIBE_DELAY.total_seconds(), + ) + # Quick calls to Withings can result in the service returning errors. + # Give them some time to cool down. + await asyncio.sleep(UNSUBSCRIBE_DELAY.total_seconds()) + await client.async_notify_revoke( + webhook_configuration.callbackurl, webhook_configuration.appli + ) + + async def async_cloudhook_generate_url(hass: HomeAssistant, entry: ConfigEntry) -> str: """Generate the full URL for a webhook_id.""" if CONF_CLOUDHOOK_URL not in entry.data: diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py index 128d4e39193..2ec2804814b 100644 --- a/homeassistant/components/withings/coordinator.py +++ b/homeassistant/components/withings/coordinator.py @@ -1,5 +1,4 @@ """Withings coordinator.""" -import asyncio from collections.abc import Callable from datetime import timedelta from typing import Any @@ -24,9 +23,6 @@ from homeassistant.util import dt as dt_util from .api import ConfigEntryWithingsApi from .const import LOGGER, Measurement -SUBSCRIBE_DELAY = timedelta(seconds=5) -UNSUBSCRIBE_DELAY = timedelta(seconds=1) - WITHINGS_MEASURE_TYPE_MAP: dict[ NotifyAppli | GetSleepSummaryField | MeasureType, Measurement ] = { @@ -84,55 +80,12 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any] super().__init__(hass, LOGGER, name="Withings", update_interval=UPDATE_INTERVAL) self._client = client - async def async_subscribe_webhooks(self, webhook_url: str) -> None: - """Subscribe to webhooks.""" - await self.async_unsubscribe_webhooks() - - current_webhooks = await self._client.async_notify_list() - - subscribed_notifications = frozenset( - profile.appli - for profile in current_webhooks.profiles - if profile.callbackurl == webhook_url - ) - - notification_to_subscribe = ( - set(NotifyAppli) - - subscribed_notifications - - {NotifyAppli.USER, NotifyAppli.UNKNOWN} - ) - - for notification in notification_to_subscribe: - LOGGER.debug( - "Subscribing %s for %s in %s seconds", - webhook_url, - notification, - SUBSCRIBE_DELAY.total_seconds(), - ) - # Withings will HTTP HEAD the callback_url and needs some downtime - # between each call or there is a higher chance of failure. - await asyncio.sleep(SUBSCRIBE_DELAY.total_seconds()) - await self._client.async_notify_subscribe(webhook_url, notification) - self.update_interval = None - - async def async_unsubscribe_webhooks(self) -> None: - """Unsubscribe to webhooks.""" - current_webhooks = await self._client.async_notify_list() - - for webhook_configuration in current_webhooks.profiles: - LOGGER.debug( - "Unsubscribing %s for %s in %s seconds", - webhook_configuration.callbackurl, - webhook_configuration.appli, - UNSUBSCRIBE_DELAY.total_seconds(), - ) - # Quick calls to Withings can result in the service returning errors. - # Give them some time to cool down. - await asyncio.sleep(UNSUBSCRIBE_DELAY.total_seconds()) - await self._client.async_notify_revoke( - webhook_configuration.callbackurl, webhook_configuration.appli - ) - self.update_interval = UPDATE_INTERVAL + def webhook_subscription_listener(self, connected: bool) -> None: + """Call when webhook status changed.""" + if connected: + self.update_interval = None + else: + self.update_interval = UPDATE_INTERVAL async def _async_update_data(self) -> dict[Measurement, Any]: try: diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 3fc2a3c6461..ad310639b43 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -160,10 +160,10 @@ def disable_webhook_delay(): mock = AsyncMock() with patch( - "homeassistant.components.withings.coordinator.SUBSCRIBE_DELAY", + "homeassistant.components.withings.SUBSCRIBE_DELAY", timedelta(seconds=0), ), patch( - "homeassistant.components.withings.coordinator.UNSUBSCRIBE_DELAY", + "homeassistant.components.withings.UNSUBSCRIBE_DELAY", timedelta(seconds=0), ): yield mock diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index dd112671945..ab83bbcfb36 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -126,7 +126,7 @@ async def test_data_manager_webhook_subscription( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) await hass.async_block_till_done() - assert withings.async_notify_subscribe.call_count == 4 + assert withings.async_notify_subscribe.call_count == 6 webhook_url = "https://example.local:8123/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e" From 1a2c9fd9a9ec8538fea05aeeddf64fc8def4850d Mon Sep 17 00:00:00 2001 From: Hessel Date: Tue, 10 Oct 2023 21:23:02 +0200 Subject: [PATCH 22/37] Change BiDirectional Prefix (#101764) --- homeassistant/components/wallbox/const.py | 2 +- homeassistant/components/wallbox/number.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 9bab8232dab..eec7bb4e8da 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -3,7 +3,7 @@ from enum import StrEnum DOMAIN = "wallbox" -BIDIRECTIONAL_MODEL_PREFIXES = ["QSX"] +BIDIRECTIONAL_MODEL_PREFIXES = ["QS"] CODE_KEY = "code" CONF_STATION = "station" diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index b8ce331146d..dff723c579b 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -79,7 +79,7 @@ class WallboxNumber(WallboxEntity, NumberEntity): self._coordinator = coordinator self._attr_unique_id = f"{description.key}-{coordinator.data[CHARGER_DATA_KEY][CHARGER_SERIAL_NUMBER_KEY]}" self._is_bidirectional = ( - coordinator.data[CHARGER_DATA_KEY][CHARGER_PART_NUMBER_KEY][0:3] + coordinator.data[CHARGER_DATA_KEY][CHARGER_PART_NUMBER_KEY][0:2] in BIDIRECTIONAL_MODEL_PREFIXES ) From 62805aed2bf5a817ffb3def4034dcf5944fae926 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Tue, 10 Oct 2023 12:43:40 -0600 Subject: [PATCH 23/37] Bump pyweatherflowudp to 1.4.5 (#101770) --- homeassistant/components/weatherflow/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weatherflow/manifest.json b/homeassistant/components/weatherflow/manifest.json index 3c34250652d..704be808867 100644 --- a/homeassistant/components/weatherflow/manifest.json +++ b/homeassistant/components/weatherflow/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyweatherflowudp"], - "requirements": ["pyweatherflowudp==1.4.3"] + "requirements": ["pyweatherflowudp==1.4.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0dfc8a1395a..bfac37c7c06 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2249,7 +2249,7 @@ pyvolumio==0.1.5 pywaze==0.5.1 # homeassistant.components.weatherflow -pyweatherflowudp==1.4.3 +pyweatherflowudp==1.4.5 # homeassistant.components.html5 pywebpush==1.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b1a5927a17..c3c15e1fe9d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1675,7 +1675,7 @@ pyvolumio==0.1.5 pywaze==0.5.1 # homeassistant.components.weatherflow -pyweatherflowudp==1.4.3 +pyweatherflowudp==1.4.5 # homeassistant.components.html5 pywebpush==1.9.2 From 785df0c8e144f47450a567a8ffdb4d9518e62409 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Wed, 11 Oct 2023 13:46:02 +0200 Subject: [PATCH 24/37] Bump bimmer_connected to 0.14.1 (#101789) Co-authored-by: rikroe --- .../bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../snapshots/test_diagnostics.ambr | 25 +++++++++++++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 0a9e9cac5af..d64541d73be 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==0.14.0"] + "requirements": ["bimmer-connected==0.14.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index bfac37c7c06..0e828d39627 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -515,7 +515,7 @@ beautifulsoup4==4.12.2 bellows==0.36.5 # homeassistant.components.bmw_connected_drive -bimmer-connected==0.14.0 +bimmer-connected==0.14.1 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3c15e1fe9d..6400fafd8ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -439,7 +439,7 @@ beautifulsoup4==4.12.2 bellows==0.36.5 # homeassistant.components.bmw_connected_drive -bimmer-connected==0.14.0 +bimmer-connected==0.14.1 # homeassistant.components.bluetooth bleak-retry-connector==3.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 70224b41ff5..32405d93e6b 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr @@ -824,6 +824,11 @@ }), 'has_combustion_drivetrain': False, 'has_electric_drivetrain': True, + 'headunit': dict({ + 'headunit_type': 'MGU', + 'idrive_version': 'ID8', + 'software_version': '07/2021.00', + }), 'is_charging_plan_supported': True, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': True, @@ -1685,6 +1690,11 @@ }), 'has_combustion_drivetrain': False, 'has_electric_drivetrain': True, + 'headunit': dict({ + 'headunit_type': 'MGU', + 'idrive_version': 'ID8', + 'software_version': '11/2021.70', + }), 'is_charging_plan_supported': True, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, @@ -2318,6 +2328,11 @@ }), 'has_combustion_drivetrain': True, 'has_electric_drivetrain': False, + 'headunit': dict({ + 'headunit_type': 'MGU', + 'idrive_version': 'ID7', + 'software_version': '07/2021.70', + }), 'is_charging_plan_supported': False, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, @@ -3015,6 +3030,11 @@ }), 'has_combustion_drivetrain': True, 'has_electric_drivetrain': True, + 'headunit': dict({ + 'headunit_type': 'NBT', + 'idrive_version': 'ID4', + 'software_version': '11/2021.10', + }), 'is_charging_plan_supported': True, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, @@ -5346,6 +5366,11 @@ }), 'has_combustion_drivetrain': True, 'has_electric_drivetrain': True, + 'headunit': dict({ + 'headunit_type': 'NBT', + 'idrive_version': 'ID4', + 'software_version': '11/2021.10', + }), 'is_charging_plan_supported': True, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, From 959d21a5763f383dcf85e3874e157bcadaafd313 Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Wed, 11 Oct 2023 05:04:33 -0400 Subject: [PATCH 25/37] Bump env_canada to 0.6.0 (#101798) --- homeassistant/components/environment_canada/manifest.json | 2 +- pyproject.toml | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 4946c1900ea..d0c34b0cf9a 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.5.37"] + "requirements": ["env-canada==0.6.0"] } diff --git a/pyproject.toml b/pyproject.toml index c16c0c70476..04be5ae89fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -437,7 +437,7 @@ filterwarnings = [ # -- design choice 3rd party # https://github.com/gwww/elkm1/blob/2.2.5/elkm1_lib/util.py#L8-L19 "ignore:ssl.TLSVersion.TLSv1 is deprecated:DeprecationWarning:elkm1_lib.util", - # https://github.com/michaeldavie/env_canada/blob/v0.5.37/env_canada/ec_cache.py + # https://github.com/michaeldavie/env_canada/blob/v0.6.0/env_canada/ec_cache.py "ignore:Inheritance class CacheClientSession from ClientSession is discouraged:DeprecationWarning:env_canada.ec_cache", # https://github.com/bachya/regenmaschine/blob/2023.08.0/regenmaschine/client.py#L51 "ignore:ssl.TLSVersion.SSLv3 is deprecated:DeprecationWarning:regenmaschine.client", diff --git a/requirements_all.txt b/requirements_all.txt index 0e828d39627..8b4b9eead07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -749,7 +749,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.5.37 +env-canada==0.6.0 # homeassistant.components.season ephem==4.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6400fafd8ef..965ffb2793a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -605,7 +605,7 @@ energyzero==0.5.0 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.5.37 +env-canada==0.6.0 # homeassistant.components.season ephem==4.1.2 From eae6f9b0f8742cae47fa9748b29ea502fd1e3a08 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Thu, 12 Oct 2023 00:18:34 -0700 Subject: [PATCH 26/37] Await set value function in ScreenLogic number entities (#101802) --- homeassistant/components/screenlogic/number.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/screenlogic/number.py b/homeassistant/components/screenlogic/number.py index d3ed25f5570..a52e894c72b 100644 --- a/homeassistant/components/screenlogic/number.py +++ b/homeassistant/components/screenlogic/number.py @@ -1,5 +1,6 @@ """Support for a ScreenLogic number entity.""" -from collections.abc import Callable +import asyncio +from collections.abc import Awaitable, Callable from dataclasses import dataclass import logging @@ -105,13 +106,13 @@ class ScreenLogicNumber(ScreenlogicEntity, NumberEntity): ) -> None: """Initialize a ScreenLogic number entity.""" super().__init__(coordinator, entity_description) - if not callable( + if not asyncio.iscoroutinefunction( func := getattr(self.gateway, entity_description.set_value_name) ): raise TypeError( - f"set_value_name '{entity_description.set_value_name}' is not a callable" + f"set_value_name '{entity_description.set_value_name}' is not a coroutine" ) - self._set_value_func: Callable[..., bool] = func + self._set_value_func: Callable[..., Awaitable[bool]] = func self._set_value_args = entity_description.set_value_args self._attr_native_unit_of_measurement = get_ha_unit( self.entity_data.get(ATTR.UNIT) @@ -145,9 +146,12 @@ class ScreenLogicNumber(ScreenlogicEntity, NumberEntity): data_key = data_path[-1] args[data_key] = self.coordinator.gateway.get_value(*data_path, strict=True) + # Current API requires int values for the currently supported numbers. + value = int(value) + args[self._data_key] = value - if self._set_value_func(*args.values()): + if await self._set_value_func(*args.values()): _LOGGER.debug("Set '%s' to %s", self._data_key, value) await self._async_refresh() else: From 59466814540c1208ee3e2df32ed25816778a0fed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Wed, 11 Oct 2023 14:44:52 +0200 Subject: [PATCH 27/37] Update aioqsw to v0.3.5 (#101809) --- homeassistant/components/qnap_qsw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/manifest.json b/homeassistant/components/qnap_qsw/manifest.json index 28e1ba7b8e4..76949b95cbd 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/qnap_qsw", "iot_class": "local_polling", "loggers": ["aioqsw"], - "requirements": ["aioqsw==0.3.4"] + "requirements": ["aioqsw==0.3.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8b4b9eead07..a61cf6a26fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -324,7 +324,7 @@ aiopvpc==4.2.2 aiopyarr==23.4.0 # homeassistant.components.qnap_qsw -aioqsw==0.3.4 +aioqsw==0.3.5 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 965ffb2793a..6c3980a92d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -299,7 +299,7 @@ aiopvpc==4.2.2 aiopyarr==23.4.0 # homeassistant.components.qnap_qsw -aioqsw==0.3.4 +aioqsw==0.3.5 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 From ffe60102fd72493ffb798c5b08cbf78c3b40c83e Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 11 Oct 2023 12:21:32 -0500 Subject: [PATCH 28/37] Dynamic wake word loading for Wyoming (#101827) * Change supported_wake_words property to async method * Add test * Add timeout + test --------- Co-authored-by: Paulus Schoutsen --- .../components/wake_word/__init__.py | 20 +++++-- homeassistant/components/wyoming/wake_word.py | 20 +++++-- tests/components/assist_pipeline/conftest.py | 5 +- tests/components/wake_word/test_init.py | 35 ++++++++++-- tests/components/wyoming/test_wake_word.py | 57 ++++++++++++++++++- 5 files changed, 120 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/wake_word/__init__.py b/homeassistant/components/wake_word/__init__.py index 6c55bd8e7e7..8c8fb85b8b3 100644 --- a/homeassistant/components/wake_word/__init__.py +++ b/homeassistant/components/wake_word/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from abc import abstractmethod +import asyncio from collections.abc import AsyncIterable import logging from typing import final @@ -34,6 +35,8 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) +TIMEOUT_FETCH_WAKE_WORDS = 10 + @callback def async_default_entity(hass: HomeAssistant) -> str | None: @@ -86,9 +89,8 @@ class WakeWordDetectionEntity(RestoreEntity): """Return the state of the entity.""" return self.__last_detected - @property @abstractmethod - def supported_wake_words(self) -> list[WakeWord]: + async def get_supported_wake_words(self) -> list[WakeWord]: """Return a list of supported wake words.""" @abstractmethod @@ -133,8 +135,9 @@ class WakeWordDetectionEntity(RestoreEntity): vol.Required("entity_id"): cv.entity_domain(DOMAIN), } ) +@websocket_api.async_response @callback -def websocket_entity_info( +async def websocket_entity_info( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Get info about wake word entity.""" @@ -147,7 +150,16 @@ def websocket_entity_info( ) return + try: + async with asyncio.timeout(TIMEOUT_FETCH_WAKE_WORDS): + wake_words = await entity.get_supported_wake_words() + except asyncio.TimeoutError: + connection.send_error( + msg["id"], websocket_api.const.ERR_TIMEOUT, "Timeout fetching wake words" + ) + return + connection.send_result( msg["id"], - {"wake_words": entity.supported_wake_words}, + {"wake_words": wake_words}, ) diff --git a/homeassistant/components/wyoming/wake_word.py b/homeassistant/components/wyoming/wake_word.py index d4cbd9b9263..fce8bbf6327 100644 --- a/homeassistant/components/wyoming/wake_word.py +++ b/homeassistant/components/wyoming/wake_word.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .data import WyomingService +from .data import WyomingService, load_wyoming_info from .error import WyomingError _LOGGER = logging.getLogger(__name__) @@ -28,7 +28,7 @@ async def async_setup_entry( service: WyomingService = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( [ - WyomingWakeWordProvider(config_entry, service), + WyomingWakeWordProvider(hass, config_entry, service), ] ) @@ -38,10 +38,12 @@ class WyomingWakeWordProvider(wake_word.WakeWordDetectionEntity): def __init__( self, + hass: HomeAssistant, config_entry: ConfigEntry, service: WyomingService, ) -> None: """Set up provider.""" + self.hass = hass self.service = service wake_service = service.info.wake[0] @@ -52,9 +54,19 @@ class WyomingWakeWordProvider(wake_word.WakeWordDetectionEntity): self._attr_name = wake_service.name self._attr_unique_id = f"{config_entry.entry_id}-wake_word" - @property - def supported_wake_words(self) -> list[wake_word.WakeWord]: + async def get_supported_wake_words(self) -> list[wake_word.WakeWord]: """Return a list of supported wake words.""" + info = await load_wyoming_info( + self.service.host, self.service.port, retries=0, timeout=1 + ) + + if info is not None: + wake_service = info.wake[0] + self._supported_wake_words = [ + wake_word.WakeWord(id=ww.name, name=ww.description or ww.name) + for ww in wake_service.models + ] + return self._supported_wake_words async def _async_process_audio_stream( diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index cde2666c1ea..1a3144ee069 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -181,8 +181,7 @@ class MockWakeWordEntity(wake_word.WakeWordDetectionEntity): url_path = "wake_word.test" _attr_name = "test" - @property - def supported_wake_words(self) -> list[wake_word.WakeWord]: + async def get_supported_wake_words(self) -> list[wake_word.WakeWord]: """Return a list of supported wake words.""" return [wake_word.WakeWord(id="test_ww", name="Test Wake Word")] @@ -191,7 +190,7 @@ class MockWakeWordEntity(wake_word.WakeWordDetectionEntity): ) -> wake_word.DetectionResult | None: """Try to detect wake word(s) in an audio stream with timestamps.""" if wake_word_id is None: - wake_word_id = self.supported_wake_words[0].id + wake_word_id = (await self.get_supported_wake_words())[0].id async for chunk, timestamp in stream: if chunk.startswith(b"wake word"): return wake_word.DetectionResult( diff --git a/tests/components/wake_word/test_init.py b/tests/components/wake_word/test_init.py index 5d1cc5a4b3f..6b147229d47 100644 --- a/tests/components/wake_word/test_init.py +++ b/tests/components/wake_word/test_init.py @@ -1,6 +1,9 @@ """Test wake_word component setup.""" +import asyncio from collections.abc import AsyncIterable, Generator +from functools import partial from pathlib import Path +from unittest.mock import patch from freezegun import freeze_time import pytest @@ -37,8 +40,7 @@ class MockProviderEntity(wake_word.WakeWordDetectionEntity): url_path = "wake_word.test" _attr_name = "test" - @property - def supported_wake_words(self) -> list[wake_word.WakeWord]: + async def get_supported_wake_words(self) -> list[wake_word.WakeWord]: """Return a list of supported wake words.""" return [ wake_word.WakeWord(id="test_ww", name="Test Wake Word"), @@ -50,7 +52,7 @@ class MockProviderEntity(wake_word.WakeWordDetectionEntity): ) -> wake_word.DetectionResult | None: """Try to detect wake word(s) in an audio stream with timestamps.""" if wake_word_id is None: - wake_word_id = self.supported_wake_words[0].id + wake_word_id = (await self.get_supported_wake_words())[0].id async for _chunk, timestamp in stream: if timestamp >= 2000: @@ -294,7 +296,7 @@ async def test_list_wake_words_unknown_entity( setup: MockProviderEntity, hass_ws_client: WebSocketGenerator, ) -> None: - """Test that the list_wake_words websocket command works.""" + """Test that the list_wake_words websocket command handles unknown entity.""" client = await hass_ws_client(hass) await client.send_json( { @@ -308,3 +310,28 @@ async def test_list_wake_words_unknown_entity( assert not msg["success"] assert msg["error"] == {"code": "not_found", "message": "Entity not found"} + + +async def test_list_wake_words_timeout( + hass: HomeAssistant, + setup: MockProviderEntity, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test that the list_wake_words websocket command handles unknown entity.""" + client = await hass_ws_client(hass) + + with patch.object( + setup, "get_supported_wake_words", partial(asyncio.sleep, 1) + ), patch("homeassistant.components.wake_word.TIMEOUT_FETCH_WAKE_WORDS", 0): + await client.send_json( + { + "id": 5, + "type": "wake_word/info", + "entity_id": setup.entity_id, + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"] == {"code": "timeout", "message": "Timeout fetching wake words"} diff --git a/tests/components/wyoming/test_wake_word.py b/tests/components/wyoming/test_wake_word.py index b3c09d4e816..36a6daf0452 100644 --- a/tests/components/wyoming/test_wake_word.py +++ b/tests/components/wyoming/test_wake_word.py @@ -6,12 +6,13 @@ from unittest.mock import patch from syrupy.assertion import SnapshotAssertion from wyoming.asr import Transcript +from wyoming.info import Info, WakeModel, WakeProgram from wyoming.wake import Detection from homeassistant.components import wake_word from homeassistant.core import HomeAssistant -from . import MockAsyncTcpClient +from . import TEST_ATTR, MockAsyncTcpClient async def test_support(hass: HomeAssistant, init_wyoming_wake_word) -> None: @@ -24,7 +25,7 @@ async def test_support(hass: HomeAssistant, init_wyoming_wake_word) -> None: ) assert entity is not None - assert entity.supported_wake_words == [ + assert (await entity.get_supported_wake_words()) == [ wake_word.WakeWord(id="Test Model", name="Test Model") ] @@ -157,3 +158,55 @@ async def test_detect_message_with_wrong_wake_word( result = await entity.async_process_audio_stream(audio_stream(), "my-wake-word") assert result is None + + +async def test_dynamic_wake_word_info( + hass: HomeAssistant, init_wyoming_wake_word +) -> None: + """Test that supported wake words are loaded dynamically.""" + entity = wake_word.async_get_wake_word_detection_entity( + hass, "wake_word.test_wake_word" + ) + assert entity is not None + + # Original info + assert (await entity.get_supported_wake_words()) == [ + wake_word.WakeWord("Test Model", "Test Model") + ] + + new_info = Info( + wake=[ + WakeProgram( + name="dynamic", + description="Dynamic Wake Word", + installed=True, + attribution=TEST_ATTR, + models=[ + WakeModel( + name="ww1", + description="Wake Word 1", + installed=True, + attribution=TEST_ATTR, + languages=[], + ), + WakeModel( + name="ww2", + description="Wake Word 2", + installed=True, + attribution=TEST_ATTR, + languages=[], + ), + ], + ) + ] + ) + + # Different Wyoming info will be fetched + with patch( + "homeassistant.components.wyoming.wake_word.load_wyoming_info", + return_value=new_info, + ): + assert (await entity.get_supported_wake_words()) == [ + wake_word.WakeWord("ww1", "Wake Word 1"), + wake_word.WakeWord("ww2", "Wake Word 2"), + ] From 3b13c9129a913f9b12ef2795363cdef055fe947c Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 11 Oct 2023 13:32:00 -0500 Subject: [PATCH 29/37] Close existing UDP server for ESPHome voice assistant (#101845) --- homeassistant/components/esphome/manager.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index dfd7376f4f4..41fd60af07d 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -327,7 +327,10 @@ class ESPHomeManager: ) -> int | None: """Start a voice assistant pipeline.""" if self.voice_assistant_udp_server is not None: - return None + _LOGGER.warning("Voice assistant UDP server was not stopped") + self.voice_assistant_udp_server.stop() + self.voice_assistant_udp_server.close() + self.voice_assistant_udp_server = None hass = self.hass self.voice_assistant_udp_server = VoiceAssistantUDPServer( From c9b985160551777e98b54b9fe0d69f683d01ef28 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler <2292715+bdr99@users.noreply.github.com> Date: Thu, 12 Oct 2023 07:13:44 -0400 Subject: [PATCH 30/37] Remove Mazda integration (#101849) Co-authored-by: Franck Nijhof --- CODEOWNERS | 2 - homeassistant/components/mazda/__init__.py | 263 +---------- .../components/mazda/binary_sensor.py | 131 ------ homeassistant/components/mazda/button.py | 150 ------- homeassistant/components/mazda/climate.py | 187 -------- homeassistant/components/mazda/config_flow.py | 107 +---- homeassistant/components/mazda/const.py | 10 - .../components/mazda/device_tracker.py | 54 --- homeassistant/components/mazda/diagnostics.py | 57 --- homeassistant/components/mazda/lock.py | 58 --- homeassistant/components/mazda/manifest.json | 8 +- homeassistant/components/mazda/sensor.py | 263 ----------- homeassistant/components/mazda/services.yaml | 30 -- homeassistant/components/mazda/strings.json | 139 +----- homeassistant/components/mazda/switch.py | 72 --- homeassistant/generated/config_flows.py | 1 - homeassistant/generated/integrations.json | 6 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/mazda/__init__.py | 79 ---- .../fixtures/diagnostics_config_entry.json | 62 --- .../mazda/fixtures/diagnostics_device.json | 60 --- .../mazda/fixtures/get_ev_vehicle_status.json | 19 - .../mazda/fixtures/get_hvac_setting.json | 6 - .../mazda/fixtures/get_vehicle_status.json | 37 -- .../mazda/fixtures/get_vehicles.json | 18 - tests/components/mazda/test_binary_sensor.py | 98 ---- tests/components/mazda/test_button.py | 145 ------ tests/components/mazda/test_climate.py | 341 -------------- tests/components/mazda/test_config_flow.py | 423 ------------------ tests/components/mazda/test_device_tracker.py | 30 -- tests/components/mazda/test_diagnostics.py | 81 ---- tests/components/mazda/test_init.py | 383 ++-------------- tests/components/mazda/test_lock.py | 58 --- tests/components/mazda/test_sensor.py | 195 -------- tests/components/mazda/test_switch.py | 69 --- 36 files changed, 66 insertions(+), 3582 deletions(-) delete mode 100644 homeassistant/components/mazda/binary_sensor.py delete mode 100644 homeassistant/components/mazda/button.py delete mode 100644 homeassistant/components/mazda/climate.py delete mode 100644 homeassistant/components/mazda/const.py delete mode 100644 homeassistant/components/mazda/device_tracker.py delete mode 100644 homeassistant/components/mazda/diagnostics.py delete mode 100644 homeassistant/components/mazda/lock.py delete mode 100644 homeassistant/components/mazda/sensor.py delete mode 100644 homeassistant/components/mazda/services.yaml delete mode 100644 homeassistant/components/mazda/switch.py delete mode 100644 tests/components/mazda/fixtures/diagnostics_config_entry.json delete mode 100644 tests/components/mazda/fixtures/diagnostics_device.json delete mode 100644 tests/components/mazda/fixtures/get_ev_vehicle_status.json delete mode 100644 tests/components/mazda/fixtures/get_hvac_setting.json delete mode 100644 tests/components/mazda/fixtures/get_vehicle_status.json delete mode 100644 tests/components/mazda/fixtures/get_vehicles.json delete mode 100644 tests/components/mazda/test_binary_sensor.py delete mode 100644 tests/components/mazda/test_button.py delete mode 100644 tests/components/mazda/test_climate.py delete mode 100644 tests/components/mazda/test_config_flow.py delete mode 100644 tests/components/mazda/test_device_tracker.py delete mode 100644 tests/components/mazda/test_diagnostics.py delete mode 100644 tests/components/mazda/test_lock.py delete mode 100644 tests/components/mazda/test_sensor.py delete mode 100644 tests/components/mazda/test_switch.py diff --git a/CODEOWNERS b/CODEOWNERS index eed0f633df3..6a8c6489739 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -738,8 +738,6 @@ build.json @home-assistant/supervisor /tests/components/matrix/ @PaarthShah /homeassistant/components/matter/ @home-assistant/matter /tests/components/matter/ @home-assistant/matter -/homeassistant/components/mazda/ @bdr99 -/tests/components/mazda/ @bdr99 /homeassistant/components/meater/ @Sotolotl @emontnemery /tests/components/meater/ @Sotolotl @emontnemery /homeassistant/components/medcom_ble/ @elafargue diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index f375b8a75cd..75e7baf7413 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -1,213 +1,26 @@ """The Mazda Connected Services integration.""" from __future__ import annotations -import asyncio -from datetime import timedelta -import logging -from typing import TYPE_CHECKING +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir -from pymazda import ( - Client as MazdaAPI, - MazdaAccountLockedException, - MazdaAPIEncryptionException, - MazdaAuthenticationException, - MazdaException, - MazdaTokenExpiredException, -) -import voluptuous as vol - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION, Platform -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ( - ConfigEntryAuthFailed, - ConfigEntryNotReady, - HomeAssistantError, -) -from homeassistant.helpers import ( - aiohttp_client, - config_validation as cv, - device_registry as dr, -) -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) - -from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_REGION, DATA_VEHICLES, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -PLATFORMS = [ - Platform.BINARY_SENSOR, - Platform.BUTTON, - Platform.CLIMATE, - Platform.DEVICE_TRACKER, - Platform.LOCK, - Platform.SENSOR, - Platform.SWITCH, -] +DOMAIN = "mazda" -async def with_timeout(task, timeout_seconds=30): - """Run an async task with a timeout.""" - async with asyncio.timeout(timeout_seconds): - return await task - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool: """Set up Mazda Connected Services from a config entry.""" - email = entry.data[CONF_EMAIL] - password = entry.data[CONF_PASSWORD] - region = entry.data[CONF_REGION] - - websession = aiohttp_client.async_get_clientsession(hass) - mazda_client = MazdaAPI( - email, password, region, websession=websession, use_cached_vehicle_list=True - ) - - try: - await mazda_client.validate_credentials() - except MazdaAuthenticationException as ex: - raise ConfigEntryAuthFailed from ex - except ( - MazdaException, - MazdaAccountLockedException, - MazdaTokenExpiredException, - MazdaAPIEncryptionException, - ) as ex: - _LOGGER.error("Error occurred during Mazda login request: %s", ex) - raise ConfigEntryNotReady from ex - - async def async_handle_service_call(service_call: ServiceCall) -> None: - """Handle a service call.""" - # Get device entry from device registry - dev_reg = dr.async_get(hass) - device_id = service_call.data["device_id"] - device_entry = dev_reg.async_get(device_id) - if TYPE_CHECKING: - # For mypy: it has already been checked in validate_mazda_device_id - assert device_entry - - # Get vehicle VIN from device identifiers - mazda_identifiers = ( - identifier - for identifier in device_entry.identifiers - if identifier[0] == DOMAIN - ) - vin_identifier = next(mazda_identifiers) - vin = vin_identifier[1] - - # Get vehicle ID and API client from hass.data - vehicle_id = 0 - api_client = None - for entry_data in hass.data[DOMAIN].values(): - for vehicle in entry_data[DATA_VEHICLES]: - if vehicle["vin"] == vin: - vehicle_id = vehicle["id"] - api_client = entry_data[DATA_CLIENT] - break - - if vehicle_id == 0 or api_client is None: - raise HomeAssistantError("Vehicle ID not found") - - api_method = getattr(api_client, service_call.service) - try: - latitude = service_call.data["latitude"] - longitude = service_call.data["longitude"] - poi_name = service_call.data["poi_name"] - await api_method(vehicle_id, latitude, longitude, poi_name) - except Exception as ex: - raise HomeAssistantError(ex) from ex - - def validate_mazda_device_id(device_id): - """Check that a device ID exists in the registry and has at least one 'mazda' identifier.""" - dev_reg = dr.async_get(hass) - - if (device_entry := dev_reg.async_get(device_id)) is None: - raise vol.Invalid("Invalid device ID") - - mazda_identifiers = [ - identifier - for identifier in device_entry.identifiers - if identifier[0] == DOMAIN - ] - if not mazda_identifiers: - raise vol.Invalid("Device ID is not a Mazda vehicle") - - return device_id - - service_schema_send_poi = vol.Schema( - { - vol.Required("device_id"): vol.All(cv.string, validate_mazda_device_id), - vol.Required("latitude"): cv.latitude, - vol.Required("longitude"): cv.longitude, - vol.Required("poi_name"): cv.string, - } - ) - - async def async_update_data(): - """Fetch data from Mazda API.""" - try: - vehicles = await with_timeout(mazda_client.get_vehicles()) - - # The Mazda API can throw an error when multiple simultaneous requests are - # made for the same account, so we can only make one request at a time here - for vehicle in vehicles: - vehicle["status"] = await with_timeout( - mazda_client.get_vehicle_status(vehicle["id"]) - ) - - # If vehicle is electric, get additional EV-specific status info - if vehicle["isElectric"]: - vehicle["evStatus"] = await with_timeout( - mazda_client.get_ev_vehicle_status(vehicle["id"]) - ) - vehicle["hvacSetting"] = await with_timeout( - mazda_client.get_hvac_setting(vehicle["id"]) - ) - - hass.data[DOMAIN][entry.entry_id][DATA_VEHICLES] = vehicles - - return vehicles - except MazdaAuthenticationException as ex: - raise ConfigEntryAuthFailed("Not authenticated with Mazda API") from ex - except Exception as ex: - _LOGGER.exception( - "Unknown error occurred during Mazda update request: %s", ex - ) - raise UpdateFailed(ex) from ex - - coordinator = DataUpdateCoordinator( + ir.async_create_issue( hass, - _LOGGER, - name=DOMAIN, - update_method=async_update_data, - update_interval=timedelta(seconds=180), - ) - - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_CLIENT: mazda_client, - DATA_COORDINATOR: coordinator, - DATA_REGION: region, - DATA_VEHICLES: [], - } - - # Fetch initial data so we have data when entities subscribe - await coordinator.async_config_entry_first_refresh() - - # Setup components - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - # Register services - hass.services.async_register( DOMAIN, - "send_poi", - async_handle_service_call, - schema=service_schema_send_poi, + DOMAIN, + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="integration_removed", + translation_placeholders={ + "dmca": "https://github.com/github/dmca/blob/master/2023/10/2023-10-10-mazda.md", + "entries": "/config/integrations/integration/mazda", + }, ) return True @@ -215,45 +28,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if all( + config_entry.state is ConfigEntryState.NOT_LOADED + for config_entry in hass.config_entries.async_entries(DOMAIN) + if config_entry.entry_id != entry.entry_id + ): + ir.async_delete_issue(hass, DOMAIN, DOMAIN) - # Only remove services if it is the last config entry - if len(hass.data[DOMAIN]) == 1: - hass.services.async_remove(DOMAIN, "send_poi") - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok - - -class MazdaEntity(CoordinatorEntity): - """Defines a base Mazda entity.""" - - _attr_has_entity_name = True - - def __init__(self, client, coordinator, index): - """Initialize the Mazda entity.""" - super().__init__(coordinator) - self.client = client - self.index = index - self.vin = self.data["vin"] - self.vehicle_id = self.data["id"] - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self.vin)}, - manufacturer="Mazda", - model=f"{self.data['modelYear']} {self.data['carlineName']}", - name=self.vehicle_name, - ) - - @property - def data(self): - """Shortcut to access coordinator data for the entity.""" - return self.coordinator.data[self.index] - - @property - def vehicle_name(self): - """Return the vehicle name, to be used as a prefix for names of other entities.""" - if "nickname" in self.data and len(self.data["nickname"]) > 0: - return self.data["nickname"] - return f"{self.data['modelYear']} {self.data['carlineName']}" + return True diff --git a/homeassistant/components/mazda/binary_sensor.py b/homeassistant/components/mazda/binary_sensor.py deleted file mode 100644 index 36c3ba27463..00000000000 --- a/homeassistant/components/mazda/binary_sensor.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Platform for Mazda binary sensor integration.""" -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass -from typing import Any - -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntity, - BinarySensorEntityDescription, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import MazdaEntity -from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN - - -@dataclass -class MazdaBinarySensorRequiredKeysMixin: - """Mixin for required keys.""" - - # Function to determine the value for this binary sensor, given the coordinator data - value_fn: Callable[[dict[str, Any]], bool] - - -@dataclass -class MazdaBinarySensorEntityDescription( - BinarySensorEntityDescription, MazdaBinarySensorRequiredKeysMixin -): - """Describes a Mazda binary sensor entity.""" - - # Function to determine whether the vehicle supports this binary sensor, given the coordinator data - is_supported: Callable[[dict[str, Any]], bool] = lambda data: True - - -def _plugged_in_supported(data): - """Determine if 'plugged in' binary sensor is supported.""" - return ( - data["isElectric"] and data["evStatus"]["chargeInfo"]["pluggedIn"] is not None - ) - - -BINARY_SENSOR_ENTITIES = [ - MazdaBinarySensorEntityDescription( - key="driver_door", - translation_key="driver_door", - icon="mdi:car-door", - device_class=BinarySensorDeviceClass.DOOR, - value_fn=lambda data: data["status"]["doors"]["driverDoorOpen"], - ), - MazdaBinarySensorEntityDescription( - key="passenger_door", - translation_key="passenger_door", - icon="mdi:car-door", - device_class=BinarySensorDeviceClass.DOOR, - value_fn=lambda data: data["status"]["doors"]["passengerDoorOpen"], - ), - MazdaBinarySensorEntityDescription( - key="rear_left_door", - translation_key="rear_left_door", - icon="mdi:car-door", - device_class=BinarySensorDeviceClass.DOOR, - value_fn=lambda data: data["status"]["doors"]["rearLeftDoorOpen"], - ), - MazdaBinarySensorEntityDescription( - key="rear_right_door", - translation_key="rear_right_door", - icon="mdi:car-door", - device_class=BinarySensorDeviceClass.DOOR, - value_fn=lambda data: data["status"]["doors"]["rearRightDoorOpen"], - ), - MazdaBinarySensorEntityDescription( - key="trunk", - translation_key="trunk", - icon="mdi:car-back", - device_class=BinarySensorDeviceClass.DOOR, - value_fn=lambda data: data["status"]["doors"]["trunkOpen"], - ), - MazdaBinarySensorEntityDescription( - key="hood", - translation_key="hood", - icon="mdi:car", - device_class=BinarySensorDeviceClass.DOOR, - value_fn=lambda data: data["status"]["doors"]["hoodOpen"], - ), - MazdaBinarySensorEntityDescription( - key="ev_plugged_in", - translation_key="ev_plugged_in", - device_class=BinarySensorDeviceClass.PLUG, - is_supported=_plugged_in_supported, - value_fn=lambda data: data["evStatus"]["chargeInfo"]["pluggedIn"], - ), -] - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the sensor platform.""" - client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] - coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] - - async_add_entities( - MazdaBinarySensorEntity(client, coordinator, index, description) - for index, data in enumerate(coordinator.data) - for description in BINARY_SENSOR_ENTITIES - if description.is_supported(data) - ) - - -class MazdaBinarySensorEntity(MazdaEntity, BinarySensorEntity): - """Representation of a Mazda vehicle binary sensor.""" - - entity_description: MazdaBinarySensorEntityDescription - - def __init__(self, client, coordinator, index, description): - """Initialize Mazda binary sensor.""" - super().__init__(client, coordinator, index) - self.entity_description = description - - self._attr_unique_id = f"{self.vin}_{description.key}" - - @property - def is_on(self): - """Return the state of the binary sensor.""" - return self.entity_description.value_fn(self.data) diff --git a/homeassistant/components/mazda/button.py b/homeassistant/components/mazda/button.py deleted file mode 100644 index ced1094981f..00000000000 --- a/homeassistant/components/mazda/button.py +++ /dev/null @@ -1,150 +0,0 @@ -"""Platform for Mazda button integration.""" -from __future__ import annotations - -from collections.abc import Awaitable, Callable -from dataclasses import dataclass -from typing import Any - -from pymazda import ( - Client as MazdaAPIClient, - MazdaAccountLockedException, - MazdaAPIEncryptionException, - MazdaAuthenticationException, - MazdaException, - MazdaLoginFailedException, - MazdaTokenExpiredException, -) - -from homeassistant.components.button import ButtonEntity, ButtonEntityDescription -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from . import MazdaEntity -from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN - - -async def handle_button_press( - client: MazdaAPIClient, - key: str, - vehicle_id: int, - coordinator: DataUpdateCoordinator, -) -> None: - """Handle a press for a Mazda button entity.""" - api_method = getattr(client, key) - - try: - await api_method(vehicle_id) - except ( - MazdaException, - MazdaAuthenticationException, - MazdaAccountLockedException, - MazdaTokenExpiredException, - MazdaAPIEncryptionException, - MazdaLoginFailedException, - ) as ex: - raise HomeAssistantError(ex) from ex - - -async def handle_refresh_vehicle_status( - client: MazdaAPIClient, - key: str, - vehicle_id: int, - coordinator: DataUpdateCoordinator, -) -> None: - """Handle a request to refresh the vehicle status.""" - await handle_button_press(client, key, vehicle_id, coordinator) - - await coordinator.async_request_refresh() - - -@dataclass -class MazdaButtonEntityDescription(ButtonEntityDescription): - """Describes a Mazda button entity.""" - - # Function to determine whether the vehicle supports this button, - # given the coordinator data - is_supported: Callable[[dict[str, Any]], bool] = lambda data: True - - async_press: Callable[ - [MazdaAPIClient, str, int, DataUpdateCoordinator], Awaitable - ] = handle_button_press - - -BUTTON_ENTITIES = [ - MazdaButtonEntityDescription( - key="start_engine", - translation_key="start_engine", - icon="mdi:engine", - is_supported=lambda data: not data["isElectric"], - ), - MazdaButtonEntityDescription( - key="stop_engine", - translation_key="stop_engine", - icon="mdi:engine-off", - is_supported=lambda data: not data["isElectric"], - ), - MazdaButtonEntityDescription( - key="turn_on_hazard_lights", - translation_key="turn_on_hazard_lights", - icon="mdi:hazard-lights", - is_supported=lambda data: not data["isElectric"], - ), - MazdaButtonEntityDescription( - key="turn_off_hazard_lights", - translation_key="turn_off_hazard_lights", - icon="mdi:hazard-lights", - is_supported=lambda data: not data["isElectric"], - ), - MazdaButtonEntityDescription( - key="refresh_vehicle_status", - translation_key="refresh_vehicle_status", - icon="mdi:refresh", - async_press=handle_refresh_vehicle_status, - is_supported=lambda data: data["isElectric"], - ), -] - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the button platform.""" - client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] - coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] - - async_add_entities( - MazdaButtonEntity(client, coordinator, index, description) - for index, data in enumerate(coordinator.data) - for description in BUTTON_ENTITIES - if description.is_supported(data) - ) - - -class MazdaButtonEntity(MazdaEntity, ButtonEntity): - """Representation of a Mazda button.""" - - entity_description: MazdaButtonEntityDescription - - def __init__( - self, - client: MazdaAPIClient, - coordinator: DataUpdateCoordinator, - index: int, - description: MazdaButtonEntityDescription, - ) -> None: - """Initialize Mazda button.""" - super().__init__(client, coordinator, index) - self.entity_description = description - - self._attr_unique_id = f"{self.vin}_{description.key}" - - async def async_press(self) -> None: - """Press the button.""" - await self.entity_description.async_press( - self.client, self.entity_description.key, self.vehicle_id, self.coordinator - ) diff --git a/homeassistant/components/mazda/climate.py b/homeassistant/components/mazda/climate.py deleted file mode 100644 index 43dc4b4151d..00000000000 --- a/homeassistant/components/mazda/climate.py +++ /dev/null @@ -1,187 +0,0 @@ -"""Platform for Mazda climate integration.""" -from __future__ import annotations - -from typing import Any - -from pymazda import Client as MazdaAPIClient - -from homeassistant.components.climate import ( - ClimateEntity, - ClimateEntityFeature, - HVACMode, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_TEMPERATURE, - PRECISION_HALVES, - PRECISION_WHOLE, - UnitOfTemperature, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.util.unit_conversion import TemperatureConverter - -from . import MazdaEntity -from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_REGION, DOMAIN - -PRESET_DEFROSTER_OFF = "Defroster Off" -PRESET_DEFROSTER_FRONT = "Front Defroster" -PRESET_DEFROSTER_REAR = "Rear Defroster" -PRESET_DEFROSTER_FRONT_AND_REAR = "Front and Rear Defroster" - - -def _front_defroster_enabled(preset_mode: str | None) -> bool: - return preset_mode in [ - PRESET_DEFROSTER_FRONT_AND_REAR, - PRESET_DEFROSTER_FRONT, - ] - - -def _rear_defroster_enabled(preset_mode: str | None) -> bool: - return preset_mode in [ - PRESET_DEFROSTER_FRONT_AND_REAR, - PRESET_DEFROSTER_REAR, - ] - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the climate platform.""" - entry_data = hass.data[DOMAIN][config_entry.entry_id] - client = entry_data[DATA_CLIENT] - coordinator = entry_data[DATA_COORDINATOR] - region = entry_data[DATA_REGION] - - async_add_entities( - MazdaClimateEntity(client, coordinator, index, region) - for index, data in enumerate(coordinator.data) - if data["isElectric"] - ) - - -class MazdaClimateEntity(MazdaEntity, ClimateEntity): - """Class for a Mazda climate entity.""" - - _attr_translation_key = "climate" - _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE - ) - _attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF] - _attr_preset_modes = [ - PRESET_DEFROSTER_OFF, - PRESET_DEFROSTER_FRONT, - PRESET_DEFROSTER_REAR, - PRESET_DEFROSTER_FRONT_AND_REAR, - ] - - def __init__( - self, - client: MazdaAPIClient, - coordinator: DataUpdateCoordinator, - index: int, - region: str, - ) -> None: - """Initialize Mazda climate entity.""" - super().__init__(client, coordinator, index) - - self.region = region - self._attr_unique_id = self.vin - - if self.data["hvacSetting"]["temperatureUnit"] == "F": - self._attr_precision = PRECISION_WHOLE - self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT - self._attr_min_temp = 61.0 - self._attr_max_temp = 83.0 - else: - self._attr_precision = PRECISION_HALVES - self._attr_temperature_unit = UnitOfTemperature.CELSIUS - if region == "MJO": - self._attr_min_temp = 18.5 - self._attr_max_temp = 31.5 - else: - self._attr_min_temp = 15.5 - self._attr_max_temp = 28.5 - - self._update_state_attributes() - - @callback - def _handle_coordinator_update(self) -> None: - """Update attributes when the coordinator data updates.""" - self._update_state_attributes() - - super()._handle_coordinator_update() - - def _update_state_attributes(self) -> None: - # Update the HVAC mode - hvac_on = self.client.get_assumed_hvac_mode(self.vehicle_id) - self._attr_hvac_mode = HVACMode.HEAT_COOL if hvac_on else HVACMode.OFF - - # Update the target temperature - hvac_setting = self.client.get_assumed_hvac_setting(self.vehicle_id) - self._attr_target_temperature = hvac_setting.get("temperature") - - # Update the current temperature - current_temperature_celsius = self.data["evStatus"]["hvacInfo"][ - "interiorTemperatureCelsius" - ] - if self.data["hvacSetting"]["temperatureUnit"] == "F": - self._attr_current_temperature = TemperatureConverter.convert( - current_temperature_celsius, - UnitOfTemperature.CELSIUS, - UnitOfTemperature.FAHRENHEIT, - ) - else: - self._attr_current_temperature = current_temperature_celsius - - # Update the preset mode based on the state of the front and rear defrosters - front_defroster = hvac_setting.get("frontDefroster") - rear_defroster = hvac_setting.get("rearDefroster") - if front_defroster and rear_defroster: - self._attr_preset_mode = PRESET_DEFROSTER_FRONT_AND_REAR - elif front_defroster: - self._attr_preset_mode = PRESET_DEFROSTER_FRONT - elif rear_defroster: - self._attr_preset_mode = PRESET_DEFROSTER_REAR - else: - self._attr_preset_mode = PRESET_DEFROSTER_OFF - - async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set a new HVAC mode.""" - if hvac_mode == HVACMode.HEAT_COOL: - await self.client.turn_on_hvac(self.vehicle_id) - elif hvac_mode == HVACMode.OFF: - await self.client.turn_off_hvac(self.vehicle_id) - - self._handle_coordinator_update() - - async def async_set_temperature(self, **kwargs: Any) -> None: - """Set a new target temperature.""" - if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None: - precision = self.precision - rounded_temperature = round(temperature / precision) * precision - - await self.client.set_hvac_setting( - self.vehicle_id, - rounded_temperature, - self.data["hvacSetting"]["temperatureUnit"], - _front_defroster_enabled(self._attr_preset_mode), - _rear_defroster_enabled(self._attr_preset_mode), - ) - - self._handle_coordinator_update() - - async def async_set_preset_mode(self, preset_mode: str) -> None: - """Turn on/off the front/rear defrosters according to the chosen preset mode.""" - await self.client.set_hvac_setting( - self.vehicle_id, - self._attr_target_temperature, - self.data["hvacSetting"]["temperatureUnit"], - _front_defroster_enabled(preset_mode), - _rear_defroster_enabled(preset_mode), - ) - - self._handle_coordinator_update() diff --git a/homeassistant/components/mazda/config_flow.py b/homeassistant/components/mazda/config_flow.py index 0b255483da1..78a939df69d 100644 --- a/homeassistant/components/mazda/config_flow.py +++ b/homeassistant/components/mazda/config_flow.py @@ -1,110 +1,11 @@ -"""Config flow for Mazda Connected Services integration.""" -from collections.abc import Mapping -import logging -from typing import Any +"""The Mazda Connected Services integration.""" -import aiohttp -from pymazda import ( - Client as MazdaAPI, - MazdaAccountLockedException, - MazdaAuthenticationException, -) -import voluptuous as vol +from homeassistant.config_entries import ConfigFlow -from homeassistant import config_entries -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION -from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import aiohttp_client - -from .const import DOMAIN, MAZDA_REGIONS - -_LOGGER = logging.getLogger(__name__) - -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_EMAIL): str, - vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_REGION): vol.In(MAZDA_REGIONS), - } -) +from . import DOMAIN -class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class MazdaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Mazda Connected Services.""" VERSION = 1 - - def __init__(self): - """Start the mazda config flow.""" - self._reauth_entry = None - self._email = None - self._region = None - - async def async_step_user(self, user_input=None): - """Handle the initial step.""" - errors = {} - - if user_input is not None: - self._email = user_input[CONF_EMAIL] - self._region = user_input[CONF_REGION] - unique_id = user_input[CONF_EMAIL].lower() - await self.async_set_unique_id(unique_id) - if not self._reauth_entry: - self._abort_if_unique_id_configured() - websession = aiohttp_client.async_get_clientsession(self.hass) - mazda_client = MazdaAPI( - user_input[CONF_EMAIL], - user_input[CONF_PASSWORD], - user_input[CONF_REGION], - websession, - ) - - try: - await mazda_client.validate_credentials() - except MazdaAuthenticationException: - errors["base"] = "invalid_auth" - except MazdaAccountLockedException: - errors["base"] = "account_locked" - except aiohttp.ClientError: - errors["base"] = "cannot_connect" - except Exception as ex: # pylint: disable=broad-except - errors["base"] = "unknown" - _LOGGER.exception( - "Unknown error occurred during Mazda login request: %s", ex - ) - else: - if not self._reauth_entry: - return self.async_create_entry( - title=user_input[CONF_EMAIL], data=user_input - ) - self.hass.config_entries.async_update_entry( - self._reauth_entry, data=user_input, unique_id=unique_id - ) - # Reload the config entry otherwise devices will remain unavailable - self.hass.async_create_task( - self.hass.config_entries.async_reload(self._reauth_entry.entry_id) - ) - return self.async_abort(reason="reauth_successful") - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_EMAIL, default=self._email): str, - vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_REGION, default=self._region): vol.In( - MAZDA_REGIONS - ), - } - ), - errors=errors, - ) - - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: - """Perform reauth if the user credentials have changed.""" - self._reauth_entry = self.hass.config_entries.async_get_entry( - self.context["entry_id"] - ) - self._email = entry_data[CONF_EMAIL] - self._region = entry_data[CONF_REGION] - return await self.async_step_user() diff --git a/homeassistant/components/mazda/const.py b/homeassistant/components/mazda/const.py deleted file mode 100644 index ebfa7f05301..00000000000 --- a/homeassistant/components/mazda/const.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Constants for the Mazda Connected Services integration.""" - -DOMAIN = "mazda" - -DATA_CLIENT = "mazda_client" -DATA_COORDINATOR = "coordinator" -DATA_REGION = "region" -DATA_VEHICLES = "vehicles" - -MAZDA_REGIONS = {"MNAO": "North America", "MME": "Europe", "MJO": "Japan"} diff --git a/homeassistant/components/mazda/device_tracker.py b/homeassistant/components/mazda/device_tracker.py deleted file mode 100644 index 2af191f97bc..00000000000 --- a/homeassistant/components/mazda/device_tracker.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Platform for Mazda device tracker integration.""" -from homeassistant.components.device_tracker import SourceType, TrackerEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import MazdaEntity -from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the device tracker platform.""" - client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] - coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] - - entities = [] - - for index, _ in enumerate(coordinator.data): - entities.append(MazdaDeviceTracker(client, coordinator, index)) - - async_add_entities(entities) - - -class MazdaDeviceTracker(MazdaEntity, TrackerEntity): - """Class for the device tracker.""" - - _attr_translation_key = "device_tracker" - _attr_icon = "mdi:car" - _attr_force_update = False - - def __init__(self, client, coordinator, index) -> None: - """Initialize Mazda device tracker.""" - super().__init__(client, coordinator, index) - - self._attr_unique_id = self.vin - - @property - def source_type(self) -> SourceType: - """Return the source type, eg gps or router, of the device.""" - return SourceType.GPS - - @property - def latitude(self): - """Return latitude value of the device.""" - return self.data["status"]["latitude"] - - @property - def longitude(self): - """Return longitude value of the device.""" - return self.data["status"]["longitude"] diff --git a/homeassistant/components/mazda/diagnostics.py b/homeassistant/components/mazda/diagnostics.py deleted file mode 100644 index 421410f4a34..00000000000 --- a/homeassistant/components/mazda/diagnostics.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Diagnostics support for the Mazda integration.""" -from __future__ import annotations - -from typing import Any - -from homeassistant.components.diagnostics.util import async_redact_data -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import DeviceEntry - -from .const import DATA_COORDINATOR, DOMAIN - -TO_REDACT_INFO = [CONF_EMAIL, CONF_PASSWORD] -TO_REDACT_DATA = ["vin", "id", "latitude", "longitude"] - - -async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry -) -> dict[str, Any]: - """Return diagnostics for a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] - - diagnostics_data = { - "info": async_redact_data(config_entry.data, TO_REDACT_INFO), - "data": [ - async_redact_data(vehicle, TO_REDACT_DATA) for vehicle in coordinator.data - ], - } - - return diagnostics_data - - -async def async_get_device_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry -) -> dict[str, Any]: - """Return diagnostics for a device.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] - - vin = next(iter(device.identifiers))[1] - - target_vehicle = None - for vehicle in coordinator.data: - if vehicle["vin"] == vin: - target_vehicle = vehicle - break - - if target_vehicle is None: - raise HomeAssistantError("Vehicle not found") - - diagnostics_data = { - "info": async_redact_data(config_entry.data, TO_REDACT_INFO), - "data": async_redact_data(target_vehicle, TO_REDACT_DATA), - } - - return diagnostics_data diff --git a/homeassistant/components/mazda/lock.py b/homeassistant/components/mazda/lock.py deleted file mode 100644 index d095ac81955..00000000000 --- a/homeassistant/components/mazda/lock.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Platform for Mazda lock integration.""" -from __future__ import annotations - -from typing import Any - -from homeassistant.components.lock import LockEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import MazdaEntity -from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the lock platform.""" - client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] - coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] - - entities = [] - - for index, _ in enumerate(coordinator.data): - entities.append(MazdaLock(client, coordinator, index)) - - async_add_entities(entities) - - -class MazdaLock(MazdaEntity, LockEntity): - """Class for the lock.""" - - _attr_translation_key = "lock" - - def __init__(self, client, coordinator, index) -> None: - """Initialize Mazda lock.""" - super().__init__(client, coordinator, index) - - self._attr_unique_id = self.vin - - @property - def is_locked(self) -> bool | None: - """Return true if lock is locked.""" - return self.client.get_assumed_lock_state(self.vehicle_id) - - async def async_lock(self, **kwargs: Any) -> None: - """Lock the vehicle doors.""" - await self.client.lock_doors(self.vehicle_id) - - self.async_write_ha_state() - - async def async_unlock(self, **kwargs: Any) -> None: - """Unlock the vehicle doors.""" - await self.client.unlock_doors(self.vehicle_id) - - self.async_write_ha_state() diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index 881120a0677..75a83a9f468 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -1,11 +1,9 @@ { "domain": "mazda", "name": "Mazda Connected Services", - "codeowners": ["@bdr99"], - "config_flow": true, + "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/mazda", + "integration_type": "system", "iot_class": "cloud_polling", - "loggers": ["pymazda"], - "quality_scale": "platinum", - "requirements": ["pymazda==0.3.11"] + "requirements": [] } diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py deleted file mode 100644 index f50533e339a..00000000000 --- a/homeassistant/components/mazda/sensor.py +++ /dev/null @@ -1,263 +0,0 @@ -"""Platform for Mazda sensor integration.""" -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass -from typing import Any - -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, UnitOfLength, UnitOfPressure -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType - -from . import MazdaEntity -from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN - - -@dataclass -class MazdaSensorRequiredKeysMixin: - """Mixin for required keys.""" - - # Function to determine the value for this sensor, given the coordinator data - # and the configured unit system - value: Callable[[dict[str, Any]], StateType] - - -@dataclass -class MazdaSensorEntityDescription( - SensorEntityDescription, MazdaSensorRequiredKeysMixin -): - """Describes a Mazda sensor entity.""" - - # Function to determine whether the vehicle supports this sensor, - # given the coordinator data - is_supported: Callable[[dict[str, Any]], bool] = lambda data: True - - -def _fuel_remaining_percentage_supported(data): - """Determine if fuel remaining percentage is supported.""" - return (not data["isElectric"]) and ( - data["status"]["fuelRemainingPercent"] is not None - ) - - -def _fuel_distance_remaining_supported(data): - """Determine if fuel distance remaining is supported.""" - return (not data["isElectric"]) and ( - data["status"]["fuelDistanceRemainingKm"] is not None - ) - - -def _front_left_tire_pressure_supported(data): - """Determine if front left tire pressure is supported.""" - return data["status"]["tirePressure"]["frontLeftTirePressurePsi"] is not None - - -def _front_right_tire_pressure_supported(data): - """Determine if front right tire pressure is supported.""" - return data["status"]["tirePressure"]["frontRightTirePressurePsi"] is not None - - -def _rear_left_tire_pressure_supported(data): - """Determine if rear left tire pressure is supported.""" - return data["status"]["tirePressure"]["rearLeftTirePressurePsi"] is not None - - -def _rear_right_tire_pressure_supported(data): - """Determine if rear right tire pressure is supported.""" - return data["status"]["tirePressure"]["rearRightTirePressurePsi"] is not None - - -def _ev_charge_level_supported(data): - """Determine if charge level is supported.""" - return ( - data["isElectric"] - and data["evStatus"]["chargeInfo"]["batteryLevelPercentage"] is not None - ) - - -def _ev_remaining_range_supported(data): - """Determine if remaining range is supported.""" - return ( - data["isElectric"] - and data["evStatus"]["chargeInfo"]["drivingRangeKm"] is not None - ) - - -def _fuel_distance_remaining_value(data): - """Get the fuel distance remaining value.""" - return round(data["status"]["fuelDistanceRemainingKm"]) - - -def _odometer_value(data): - """Get the odometer value.""" - # In order to match the behavior of the Mazda mobile app, we always round down - return int(data["status"]["odometerKm"]) - - -def _front_left_tire_pressure_value(data): - """Get the front left tire pressure value.""" - return round(data["status"]["tirePressure"]["frontLeftTirePressurePsi"]) - - -def _front_right_tire_pressure_value(data): - """Get the front right tire pressure value.""" - return round(data["status"]["tirePressure"]["frontRightTirePressurePsi"]) - - -def _rear_left_tire_pressure_value(data): - """Get the rear left tire pressure value.""" - return round(data["status"]["tirePressure"]["rearLeftTirePressurePsi"]) - - -def _rear_right_tire_pressure_value(data): - """Get the rear right tire pressure value.""" - return round(data["status"]["tirePressure"]["rearRightTirePressurePsi"]) - - -def _ev_charge_level_value(data): - """Get the charge level value.""" - return round(data["evStatus"]["chargeInfo"]["batteryLevelPercentage"]) - - -def _ev_remaining_range_value(data): - """Get the remaining range value.""" - return round(data["evStatus"]["chargeInfo"]["drivingRangeKm"]) - - -SENSOR_ENTITIES = [ - MazdaSensorEntityDescription( - key="fuel_remaining_percentage", - translation_key="fuel_remaining_percentage", - icon="mdi:gas-station", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - is_supported=_fuel_remaining_percentage_supported, - value=lambda data: data["status"]["fuelRemainingPercent"], - ), - MazdaSensorEntityDescription( - key="fuel_distance_remaining", - translation_key="fuel_distance_remaining", - icon="mdi:gas-station", - device_class=SensorDeviceClass.DISTANCE, - native_unit_of_measurement=UnitOfLength.KILOMETERS, - state_class=SensorStateClass.MEASUREMENT, - is_supported=_fuel_distance_remaining_supported, - value=_fuel_distance_remaining_value, - ), - MazdaSensorEntityDescription( - key="odometer", - translation_key="odometer", - icon="mdi:speedometer", - device_class=SensorDeviceClass.DISTANCE, - native_unit_of_measurement=UnitOfLength.KILOMETERS, - state_class=SensorStateClass.TOTAL_INCREASING, - is_supported=lambda data: data["status"]["odometerKm"] is not None, - value=_odometer_value, - ), - MazdaSensorEntityDescription( - key="front_left_tire_pressure", - translation_key="front_left_tire_pressure", - icon="mdi:car-tire-alert", - device_class=SensorDeviceClass.PRESSURE, - native_unit_of_measurement=UnitOfPressure.PSI, - state_class=SensorStateClass.MEASUREMENT, - is_supported=_front_left_tire_pressure_supported, - value=_front_left_tire_pressure_value, - ), - MazdaSensorEntityDescription( - key="front_right_tire_pressure", - translation_key="front_right_tire_pressure", - icon="mdi:car-tire-alert", - device_class=SensorDeviceClass.PRESSURE, - native_unit_of_measurement=UnitOfPressure.PSI, - state_class=SensorStateClass.MEASUREMENT, - is_supported=_front_right_tire_pressure_supported, - value=_front_right_tire_pressure_value, - ), - MazdaSensorEntityDescription( - key="rear_left_tire_pressure", - translation_key="rear_left_tire_pressure", - icon="mdi:car-tire-alert", - device_class=SensorDeviceClass.PRESSURE, - native_unit_of_measurement=UnitOfPressure.PSI, - state_class=SensorStateClass.MEASUREMENT, - is_supported=_rear_left_tire_pressure_supported, - value=_rear_left_tire_pressure_value, - ), - MazdaSensorEntityDescription( - key="rear_right_tire_pressure", - translation_key="rear_right_tire_pressure", - icon="mdi:car-tire-alert", - device_class=SensorDeviceClass.PRESSURE, - native_unit_of_measurement=UnitOfPressure.PSI, - state_class=SensorStateClass.MEASUREMENT, - is_supported=_rear_right_tire_pressure_supported, - value=_rear_right_tire_pressure_value, - ), - MazdaSensorEntityDescription( - key="ev_charge_level", - translation_key="ev_charge_level", - device_class=SensorDeviceClass.BATTERY, - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - is_supported=_ev_charge_level_supported, - value=_ev_charge_level_value, - ), - MazdaSensorEntityDescription( - key="ev_remaining_range", - translation_key="ev_remaining_range", - icon="mdi:ev-station", - device_class=SensorDeviceClass.DISTANCE, - native_unit_of_measurement=UnitOfLength.KILOMETERS, - state_class=SensorStateClass.MEASUREMENT, - is_supported=_ev_remaining_range_supported, - value=_ev_remaining_range_value, - ), -] - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the sensor platform.""" - client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] - coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] - - entities: list[SensorEntity] = [] - - for index, data in enumerate(coordinator.data): - for description in SENSOR_ENTITIES: - if description.is_supported(data): - entities.append( - MazdaSensorEntity(client, coordinator, index, description) - ) - - async_add_entities(entities) - - -class MazdaSensorEntity(MazdaEntity, SensorEntity): - """Representation of a Mazda vehicle sensor.""" - - entity_description: MazdaSensorEntityDescription - - def __init__(self, client, coordinator, index, description): - """Initialize Mazda sensor.""" - super().__init__(client, coordinator, index) - self.entity_description = description - - self._attr_unique_id = f"{self.vin}_{description.key}" - - @property - def native_value(self) -> StateType: - """Return the state of the sensor.""" - return self.entity_description.value(self.data) diff --git a/homeassistant/components/mazda/services.yaml b/homeassistant/components/mazda/services.yaml deleted file mode 100644 index b401c01f3a3..00000000000 --- a/homeassistant/components/mazda/services.yaml +++ /dev/null @@ -1,30 +0,0 @@ -send_poi: - fields: - device_id: - required: true - selector: - device: - integration: mazda - latitude: - example: 12.34567 - required: true - selector: - number: - min: -90 - max: 90 - unit_of_measurement: ° - mode: box - longitude: - example: -34.56789 - required: true - selector: - number: - min: -180 - max: 180 - unit_of_measurement: ° - mode: box - poi_name: - example: Work - required: true - selector: - text: diff --git a/homeassistant/components/mazda/strings.json b/homeassistant/components/mazda/strings.json index 6c1214f76c6..1d0fedf3e97 100644 --- a/homeassistant/components/mazda/strings.json +++ b/homeassistant/components/mazda/strings.json @@ -1,139 +1,8 @@ { - "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - }, - "error": { - "account_locked": "Account locked. Please try again later.", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "step": { - "user": { - "data": { - "email": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]", - "region": "Region" - }, - "description": "Please enter the email address and password you use to log into the MyMazda mobile app." - } - } - }, - "entity": { - "binary_sensor": { - "driver_door": { - "name": "Driver door" - }, - "passenger_door": { - "name": "Passenger door" - }, - "rear_left_door": { - "name": "Rear left door" - }, - "rear_right_door": { - "name": "Rear right door" - }, - "trunk": { - "name": "Trunk" - }, - "hood": { - "name": "Hood" - }, - "ev_plugged_in": { - "name": "Plugged in" - } - }, - "button": { - "start_engine": { - "name": "Start engine" - }, - "stop_engine": { - "name": "Stop engine" - }, - "turn_on_hazard_lights": { - "name": "Turn on hazard lights" - }, - "turn_off_hazard_lights": { - "name": "Turn off hazard lights" - }, - "refresh_vehicle_status": { - "name": "Refresh status" - } - }, - "climate": { - "climate": { - "name": "[%key:component::climate::title%]" - } - }, - "device_tracker": { - "device_tracker": { - "name": "[%key:component::device_tracker::title%]" - } - }, - "lock": { - "lock": { - "name": "[%key:component::lock::title%]" - } - }, - "sensor": { - "fuel_remaining_percentage": { - "name": "Fuel remaining percentage" - }, - "fuel_distance_remaining": { - "name": "Fuel distance remaining" - }, - "odometer": { - "name": "Odometer" - }, - "front_left_tire_pressure": { - "name": "Front left tire pressure" - }, - "front_right_tire_pressure": { - "name": "Front right tire pressure" - }, - "rear_left_tire_pressure": { - "name": "Rear left tire pressure" - }, - "rear_right_tire_pressure": { - "name": "Rear right tire pressure" - }, - "ev_charge_level": { - "name": "Charge level" - }, - "ev_remaining_range": { - "name": "Remaining range" - } - }, - "switch": { - "charging": { - "name": "Charging" - } - } - }, - "services": { - "send_poi": { - "name": "Send POI", - "description": "Sends a GPS location to the vehicle's navigation system as a POI (Point of Interest). Requires a navigation SD card installed in the vehicle.", - "fields": { - "device_id": { - "name": "Vehicle", - "description": "The vehicle to send the GPS location to." - }, - "latitude": { - "name": "[%key:common::config_flow::data::latitude%]", - "description": "The latitude of the location to send." - }, - "longitude": { - "name": "[%key:common::config_flow::data::longitude%]", - "description": "The longitude of the location to send." - }, - "poi_name": { - "name": "POI name", - "description": "A friendly name for the location." - } - } + "issues": { + "integration_removed": { + "title": "The Mazda integration has been removed", + "description": "The Mazda integration has been removed from Home Assistant.\n\nThe library that Home Assistant uses to connect with their services, [has been taken offline by Mazda]({dmca}).\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Mazda integration entries]({entries})." } } } diff --git a/homeassistant/components/mazda/switch.py b/homeassistant/components/mazda/switch.py deleted file mode 100644 index 327d371769b..00000000000 --- a/homeassistant/components/mazda/switch.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Platform for Mazda switch integration.""" -from typing import Any - -from pymazda import Client as MazdaAPIClient - -from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from . import MazdaEntity -from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the switch platform.""" - client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] - coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] - - async_add_entities( - MazdaChargingSwitch(client, coordinator, index) - for index, data in enumerate(coordinator.data) - if data["isElectric"] - ) - - -class MazdaChargingSwitch(MazdaEntity, SwitchEntity): - """Class for the charging switch.""" - - _attr_translation_key = "charging" - _attr_icon = "mdi:ev-station" - - def __init__( - self, - client: MazdaAPIClient, - coordinator: DataUpdateCoordinator, - index: int, - ) -> None: - """Initialize Mazda charging switch.""" - super().__init__(client, coordinator, index) - - self._attr_unique_id = self.vin - - @property - def is_on(self): - """Return true if the vehicle is charging.""" - return self.data["evStatus"]["chargeInfo"]["charging"] - - async def refresh_status_and_write_state(self): - """Request a status update, retrieve it through the coordinator, and write the state.""" - await self.client.refresh_vehicle_status(self.vehicle_id) - - await self.coordinator.async_request_refresh() - - self.async_write_ha_state() - - async def async_turn_on(self, **kwargs: Any) -> None: - """Start charging the vehicle.""" - await self.client.start_charging(self.vehicle_id) - - await self.refresh_status_and_write_state() - - async def async_turn_off(self, **kwargs: Any) -> None: - """Stop charging the vehicle.""" - await self.client.stop_charging(self.vehicle_id) - - await self.refresh_status_and_write_state() diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ef22ac4f653..29d067657b5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -270,7 +270,6 @@ FLOWS = { "lyric", "mailgun", "matter", - "mazda", "meater", "medcom_ble", "melcloud", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 1d9c2208ad0..3a8ffea866d 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3258,12 +3258,6 @@ "config_flow": true, "iot_class": "local_push" }, - "mazda": { - "name": "Mazda Connected Services", - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_polling" - }, "meater": { "name": "Meater", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index a61cf6a26fa..e720d641ccf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1844,9 +1844,6 @@ pymailgunner==1.4 # homeassistant.components.firmata pymata-express==1.19 -# homeassistant.components.mazda -pymazda==0.3.11 - # homeassistant.components.mediaroom pymediaroom==0.6.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c3980a92d4..d5c324d1964 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1387,9 +1387,6 @@ pymailgunner==1.4 # homeassistant.components.firmata pymata-express==1.19 -# homeassistant.components.mazda -pymazda==0.3.11 - # homeassistant.components.melcloud pymelcloud==2.5.8 diff --git a/tests/components/mazda/__init__.py b/tests/components/mazda/__init__.py index 59b1d474140..cc3d81df4dd 100644 --- a/tests/components/mazda/__init__.py +++ b/tests/components/mazda/__init__.py @@ -1,80 +1 @@ """Tests for the Mazda Connected Services integration.""" - -import json -from unittest.mock import AsyncMock, MagicMock, Mock, patch - -from pymazda import Client as MazdaAPI - -from homeassistant.components.mazda.const import DOMAIN -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION -from homeassistant.core import HomeAssistant -from homeassistant.helpers import aiohttp_client - -from tests.common import MockConfigEntry, load_fixture - -FIXTURE_USER_INPUT = { - CONF_EMAIL: "example@example.com", - CONF_PASSWORD: "password", - CONF_REGION: "MNAO", -} - - -async def init_integration( - hass: HomeAssistant, use_nickname=True, electric_vehicle=False -) -> MockConfigEntry: - """Set up the Mazda Connected Services integration in Home Assistant.""" - get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json")) - if not use_nickname: - get_vehicles_fixture[0].pop("nickname") - if electric_vehicle: - get_vehicles_fixture[0]["isElectric"] = True - - get_vehicle_status_fixture = json.loads( - load_fixture("mazda/get_vehicle_status.json") - ) - get_ev_vehicle_status_fixture = json.loads( - load_fixture("mazda/get_ev_vehicle_status.json") - ) - get_hvac_setting_fixture = json.loads(load_fixture("mazda/get_hvac_setting.json")) - - config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) - config_entry.add_to_hass(hass) - - client_mock = MagicMock( - MazdaAPI( - FIXTURE_USER_INPUT[CONF_EMAIL], - FIXTURE_USER_INPUT[CONF_PASSWORD], - FIXTURE_USER_INPUT[CONF_REGION], - aiohttp_client.async_get_clientsession(hass), - ) - ) - client_mock.get_vehicles = AsyncMock(return_value=get_vehicles_fixture) - client_mock.get_vehicle_status = AsyncMock(return_value=get_vehicle_status_fixture) - client_mock.get_ev_vehicle_status = AsyncMock( - return_value=get_ev_vehicle_status_fixture - ) - client_mock.lock_doors = AsyncMock() - client_mock.unlock_doors = AsyncMock() - client_mock.send_poi = AsyncMock() - client_mock.start_charging = AsyncMock() - client_mock.start_engine = AsyncMock() - client_mock.stop_charging = AsyncMock() - client_mock.stop_engine = AsyncMock() - client_mock.turn_off_hazard_lights = AsyncMock() - client_mock.turn_on_hazard_lights = AsyncMock() - client_mock.refresh_vehicle_status = AsyncMock() - client_mock.get_hvac_setting = AsyncMock(return_value=get_hvac_setting_fixture) - client_mock.get_assumed_hvac_setting = Mock(return_value=get_hvac_setting_fixture) - client_mock.get_assumed_hvac_mode = Mock(return_value=True) - client_mock.set_hvac_setting = AsyncMock() - client_mock.turn_on_hvac = AsyncMock() - client_mock.turn_off_hvac = AsyncMock() - - with patch( - "homeassistant.components.mazda.config_flow.MazdaAPI", - return_value=client_mock, - ), patch("homeassistant.components.mazda.MazdaAPI", return_value=client_mock): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return client_mock diff --git a/tests/components/mazda/fixtures/diagnostics_config_entry.json b/tests/components/mazda/fixtures/diagnostics_config_entry.json deleted file mode 100644 index 87f49bc29cb..00000000000 --- a/tests/components/mazda/fixtures/diagnostics_config_entry.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "info": { - "email": "**REDACTED**", - "password": "**REDACTED**", - "region": "MNAO" - }, - "data": [ - { - "vin": "**REDACTED**", - "id": "**REDACTED**", - "nickname": "My Mazda3", - "carlineCode": "M3S", - "carlineName": "MAZDA3 2.5 S SE AWD", - "modelYear": "2021", - "modelCode": "M3S SE XA", - "modelName": "W/ SELECT PKG AWD SDN", - "automaticTransmission": true, - "interiorColorCode": "BY3", - "interiorColorName": "BLACK", - "exteriorColorCode": "42M", - "exteriorColorName": "DEEP CRYSTAL BLUE MICA", - "isElectric": false, - "status": { - "lastUpdatedTimestamp": "20210123143809", - "latitude": "**REDACTED**", - "longitude": "**REDACTED**", - "positionTimestamp": "20210123143808", - "fuelRemainingPercent": 87.0, - "fuelDistanceRemainingKm": 380.8, - "odometerKm": 2795.8, - "doors": { - "driverDoorOpen": false, - "passengerDoorOpen": true, - "rearLeftDoorOpen": false, - "rearRightDoorOpen": false, - "trunkOpen": false, - "hoodOpen": true, - "fuelLidOpen": false - }, - "doorLocks": { - "driverDoorUnlocked": false, - "passengerDoorUnlocked": false, - "rearLeftDoorUnlocked": false, - "rearRightDoorUnlocked": false - }, - "windows": { - "driverWindowOpen": false, - "passengerWindowOpen": false, - "rearLeftWindowOpen": false, - "rearRightWindowOpen": false - }, - "hazardLightsOn": false, - "tirePressure": { - "frontLeftTirePressurePsi": 35.0, - "frontRightTirePressurePsi": 35.0, - "rearLeftTirePressurePsi": 33.0, - "rearRightTirePressurePsi": 33.0 - } - } - } - ] -} diff --git a/tests/components/mazda/fixtures/diagnostics_device.json b/tests/components/mazda/fixtures/diagnostics_device.json deleted file mode 100644 index f2ddd658f70..00000000000 --- a/tests/components/mazda/fixtures/diagnostics_device.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "info": { - "email": "**REDACTED**", - "password": "**REDACTED**", - "region": "MNAO" - }, - "data": { - "vin": "**REDACTED**", - "id": "**REDACTED**", - "nickname": "My Mazda3", - "carlineCode": "M3S", - "carlineName": "MAZDA3 2.5 S SE AWD", - "modelYear": "2021", - "modelCode": "M3S SE XA", - "modelName": "W/ SELECT PKG AWD SDN", - "automaticTransmission": true, - "interiorColorCode": "BY3", - "interiorColorName": "BLACK", - "exteriorColorCode": "42M", - "exteriorColorName": "DEEP CRYSTAL BLUE MICA", - "isElectric": false, - "status": { - "lastUpdatedTimestamp": "20210123143809", - "latitude": "**REDACTED**", - "longitude": "**REDACTED**", - "positionTimestamp": "20210123143808", - "fuelRemainingPercent": 87.0, - "fuelDistanceRemainingKm": 380.8, - "odometerKm": 2795.8, - "doors": { - "driverDoorOpen": false, - "passengerDoorOpen": true, - "rearLeftDoorOpen": false, - "rearRightDoorOpen": false, - "trunkOpen": false, - "hoodOpen": true, - "fuelLidOpen": false - }, - "doorLocks": { - "driverDoorUnlocked": false, - "passengerDoorUnlocked": false, - "rearLeftDoorUnlocked": false, - "rearRightDoorUnlocked": false - }, - "windows": { - "driverWindowOpen": false, - "passengerWindowOpen": false, - "rearLeftWindowOpen": false, - "rearRightWindowOpen": false - }, - "hazardLightsOn": false, - "tirePressure": { - "frontLeftTirePressurePsi": 35.0, - "frontRightTirePressurePsi": 35.0, - "rearLeftTirePressurePsi": 33.0, - "rearRightTirePressurePsi": 33.0 - } - } - } -} diff --git a/tests/components/mazda/fixtures/get_ev_vehicle_status.json b/tests/components/mazda/fixtures/get_ev_vehicle_status.json deleted file mode 100644 index a577cab3054..00000000000 --- a/tests/components/mazda/fixtures/get_ev_vehicle_status.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "lastUpdatedTimestamp": "20210807083956", - "chargeInfo": { - "batteryLevelPercentage": 80, - "drivingRangeKm": 218, - "pluggedIn": true, - "charging": true, - "basicChargeTimeMinutes": 30, - "quickChargeTimeMinutes": 15, - "batteryHeaterAuto": true, - "batteryHeaterOn": true - }, - "hvacInfo": { - "hvacOn": true, - "frontDefroster": false, - "rearDefroster": false, - "interiorTemperatureCelsius": 15.1 - } -} diff --git a/tests/components/mazda/fixtures/get_hvac_setting.json b/tests/components/mazda/fixtures/get_hvac_setting.json deleted file mode 100644 index 3b95832ba65..00000000000 --- a/tests/components/mazda/fixtures/get_hvac_setting.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "temperature": 20, - "temperatureUnit": "C", - "frontDefroster": true, - "rearDefroster": false -} diff --git a/tests/components/mazda/fixtures/get_vehicle_status.json b/tests/components/mazda/fixtures/get_vehicle_status.json deleted file mode 100644 index 17fe86c642b..00000000000 --- a/tests/components/mazda/fixtures/get_vehicle_status.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "lastUpdatedTimestamp": "20210123143809", - "latitude": 1.234567, - "longitude": -2.345678, - "positionTimestamp": "20210123143808", - "fuelRemainingPercent": 87.0, - "fuelDistanceRemainingKm": 380.8, - "odometerKm": 2795.8, - "doors": { - "driverDoorOpen": false, - "passengerDoorOpen": true, - "rearLeftDoorOpen": false, - "rearRightDoorOpen": false, - "trunkOpen": false, - "hoodOpen": true, - "fuelLidOpen": false - }, - "doorLocks": { - "driverDoorUnlocked": false, - "passengerDoorUnlocked": false, - "rearLeftDoorUnlocked": false, - "rearRightDoorUnlocked": false - }, - "windows": { - "driverWindowOpen": false, - "passengerWindowOpen": false, - "rearLeftWindowOpen": false, - "rearRightWindowOpen": false - }, - "hazardLightsOn": false, - "tirePressure": { - "frontLeftTirePressurePsi": 35.0, - "frontRightTirePressurePsi": 35.0, - "rearLeftTirePressurePsi": 33.0, - "rearRightTirePressurePsi": 33.0 - } -} diff --git a/tests/components/mazda/fixtures/get_vehicles.json b/tests/components/mazda/fixtures/get_vehicles.json deleted file mode 100644 index a80a09f380a..00000000000 --- a/tests/components/mazda/fixtures/get_vehicles.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "vin": "JM000000000000000", - "id": 12345, - "nickname": "My Mazda3", - "carlineCode": "M3S", - "carlineName": "MAZDA3 2.5 S SE AWD", - "modelYear": "2021", - "modelCode": "M3S SE XA", - "modelName": "W/ SELECT PKG AWD SDN", - "automaticTransmission": true, - "interiorColorCode": "BY3", - "interiorColorName": "BLACK", - "exteriorColorCode": "42M", - "exteriorColorName": "DEEP CRYSTAL BLUE MICA", - "isElectric": false - } -] diff --git a/tests/components/mazda/test_binary_sensor.py b/tests/components/mazda/test_binary_sensor.py deleted file mode 100644 index d5bae776320..00000000000 --- a/tests/components/mazda/test_binary_sensor.py +++ /dev/null @@ -1,98 +0,0 @@ -"""The binary sensor tests for the Mazda Connected Services integration.""" -from homeassistant.components.binary_sensor import BinarySensorDeviceClass -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er - -from . import init_integration - - -async def test_binary_sensors(hass: HomeAssistant) -> None: - """Test creation of the binary sensors.""" - await init_integration(hass) - - entity_registry = er.async_get(hass) - - # Driver Door - state = hass.states.get("binary_sensor.my_mazda3_driver_door") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Driver door" - assert state.attributes.get(ATTR_ICON) == "mdi:car-door" - assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR - assert state.state == "off" - entry = entity_registry.async_get("binary_sensor.my_mazda3_driver_door") - assert entry - assert entry.unique_id == "JM000000000000000_driver_door" - - # Passenger Door - state = hass.states.get("binary_sensor.my_mazda3_passenger_door") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Passenger door" - assert state.attributes.get(ATTR_ICON) == "mdi:car-door" - assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR - assert state.state == "on" - entry = entity_registry.async_get("binary_sensor.my_mazda3_passenger_door") - assert entry - assert entry.unique_id == "JM000000000000000_passenger_door" - - # Rear Left Door - state = hass.states.get("binary_sensor.my_mazda3_rear_left_door") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear left door" - assert state.attributes.get(ATTR_ICON) == "mdi:car-door" - assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR - assert state.state == "off" - entry = entity_registry.async_get("binary_sensor.my_mazda3_rear_left_door") - assert entry - assert entry.unique_id == "JM000000000000000_rear_left_door" - - # Rear Right Door - state = hass.states.get("binary_sensor.my_mazda3_rear_right_door") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear right door" - assert state.attributes.get(ATTR_ICON) == "mdi:car-door" - assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR - assert state.state == "off" - entry = entity_registry.async_get("binary_sensor.my_mazda3_rear_right_door") - assert entry - assert entry.unique_id == "JM000000000000000_rear_right_door" - - # Trunk - state = hass.states.get("binary_sensor.my_mazda3_trunk") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Trunk" - assert state.attributes.get(ATTR_ICON) == "mdi:car-back" - assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR - assert state.state == "off" - entry = entity_registry.async_get("binary_sensor.my_mazda3_trunk") - assert entry - assert entry.unique_id == "JM000000000000000_trunk" - - # Hood - state = hass.states.get("binary_sensor.my_mazda3_hood") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Hood" - assert state.attributes.get(ATTR_ICON) == "mdi:car" - assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR - assert state.state == "on" - entry = entity_registry.async_get("binary_sensor.my_mazda3_hood") - assert entry - assert entry.unique_id == "JM000000000000000_hood" - - -async def test_electric_vehicle_binary_sensors(hass: HomeAssistant) -> None: - """Test sensors which are specific to electric vehicles.""" - - await init_integration(hass, electric_vehicle=True) - - entity_registry = er.async_get(hass) - - # Plugged In - state = hass.states.get("binary_sensor.my_mazda3_plugged_in") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Plugged in" - assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PLUG - assert state.state == "on" - entry = entity_registry.async_get("binary_sensor.my_mazda3_plugged_in") - assert entry - assert entry.unique_id == "JM000000000000000_ev_plugged_in" diff --git a/tests/components/mazda/test_button.py b/tests/components/mazda/test_button.py deleted file mode 100644 index ba80c10b38d..00000000000 --- a/tests/components/mazda/test_button.py +++ /dev/null @@ -1,145 +0,0 @@ -"""The button tests for the Mazda Connected Services integration.""" - -from pymazda import MazdaException -import pytest - -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er - -from . import init_integration - - -async def test_button_setup_non_electric_vehicle(hass: HomeAssistant) -> None: - """Test creation of button entities.""" - await init_integration(hass) - - entity_registry = er.async_get(hass) - - entry = entity_registry.async_get("button.my_mazda3_start_engine") - assert entry - assert entry.unique_id == "JM000000000000000_start_engine" - state = hass.states.get("button.my_mazda3_start_engine") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start engine" - assert state.attributes.get(ATTR_ICON) == "mdi:engine" - - entry = entity_registry.async_get("button.my_mazda3_stop_engine") - assert entry - assert entry.unique_id == "JM000000000000000_stop_engine" - state = hass.states.get("button.my_mazda3_stop_engine") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop engine" - assert state.attributes.get(ATTR_ICON) == "mdi:engine-off" - - entry = entity_registry.async_get("button.my_mazda3_turn_on_hazard_lights") - assert entry - assert entry.unique_id == "JM000000000000000_turn_on_hazard_lights" - state = hass.states.get("button.my_mazda3_turn_on_hazard_lights") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn on hazard lights" - assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" - - entry = entity_registry.async_get("button.my_mazda3_turn_off_hazard_lights") - assert entry - assert entry.unique_id == "JM000000000000000_turn_off_hazard_lights" - state = hass.states.get("button.my_mazda3_turn_off_hazard_lights") - assert state - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn off hazard lights" - ) - assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" - - # Since this is a non-electric vehicle, electric vehicle buttons should not be created - entry = entity_registry.async_get("button.my_mazda3_refresh_vehicle_status") - assert entry is None - state = hass.states.get("button.my_mazda3_refresh_vehicle_status") - assert state is None - - -async def test_button_setup_electric_vehicle(hass: HomeAssistant) -> None: - """Test creation of button entities for an electric vehicle.""" - await init_integration(hass, electric_vehicle=True) - - entity_registry = er.async_get(hass) - - entry = entity_registry.async_get("button.my_mazda3_refresh_status") - assert entry - assert entry.unique_id == "JM000000000000000_refresh_vehicle_status" - state = hass.states.get("button.my_mazda3_refresh_status") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Refresh status" - assert state.attributes.get(ATTR_ICON) == "mdi:refresh" - - -@pytest.mark.parametrize( - ("electric_vehicle", "entity_id_suffix"), - [ - (True, "start_engine"), - (True, "stop_engine"), - (True, "turn_on_hazard_lights"), - (True, "turn_off_hazard_lights"), - (False, "refresh_status"), - ], -) -async def test_button_not_created( - hass: HomeAssistant, electric_vehicle, entity_id_suffix -) -> None: - """Test that button entities are not created when they should not be.""" - await init_integration(hass, electric_vehicle=electric_vehicle) - - entity_registry = er.async_get(hass) - - entity_id = f"button.my_mazda3_{entity_id_suffix}" - entry = entity_registry.async_get(entity_id) - assert entry is None - state = hass.states.get(entity_id) - assert state is None - - -@pytest.mark.parametrize( - ("electric_vehicle", "entity_id_suffix", "api_method_name"), - [ - (False, "start_engine", "start_engine"), - (False, "stop_engine", "stop_engine"), - (False, "turn_on_hazard_lights", "turn_on_hazard_lights"), - (False, "turn_off_hazard_lights", "turn_off_hazard_lights"), - (True, "refresh_status", "refresh_vehicle_status"), - ], -) -async def test_button_press( - hass: HomeAssistant, electric_vehicle, entity_id_suffix, api_method_name -) -> None: - """Test pressing the button entities.""" - client_mock = await init_integration(hass, electric_vehicle=electric_vehicle) - - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: f"button.my_mazda3_{entity_id_suffix}"}, - blocking=True, - ) - await hass.async_block_till_done() - - api_method = getattr(client_mock, api_method_name) - api_method.assert_called_once_with(12345) - - -async def test_button_press_error(hass: HomeAssistant) -> None: - """Test the Mazda API raising an error when a button entity is pressed.""" - client_mock = await init_integration(hass) - - client_mock.start_engine.side_effect = MazdaException("Test error") - - with pytest.raises(HomeAssistantError) as err: - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.my_mazda3_start_engine"}, - blocking=True, - ) - await hass.async_block_till_done() - - assert str(err.value) == "Test error" diff --git a/tests/components/mazda/test_climate.py b/tests/components/mazda/test_climate.py deleted file mode 100644 index ef3840f5cee..00000000000 --- a/tests/components/mazda/test_climate.py +++ /dev/null @@ -1,341 +0,0 @@ -"""The climate tests for the Mazda Connected Services integration.""" -import json -from unittest.mock import patch - -import pytest - -from homeassistant.components.climate import ( - ATTR_HVAC_MODE, - ATTR_PRESET_MODE, - DOMAIN as CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - SERVICE_SET_PRESET_MODE, - SERVICE_SET_TEMPERATURE, -) -from homeassistant.components.climate.const import ( - ATTR_CURRENT_TEMPERATURE, - ATTR_HVAC_MODES, - ATTR_MAX_TEMP, - ATTR_MIN_TEMP, - ATTR_PRESET_MODES, - ClimateEntityFeature, - HVACMode, -) -from homeassistant.components.mazda.climate import ( - PRESET_DEFROSTER_FRONT, - PRESET_DEFROSTER_FRONT_AND_REAR, - PRESET_DEFROSTER_OFF, - PRESET_DEFROSTER_REAR, -) -from homeassistant.components.mazda.const import DOMAIN -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_FRIENDLY_NAME, - ATTR_SUPPORTED_FEATURES, - ATTR_TEMPERATURE, - CONF_EMAIL, - CONF_PASSWORD, - CONF_REGION, - UnitOfTemperature, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM - -from . import init_integration - -from tests.common import MockConfigEntry, load_fixture - - -async def test_climate_setup(hass: HomeAssistant) -> None: - """Test the setup of the climate entity.""" - await init_integration(hass, electric_vehicle=True) - - entity_registry = er.async_get(hass) - entry = entity_registry.async_get("climate.my_mazda3_climate") - assert entry - assert entry.unique_id == "JM000000000000000" - - state = hass.states.get("climate.my_mazda3_climate") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Climate" - - -@pytest.mark.parametrize( - ( - "region", - "hvac_on", - "target_temperature", - "temperature_unit", - "front_defroster", - "rear_defroster", - "current_temperature_celsius", - "expected_hvac_mode", - "expected_preset_mode", - "expected_min_temp", - "expected_max_temp", - ), - [ - # Test with HVAC off - ( - "MNAO", - False, - 20, - "C", - False, - False, - 22, - HVACMode.OFF, - PRESET_DEFROSTER_OFF, - 15.5, - 28.5, - ), - # Test with HVAC on - ( - "MNAO", - True, - 20, - "C", - False, - False, - 22, - HVACMode.HEAT_COOL, - PRESET_DEFROSTER_OFF, - 15.5, - 28.5, - ), - # Test with front defroster on - ( - "MNAO", - False, - 20, - "C", - True, - False, - 22, - HVACMode.OFF, - PRESET_DEFROSTER_FRONT, - 15.5, - 28.5, - ), - # Test with rear defroster on - ( - "MNAO", - False, - 20, - "C", - False, - True, - 22, - HVACMode.OFF, - PRESET_DEFROSTER_REAR, - 15.5, - 28.5, - ), - # Test with front and rear defrosters on - ( - "MNAO", - False, - 20, - "C", - True, - True, - 22, - HVACMode.OFF, - PRESET_DEFROSTER_FRONT_AND_REAR, - 15.5, - 28.5, - ), - # Test with temperature unit F - ( - "MNAO", - False, - 70, - "F", - False, - False, - 22, - HVACMode.OFF, - PRESET_DEFROSTER_OFF, - 61.0, - 83.0, - ), - # Test with Japan region (uses different min/max temp settings) - ( - "MJO", - False, - 20, - "C", - False, - False, - 22, - HVACMode.OFF, - PRESET_DEFROSTER_OFF, - 18.5, - 31.5, - ), - ], -) -async def test_climate_state( - hass: HomeAssistant, - region, - hvac_on, - target_temperature, - temperature_unit, - front_defroster, - rear_defroster, - current_temperature_celsius, - expected_hvac_mode, - expected_preset_mode, - expected_min_temp, - expected_max_temp, -) -> None: - """Test getting the state of the climate entity.""" - if temperature_unit == "F": - hass.config.units = US_CUSTOMARY_SYSTEM - - get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json")) - get_vehicles_fixture[0]["isElectric"] = True - get_vehicle_status_fixture = json.loads( - load_fixture("mazda/get_vehicle_status.json") - ) - get_ev_vehicle_status_fixture = json.loads( - load_fixture("mazda/get_ev_vehicle_status.json") - ) - get_ev_vehicle_status_fixture["hvacInfo"][ - "interiorTemperatureCelsius" - ] = current_temperature_celsius - get_hvac_setting_fixture = { - "temperature": target_temperature, - "temperatureUnit": temperature_unit, - "frontDefroster": front_defroster, - "rearDefroster": rear_defroster, - } - - with patch( - "homeassistant.components.mazda.MazdaAPI.validate_credentials", - return_value=True, - ), patch( - "homeassistant.components.mazda.MazdaAPI.get_vehicles", - return_value=get_vehicles_fixture, - ), patch( - "homeassistant.components.mazda.MazdaAPI.get_vehicle_status", - return_value=get_vehicle_status_fixture, - ), patch( - "homeassistant.components.mazda.MazdaAPI.get_ev_vehicle_status", - return_value=get_ev_vehicle_status_fixture, - ), patch( - "homeassistant.components.mazda.MazdaAPI.get_assumed_hvac_mode", - return_value=hvac_on, - ), patch( - "homeassistant.components.mazda.MazdaAPI.get_assumed_hvac_setting", - return_value=get_hvac_setting_fixture, - ), patch( - "homeassistant.components.mazda.MazdaAPI.get_hvac_setting", - return_value=get_hvac_setting_fixture, - ): - config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_EMAIL: "example@example.com", - CONF_PASSWORD: "password", - CONF_REGION: region, - }, - ) - config_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("climate.my_mazda3_climate") - assert state - assert state.state == expected_hvac_mode - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Climate" - assert ( - state.attributes.get(ATTR_SUPPORTED_FEATURES) - == ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE - ) - assert state.attributes.get(ATTR_HVAC_MODES) == [HVACMode.HEAT_COOL, HVACMode.OFF] - assert state.attributes.get(ATTR_PRESET_MODES) == [ - PRESET_DEFROSTER_OFF, - PRESET_DEFROSTER_FRONT, - PRESET_DEFROSTER_REAR, - PRESET_DEFROSTER_FRONT_AND_REAR, - ] - assert state.attributes.get(ATTR_MIN_TEMP) == expected_min_temp - assert state.attributes.get(ATTR_MAX_TEMP) == expected_max_temp - assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == round( - hass.config.units.temperature( - current_temperature_celsius, UnitOfTemperature.CELSIUS - ) - ) - assert state.attributes.get(ATTR_TEMPERATURE) == target_temperature - assert state.attributes.get(ATTR_PRESET_MODE) == expected_preset_mode - - -@pytest.mark.parametrize( - ("hvac_mode", "api_method"), - [ - (HVACMode.HEAT_COOL, "turn_on_hvac"), - (HVACMode.OFF, "turn_off_hvac"), - ], -) -async def test_set_hvac_mode(hass: HomeAssistant, hvac_mode, api_method) -> None: - """Test turning on and off the HVAC system.""" - client_mock = await init_integration(hass, electric_vehicle=True) - - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.my_mazda3_climate", ATTR_HVAC_MODE: hvac_mode}, - blocking=True, - ) - await hass.async_block_till_done() - - getattr(client_mock, api_method).assert_called_once_with(12345) - - -async def test_set_target_temperature(hass: HomeAssistant) -> None: - """Test setting the target temperature of the climate entity.""" - client_mock = await init_integration(hass, electric_vehicle=True) - - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.my_mazda3_climate", ATTR_TEMPERATURE: 22}, - blocking=True, - ) - await hass.async_block_till_done() - - client_mock.set_hvac_setting.assert_called_once_with(12345, 22, "C", True, False) - - -@pytest.mark.parametrize( - ("preset_mode", "front_defroster", "rear_defroster"), - [ - (PRESET_DEFROSTER_OFF, False, False), - (PRESET_DEFROSTER_FRONT, True, False), - (PRESET_DEFROSTER_REAR, False, True), - (PRESET_DEFROSTER_FRONT_AND_REAR, True, True), - ], -) -async def test_set_preset_mode( - hass: HomeAssistant, preset_mode, front_defroster, rear_defroster -) -> None: - """Test turning on and off the front and rear defrosters.""" - client_mock = await init_integration(hass, electric_vehicle=True) - - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - { - ATTR_ENTITY_ID: "climate.my_mazda3_climate", - ATTR_PRESET_MODE: preset_mode, - }, - blocking=True, - ) - await hass.async_block_till_done() - - client_mock.set_hvac_setting.assert_called_once_with( - 12345, 20, "C", front_defroster, rear_defroster - ) diff --git a/tests/components/mazda/test_config_flow.py b/tests/components/mazda/test_config_flow.py deleted file mode 100644 index da7f0369079..00000000000 --- a/tests/components/mazda/test_config_flow.py +++ /dev/null @@ -1,423 +0,0 @@ -"""Test the Mazda Connected Services config flow.""" -from unittest.mock import patch - -import aiohttp - -from homeassistant import config_entries, data_entry_flow -from homeassistant.components.mazda.config_flow import ( - MazdaAccountLockedException, - MazdaAuthenticationException, -) -from homeassistant.components.mazda.const import DOMAIN -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - -FIXTURE_USER_INPUT = { - CONF_EMAIL: "example@example.com", - CONF_PASSWORD: "password", - CONF_REGION: "MNAO", -} -FIXTURE_USER_INPUT_REAUTH = { - CONF_EMAIL: "example@example.com", - CONF_PASSWORD: "password_fixed", - CONF_REGION: "MNAO", -} -FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL = { - CONF_EMAIL: "example2@example.com", - CONF_PASSWORD: "password_fixed", - CONF_REGION: "MNAO", -} - - -async def test_form(hass: HomeAssistant) -> None: - """Test the entire flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - - with patch( - "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", - return_value=True, - ), patch( - "homeassistant.components.mazda.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT, - ) - await hass.async_block_till_done() - - assert result2["type"] == "create_entry" - assert result2["title"] == FIXTURE_USER_INPUT[CONF_EMAIL] - assert result2["data"] == FIXTURE_USER_INPUT - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_account_already_exists(hass: HomeAssistant) -> None: - """Test account already exists.""" - mock_config = MockConfigEntry( - domain=DOMAIN, - unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], - data=FIXTURE_USER_INPUT, - ) - mock_config.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - - with patch( - "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", - return_value=True, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT, - ) - await hass.async_block_till_done() - - assert result2["type"] == "abort" - assert result2["reason"] == "already_configured" - - -async def test_form_invalid_auth(hass: HomeAssistant) -> None: - """Test we handle invalid auth.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - - with patch( - "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", - side_effect=MazdaAuthenticationException("Failed to authenticate"), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT, - ) - await hass.async_block_till_done() - - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_form_account_locked(hass: HomeAssistant) -> None: - """Test we handle account locked error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - - with patch( - "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", - side_effect=MazdaAccountLockedException("Account locked"), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT, - ) - await hass.async_block_till_done() - - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "account_locked"} - - -async def test_form_cannot_connect(hass: HomeAssistant) -> None: - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", - side_effect=aiohttp.ClientError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_unknown_error(hass: HomeAssistant) -> None: - """Test we handle unknown error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", - side_effect=Exception, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "unknown"} - - -async def test_reauth_flow(hass: HomeAssistant) -> None: - """Test reauth works.""" - - mock_config = MockConfigEntry( - domain=DOMAIN, - unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], - data=FIXTURE_USER_INPUT, - ) - mock_config.add_to_hass(hass) - - with patch( - "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", - side_effect=MazdaAuthenticationException("Failed to authenticate"), - ), patch( - "homeassistant.components.mazda.async_setup_entry", - return_value=True, - ): - await hass.config_entries.async_setup(mock_config.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "entry_id": mock_config.entry_id, - }, - data=FIXTURE_USER_INPUT, - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - - with patch( - "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", - return_value=True, - ), patch("homeassistant.components.mazda.async_setup_entry", return_value=True): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT_REAUTH, - ) - await hass.async_block_till_done() - - assert result2["type"] == data_entry_flow.FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" - - -async def test_reauth_authorization_error(hass: HomeAssistant) -> None: - """Test we show user form on authorization error.""" - mock_config = MockConfigEntry( - domain=DOMAIN, - unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], - data=FIXTURE_USER_INPUT, - ) - mock_config.add_to_hass(hass) - - with patch( - "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", - side_effect=MazdaAuthenticationException("Failed to authenticate"), - ), patch( - "homeassistant.components.mazda.async_setup_entry", - return_value=True, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "entry_id": mock_config.entry_id, - }, - data=FIXTURE_USER_INPUT, - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT_REAUTH, - ) - await hass.async_block_till_done() - - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_reauth_account_locked(hass: HomeAssistant) -> None: - """Test we show user form on account_locked error.""" - mock_config = MockConfigEntry( - domain=DOMAIN, - unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], - data=FIXTURE_USER_INPUT, - ) - mock_config.add_to_hass(hass) - - with patch( - "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", - side_effect=MazdaAccountLockedException("Account locked"), - ), patch( - "homeassistant.components.mazda.async_setup_entry", - return_value=True, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "entry_id": mock_config.entry_id, - }, - data=FIXTURE_USER_INPUT, - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT_REAUTH, - ) - await hass.async_block_till_done() - - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "account_locked"} - - -async def test_reauth_connection_error(hass: HomeAssistant) -> None: - """Test we show user form on connection error.""" - mock_config = MockConfigEntry( - domain=DOMAIN, - unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], - data=FIXTURE_USER_INPUT, - ) - mock_config.add_to_hass(hass) - - with patch( - "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", - side_effect=aiohttp.ClientError, - ), patch( - "homeassistant.components.mazda.async_setup_entry", - return_value=True, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "entry_id": mock_config.entry_id, - }, - data=FIXTURE_USER_INPUT, - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT_REAUTH, - ) - await hass.async_block_till_done() - - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_reauth_unknown_error(hass: HomeAssistant) -> None: - """Test we show user form on unknown error.""" - mock_config = MockConfigEntry( - domain=DOMAIN, - unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], - data=FIXTURE_USER_INPUT, - ) - mock_config.add_to_hass(hass) - - with patch( - "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", - side_effect=Exception, - ), patch( - "homeassistant.components.mazda.async_setup_entry", - return_value=True, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "entry_id": mock_config.entry_id, - }, - data=FIXTURE_USER_INPUT, - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT_REAUTH, - ) - await hass.async_block_till_done() - - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "unknown"} - - -async def test_reauth_user_has_new_email_address(hass: HomeAssistant) -> None: - """Test reauth with a new email address but same account.""" - mock_config = MockConfigEntry( - domain=DOMAIN, - unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], - data=FIXTURE_USER_INPUT, - ) - mock_config.add_to_hass(hass) - - with patch( - "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", - return_value=True, - ), patch( - "homeassistant.components.mazda.async_setup_entry", - return_value=True, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "entry_id": mock_config.entry_id, - }, - data=FIXTURE_USER_INPUT, - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - - # Change the email and ensure the entry and its unique id gets - # updated in the event the user has changed their email with mazda - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL, - ) - await hass.async_block_till_done() - - assert ( - mock_config.unique_id == FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL[CONF_EMAIL] - ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" diff --git a/tests/components/mazda/test_device_tracker.py b/tests/components/mazda/test_device_tracker.py deleted file mode 100644 index 72168ef3c27..00000000000 --- a/tests/components/mazda/test_device_tracker.py +++ /dev/null @@ -1,30 +0,0 @@ -"""The device tracker tests for the Mazda Connected Services integration.""" -from homeassistant.components.device_tracker import ATTR_SOURCE_TYPE, SourceType -from homeassistant.const import ( - ATTR_FRIENDLY_NAME, - ATTR_ICON, - ATTR_LATITUDE, - ATTR_LONGITUDE, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er - -from . import init_integration - - -async def test_device_tracker(hass: HomeAssistant) -> None: - """Test creation of the device tracker.""" - await init_integration(hass) - - entity_registry = er.async_get(hass) - - state = hass.states.get("device_tracker.my_mazda3_device_tracker") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Device tracker" - assert state.attributes.get(ATTR_ICON) == "mdi:car" - assert state.attributes.get(ATTR_LATITUDE) == 1.234567 - assert state.attributes.get(ATTR_LONGITUDE) == -2.345678 - assert state.attributes.get(ATTR_SOURCE_TYPE) == SourceType.GPS - entry = entity_registry.async_get("device_tracker.my_mazda3_device_tracker") - assert entry - assert entry.unique_id == "JM000000000000000" diff --git a/tests/components/mazda/test_diagnostics.py b/tests/components/mazda/test_diagnostics.py deleted file mode 100644 index 9dccf8f6afd..00000000000 --- a/tests/components/mazda/test_diagnostics.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Test Mazda diagnostics.""" -import json - -import pytest - -from homeassistant.components.mazda.const import DATA_COORDINATOR, DOMAIN -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr - -from . import init_integration - -from tests.common import load_fixture -from tests.components.diagnostics import ( - get_diagnostics_for_config_entry, - get_diagnostics_for_device, -) -from tests.typing import ClientSessionGenerator - - -async def test_config_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator -) -> None: - """Test config entry diagnostics.""" - await init_integration(hass) - assert hass.data[DOMAIN] - - config_entry = hass.config_entries.async_entries(DOMAIN)[0] - - diagnostics_fixture = json.loads( - load_fixture("mazda/diagnostics_config_entry.json") - ) - - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - == diagnostics_fixture - ) - - -async def test_device_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator -) -> None: - """Test device diagnostics.""" - await init_integration(hass) - assert hass.data[DOMAIN] - - config_entry = hass.config_entries.async_entries(DOMAIN)[0] - - device_registry = dr.async_get(hass) - reg_device = device_registry.async_get_device( - identifiers={(DOMAIN, "JM000000000000000")}, - ) - assert reg_device is not None - - diagnostics_fixture = json.loads(load_fixture("mazda/diagnostics_device.json")) - - assert ( - await get_diagnostics_for_device(hass, hass_client, config_entry, reg_device) - == diagnostics_fixture - ) - - -async def test_device_diagnostics_vehicle_not_found( - hass: HomeAssistant, hass_client: ClientSessionGenerator -) -> None: - """Test device diagnostics when the vehicle cannot be found.""" - await init_integration(hass) - assert hass.data[DOMAIN] - - config_entry = hass.config_entries.async_entries(DOMAIN)[0] - - device_registry = dr.async_get(hass) - reg_device = device_registry.async_get_device( - identifiers={(DOMAIN, "JM000000000000000")}, - ) - assert reg_device is not None - - # Remove vehicle info from hass.data so that vehicle will not be found - hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR].data = [] - - with pytest.raises(AssertionError): - await get_diagnostics_for_device(hass, hass_client, config_entry, reg_device) diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index 3556f687989..5d15f01389b 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -1,365 +1,50 @@ """Tests for the Mazda Connected Services integration.""" -from datetime import timedelta -import json -from unittest.mock import patch -from pymazda import MazdaAuthenticationException, MazdaException -import pytest -import voluptuous as vol - -from homeassistant.components.mazda.const import DOMAIN +from homeassistant.components.mazda import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ( - CONF_EMAIL, - CONF_PASSWORD, - CONF_REGION, - STATE_UNAVAILABLE, -) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry as dr -from homeassistant.util import dt as dt_util +from homeassistant.helpers import issue_registry as ir -from . import init_integration - -from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture - -FIXTURE_USER_INPUT = { - CONF_EMAIL: "example@example.com", - CONF_PASSWORD: "password", - CONF_REGION: "MNAO", -} +from tests.common import MockConfigEntry -async def test_config_entry_not_ready(hass: HomeAssistant) -> None: - """Test the Mazda configuration entry not ready.""" - config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.mazda.MazdaAPI.validate_credentials", - side_effect=MazdaException("Unknown error"), - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.SETUP_RETRY - - -async def test_init_auth_failure(hass: HomeAssistant) -> None: - """Test auth failure during setup.""" - with patch( - "homeassistant.components.mazda.MazdaAPI.validate_credentials", - side_effect=MazdaAuthenticationException("Login failed"), - ): - config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) - config_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state is ConfigEntryState.SETUP_ERROR - - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - assert flows[0]["step_id"] == "user" - - -async def test_update_auth_failure(hass: HomeAssistant) -> None: - """Test auth failure during data update.""" - get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json")) - get_vehicle_status_fixture = json.loads( - load_fixture("mazda/get_vehicle_status.json") - ) - - with patch( - "homeassistant.components.mazda.MazdaAPI.validate_credentials", - return_value=True, - ), patch( - "homeassistant.components.mazda.MazdaAPI.get_vehicles", - return_value=get_vehicles_fixture, - ), patch( - "homeassistant.components.mazda.MazdaAPI.get_vehicle_status", - return_value=get_vehicle_status_fixture, - ): - config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) - config_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state is ConfigEntryState.LOADED - - with patch( - "homeassistant.components.mazda.MazdaAPI.get_vehicles", - side_effect=MazdaAuthenticationException("Login failed"), - ): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=181)) - await hass.async_block_till_done() - - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - assert flows[0]["step_id"] == "user" - - -async def test_update_general_failure(hass: HomeAssistant) -> None: - """Test general failure during data update.""" - get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json")) - get_vehicle_status_fixture = json.loads( - load_fixture("mazda/get_vehicle_status.json") - ) - - with patch( - "homeassistant.components.mazda.MazdaAPI.validate_credentials", - return_value=True, - ), patch( - "homeassistant.components.mazda.MazdaAPI.get_vehicles", - return_value=get_vehicles_fixture, - ), patch( - "homeassistant.components.mazda.MazdaAPI.get_vehicle_status", - return_value=get_vehicle_status_fixture, - ): - config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) - config_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state is ConfigEntryState.LOADED - - with patch( - "homeassistant.components.mazda.MazdaAPI.get_vehicles", - side_effect=Exception("Unknown exception"), - ): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=181)) - await hass.async_block_till_done() - - entity = hass.states.get("sensor.my_mazda3_fuel_remaining_percentage") - assert entity is not None - assert entity.state == STATE_UNAVAILABLE - - -async def test_unload_config_entry(hass: HomeAssistant) -> None: - """Test the Mazda configuration entry unloading.""" - await init_integration(hass) - assert hass.data[DOMAIN] - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state is ConfigEntryState.LOADED - - await hass.config_entries.async_unload(entries[0].entry_id) - await hass.async_block_till_done() - assert entries[0].state is ConfigEntryState.NOT_LOADED - - -async def test_init_electric_vehicle(hass: HomeAssistant) -> None: - """Test initialization of the integration with an electric vehicle.""" - client_mock = await init_integration(hass, electric_vehicle=True) - - client_mock.get_vehicles.assert_called_once() - client_mock.get_vehicle_status.assert_called_once() - client_mock.get_ev_vehicle_status.assert_called_once() - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state is ConfigEntryState.LOADED - - -async def test_device_nickname(hass: HomeAssistant) -> None: - """Test creation of the device when vehicle has a nickname.""" - await init_integration(hass, use_nickname=True) - - device_registry = dr.async_get(hass) - reg_device = device_registry.async_get_device( - identifiers={(DOMAIN, "JM000000000000000")}, - ) - - assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD" - assert reg_device.manufacturer == "Mazda" - assert reg_device.name == "My Mazda3" - - -async def test_device_no_nickname(hass: HomeAssistant) -> None: - """Test creation of the device when vehicle has no nickname.""" - await init_integration(hass, use_nickname=False) - - device_registry = dr.async_get(hass) - reg_device = device_registry.async_get_device( - identifiers={(DOMAIN, "JM000000000000000")}, - ) - - assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD" - assert reg_device.manufacturer == "Mazda" - assert reg_device.name == "2021 MAZDA3 2.5 S SE AWD" - - -@pytest.mark.parametrize( - ("service", "service_data", "expected_args"), - [ - ( - "send_poi", - {"latitude": 1.2345, "longitude": 2.3456, "poi_name": "Work"}, - [12345, 1.2345, 2.3456, "Work"], - ), - ], -) -async def test_services( - hass: HomeAssistant, service, service_data, expected_args +async def test_mazda_repair_issue( + hass: HomeAssistant, issue_registry: ir.IssueRegistry ) -> None: - """Test service calls.""" - client_mock = await init_integration(hass) - - device_registry = dr.async_get(hass) - reg_device = device_registry.async_get_device( - identifiers={(DOMAIN, "JM000000000000000")}, + """Test the Mazda configuration entry loading/unloading handles the repair.""" + config_entry_1 = MockConfigEntry( + title="Example 1", + domain=DOMAIN, ) - device_id = reg_device.id + config_entry_1.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_1.entry_id) + await hass.async_block_till_done() + assert config_entry_1.state is ConfigEntryState.LOADED - service_data["device_id"] = device_id - - await hass.services.async_call(DOMAIN, service, service_data, blocking=True) + # Add a second one + config_entry_2 = MockConfigEntry( + title="Example 2", + domain=DOMAIN, + ) + config_entry_2.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_2.entry_id) await hass.async_block_till_done() - api_method = getattr(client_mock, service) - api_method.assert_called_once_with(*expected_args) + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) + # Remove the first one + await hass.config_entries.async_remove(config_entry_1.entry_id) + await hass.async_block_till_done() -async def test_service_invalid_device_id(hass: HomeAssistant) -> None: - """Test service call when the specified device ID is invalid.""" - await init_integration(hass) + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) - with pytest.raises(vol.error.MultipleInvalid) as err: - await hass.services.async_call( - DOMAIN, - "send_poi", - { - "device_id": "invalid", - "latitude": 1.2345, - "longitude": 6.7890, - "poi_name": "poi_name", - }, - blocking=True, - ) - await hass.async_block_till_done() + # Remove the second one + await hass.config_entries.async_remove(config_entry_2.entry_id) + await hass.async_block_till_done() - assert "Invalid device ID" in str(err.value) - - -async def test_service_device_id_not_mazda_vehicle(hass: HomeAssistant) -> None: - """Test service call when the specified device ID is not the device ID of a Mazda vehicle.""" - await init_integration(hass) - - device_registry = dr.async_get(hass) - # Create another device and pass its device ID. - # Service should fail because device is from wrong domain. - other_config_entry = MockConfigEntry() - other_config_entry.add_to_hass(hass) - other_device = device_registry.async_get_or_create( - config_entry_id=other_config_entry.entry_id, - identifiers={("OTHER_INTEGRATION", "ID_FROM_OTHER_INTEGRATION")}, - ) - - with pytest.raises(vol.error.MultipleInvalid) as err: - await hass.services.async_call( - DOMAIN, - "send_poi", - { - "device_id": other_device.id, - "latitude": 1.2345, - "longitude": 6.7890, - "poi_name": "poi_name", - }, - blocking=True, - ) - await hass.async_block_till_done() - - assert "Device ID is not a Mazda vehicle" in str(err.value) - - -async def test_service_vehicle_id_not_found(hass: HomeAssistant) -> None: - """Test service call when the vehicle ID is not found.""" - await init_integration(hass) - - device_registry = dr.async_get(hass) - reg_device = device_registry.async_get_device( - identifiers={(DOMAIN, "JM000000000000000")}, - ) - device_id = reg_device.id - - entries = hass.config_entries.async_entries(DOMAIN) - entry_id = entries[0].entry_id - - # Remove vehicle info from hass.data so that vehicle ID will not be found - hass.data[DOMAIN][entry_id]["vehicles"] = [] - - with pytest.raises(HomeAssistantError) as err: - await hass.services.async_call( - DOMAIN, - "send_poi", - { - "device_id": device_id, - "latitude": 1.2345, - "longitude": 6.7890, - "poi_name": "poi_name", - }, - blocking=True, - ) - await hass.async_block_till_done() - - assert str(err.value) == "Vehicle ID not found" - - -async def test_service_mazda_api_error(hass: HomeAssistant) -> None: - """Test the Mazda API raising an error when a service is called.""" - get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json")) - get_vehicle_status_fixture = json.loads( - load_fixture("mazda/get_vehicle_status.json") - ) - - with patch( - "homeassistant.components.mazda.MazdaAPI.validate_credentials", - return_value=True, - ), patch( - "homeassistant.components.mazda.MazdaAPI.get_vehicles", - return_value=get_vehicles_fixture, - ), patch( - "homeassistant.components.mazda.MazdaAPI.get_vehicle_status", - return_value=get_vehicle_status_fixture, - ): - config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) - config_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - device_registry = dr.async_get(hass) - reg_device = device_registry.async_get_device( - identifiers={(DOMAIN, "JM000000000000000")}, - ) - device_id = reg_device.id - - with patch( - "homeassistant.components.mazda.MazdaAPI.send_poi", - side_effect=MazdaException("Test error"), - ), pytest.raises(HomeAssistantError) as err: - await hass.services.async_call( - DOMAIN, - "send_poi", - { - "device_id": device_id, - "latitude": 1.2345, - "longitude": 6.7890, - "poi_name": "poi_name", - }, - blocking=True, - ) - await hass.async_block_till_done() - - assert str(err.value) == "Test error" + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.NOT_LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None diff --git a/tests/components/mazda/test_lock.py b/tests/components/mazda/test_lock.py deleted file mode 100644 index 0de4573c5f8..00000000000 --- a/tests/components/mazda/test_lock.py +++ /dev/null @@ -1,58 +0,0 @@ -"""The lock tests for the Mazda Connected Services integration.""" -from homeassistant.components.lock import ( - DOMAIN as LOCK_DOMAIN, - SERVICE_LOCK, - SERVICE_UNLOCK, - STATE_LOCKED, -) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er - -from . import init_integration - - -async def test_lock_setup(hass: HomeAssistant) -> None: - """Test locking and unlocking the vehicle.""" - await init_integration(hass) - - entity_registry = er.async_get(hass) - entry = entity_registry.async_get("lock.my_mazda3_lock") - assert entry - assert entry.unique_id == "JM000000000000000" - - state = hass.states.get("lock.my_mazda3_lock") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Lock" - - assert state.state == STATE_LOCKED - - -async def test_locking(hass: HomeAssistant) -> None: - """Test locking the vehicle.""" - client_mock = await init_integration(hass) - - await hass.services.async_call( - LOCK_DOMAIN, - SERVICE_LOCK, - {ATTR_ENTITY_ID: "lock.my_mazda3_lock"}, - blocking=True, - ) - await hass.async_block_till_done() - - client_mock.lock_doors.assert_called_once() - - -async def test_unlocking(hass: HomeAssistant) -> None: - """Test unlocking the vehicle.""" - client_mock = await init_integration(hass) - - await hass.services.async_call( - LOCK_DOMAIN, - SERVICE_UNLOCK, - {ATTR_ENTITY_ID: "lock.my_mazda3_lock"}, - blocking=True, - ) - await hass.async_block_till_done() - - client_mock.unlock_doors.assert_called_once() diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py deleted file mode 100644 index 0fb92c34baf..00000000000 --- a/tests/components/mazda/test_sensor.py +++ /dev/null @@ -1,195 +0,0 @@ -"""The sensor tests for the Mazda Connected Services integration.""" -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - SensorDeviceClass, - SensorStateClass, -) -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_FRIENDLY_NAME, - ATTR_ICON, - ATTR_UNIT_OF_MEASUREMENT, - PERCENTAGE, - UnitOfLength, - UnitOfPressure, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM - -from . import init_integration - - -async def test_sensors(hass: HomeAssistant) -> None: - """Test creation of the sensors.""" - await init_integration(hass) - - entity_registry = er.async_get(hass) - - # Fuel Remaining Percentage - state = hass.states.get("sensor.my_mazda3_fuel_remaining_percentage") - assert state - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) - == "My Mazda3 Fuel remaining percentage" - ) - assert state.attributes.get(ATTR_ICON) == "mdi:gas-station" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.state == "87.0" - entry = entity_registry.async_get("sensor.my_mazda3_fuel_remaining_percentage") - assert entry - assert entry.unique_id == "JM000000000000000_fuel_remaining_percentage" - - # Fuel Distance Remaining - state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining") - assert state - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Fuel distance remaining" - ) - assert state.attributes.get(ATTR_ICON) == "mdi:gas-station" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.KILOMETERS - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.state == "381" - entry = entity_registry.async_get("sensor.my_mazda3_fuel_distance_remaining") - assert entry - assert entry.unique_id == "JM000000000000000_fuel_distance_remaining" - - # Odometer - state = hass.states.get("sensor.my_mazda3_odometer") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Odometer" - assert state.attributes.get(ATTR_ICON) == "mdi:speedometer" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.KILOMETERS - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING - assert state.state == "2795" - entry = entity_registry.async_get("sensor.my_mazda3_odometer") - assert entry - assert entry.unique_id == "JM000000000000000_odometer" - - # Front Left Tire Pressure - state = hass.states.get("sensor.my_mazda3_front_left_tire_pressure") - assert state - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Front left tire pressure" - ) - assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.KPA - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.state == "241" - entry = entity_registry.async_get("sensor.my_mazda3_front_left_tire_pressure") - assert entry - assert entry.unique_id == "JM000000000000000_front_left_tire_pressure" - - # Front Right Tire Pressure - state = hass.states.get("sensor.my_mazda3_front_right_tire_pressure") - assert state - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) - == "My Mazda3 Front right tire pressure" - ) - assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.KPA - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.state == "241" - entry = entity_registry.async_get("sensor.my_mazda3_front_right_tire_pressure") - assert entry - assert entry.unique_id == "JM000000000000000_front_right_tire_pressure" - - # Rear Left Tire Pressure - state = hass.states.get("sensor.my_mazda3_rear_left_tire_pressure") - assert state - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear left tire pressure" - ) - assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.KPA - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.state == "228" - entry = entity_registry.async_get("sensor.my_mazda3_rear_left_tire_pressure") - assert entry - assert entry.unique_id == "JM000000000000000_rear_left_tire_pressure" - - # Rear Right Tire Pressure - state = hass.states.get("sensor.my_mazda3_rear_right_tire_pressure") - assert state - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear right tire pressure" - ) - assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.KPA - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.state == "228" - entry = entity_registry.async_get("sensor.my_mazda3_rear_right_tire_pressure") - assert entry - assert entry.unique_id == "JM000000000000000_rear_right_tire_pressure" - - -async def test_sensors_us_customary_units(hass: HomeAssistant) -> None: - """Test that the sensors work properly with US customary units.""" - hass.config.units = US_CUSTOMARY_SYSTEM - - await init_integration(hass) - - # In the US, miles are used for vehicle odometers. - # These tests verify that the unit conversion logic for the distance - # sensor device class automatically converts the unit to miles. - - # Fuel Distance Remaining - state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining") - assert state - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.MILES - assert state.state == "237" - - # Odometer - state = hass.states.get("sensor.my_mazda3_odometer") - assert state - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.MILES - assert state.state == "1737" - - -async def test_electric_vehicle_sensors(hass: HomeAssistant) -> None: - """Test sensors which are specific to electric vehicles.""" - - await init_integration(hass, electric_vehicle=True) - - entity_registry = er.async_get(hass) - - # Fuel Remaining Percentage should not exist for an electric vehicle - entry = entity_registry.async_get("sensor.my_mazda3_fuel_remaining_percentage") - assert entry is None - - # Fuel Distance Remaining should not exist for an electric vehicle - entry = entity_registry.async_get("sensor.my_mazda3_fuel_distance_remaining") - assert entry is None - - # Charge Level - state = hass.states.get("sensor.my_mazda3_charge_level") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Charge level" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.BATTERY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.state == "80" - entry = entity_registry.async_get("sensor.my_mazda3_charge_level") - assert entry - assert entry.unique_id == "JM000000000000000_ev_charge_level" - - # Remaining Range - state = hass.states.get("sensor.my_mazda3_remaining_range") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Remaining range" - assert state.attributes.get(ATTR_ICON) == "mdi:ev-station" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.KILOMETERS - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.state == "218" - entry = entity_registry.async_get("sensor.my_mazda3_remaining_range") - assert entry - assert entry.unique_id == "JM000000000000000_ev_remaining_range" diff --git a/tests/components/mazda/test_switch.py b/tests/components/mazda/test_switch.py deleted file mode 100644 index a2d8ca649f3..00000000000 --- a/tests/components/mazda/test_switch.py +++ /dev/null @@ -1,69 +0,0 @@ -"""The switch tests for the Mazda Connected Services integration.""" -from homeassistant.components.switch import ( - DOMAIN as SWITCH_DOMAIN, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - STATE_ON, -) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er - -from . import init_integration - - -async def test_switch_setup(hass: HomeAssistant) -> None: - """Test setup of the switch entity.""" - await init_integration(hass, electric_vehicle=True) - - entity_registry = er.async_get(hass) - entry = entity_registry.async_get("switch.my_mazda3_charging") - assert entry - assert entry.unique_id == "JM000000000000000" - - state = hass.states.get("switch.my_mazda3_charging") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Charging" - assert state.attributes.get(ATTR_ICON) == "mdi:ev-station" - - assert state.state == STATE_ON - - -async def test_start_charging(hass: HomeAssistant) -> None: - """Test turning on the charging switch.""" - client_mock = await init_integration(hass, electric_vehicle=True) - - client_mock.reset_mock() - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.my_mazda3_charging"}, - blocking=True, - ) - await hass.async_block_till_done() - - client_mock.start_charging.assert_called_once() - client_mock.refresh_vehicle_status.assert_called_once() - client_mock.get_vehicle_status.assert_called_once() - client_mock.get_ev_vehicle_status.assert_called_once() - - -async def test_stop_charging(hass: HomeAssistant) -> None: - """Test turning off the charging switch.""" - client_mock = await init_integration(hass, electric_vehicle=True) - - client_mock.reset_mock() - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.my_mazda3_charging"}, - blocking=True, - ) - await hass.async_block_till_done() - - client_mock.stop_charging.assert_called_once() - client_mock.refresh_vehicle_status.assert_called_once() - client_mock.get_vehicle_status.assert_called_once() - client_mock.get_ev_vehicle_status.assert_called_once() From 34693d4a9b9fc20859dbf354df83a02f202f31a5 Mon Sep 17 00:00:00 2001 From: Justin Lindh Date: Thu, 12 Oct 2023 01:42:40 -0700 Subject: [PATCH 31/37] Bump Python-MyQ to v3.1.13 (#101852) --- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 5efcb8e1bb0..e924d06955b 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -14,5 +14,5 @@ }, "iot_class": "cloud_polling", "loggers": ["pkce", "pymyq"], - "requirements": ["python-myq==3.1.11"] + "requirements": ["python-myq==3.1.13"] } diff --git a/requirements_all.txt b/requirements_all.txt index e720d641ccf..b178c78b815 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2146,7 +2146,7 @@ python-miio==0.5.12 python-mpd2==3.0.5 # homeassistant.components.myq -python-myq==3.1.11 +python-myq==3.1.13 # homeassistant.components.mystrom python-mystrom==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d5c324d1964..c67fa8ef757 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1596,7 +1596,7 @@ python-matter-server==3.7.0 python-miio==0.5.12 # homeassistant.components.myq -python-myq==3.1.11 +python-myq==3.1.13 # homeassistant.components.mystrom python-mystrom==2.2.0 From c2cf4973021e4a00b5d2f964eac3d2f4cb3561d3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 12 Oct 2023 12:51:40 +0200 Subject: [PATCH 32/37] Fix translation key in Plugwise (#101862) Co-authored-by: Robert Resch --- homeassistant/components/plugwise/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index f85c83819fa..82228ee94e7 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -259,7 +259,7 @@ "name": "DHW comfort mode" }, "lock": { - "name": "[%key:component::lock::entity_component::_::name%]" + "name": "[%key:component::lock::title%]" }, "relay": { "name": "Relay" From 04dc44c06905b78bc9908686fb204a194c7bc8a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Thu, 12 Oct 2023 13:03:09 +0200 Subject: [PATCH 33/37] Fix SMA incorrect device class (#101866) --- homeassistant/components/sma/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index f0fc475e0db..abf5c9a878f 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -274,8 +274,6 @@ SENSOR_ENTITIES: dict[str, SensorEntityDescription] = { "grid_power_factor_excitation": SensorEntityDescription( key="grid_power_factor_excitation", name="Grid Power Factor Excitation", - state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.POWER_FACTOR, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), From 7670b5d3b0f716cd7f54864d848936e86ad53cdd Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 12 Oct 2023 13:01:08 +0200 Subject: [PATCH 34/37] Fix mysensors battery level attribute (#101868) --- homeassistant/components/mysensors/device.py | 4 +++- tests/components/mysensors/test_binary_sensor.py | 3 ++- tests/components/mysensors/test_climate.py | 5 ++++- tests/components/mysensors/test_cover.py | 3 ++- tests/components/mysensors/test_device_tracker.py | 8 +++++++- tests/components/mysensors/test_light.py | 4 ++++ tests/components/mysensors/test_remote.py | 8 +++++++- tests/components/mysensors/test_sensor.py | 10 ++++++++++ tests/components/mysensors/test_switch.py | 2 ++ tests/components/mysensors/test_text.py | 3 ++- 10 files changed, 43 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index 9e1d91c7cce..6d7decf14f4 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -8,7 +8,7 @@ from typing import Any from mysensors import BaseAsyncGateway, Sensor from mysensors.sensor import ChildSensor -from homeassistant.const import STATE_OFF, STATE_ON, Platform +from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.device_registry import DeviceInfo @@ -212,6 +212,8 @@ class MySensorsChildEntity(MySensorNodeEntity): attr[ATTR_CHILD_ID] = self.child_id attr[ATTR_DESCRIPTION] = self._child.description + # We should deprecate the battery level attribute in the future. + attr[ATTR_BATTERY_LEVEL] = self._node.battery_level set_req = self.gateway.const.SetReq for value_type, value in self._values.items(): diff --git a/tests/components/mysensors/test_binary_sensor.py b/tests/components/mysensors/test_binary_sensor.py index 886c13e6ff5..a6dce9c78b9 100644 --- a/tests/components/mysensors/test_binary_sensor.py +++ b/tests/components/mysensors/test_binary_sensor.py @@ -6,7 +6,7 @@ from collections.abc import Callable from mysensors.sensor import Sensor from homeassistant.components.binary_sensor import BinarySensorDeviceClass -from homeassistant.const import ATTR_DEVICE_CLASS +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_DEVICE_CLASS from homeassistant.core import HomeAssistant @@ -23,6 +23,7 @@ async def test_door_sensor( assert state assert state.state == "off" assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 receive_message("1;1;1;0;16;1\n") await hass.async_block_till_done() diff --git a/tests/components/mysensors/test_climate.py b/tests/components/mysensors/test_climate.py index 730960f118d..6c386af6fd6 100644 --- a/tests/components/mysensors/test_climate.py +++ b/tests/components/mysensors/test_climate.py @@ -19,7 +19,7 @@ from homeassistant.components.climate import ( SERVICE_SET_TEMPERATURE, HVACMode, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant @@ -36,6 +36,7 @@ async def test_hvac_node_auto( assert state assert state.state == HVACMode.OFF + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 # Test set hvac mode auto await hass.services.async_call( @@ -150,6 +151,7 @@ async def test_hvac_node_heat( assert state assert state.state == HVACMode.OFF + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 # Test set hvac mode heat await hass.services.async_call( @@ -259,6 +261,7 @@ async def test_hvac_node_cool( assert state assert state.state == HVACMode.OFF + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 # Test set hvac mode heat await hass.services.async_call( diff --git a/tests/components/mysensors/test_cover.py b/tests/components/mysensors/test_cover.py index 494800f388f..7d0a098fc0a 100644 --- a/tests/components/mysensors/test_cover.py +++ b/tests/components/mysensors/test_cover.py @@ -19,7 +19,7 @@ from homeassistant.components.cover import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant @@ -37,6 +37,7 @@ async def test_cover_node_percentage( assert state assert state.state == STATE_CLOSED assert state.attributes[ATTR_CURRENT_POSITION] == 0 + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 await hass.services.async_call( COVER_DOMAIN, diff --git a/tests/components/mysensors/test_device_tracker.py b/tests/components/mysensors/test_device_tracker.py index 63c9ed7b1da..4d6e638e665 100644 --- a/tests/components/mysensors/test_device_tracker.py +++ b/tests/components/mysensors/test_device_tracker.py @@ -6,7 +6,12 @@ from collections.abc import Callable from mysensors.sensor import Sensor from homeassistant.components.device_tracker import ATTR_SOURCE_TYPE, SourceType -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, STATE_NOT_HOME +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + ATTR_LATITUDE, + ATTR_LONGITUDE, + STATE_NOT_HOME, +) from homeassistant.core import HomeAssistant @@ -32,6 +37,7 @@ async def test_gps_sensor( assert state.attributes[ATTR_SOURCE_TYPE] == SourceType.GPS assert state.attributes[ATTR_LATITUDE] == float(latitude) assert state.attributes[ATTR_LONGITUDE] == float(longitude) + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 latitude = "40.782" longitude = "-73.965" diff --git a/tests/components/mysensors/test_light.py b/tests/components/mysensors/test_light.py index 8d4ce445779..9696c6e622a 100644 --- a/tests/components/mysensors/test_light.py +++ b/tests/components/mysensors/test_light.py @@ -12,6 +12,7 @@ from homeassistant.components.light import ( ATTR_RGBW_COLOR, DOMAIN as LIGHT_DOMAIN, ) +from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import HomeAssistant @@ -28,6 +29,7 @@ async def test_dimmer_node( assert state assert state.state == "off" + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 # Test turn on await hass.services.async_call( @@ -108,6 +110,7 @@ async def test_rgb_node( assert state assert state.state == "off" + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 # Test turn on await hass.services.async_call( @@ -218,6 +221,7 @@ async def test_rgbw_node( assert state assert state.state == "off" + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 # Test turn on await hass.services.async_call( diff --git a/tests/components/mysensors/test_remote.py b/tests/components/mysensors/test_remote.py index adc8590914c..586e2e2d048 100644 --- a/tests/components/mysensors/test_remote.py +++ b/tests/components/mysensors/test_remote.py @@ -14,7 +14,12 @@ from homeassistant.components.remote import ( SERVICE_LEARN_COMMAND, SERVICE_SEND_COMMAND, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) from homeassistant.core import HomeAssistant @@ -31,6 +36,7 @@ async def test_ir_transceiver( assert state assert state.state == "off" + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 # Test turn on await hass.services.async_call( diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index 17301e4b212..d80fddea9e3 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, @@ -41,6 +42,7 @@ async def test_gps_sensor( assert state assert state.state == "40.741894,-73.989311,12" + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 altitude = 0 new_coords = "40.782,-73.965" @@ -67,6 +69,7 @@ async def test_ir_transceiver( assert state assert state.state == "test_code" + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 receive_message("1;1;1;0;50;new_code\n") await hass.async_block_till_done() @@ -87,6 +90,7 @@ async def test_battery_entity( state = hass.states.get(battery_entity_id) assert state assert state.state == "42" + assert ATTR_BATTERY_LEVEL not in state.attributes receive_message("1;255;3;0;0;84\n") await hass.async_block_till_done() @@ -94,6 +98,7 @@ async def test_battery_entity( state = hass.states.get(battery_entity_id) assert state assert state.state == "84" + assert ATTR_BATTERY_LEVEL not in state.attributes async def test_power_sensor( @@ -111,6 +116,7 @@ async def test_power_sensor( assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfPower.WATT assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 async def test_energy_sensor( @@ -128,6 +134,7 @@ async def test_energy_sensor( assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 async def test_sound_sensor( @@ -144,6 +151,7 @@ async def test_sound_sensor( assert state.state == "10" assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.SOUND_PRESSURE assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "dB" + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 async def test_distance_sensor( @@ -161,6 +169,7 @@ async def test_distance_sensor( assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.DISTANCE assert ATTR_ICON not in state.attributes assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "cm" + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 @pytest.mark.parametrize( @@ -193,3 +202,4 @@ async def test_temperature_sensor( assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == unit assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 diff --git a/tests/components/mysensors/test_switch.py b/tests/components/mysensors/test_switch.py index 59cea514d77..49786768ff7 100644 --- a/tests/components/mysensors/test_switch.py +++ b/tests/components/mysensors/test_switch.py @@ -7,6 +7,7 @@ from unittest.mock import MagicMock, call from mysensors.sensor import Sensor from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import HomeAssistant @@ -23,6 +24,7 @@ async def test_relay_node( assert state assert state.state == "off" + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 await hass.services.async_call( SWITCH_DOMAIN, diff --git a/tests/components/mysensors/test_text.py b/tests/components/mysensors/test_text.py index 7ed46532c8a..7490cfddfbf 100644 --- a/tests/components/mysensors/test_text.py +++ b/tests/components/mysensors/test_text.py @@ -12,7 +12,7 @@ from homeassistant.components.text import ( DOMAIN as TEXT_DOMAIN, SERVICE_SET_VALUE, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant @@ -29,6 +29,7 @@ async def test_text_node( assert state assert state.state == "test" + assert state.attributes[ATTR_BATTERY_LEVEL] == 0 await hass.services.async_call( TEXT_DOMAIN, From ca1d6ddbb6c04b459ac735a38cbe3838fe0a37bd Mon Sep 17 00:00:00 2001 From: Betacart Date: Thu, 12 Oct 2023 13:18:43 +0200 Subject: [PATCH 35/37] Fix typo in remember the milk strings (#101869) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/remember_the_milk/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/remember_the_milk/strings.json b/homeassistant/components/remember_the_milk/strings.json index 5590691e245..da499b0584c 100644 --- a/homeassistant/components/remember_the_milk/strings.json +++ b/homeassistant/components/remember_the_milk/strings.json @@ -16,7 +16,7 @@ }, "complete_task": { "name": "Complete task", - "description": "Completes a tasks that was privously created.", + "description": "Completes a task that was previously created.", "fields": { "id": { "name": "ID", From b0dabfa3f7adbe4c0200a0bd8839b70a91ff9231 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 12 Oct 2023 12:56:10 +0200 Subject: [PATCH 36/37] Only reload Withings config entry on reauth (#101638) * Only reload on reauth * Reload if entry is loaded * Make async_cloudhook_generate_url protected * Fix feedback --- homeassistant/components/withings/__init__.py | 20 ++++--------------- .../components/withings/config_flow.py | 1 + homeassistant/components/withings/const.py | 1 - 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 16606a40645..225ff5603c4 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -43,14 +43,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss from homeassistant.helpers.typing import ConfigType from .api import ConfigEntryWithingsApi -from .const import ( - CONF_CLOUDHOOK_URL, - CONF_PROFILES, - CONF_USE_WEBHOOK, - DEFAULT_TITLE, - DOMAIN, - LOGGER, -) +from .const import CONF_PROFILES, CONF_USE_WEBHOOK, DEFAULT_TITLE, DOMAIN, LOGGER from .coordinator import WithingsDataUpdateCoordinator PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -82,6 +75,7 @@ CONFIG_SCHEMA = vol.Schema( ) SUBSCRIBE_DELAY = timedelta(seconds=5) UNSUBSCRIBE_DELAY = timedelta(seconds=1) +CONF_CLOUDHOOK_URL = "cloudhook_url" async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -152,7 +146,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _: Any, ) -> None: if cloud.async_active_subscription(hass): - webhook_url = await async_cloudhook_generate_url(hass, entry) + webhook_url = await _async_cloudhook_generate_url(hass, entry) else: webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID]) @@ -200,7 +194,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(async_call_later(hass, 1, register_webhook)) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(update_listener)) return True @@ -214,11 +207,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle options update.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_subscribe_webhooks( client: ConfigEntryWithingsApi, webhook_url: str ) -> None: @@ -266,7 +254,7 @@ async def async_unsubscribe_webhooks(client: ConfigEntryWithingsApi) -> None: ) -async def async_cloudhook_generate_url(hass: HomeAssistant, entry: ConfigEntry) -> str: +async def _async_cloudhook_generate_url(hass: HomeAssistant, entry: ConfigEntry) -> str: """Generate the full URL for a webhook_id.""" if CONF_CLOUDHOOK_URL not in entry.data: webhook_id = entry.data[CONF_WEBHOOK_ID] diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index 35a4582ae4d..8cab297b96a 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -76,6 +76,7 @@ class WithingsFlowHandler( self.hass.config_entries.async_update_entry( self.reauth_entry, data={**self.reauth_entry.data, **data} ) + await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) return self.async_abort(reason="reauth_successful") return self.async_abort(reason="wrong_account") diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py index 6129e0c4b29..545c7bfcb26 100644 --- a/homeassistant/components/withings/const.py +++ b/homeassistant/components/withings/const.py @@ -5,7 +5,6 @@ import logging DEFAULT_TITLE = "Withings" CONF_PROFILES = "profiles" CONF_USE_WEBHOOK = "use_webhook" -CONF_CLOUDHOOK_URL = "cloudhook_url" DATA_MANAGER = "data_manager" From 014546c75e6bcf9adc6b013a4ab26884e70d3105 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 Oct 2023 13:34:26 +0200 Subject: [PATCH 37/37] Bumped version to 2023.10.2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c2accfa83b5..0e3595d6e0e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 10 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 04be5ae89fb..e254462f866 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.1" +version = "2023.10.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"