From c11dd58c1d32f5767d0e124d2f5ac81d5194d532 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Thu, 12 Oct 2023 11:52:01 +0100 Subject: [PATCH 01/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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/81] 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" From a36115993cea1bfd086766cef01eab34dd09b9ce Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 Oct 2023 10:20:12 +0200 Subject: [PATCH 38/81] Downgrade aiohttp to 3.8.5 (#101913) --- 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 12fbc6f9f9a..005a6735e03 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,5 +1,5 @@ aiodiscover==1.5.1 -aiohttp==3.8.6 +aiohttp==3.8.5 aiohttp_cors==0.7.0 astral==2.2 async-upnp-client==0.36.1 diff --git a/pyproject.toml b/pyproject.toml index e254462f866..64a6ceb78d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ ] requires-python = ">=3.11.0" dependencies = [ - "aiohttp==3.8.6", + "aiohttp==3.8.5", "astral==2.2", "attrs==23.1.0", "atomicwrites-homeassistant==1.4.1", diff --git a/requirements.txt b/requirements.txt index a7ede68c9ec..60eb2359ba5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiohttp==3.8.6 +aiohttp==3.8.5 astral==2.2 attrs==23.1.0 atomicwrites-homeassistant==1.4.1 From 497da016af72e363cbd35505dff8b544e822ca7a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 12 Oct 2023 18:05:51 +0200 Subject: [PATCH 39/81] Add missing fan mode in Sensibo (#101883) * Add missing fan mode in Sensibo * translations --- homeassistant/components/sensibo/climate.py | 1 + homeassistant/components/sensibo/strings.json | 3 +++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index f8ecd1b9b80..40aa54e5d56 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -57,6 +57,7 @@ BOOST_INCLUSIVE = "boost_inclusive" AVAILABLE_FAN_MODES = { "quiet", "low", + "medium_low", "medium", "medium_high", "high", diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index ddd164225fc..9af6139b789 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -125,6 +125,7 @@ "auto": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::auto%]", "high": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::high%]", "low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]", + "medium_low": "Medium low", "medium": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]", "medium_high": "Medium high", "strong": "Strong", @@ -210,6 +211,7 @@ "auto": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::auto%]", "high": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::high%]", "low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]", + "medium_low": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::fanlevel::state::medium_low%]", "medium": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]", "medium_high": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::fanlevel::state::medium_high%]", "strong": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::fanlevel::state::strong%]", @@ -351,6 +353,7 @@ "quiet": "Quiet", "strong": "Strong", "low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]", + "medium_low": "Medium low", "medium": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]", "medium_high": "Medium high", "high": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::high%]", From e3122ec6dc61c2e854d917f22208e67a983d99ba Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 13 Oct 2023 10:23:32 +0200 Subject: [PATCH 40/81] Uncancel task when swallowing CancelledError (#101884) --- homeassistant/components/reolink/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 5cfb2ceecb7..fd62f8451fb 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -95,6 +95,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: return await host.api.check_new_firmware() except (ReolinkError, asyncio.exceptions.CancelledError) as err: + task = asyncio.current_task() + if task is not None: + task.uncancel() if starting: _LOGGER.debug( "Error checking Reolink firmware update at startup " From e17a25ca4ad997fd584319bd3df2d9bbddbf9600 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 12 Oct 2023 19:50:29 +0200 Subject: [PATCH 41/81] Bump reolink-aio to 0.7.11 (#101886) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 221a6b8b59d..9d9d8d59e88 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.7.10"] + "requirements": ["reolink-aio==0.7.11"] } diff --git a/requirements_all.txt b/requirements_all.txt index b178c78b815..d396581a2c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2309,7 +2309,7 @@ renault-api==0.2.0 renson-endura-delta==1.6.0 # homeassistant.components.reolink -reolink-aio==0.7.10 +reolink-aio==0.7.11 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c67fa8ef757..5ba3c3f85f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1720,7 +1720,7 @@ renault-api==0.2.0 renson-endura-delta==1.6.0 # homeassistant.components.reolink -reolink-aio==0.7.10 +reolink-aio==0.7.11 # homeassistant.components.rflink rflink==0.0.65 From 2fac26c6c616394fe193be6e969244ed51cc9ae3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Oct 2023 23:05:25 -1000 Subject: [PATCH 42/81] Fix implicit device name in wiz switch (#101914) --- homeassistant/components/wiz/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/wiz/switch.py b/homeassistant/components/wiz/switch.py index ffe75910b40..1bebaba7579 100644 --- a/homeassistant/components/wiz/switch.py +++ b/homeassistant/components/wiz/switch.py @@ -30,6 +30,8 @@ async def async_setup_entry( class WizSocketEntity(WizToggleEntity, SwitchEntity): """Representation of a WiZ socket.""" + _attr_name = None + def __init__(self, wiz_data: WizData, name: str) -> None: """Initialize a WiZ socket.""" super().__init__(wiz_data, name) From 8b8df2ec3b7a5d5a48ea3681c1f5ff60eb962254 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 Oct 2023 11:17:45 +0200 Subject: [PATCH 43/81] Bumped version to 2023.10.3 --- 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 0e3595d6e0e..82eae8434bd 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 = "2" +PATCH_VERSION: Final = "3" __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 64a6ceb78d8..41152c944dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.2" +version = "2023.10.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e9e677d933f611851e9881361143b20414a27514 Mon Sep 17 00:00:00 2001 From: Archomeda Date: Fri, 20 Oct 2023 14:00:31 +0200 Subject: [PATCH 44/81] Fix Spotify media position update value (#100044) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/spotify/media_player.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index d05e4282edf..6ef2697ba77 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -2,7 +2,6 @@ from __future__ import annotations from asyncio import run_coroutine_threadsafe -import datetime as dt from datetime import timedelta import logging from typing import Any @@ -27,7 +26,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.dt import utc_from_timestamp +from homeassistant.util.dt import utcnow from . import HomeAssistantSpotifyData from .browse_media import async_browse_media_internal @@ -199,13 +198,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity): return None return self._currently_playing["progress_ms"] / 1000 - @property - def media_position_updated_at(self) -> dt.datetime | None: - """When was the position of the current playing media valid.""" - if not self._currently_playing: - return None - return utc_from_timestamp(self._currently_playing["timestamp"] / 1000) - @property def media_image_url(self) -> str | None: """Return the media image URL.""" @@ -413,6 +405,9 @@ class SpotifyMediaPlayer(MediaPlayerEntity): additional_types=[MediaType.EPISODE] ) self._currently_playing = current or {} + # Record the last updated time, because Spotify's timestamp property is unreliable + # and doesn't actually return the fetch time as is mentioned in the API description + self._attr_media_position_updated_at = utcnow() if current is not None else None context = self._currently_playing.get("context") or {} From 406e58df6916f6e38096201f2427faf24264c041 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 20 Oct 2023 15:16:45 +0200 Subject: [PATCH 45/81] Fix error handling on subscribe when mqtt is not initialized (#101832) --- homeassistant/components/mqtt/client.py | 8 +++++++- tests/components/mqtt/test_init.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 733645c4788..5acaefcdaeb 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -176,7 +176,13 @@ async def async_subscribe( raise HomeAssistantError( f"Cannot subscribe to topic '{topic}', MQTT is not enabled" ) - mqtt_data = get_mqtt_data(hass) + try: + mqtt_data = get_mqtt_data(hass) + except KeyError as ex: + raise HomeAssistantError( + f"Cannot subscribe to topic '{topic}', " + "make sure MQTT is set up correctly" + ) from ex async_remove = await mqtt_data.client.async_subscribe( topic, catch_log_exception( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 48d949ae927..cd533cc6588 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -959,6 +959,17 @@ async def test_subscribe_topic( unsub() +async def test_subscribe_topic_not_initialize( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test the subscription of a topic when MQTT was not initialized.""" + with pytest.raises( + HomeAssistantError, match=r".*make sure MQTT is set up correctly" + ): + await mqtt.async_subscribe(hass, "test-topic", record_calls) + + @patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.2) async def test_subscribe_and_resubscribe( From 08d5d5336a86c68a817b496e91468771c085f2a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Oct 2023 11:48:00 -1000 Subject: [PATCH 46/81] Bump aioesphomeapi to 17.1.4 (#101897) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 8169eeb70e3..8488dcc4127 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async-interrupt==1.1.1", - "aioesphomeapi==17.0.1", + "aioesphomeapi==17.1.4", "bluetooth-data-tools==1.12.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index d396581a2c8..9c3fbede575 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==17.0.1 +aioesphomeapi==17.1.4 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ba3c3f85f2..9164ae8d12e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==17.0.1 +aioesphomeapi==17.1.4 # homeassistant.components.flo aioflo==2021.11.0 From 1b83620213e509c423569865f9c7db799fd69cfe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Oct 2023 23:05:52 -1000 Subject: [PATCH 47/81] Bump aioesphomeapi to 17.1.5 (#101916) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 8488dcc4127..82567d7310b 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async-interrupt==1.1.1", - "aioesphomeapi==17.1.4", + "aioesphomeapi==17.1.5", "bluetooth-data-tools==1.12.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 9c3fbede575..f795bbdc939 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==17.1.4 +aioesphomeapi==17.1.5 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9164ae8d12e..ec5bcc500ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==17.1.4 +aioesphomeapi==17.1.5 # homeassistant.components.flo aioflo==2021.11.0 From 1371a03d14a848077681200cc64174a13100bd1a Mon Sep 17 00:00:00 2001 From: Vadym Holoveichuk Date: Fri, 13 Oct 2023 15:09:35 +0300 Subject: [PATCH 48/81] Fix Setpoint in Matter climate platform (#101929) fix matter max setpoint --- homeassistant/components/matter/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index 6da88533edc..44e5d30fec4 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -250,9 +250,9 @@ class MatterClimate(MatterEntity, ClimateEntity): self._attr_min_temp = DEFAULT_MIN_TEMP # update max_temp if self._attr_hvac_mode in (HVACMode.COOL, HVACMode.HEAT_COOL): - attribute = clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit - else: attribute = clusters.Thermostat.Attributes.AbsMaxCoolSetpointLimit + else: + attribute = clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit if (value := self._get_temperature_in_degrees(attribute)) is not None: self._attr_max_temp = value else: From 24a1e540b996e0e80d34ea436cdf47ccb58fdf7c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 13 Oct 2023 19:14:43 -0400 Subject: [PATCH 49/81] Update zwave issue repair strings (#101940) --- homeassistant/components/zwave_js/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 6994ce15a0c..4bb9494eb6b 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -163,12 +163,12 @@ } }, "device_config_file_changed": { - "title": "Z-Wave device configuration file changed: {device_name}", + "title": "Device configuration file changed: {device_name}", "fix_flow": { "step": { "confirm": { - "title": "Z-Wave device configuration file changed: {device_name}", - "description": "Z-Wave JS discovers a lot of device metadata by interviewing the device. However, some of the information has to be loaded from a configuration file. Some of this information is only evaluated once, during the device interview.\n\nWhen a device config file is updated, this information may be stale and and the device must be re-interviewed to pick up the changes.\n\n This is not a required operation and device functionality will be impacted during the re-interview process, but you may see improvements for your device once it is complete.\n\nIf you'd like to proceed, click on SUBMIT below. The re-interview will take place in the background." + "title": "Device configuration file changed: {device_name}", + "description": "The device configuration file for {device_name} has changed.\n\nZ-Wave JS discovers a lot of device metadata by interviewing the device. However, some of the information has to be loaded from a configuration file. Some of this information is only evaluated once, during the device interview.\n\nWhen a device config file is updated, this information may be stale and and the device must be re-interviewed to pick up the changes.\n\n This is not a required operation and device functionality will be impacted during the re-interview process, but you may see improvements for your device once it is complete.\n\nIf you'd like to proceed, click on SUBMIT below. The re-interview will take place in the background." } }, "abort": { From 1e1dbf3cefd934ae91e1238ca621c6df31ff5530 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sun, 15 Oct 2023 13:07:32 -0700 Subject: [PATCH 50/81] Bump screenlogicpy to v0.9.3 (#101957) --- 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 a57ad0026e6..e61ca04374f 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.2"] + "requirements": ["screenlogicpy==0.9.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index f795bbdc939..fe00991b49a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2372,7 +2372,7 @@ satel-integra==0.3.7 scapy==2.5.0 # homeassistant.components.screenlogic -screenlogicpy==0.9.2 +screenlogicpy==0.9.3 # homeassistant.components.scsgate scsgate==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec5bcc500ac..2e358cdd7fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1759,7 +1759,7 @@ samsungtvws[async,encrypted]==2.6.0 scapy==2.5.0 # homeassistant.components.screenlogic -screenlogicpy==0.9.2 +screenlogicpy==0.9.3 # homeassistant.components.backup securetar==2023.3.0 From edce212dc9368204eede0e7d18a3f591196d7a49 Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Sun, 15 Oct 2023 17:01:05 +0000 Subject: [PATCH 51/81] Bump pynina to 0.3.3 (#101960) --- homeassistant/components/nina/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nina/manifest.json b/homeassistant/components/nina/manifest.json index df09d168827..1bf670aedf0 100644 --- a/homeassistant/components/nina/manifest.json +++ b/homeassistant/components/nina/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nina", "iot_class": "cloud_polling", "loggers": ["pynina"], - "requirements": ["PyNINA==0.3.2"] + "requirements": ["PyNINA==0.3.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index fe00991b49a..32b7d992921 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -80,7 +80,7 @@ PyMetno==0.11.0 PyMicroBot==0.0.9 # homeassistant.components.nina -PyNINA==0.3.2 +PyNINA==0.3.3 # homeassistant.components.mobile_app # homeassistant.components.owntracks diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e358cdd7fc..6517e3e0c72 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -70,7 +70,7 @@ PyMetno==0.11.0 PyMicroBot==0.0.9 # homeassistant.components.nina -PyNINA==0.3.2 +PyNINA==0.3.3 # homeassistant.components.mobile_app # homeassistant.components.owntracks From b2bbe4f4b8cebd7dff55ca34b6264f4cb870aad3 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Sun, 15 Oct 2023 16:39:41 -0500 Subject: [PATCH 52/81] Fix google_maps same last_seen bug (#101971) --- homeassistant/components/google_maps/device_tracker.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index be776df1751..93810d0f21d 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -118,9 +118,7 @@ class GoogleMapsScanner: ) _LOGGER.debug("%s < %s", last_seen, self._prev_seen[dev_id]) continue - if last_seen == self._prev_seen.get(dev_id, last_seen) and hasattr( - self, "success_init" - ): + if last_seen == self._prev_seen.get(dev_id): _LOGGER.debug( "Ignoring %s update because timestamp " "is the same as the last timestamp %s", From 3fb79829efbf05bbac5e49f7e16e16147be5c293 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Oct 2023 23:48:59 -1000 Subject: [PATCH 53/81] Bump aioesphomeapi to 17.2.0 (#101981) * Bump aioesphomeapi to 17.2.0 changelog: https://github.com/esphome/aioesphomeapi/compare/v17.1.5...v17.2.0 * fix import from wrong module --- homeassistant/components/esphome/bluetooth/client.py | 7 +++++-- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 411a5b989a3..d44d331248b 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -25,8 +25,11 @@ from aioesphomeapi import ( BluetoothProxyFeature, DeviceInfo, ) -from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError -from aioesphomeapi.core import BluetoothGATTAPIError +from aioesphomeapi.core import ( + APIConnectionError, + BluetoothGATTAPIError, + TimeoutAPIError, +) from async_interrupt import interrupt from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.client import BaseBleakClient, NotifyCallback diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 82567d7310b..8a2ede93b3e 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async-interrupt==1.1.1", - "aioesphomeapi==17.1.5", + "aioesphomeapi==17.2.0", "bluetooth-data-tools==1.12.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 32b7d992921..48d4b06d68a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==17.1.5 +aioesphomeapi==17.2.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6517e3e0c72..50b3a25b914 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==17.1.5 +aioesphomeapi==17.2.0 # homeassistant.components.flo aioflo==2021.11.0 From 2d7f054058637cb1b1b4b03f740ee7d42701fb66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Oct 2023 23:42:22 -1000 Subject: [PATCH 54/81] Bump aioesphomeapi to 18.0.1 (#102028) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/esphome/manager.py | 2 +- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/esphome/conftest.py | 1 + 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 41fd60af07d..211404431c0 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -538,7 +538,7 @@ class ESPHomeManager: on_connect=self.on_connect, on_disconnect=self.on_disconnect, zeroconf_instance=self.zeroconf_instance, - name=self.host, + name=entry.data.get(CONF_DEVICE_NAME, self.host), on_connect_error=self.on_connect_error, ) self.reconnect_logic = reconnect_logic diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 8a2ede93b3e..463404fae1c 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async-interrupt==1.1.1", - "aioesphomeapi==17.2.0", + "aioesphomeapi==18.0.1", "bluetooth-data-tools==1.12.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 48d4b06d68a..995a492a1e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==17.2.0 +aioesphomeapi==18.0.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 50b3a25b914..0458a98d0ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==17.2.0 +aioesphomeapi==18.0.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 6b06545a06b..4ff6b503b3c 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -129,6 +129,7 @@ def mock_client(mock_device_info) -> APIClient: mock_client.connect = AsyncMock() mock_client.disconnect = AsyncMock() mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.address = "127.0.0.1" mock_client.api_version = APIVersion(99, 99) with patch("homeassistant.components.esphome.APIClient", mock_client), patch( From f7e84fcb60f0a7ee8095e3cd03556382553135b9 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 15 Oct 2023 16:59:19 -0700 Subject: [PATCH 55/81] Fix bug in calendar state transitions (#102083) --- homeassistant/components/calendar/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index f868f951646..7261e422bb7 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -483,7 +483,7 @@ class CalendarEntity(Entity): _entity_component_unrecorded_attributes = frozenset({"description"}) - _alarm_unsubs: list[CALLBACK_TYPE] = [] + _alarm_unsubs: list[CALLBACK_TYPE] | None = None @property def event(self) -> CalendarEvent | None: @@ -528,6 +528,8 @@ class CalendarEntity(Entity): the current or upcoming event. """ super().async_write_ha_state() + if self._alarm_unsubs is None: + self._alarm_unsubs = [] _LOGGER.debug( "Clearing %s alarms (%s)", self.entity_id, len(self._alarm_unsubs) ) @@ -571,9 +573,9 @@ class CalendarEntity(Entity): To be extended by integrations. """ - for unsub in self._alarm_unsubs: + for unsub in self._alarm_unsubs or (): unsub() - self._alarm_unsubs.clear() + self._alarm_unsubs = None async def async_get_events( self, From c916ac67bdd9b4473061cd80289b17991f7023f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Mon, 16 Oct 2023 01:00:19 +0100 Subject: [PATCH 56/81] Call disconnected callbacks from BT ESPHome client (#102084) Co-authored-by: J. Nick Koston --- homeassistant/components/esphome/bluetooth/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index d44d331248b..970e866b27b 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -392,8 +392,8 @@ class ESPHomeClient(BaseBleakClient): return await self._disconnect() async def _disconnect(self) -> bool: - self._async_disconnected_cleanup() await self._client.bluetooth_device_disconnect(self._address_as_int) + self._async_ble_device_disconnected() await self._wait_for_free_connection_slot(DISCONNECT_TIMEOUT) return True From 1a45b0af283a6824a2d2fe14a84a149fcb09b75d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Oct 2023 16:32:55 -1000 Subject: [PATCH 57/81] Bump aioesphomeapi to 18.0.3 (#102085) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 463404fae1c..f203f8323bd 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async-interrupt==1.1.1", - "aioesphomeapi==18.0.1", + "aioesphomeapi==18.0.3", "bluetooth-data-tools==1.12.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 995a492a1e5..72ae982cc8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.0.1 +aioesphomeapi==18.0.3 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0458a98d0ee..13ec627611a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.0.1 +aioesphomeapi==18.0.3 # homeassistant.components.flo aioflo==2021.11.0 From 3aba98a297770fae1fb48240fc2251896389e2d7 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 16 Oct 2023 14:23:43 +0200 Subject: [PATCH 58/81] Correct sensor state attribute and device class in Velbus sensors (#102099) --- homeassistant/components/velbus/sensor.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/velbus/sensor.py b/homeassistant/components/velbus/sensor.py index 0805ae2699a..8e1f8bba74a 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -45,25 +45,18 @@ class VelbusSensor(VelbusEntity, SensorEntity): """Initialize a sensor Velbus entity.""" super().__init__(channel) self._is_counter: bool = counter - # define the unique id if self._is_counter: - self._attr_unique_id = f"{self._attr_unique_id}-counter" - # define the name - if self._is_counter: - self._attr_name = f"{self._attr_name}-counter" - # define the device class - if self._is_counter: - self._attr_device_class = SensorDeviceClass.POWER - elif channel.is_counter_channel(): self._attr_device_class = SensorDeviceClass.ENERGY + self._attr_icon = "mdi:counter" + self._attr_name = f"{self._attr_name}-counter" + self._attr_state_class = SensorStateClass.TOTAL_INCREASING + self._attr_unique_id = f"{self._attr_unique_id}-counter" + elif channel.is_counter_channel(): + self._attr_device_class = SensorDeviceClass.POWER + self._attr_state_class = SensorStateClass.MEASUREMENT elif channel.is_temperature(): self._attr_device_class = SensorDeviceClass.TEMPERATURE - # define the icon - if self._is_counter: - self._attr_icon = "mdi:counter" - # the state class - if self._is_counter: - self._attr_state_class = SensorStateClass.TOTAL_INCREASING + self._attr_state_class = SensorStateClass.MEASUREMENT else: self._attr_state_class = SensorStateClass.MEASUREMENT # unit From f12ce41d0049deb911f653c574ac2d2e88c4b757 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 16 Oct 2023 14:22:01 +0200 Subject: [PATCH 59/81] Bump velbusaio to 2023.10.0 (#102100) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 220c416cfe9..82b66cc0e7f 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -13,7 +13,7 @@ "velbus-packet", "velbus-protocol" ], - "requirements": ["velbus-aio==2023.2.0"], + "requirements": ["velbus-aio==2023.10.0"], "usb": [ { "vid": "10CF", diff --git a/requirements_all.txt b/requirements_all.txt index 72ae982cc8e..cf56ca70f50 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2651,7 +2651,7 @@ vallox-websocket-api==3.3.0 vehicle==1.0.1 # homeassistant.components.velbus -velbus-aio==2023.2.0 +velbus-aio==2023.10.0 # homeassistant.components.venstar venstarcolortouch==0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13ec627611a..0b8b72bb0dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1969,7 +1969,7 @@ vallox-websocket-api==3.3.0 vehicle==1.0.1 # homeassistant.components.velbus -velbus-aio==2023.2.0 +velbus-aio==2023.10.0 # homeassistant.components.venstar venstarcolortouch==0.19 From a187f05da03e5d1ed581fdf20989ade4fa604f0e Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Mon, 16 Oct 2023 21:57:37 +0200 Subject: [PATCH 60/81] Bump zha-quirks to 0.0.105 (#102113) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 9ce3a3eb7db..5cde71f8d07 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -24,7 +24,7 @@ "bellows==0.36.5", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.104", + "zha-quirks==0.0.105", "zigpy-deconz==0.21.1", "zigpy==0.57.2", "zigpy-xbee==0.18.3", diff --git a/requirements_all.txt b/requirements_all.txt index cf56ca70f50..cd6c3ebd22d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2787,7 +2787,7 @@ zeroconf==0.115.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.104 +zha-quirks==0.0.105 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b8b72bb0dd..589332449fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2081,7 +2081,7 @@ zeroconf==0.115.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.104 +zha-quirks==0.0.105 # homeassistant.components.zha zigpy-deconz==0.21.1 From ec6128d9c4d59e602b442ebbdd359d212ed92c67 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 20 Oct 2023 08:49:22 +0200 Subject: [PATCH 61/81] Fix UniFi client tracker entities being unavailable when away on restart (#102125) --- homeassistant/components/unifi/controller.py | 21 +++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 620b928176e..278af31b6b9 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -20,9 +20,14 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL, + Platform, ) from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback -from homeassistant.helpers import aiohttp_client, device_registry as dr +from homeassistant.helpers import ( + aiohttp_client, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.device_registry import ( DeviceEntry, DeviceEntryType, @@ -33,6 +38,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.helpers.event import async_call_later, async_track_time_interval import homeassistant.util.dt as dt_util @@ -131,7 +137,7 @@ class UniFiController: # Client control options # Config entry option with list of clients to control network access. - self.option_block_clients = options.get(CONF_BLOCK_CLIENT, []) + self.option_block_clients: list[str] = options.get(CONF_BLOCK_CLIENT, []) # Config entry option to control DPI restriction groups. self.option_dpi_restrictions: bool = options.get( CONF_DPI_RESTRICTIONS, DEFAULT_DPI_RESTRICTIONS @@ -244,7 +250,16 @@ class UniFiController: assert self.config_entry.unique_id is not None self.is_admin = self.api.sites[self.config_entry.unique_id].role == "admin" - for mac in self.option_block_clients: + # Restore device tracker clients that are not a part of active clients list. + macs: list[str] = [] + entity_registry = er.async_get(self.hass) + for entry in async_entries_for_config_entry( + entity_registry, self.config_entry.entry_id + ): + if entry.domain == Platform.DEVICE_TRACKER: + macs.append(entry.unique_id.split("-", 1)[0]) + + for mac in self.option_block_clients + macs: if mac not in self.api.clients and mac in self.api.clients_all: self.api.clients.process_raw([dict(self.api.clients_all[mac].raw)]) From f896b82b499d626529ed2fd8d71dc12a7eb49e4f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 17 Oct 2023 19:46:25 +1300 Subject: [PATCH 62/81] Send events for tts stream start/end (#102139) --- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/esphome/voice_assistant.py | 9 +++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index f203f8323bd..d06cf1e00d3 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async-interrupt==1.1.1", - "aioesphomeapi==18.0.3", + "aioesphomeapi==18.0.4", "bluetooth-data-tools==1.12.0", "esphome-dashboard-api==1.2.3" ], diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index 8fba4bfb39a..de6313f45aa 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -55,6 +55,8 @@ _VOICE_ASSISTANT_EVENT_TYPES: EsphomeEnumMapper[ VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END: PipelineEventType.TTS_END, VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_START: PipelineEventType.WAKE_WORD_START, VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_END: PipelineEventType.WAKE_WORD_END, + VoiceAssistantEventType.VOICE_ASSISTANT_STT_VAD_START: PipelineEventType.STT_VAD_START, + VoiceAssistantEventType.VOICE_ASSISTANT_STT_VAD_END: PipelineEventType.STT_VAD_END, } ) @@ -296,6 +298,10 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): if self.transport is None: return + self.handle_event( + VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_START, {} + ) + _extension, audio_bytes = await tts.async_get_media_source_audio( self.hass, media_id, @@ -321,4 +327,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): sample_offset += samples_in_chunk finally: + self.handle_event( + VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_END, {} + ) self._tts_done.set() diff --git a/requirements_all.txt b/requirements_all.txt index cd6c3ebd22d..8658b50c25c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.0.3 +aioesphomeapi==18.0.4 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 589332449fa..eb2178a3316 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.0.3 +aioesphomeapi==18.0.4 # homeassistant.components.flo aioflo==2021.11.0 From 221efd7d4d2a5a824681fa7ddbb2a4ff64c7e3ed Mon Sep 17 00:00:00 2001 From: tronikos Date: Tue, 17 Oct 2023 01:35:32 -0700 Subject: [PATCH 63/81] Bump opower to 0.0.36 (#102150) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 71fd841d0fc..02c73238ef9 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.0.35"] + "requirements": ["opower==0.0.36"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8658b50c25c..8d43d8b55cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1383,7 +1383,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.35 +opower==0.0.36 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb2178a3316..af84e5a973f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1061,7 +1061,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.35 +opower==0.0.36 # homeassistant.components.oralb oralb-ble==0.17.6 From f5e681ad3398ff939af8a9d1d93a8c12f21898a7 Mon Sep 17 00:00:00 2001 From: iain MacDonnell Date: Tue, 17 Oct 2023 16:59:15 +0100 Subject: [PATCH 64/81] Explicitly set entity name for VenstarSensor (#102158) VenstarSensor entity name Set entity name using _attr_name instead of a name property, to avoid warnings about name being implicitly set to None. --- homeassistant/components/venstar/sensor.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/venstar/sensor.py b/homeassistant/components/venstar/sensor.py index 2d919bbc1bc..7125dfd4540 100644 --- a/homeassistant/components/venstar/sensor.py +++ b/homeassistant/components/venstar/sensor.py @@ -146,6 +146,7 @@ class VenstarSensor(VenstarEntity, SensorEntity): super().__init__(coordinator, config) self.entity_description = entity_description self.sensor_name = sensor_name + self._attr_name = entity_description.name_fn(sensor_name) self._config = config @property @@ -153,11 +154,6 @@ class VenstarSensor(VenstarEntity, SensorEntity): """Return the unique id.""" return f"{self._config.entry_id}_{self.sensor_name.replace(' ', '_')}_{self.entity_description.key}" - @property - def name(self): - """Return the name of the device.""" - return self.entity_description.name_fn(self.sensor_name) - @property def native_value(self) -> int: """Return state of the sensor.""" From 3599ddfd9f4cdbeb1d935b479f398e032242ecff Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 17 Oct 2023 10:57:08 -0500 Subject: [PATCH 65/81] Don't warn about unknown pipeline events in ESPHome (#102174) Don't warn about unknown events (debug) --- homeassistant/components/esphome/voice_assistant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index de6313f45aa..26c0780d735 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -163,7 +163,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): try: event_type = _VOICE_ASSISTANT_EVENT_TYPES.from_hass(event.type) except KeyError: - _LOGGER.warning("Received unknown pipeline event type: %s", event.type) + _LOGGER.debug("Received unknown pipeline event type: %s", event.type) return data_to_send = None From 7963008a1ea35b2d07c06ca45242ba2641768b84 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Wed, 18 Oct 2023 22:00:55 +0200 Subject: [PATCH 66/81] Bump velbusaio to 2023.10.1 (#102178) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 82b66cc0e7f..229ee8458c6 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -13,7 +13,7 @@ "velbus-packet", "velbus-protocol" ], - "requirements": ["velbus-aio==2023.10.0"], + "requirements": ["velbus-aio==2023.10.1"], "usb": [ { "vid": "10CF", diff --git a/requirements_all.txt b/requirements_all.txt index 8d43d8b55cd..e4aefb03479 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2651,7 +2651,7 @@ vallox-websocket-api==3.3.0 vehicle==1.0.1 # homeassistant.components.velbus -velbus-aio==2023.10.0 +velbus-aio==2023.10.1 # homeassistant.components.venstar venstarcolortouch==0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af84e5a973f..a6c580aebae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1969,7 +1969,7 @@ vallox-websocket-api==3.3.0 vehicle==1.0.1 # homeassistant.components.velbus -velbus-aio==2023.10.0 +velbus-aio==2023.10.1 # homeassistant.components.venstar venstarcolortouch==0.19 From dd15e3f7066de2bdf0e01d69c79530744da284b9 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 18 Oct 2023 13:24:57 +0200 Subject: [PATCH 67/81] Bump aiowaqi to 2.1.0 (#102209) --- homeassistant/components/waqi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/waqi/manifest.json b/homeassistant/components/waqi/manifest.json index a866dc2c902..1cac5be375b 100644 --- a/homeassistant/components/waqi/manifest.json +++ b/homeassistant/components/waqi/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/waqi", "iot_class": "cloud_polling", "loggers": ["aiowaqi"], - "requirements": ["aiowaqi==2.0.0"] + "requirements": ["aiowaqi==2.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index e4aefb03479..76285bd68e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -372,7 +372,7 @@ aiovlc==0.1.0 aiovodafone==0.3.1 # homeassistant.components.waqi -aiowaqi==2.0.0 +aiowaqi==2.1.0 # homeassistant.components.watttime aiowatttime==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6c580aebae..68c912812e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -347,7 +347,7 @@ aiovlc==0.1.0 aiovodafone==0.3.1 # homeassistant.components.waqi -aiowaqi==2.0.0 +aiowaqi==2.1.0 # homeassistant.components.watttime aiowatttime==0.1.1 From d55b6a0839e5d2bcd513c3f976d73071e9604b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 19 Oct 2023 10:42:31 +0200 Subject: [PATCH 68/81] Handle timeouts on AEMET init (#102289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/aemet/__init__.py | 4 ++++ tests/components/aemet/test_init.py | 27 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index bcddce5868c..13e636b2196 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -1,5 +1,6 @@ """The AEMET OpenData component.""" +import asyncio import logging from aemet_opendata.exceptions import TownNotFound @@ -8,6 +9,7 @@ from aemet_opendata.interface import AEMET, ConnectionOptions from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from .const import ( @@ -37,6 +39,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except TownNotFound as err: _LOGGER.error(err) return False + except asyncio.TimeoutError as err: + raise ConfigEntryNotReady("AEMET OpenData API timed out") from err weather_coordinator = WeatherUpdateCoordinator(hass, aemet) await weather_coordinator.async_config_entry_first_refresh() diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index 5055575e3fe..9389acf07c9 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -1,4 +1,5 @@ """Define tests for the AEMET OpenData init.""" +import asyncio from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory @@ -70,3 +71,29 @@ async def test_init_town_not_found( config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) is False + + +async def test_init_api_timeout( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test API timeouts when loading the AEMET integration.""" + + hass.config.set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + with patch( + "homeassistant.components.aemet.AEMET.api_call", + side_effect=asyncio.TimeoutError, + ): + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "api-key", + CONF_LATITUDE: "0.0", + CONF_LONGITUDE: "0.0", + CONF_NAME: "AEMET", + }, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) is False From ab67304ebceaf6771a04d20f61dbdc0c153188a9 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Fri, 20 Oct 2023 08:14:55 +0200 Subject: [PATCH 69/81] Bump pyduotecno to 2023.10.1 (#102344) --- 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 c7885496af8..96f76517a92 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.10.0"] + "requirements": ["pyDuotecno==2023.10.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 76285bd68e5..087f22c6e4a 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.10.0 +pyDuotecno==2023.10.1 # homeassistant.components.eight_sleep pyEight==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 68c912812e2..b8e93379a08 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.10.0 +pyDuotecno==2023.10.1 # homeassistant.components.eight_sleep pyEight==0.3.2 From 467d24548dd89de18d1fbe5734fabe25252957b8 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 20 Oct 2023 02:11:49 -0400 Subject: [PATCH 70/81] Bump ZHA dependencies (#102358) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 5cde71f8d07..a2b48655100 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.36.5", + "bellows==0.36.7", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.105", @@ -29,7 +29,7 @@ "zigpy==0.57.2", "zigpy-xbee==0.18.3", "zigpy-zigate==0.11.0", - "zigpy-znp==0.11.5", + "zigpy-znp==0.11.6", "universal-silabs-flasher==0.0.14", "pyserial-asyncio-fast==0.11" ], diff --git a/requirements_all.txt b/requirements_all.txt index 087f22c6e4a..49837bd2dea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -512,7 +512,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.36.5 +bellows==0.36.7 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.1 @@ -2805,7 +2805,7 @@ zigpy-xbee==0.18.3 zigpy-zigate==0.11.0 # homeassistant.components.zha -zigpy-znp==0.11.5 +zigpy-znp==0.11.6 # homeassistant.components.zha zigpy==0.57.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b8e93379a08..6ddda265ea3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -436,7 +436,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.36.5 +bellows==0.36.7 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.1 @@ -2093,7 +2093,7 @@ zigpy-xbee==0.18.3 zigpy-zigate==0.11.0 # homeassistant.components.zha -zigpy-znp==0.11.5 +zigpy-znp==0.11.6 # homeassistant.components.zha zigpy==0.57.2 From c9cba779405dcb7de20c112a5a2b3ea850cbe0a2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 20 Oct 2023 14:54:51 +0200 Subject: [PATCH 71/81] Bump vehicle to 2.0.0 (#102379) --- homeassistant/components/rdw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rdw/manifest.json b/homeassistant/components/rdw/manifest.json index 5df34652f2b..bc8d3be8451 100644 --- a/homeassistant/components/rdw/manifest.json +++ b/homeassistant/components/rdw/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["vehicle==1.0.1"] + "requirements": ["vehicle==2.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 49837bd2dea..5843138a53c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2648,7 +2648,7 @@ uvcclient==0.11.0 vallox-websocket-api==3.3.0 # homeassistant.components.rdw -vehicle==1.0.1 +vehicle==2.0.0 # homeassistant.components.velbus velbus-aio==2023.10.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ddda265ea3..6ed041f8e61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1966,7 +1966,7 @@ uvcclient==0.11.0 vallox-websocket-api==3.3.0 # homeassistant.components.rdw -vehicle==1.0.1 +vehicle==2.0.0 # homeassistant.components.velbus velbus-aio==2023.10.1 From 00978026cd450eedd073c3bef81cabffbe7726ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 20 Oct 2023 15:11:48 +0200 Subject: [PATCH 72/81] Update aioairzone to v0.6.9 (#102383) --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index c0b24b2cc3e..e9485f1b9d0 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.6.8"] + "requirements": ["aioairzone==0.6.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5843138a53c..5b00e818745 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -189,7 +189,7 @@ aioairq==0.2.4 aioairzone-cloud==0.2.3 # homeassistant.components.airzone -aioairzone==0.6.8 +aioairzone==0.6.9 # homeassistant.components.ambient_station aioambient==2023.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ed041f8e61..2a4fef571ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairq==0.2.4 aioairzone-cloud==0.2.3 # homeassistant.components.airzone -aioairzone==0.6.8 +aioairzone==0.6.9 # homeassistant.components.ambient_station aioambient==2023.04.0 From 73ac2d3dccbf2edbdc1ca7a3b7f0f991ddfb0f25 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 20 Oct 2023 17:40:08 +0200 Subject: [PATCH 73/81] Bumped version to 2023.10.4 --- 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 82eae8434bd..def053f90cd 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 = "3" +PATCH_VERSION: Final = "4" __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 41152c944dc..a20ce5bff6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.3" +version = "2023.10.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 14483db8922270ddde22af41f272212b387659ae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Oct 2023 21:31:40 -1000 Subject: [PATCH 74/81] Bump aioesphomeapi to 18.0.6 (#102195) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index d06cf1e00d3..dbcd2042b5a 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async-interrupt==1.1.1", - "aioesphomeapi==18.0.4", + "aioesphomeapi==18.0.6", "bluetooth-data-tools==1.12.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 5b00e818745..425c62d6bc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.0.4 +aioesphomeapi==18.0.6 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a4fef571ad..d8070c0ecb7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.0.4 +aioesphomeapi==18.0.6 # homeassistant.components.flo aioflo==2021.11.0 From 1939beeca6eefdc073d9decef855b806686419a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Oct 2023 07:14:13 -1000 Subject: [PATCH 75/81] Bump dbus-fast to 2.12.0 (#102206) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 04815dc8972..9cdf7f2df35 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -19,6 +19,6 @@ "bluetooth-adapters==0.16.1", "bluetooth-auto-recovery==1.2.3", "bluetooth-data-tools==1.12.0", - "dbus-fast==2.11.1" + "dbus-fast==2.12.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 005a6735e03..fc0c4872a5f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ bluetooth-data-tools==1.12.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.4 -dbus-fast==2.11.1 +dbus-fast==2.12.0 fnv-hash-fast==0.4.1 ha-av==10.1.1 hass-nabucasa==0.71.0 diff --git a/requirements_all.txt b/requirements_all.txt index 425c62d6bc4..34b68a4a902 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -645,7 +645,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==2.11.1 +dbus-fast==2.12.0 # homeassistant.components.debugpy debugpy==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d8070c0ecb7..314e1276590 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -528,7 +528,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==2.11.1 +dbus-fast==2.12.0 # homeassistant.components.debugpy debugpy==1.8.0 From 31daac61a9f8d072c69860c0c7433972c0400128 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Oct 2023 08:54:17 -1000 Subject: [PATCH 76/81] Bump bluetooth-data-tools to 1.13.0 (#102208) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/ld2410_ble/manifest.json | 2 +- homeassistant/components/led_ble/manifest.json | 2 +- homeassistant/components/private_ble_device/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 9cdf7f2df35..960a86637ae 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,7 +18,7 @@ "bleak-retry-connector==3.2.1", "bluetooth-adapters==0.16.1", "bluetooth-auto-recovery==1.2.3", - "bluetooth-data-tools==1.12.0", + "bluetooth-data-tools==1.13.0", "dbus-fast==2.12.0" ] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index dbcd2042b5a..a024abfe875 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "requirements": [ "async-interrupt==1.1.1", "aioesphomeapi==18.0.6", - "bluetooth-data-tools==1.12.0", + "bluetooth-data-tools==1.13.0", "esphome-dashboard-api==1.2.3" ], "zeroconf": ["_esphomelib._tcp.local."] diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index 7971f6bfaf4..f82b2fff62b 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/ld2410_ble", "integration_type": "device", "iot_class": "local_push", - "requirements": ["bluetooth-data-tools==1.12.0", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.13.0", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 7b936eaad1a..a0f7685a2ec 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -32,5 +32,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/led_ble", "iot_class": "local_polling", - "requirements": ["bluetooth-data-tools==1.12.0", "led-ble==1.0.1"] + "requirements": ["bluetooth-data-tools==1.13.0", "led-ble==1.0.1"] } diff --git a/homeassistant/components/private_ble_device/manifest.json b/homeassistant/components/private_ble_device/manifest.json index 9900c854657..91ef843a864 100644 --- a/homeassistant/components/private_ble_device/manifest.json +++ b/homeassistant/components/private_ble_device/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/private_ble_device", "iot_class": "local_push", - "requirements": ["bluetooth-data-tools==1.12.0"] + "requirements": ["bluetooth-data-tools==1.13.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fc0c4872a5f..1295f177468 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ bleak-retry-connector==3.2.1 bleak==0.21.1 bluetooth-adapters==0.16.1 bluetooth-auto-recovery==1.2.3 -bluetooth-data-tools==1.12.0 +bluetooth-data-tools==1.13.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index 34b68a4a902..0e952ad0a62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -553,7 +553,7 @@ bluetooth-auto-recovery==1.2.3 # homeassistant.components.ld2410_ble # homeassistant.components.led_ble # homeassistant.components.private_ble_device -bluetooth-data-tools==1.12.0 +bluetooth-data-tools==1.13.0 # homeassistant.components.bond bond-async==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 314e1276590..ab8174ec820 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -467,7 +467,7 @@ bluetooth-auto-recovery==1.2.3 # homeassistant.components.ld2410_ble # homeassistant.components.led_ble # homeassistant.components.private_ble_device -bluetooth-data-tools==1.12.0 +bluetooth-data-tools==1.13.0 # homeassistant.components.bond bond-async==0.2.1 From 2d6dc2bcccff7518366655a67947d73506fc1e50 Mon Sep 17 00:00:00 2001 From: kpine Date: Fri, 20 Oct 2023 10:57:00 -0700 Subject: [PATCH 77/81] Fix temperature setting for multi-setpoint z-wave device (#102395) * Fix temperature setting for multi-setpoint z-wave device * Add missing fixture file * Apply suggestions from code review --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/climate.py | 8 +- tests/components/zwave_js/conftest.py | 14 + .../climate_intermatic_pe653_state.json | 4508 +++++++++++++++++ tests/components/zwave_js/test_climate.py | 193 + 4 files changed, 4720 insertions(+), 3 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/climate_intermatic_pe653_state.json diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index d511a030fb1..28084eecfa6 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -259,9 +259,11 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): def _current_mode_setpoint_enums(self) -> list[ThermostatSetpointType]: """Return the list of enums that are relevant to the current thermostat mode.""" if self._current_mode is None or self._current_mode.value is None: - # Thermostat(valve) with no support for setting a mode - # is considered heating-only - return [ThermostatSetpointType.HEATING] + # Thermostat with no support for setting a mode is just a setpoint + if self.info.primary_value.property_key is None: + return [] + return [ThermostatSetpointType(int(self.info.primary_value.property_key))] + return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), []) @property diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index bbc836488c2..b9feeab1f2f 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -662,6 +662,12 @@ def logic_group_zdb5100_state_fixture(): return json.loads(load_fixture("zwave_js/logic_group_zdb5100_state.json")) +@pytest.fixture(name="climate_intermatic_pe653_state", scope="session") +def climate_intermatic_pe653_state_fixture(): + """Load Intermatic PE653 Pool Control node state fixture data.""" + return json.loads(load_fixture("zwave_js/climate_intermatic_pe653_state.json")) + + # model fixtures @@ -1290,3 +1296,11 @@ def logic_group_zdb5100_fixture(client, logic_group_zdb5100_state): node = Node(client, copy.deepcopy(logic_group_zdb5100_state)) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="climate_intermatic_pe653") +def climate_intermatic_pe653_fixture(client, climate_intermatic_pe653_state): + """Mock an Intermatic PE653 node.""" + node = Node(client, copy.deepcopy(climate_intermatic_pe653_state)) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/fixtures/climate_intermatic_pe653_state.json b/tests/components/zwave_js/fixtures/climate_intermatic_pe653_state.json new file mode 100644 index 00000000000..a5e86b9c013 --- /dev/null +++ b/tests/components/zwave_js/fixtures/climate_intermatic_pe653_state.json @@ -0,0 +1,4508 @@ +{ + "nodeId": 19, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 5, + "productId": 1619, + "productType": 20549, + "firmwareVersion": "3.9", + "deviceConfig": { + "filename": "/data/db/devices/0x0005/pe653.json", + "isEmbedded": true, + "manufacturer": "Intermatic", + "manufacturerId": 5, + "label": "PE653", + "description": "Pool Control", + "devices": [ + { + "productType": 20549, + "productId": 1619 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "compat": { + "addCCs": {}, + "overrideQueries": { + "overrides": {} + } + } + }, + "label": "PE653", + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": false, + "individualEndpointCount": 39, + "aggregatedEndpointCount": 0, + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 19, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 145, + "name": "Manufacturer Proprietary", + "version": 1, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 1, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 1, + "isSecure": false + }, + { + "id": 48, + "name": "Binary Sensor", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 145, + "name": "Manufacturer Proprietary", + "version": 1, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 1, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 1, + "isSecure": false + }, + { + "id": 48, + "name": "Binary Sensor", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 2, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 3, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 4, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 6, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 7, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 8, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 9, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 10, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 11, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 12, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 13, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 14, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 15, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 16, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 17, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 18, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 19, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 1, + "isSecure": false + }, + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 20, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 1, + "isSecure": false + }, + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 21, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 1, + "isSecure": false + }, + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 22, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 1, + "isSecure": false + }, + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 23, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 1, + "isSecure": false + }, + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 24, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 25, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 26, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 27, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 28, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 29, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 30, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 31, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 32, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 33, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 34, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 35, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 36, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 37, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 38, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + }, + { + "nodeId": 19, + "index": 39, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + } + ] + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 7, + "propertyName": "setpoint", + "propertyKeyName": "Furnace", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Setpoint (Furnace)", + "ccSpecific": { + "setpointType": 7 + }, + "unit": "°F", + "stateful": true, + "secret": false + }, + "value": 60 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyKey": 2, + "propertyName": "Installed Pump Type", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Installed Pump Type", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "One Speed", + "1": "Two Speed" + }, + "valueSize": 2, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Installed Pump Type" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyKey": 1, + "propertyName": "Booster (Cleaner) Pump Installed", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Booster (Cleaner) Pump Installed", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 2, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Booster (Cleaner) Pump Installed" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyKey": 65280, + "propertyName": "Booster (Cleaner) Pump Operation Mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Set the filter pump mode to use when the booster (cleaner) pump is running.", + "label": "Booster (Cleaner) Pump Operation Mode", + "default": 1, + "min": 1, + "max": 6, + "states": { + "1": "Disable", + "2": "Circuit 1", + "3": "VSP Speed 1", + "4": "VSP Speed 2", + "5": "VSP Speed 3", + "6": "VSP Speed 4" + }, + "valueSize": 2, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Booster (Cleaner) Pump Operation Mode", + "info": "Set the filter pump mode to use when the booster (cleaner) pump is running." + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyKey": 65280, + "propertyName": "Heater Cooldown Period", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Heater Cooldown Period", + "default": -1, + "min": -1, + "max": 15, + "states": { + "0": "Heater installed with no cooldown", + "-1": "No heater installed" + }, + "unit": "minutes", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Heater Cooldown Period" + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyKey": 1, + "propertyName": "Heater Safety Setting", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Prevent the heater from turning on while the pump is off.", + "label": "Heater Safety Setting", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 2, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Heater Safety Setting", + "info": "Prevent the heater from turning on while the pump is off." + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyKey": 4278190080, + "propertyName": "Water Temperature Offset", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Water Temperature Offset", + "default": 0, + "min": -5, + "max": 5, + "unit": "°F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Water Temperature Offset" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyKey": 16711680, + "propertyName": "Air/Freeze Temperature Offset", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Air/Freeze Temperature Offset", + "default": 0, + "min": -5, + "max": 5, + "unit": "°F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Air/Freeze Temperature Offset" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyKey": 65280, + "propertyName": "Solar Temperature Offset", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Solar Temperature Offset", + "default": 0, + "min": -5, + "max": 5, + "unit": "°F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Solar Temperature Offset" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 22, + "propertyName": "Pool/Spa Configuration", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Pool/Spa Configuration", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "Pool", + "1": "Spa", + "2": "Both" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Pool/Spa Configuration" + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyName": "Spa Mode Pump Speed", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Requires pool/spa configuration.", + "label": "Spa Mode Pump Speed", + "default": 1, + "min": 1, + "max": 6, + "states": { + "1": "Disabled", + "2": "Circuit 1", + "3": "VSP Speed 1", + "4": "VSP Speed 2", + "5": "VSP Speed 3", + "6": "VSP Speed 4" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Spa Mode Pump Speed", + "info": "Requires pool/spa configuration." + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 32, + "propertyName": "Variable Speed Pump - Speed 1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Requires connected variable speed pump.", + "label": "Variable Speed Pump - Speed 1", + "default": 750, + "min": 400, + "max": 3450, + "unit": "RPM", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump - Speed 1", + "info": "Requires connected variable speed pump." + }, + "value": 1400 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 33, + "propertyName": "Variable Speed Pump - Speed 2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Requires connected variable speed pump.", + "label": "Variable Speed Pump - Speed 2", + "default": 1500, + "min": 400, + "max": 3450, + "unit": "RPM", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump - Speed 2", + "info": "Requires connected variable speed pump." + }, + "value": 1700 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 34, + "propertyName": "Variable Speed Pump - Speed 3", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Requires connected variable speed pump.", + "label": "Variable Speed Pump - Speed 3", + "default": 2350, + "min": 400, + "max": 3450, + "unit": "RPM", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump - Speed 3", + "info": "Requires connected variable speed pump." + }, + "value": 2500 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 35, + "propertyName": "Variable Speed Pump - Speed 4", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Requires connected variable speed pump.", + "label": "Variable Speed Pump - Speed 4", + "default": 3110, + "min": 400, + "max": 3450, + "unit": "RPM", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump - Speed 4", + "info": "Requires connected variable speed pump." + }, + "value": 2500 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 49, + "propertyName": "Variable Speed Pump - Max Speed", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Requires connected variable speed pump.", + "label": "Variable Speed Pump - Max Speed", + "default": 3450, + "min": 400, + "max": 3450, + "unit": "RPM", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump - Max Speed", + "info": "Requires connected variable speed pump." + }, + "value": 3000 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 4278190080, + "propertyName": "Freeze Protection: Temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Freeze Protection: Temperature", + "default": 0, + "min": 0, + "max": 44, + "states": { + "0": "Disabled", + "40": "40 °F", + "41": "41 °F", + "42": "42 °F", + "43": "43 °F", + "44": "44 °F" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Freeze Protection: Temperature" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 65536, + "propertyName": "Freeze Protection: Turn On Circuit 1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Freeze Protection: Turn On Circuit 1", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Freeze Protection: Turn On Circuit 1" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 131072, + "propertyName": "Freeze Protection: Turn On Circuit 2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Freeze Protection: Turn On Circuit 2", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Freeze Protection: Turn On Circuit 2" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 262144, + "propertyName": "Freeze Protection: Turn On Circuit 3", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Freeze Protection: Turn On Circuit 3", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Freeze Protection: Turn On Circuit 3" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 524288, + "propertyName": "Freeze Protection: Turn On Circuit 4", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Freeze Protection: Turn On Circuit 4", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Freeze Protection: Turn On Circuit 4" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 1048576, + "propertyName": "Freeze Protection: Turn On Circuit 5", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Freeze Protection: Turn On Circuit 5", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Freeze Protection: Turn On Circuit 5" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 65280, + "propertyName": "Freeze Protection: Turn On VSP Speed", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Requires variable speed pump and connected air/freeze sensor.", + "label": "Freeze Protection: Turn On VSP Speed", + "default": 0, + "min": 0, + "max": 5, + "states": { + "0": "None", + "2": "VSP Speed 1", + "3": "VSP Speed 2", + "4": "VSP Speed 3", + "5": "VSP Speed 4" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Freeze Protection: Turn On VSP Speed", + "info": "Requires variable speed pump and connected air/freeze sensor." + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 128, + "propertyName": "Freeze Protection: Turn On Heater", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Requires heater and connected air/freeze sensor.", + "label": "Freeze Protection: Turn On Heater", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true, + "name": "Freeze Protection: Turn On Heater", + "info": "Requires heater and connected air/freeze sensor." + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 127, + "propertyName": "Freeze Protection: Pool/Spa Cycle Time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Requires pool/spa configuration and connected air/freeze sensor.", + "label": "Freeze Protection: Pool/Spa Cycle Time", + "default": 0, + "min": 0, + "max": 30, + "unit": "minutes", + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Freeze Protection: Pool/Spa Cycle Time", + "info": "Requires pool/spa configuration and connected air/freeze sensor." + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Circuit 1 Schedule 1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Start time (first two bytes, little endian) and stop time (last two bytes, little endian) of schedule in minutes past midnight, e.g. 12:05am (0x0500) to 3:00pm (0x8403) is entered as 83919875. Set to 4294967295 (0xffffffff) to disable.", + "label": "Circuit 1 Schedule 1", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Circuit 1 Schedule 1", + "info": "Start time (first two bytes, little endian) and stop time (last two bytes, little endian) of schedule in minutes past midnight, e.g. 12:05am (0x0500) to 3:00pm (0x8403) is entered as 83919875. Set to 4294967295 (0xffffffff) to disable." + }, + "value": 1979884035 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Circuit 1 Schedule 2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Circuit 1 Schedule 2", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Circuit 1 Schedule 2", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Circuit 1 Schedule 3", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Circuit 1 Schedule 3", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Circuit 1 Schedule 3", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Circuit 2 Schedule 1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Circuit 2 Schedule 1", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Circuit 2 Schedule 1", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Circuit 2 Schedule 2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Circuit 2 Schedule 2", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Circuit 2 Schedule 2", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "Circuit 2 Schedule 3", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Circuit 2 Schedule 3", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Circuit 2 Schedule 3", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Circuit 3 Schedule 1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Circuit 3 Schedule 1", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Circuit 3 Schedule 1", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Circuit 3 Schedule 2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Circuit 3 Schedule 2", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Circuit 3 Schedule 2", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "Circuit 3 Schedule 3", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Circuit 3 Schedule 3", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Circuit 3 Schedule 3", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 13, + "propertyName": "Circuit 4 Schedule 1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Circuit 4 Schedule 1", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Circuit 4 Schedule 1", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 14, + "propertyName": "Circuit 4 Schedule 2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Circuit 4 Schedule 2", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Circuit 4 Schedule 2", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 15, + "propertyName": "Circuit 4 Schedule 3", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Circuit 4 Schedule 3", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Circuit 4 Schedule 3", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyName": "Circuit 5 Schedule 1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Circuit 5 Schedule 1", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Circuit 5 Schedule 1", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyName": "Circuit 5 Schedule 2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Circuit 5 Schedule 2", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Circuit 5 Schedule 2", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 18, + "propertyName": "Circuit 5 Schedule 3", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Circuit 5 Schedule 3", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Circuit 5 Schedule 3", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 19, + "propertyName": "Pool/Spa Mode Schedule 1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Pool/Spa Mode Schedule 1", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Pool/Spa Mode Schedule 1", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyName": "Pool/Spa Mode Schedule 2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Pool/Spa Mode Schedule 2", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Pool/Spa Mode Schedule 2", + "info": "Refer to parameter 4 for usage." + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 21, + "propertyName": "Pool/Spa Mode Schedule 3", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Pool/Spa Mode Schedule 3", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Pool/Spa Mode Schedule 3", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 36, + "propertyName": "Variable Speed Pump Speed 1 Schedule 1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Variable Speed Pump Speed 1 Schedule 1", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump Speed 1 Schedule 1", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 37, + "propertyName": "Variable Speed Pump Speed 1 Schedule 2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Variable Speed Pump Speed 1 Schedule 2", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump Speed 1 Schedule 2", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyName": "Variable Speed Pump Speed 1 Schedule 3", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Variable Speed Pump Speed 1 Schedule 3", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump Speed 1 Schedule 3", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 39, + "propertyName": "Variable Speed Pump Speed 2 Schedule 1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Variable Speed Pump Speed 2 Schedule 1", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump Speed 2 Schedule 1", + "info": "Refer to parameter 4 for usage." + }, + "value": 1476575235 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 40, + "propertyName": "Variable Speed Pump Speed 2 Schedule 2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Variable Speed Pump Speed 2 Schedule 2", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump Speed 2 Schedule 2", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 41, + "propertyName": "Variable Speed Pump Speed 2 Schedule 3", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Variable Speed Pump Speed 2 Schedule 3", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump Speed 2 Schedule 3", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 42, + "propertyName": "Variable Speed Pump Speed 3 Schedule 1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Variable Speed Pump Speed 3 Schedule 1", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump Speed 3 Schedule 1", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 43, + "propertyName": "Variable Speed Pump Speed 3 Schedule 2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Variable Speed Pump Speed 3 Schedule 2", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump Speed 3 Schedule 2", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 44, + "propertyName": "Variable Speed Pump Speed 3 Schedule 3", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Variable Speed Pump Speed 3 Schedule 3", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump Speed 3 Schedule 3", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 45, + "propertyName": "Variable Speed Pump Speed 4 Schedule 1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Variable Speed Pump Speed 4 Schedule 1", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump Speed 4 Schedule 1", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 46, + "propertyName": "Variable Speed Pump Speed 4 Schedule 2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Variable Speed Pump Speed 4 Schedule 2", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump Speed 4 Schedule 2", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 47, + "propertyName": "Variable Speed Pump Speed 4 Schedule 3", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Refer to parameter 4 for usage.", + "label": "Variable Speed Pump Speed 4 Schedule 3", + "default": 4294967295, + "min": 0, + "max": 4294967295, + "states": { + "4294967295": "Disabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true, + "name": "Variable Speed Pump Speed 4 Schedule 3", + "info": "Refer to parameter 4 for usage." + }, + "value": 4294967295 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 20549 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 1619 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 1, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "2.78" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 1, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + }, + "value": ["3.9"] + }, + { + "endpoint": 1, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": true + }, + { + "endpoint": 1, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 1, + "commandClass": 48, + "commandClassName": "Binary Sensor", + "property": "Any", + "propertyName": "Any", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Sensor state (Any)", + "ccSpecific": { + "sensorType": 255 + }, + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 1, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 1 + }, + "unit": "°F", + "stateful": true, + "secret": false + }, + "value": 81, + "nodeId": 19 + }, + { + "endpoint": 1, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Setpoint (Heating)", + "ccSpecific": { + "setpointType": 1 + }, + "unit": "°F", + "stateful": true, + "secret": false + }, + "value": 39 + }, + { + "endpoint": 2, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 2, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 2, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 1 + }, + "unit": "°F", + "stateful": true, + "secret": false + }, + "value": 84, + "nodeId": 19 + }, + { + "endpoint": 3, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 3, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 3, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 1 + }, + "unit": "°F", + "stateful": true, + "secret": false + }, + "value": 86, + "nodeId": 19 + }, + { + "endpoint": 4, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 4, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 4, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 1 + }, + "unit": "°F", + "stateful": true, + "secret": false + }, + "value": 80, + "nodeId": 19 + }, + { + "endpoint": 5, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 5, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 5, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 1 + }, + "unit": "°F", + "stateful": true, + "secret": false + }, + "value": 83, + "nodeId": 19 + }, + { + "endpoint": 6, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 6, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 7, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 7, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 8, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 8, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 9, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 9, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 10, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 10, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 11, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 11, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 12, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 12, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 13, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 13, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 14, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 14, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 15, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 15, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 16, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 16, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 17, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": true + }, + { + "endpoint": 17, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 18, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 18, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 19, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 19, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 20, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 20, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 21, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 21, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 22, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 22, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 23, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 23, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 24, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 24, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 25, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 25, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 26, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 26, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 27, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 27, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 28, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 28, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 29, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 29, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 30, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 30, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 31, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 31, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 32, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 32, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 33, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 33, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 34, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 34, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 35, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 35, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 36, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 36, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 37, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 37, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 38, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 38, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + }, + { + "endpoint": 39, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": true + }, + { + "endpoint": 39, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + } + } + ], + "isFrequentListening": false, + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 2, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0005:0x5045:0x0653:3.9", + "highestSecurityClass": -1, + "isControllerNode": false, + "keepAwake": false +} diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index e9040dfd397..cdc1e9959a7 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -792,3 +792,196 @@ async def test_thermostat_raise_repair_issue_and_warning_when_setting_fan_preset "Dry and Fan preset modes are deprecated and will be removed in Home Assistant 2024.2. Please use the corresponding Dry and Fan HVAC modes instead" in caplog.text ) + + +async def test_multi_setpoint_thermostat( + hass: HomeAssistant, client, climate_intermatic_pe653, integration +) -> None: + """Test a thermostat with multiple setpoints.""" + node = climate_intermatic_pe653 + + heating_entity_id = "climate.pool_control_2" + heating = hass.states.get(heating_entity_id) + assert heating + assert heating.state == HVACMode.HEAT + assert heating.attributes[ATTR_TEMPERATURE] == 3.9 + assert heating.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT] + assert ( + heating.attributes[ATTR_SUPPORTED_FEATURES] + == ClimateEntityFeature.TARGET_TEMPERATURE + ) + + furnace_entity_id = "climate.pool_control" + furnace = hass.states.get(furnace_entity_id) + assert furnace + assert furnace.state == HVACMode.HEAT + assert furnace.attributes[ATTR_TEMPERATURE] == 15.6 + assert furnace.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT] + assert ( + furnace.attributes[ATTR_SUPPORTED_FEATURES] + == ClimateEntityFeature.TARGET_TEMPERATURE + ) + + client.async_send_command_no_wait.reset_mock() + + # Test setting temperature of heating setpoint + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: heating_entity_id, + ATTR_TEMPERATURE: 20.0, + }, + blocking=True, + ) + + # Test setting temperature of furnace setpoint + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: furnace_entity_id, + ATTR_TEMPERATURE: 2.0, + }, + blocking=True, + ) + + # Test setting illegal mode raises an error + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: heating_entity_id, + ATTR_HVAC_MODE: HVACMode.COOL, + }, + blocking=True, + ) + + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: furnace_entity_id, + ATTR_HVAC_MODE: HVACMode.COOL, + }, + blocking=True, + ) + + # this is a no-op since there's no mode + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: heating_entity_id, + ATTR_HVAC_MODE: HVACMode.HEAT, + }, + blocking=True, + ) + + # this is a no-op since there's no mode + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: furnace_entity_id, + ATTR_HVAC_MODE: HVACMode.HEAT, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 19 + assert args["valueId"] == { + "endpoint": 1, + "commandClass": 67, + "property": "setpoint", + "propertyKey": 1, + } + assert args["value"] == 68.0 + + args = client.async_send_command.call_args_list[1][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 19 + assert args["valueId"] == { + "endpoint": 0, + "commandClass": 67, + "property": "setpoint", + "propertyKey": 7, + } + assert args["value"] == 35.6 + + client.async_send_command.reset_mock() + + # Test heating setpoint value update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 19, + "args": { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 1, + "property": "setpoint", + "propertyKey": 1, + "propertyKeyName": "Heating", + "propertyName": "setpoint", + "newValue": 23, + "prevValue": 21.5, + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(heating_entity_id) + assert state + assert state.state == HVACMode.HEAT + assert state.attributes[ATTR_TEMPERATURE] == -5 + + # furnace not changed + state = hass.states.get(furnace_entity_id) + assert state + assert state.state == HVACMode.HEAT + assert state.attributes[ATTR_TEMPERATURE] == 15.6 + + client.async_send_command.reset_mock() + + # Test furnace setpoint value update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 19, + "args": { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 7, + "propertyKeyName": "Furnace", + "propertyName": "setpoint", + "newValue": 68, + "prevValue": 21.5, + }, + }, + ) + node.receive_event(event) + + # heating not changed + state = hass.states.get(heating_entity_id) + assert state + assert state.state == HVACMode.HEAT + assert state.attributes[ATTR_TEMPERATURE] == -5 + + state = hass.states.get(furnace_entity_id) + assert state + assert state.state == HVACMode.HEAT + assert state.attributes[ATTR_TEMPERATURE] == 20 + + client.async_send_command.reset_mock() From 6be401918e33f2679cb434b2bea84510f8d3241e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Oct 2023 08:00:55 -1000 Subject: [PATCH 78/81] Bump aioesphomeapi to 18.0.7 (#102399) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index a024abfe875..8db47d83cad 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async-interrupt==1.1.1", - "aioesphomeapi==18.0.6", + "aioesphomeapi==18.0.7", "bluetooth-data-tools==1.13.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 0e952ad0a62..cb2707d5762 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.0.6 +aioesphomeapi==18.0.7 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab8174ec820..2e2015d7ed9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.0.6 +aioesphomeapi==18.0.7 # homeassistant.components.flo aioflo==2021.11.0 From 00607fb7789aaa1ddee0b29dd7d89e522a545422 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 21 Oct 2023 13:19:37 -0400 Subject: [PATCH 79/81] Downgrade ZHA dependency bellows (#102471) Downgrade bellows --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index a2b48655100..88864974eba 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.36.7", + "bellows==0.36.5", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.105", diff --git a/requirements_all.txt b/requirements_all.txt index cb2707d5762..7cfa083d273 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -512,7 +512,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.36.7 +bellows==0.36.5 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e2015d7ed9..578a7750e85 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -436,7 +436,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.36.7 +bellows==0.36.5 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.1 From 9820f4288fc7dd5807ea3680257b5fb2c98351a2 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 21 Oct 2023 22:09:09 +0200 Subject: [PATCH 80/81] Bump async-upnp-client to 0.36.2 (#102472) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index e269d75e0f6..0fa884319c4 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "iot_class": "local_push", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.36.1", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.36.2", "getmac==0.8.2"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 0d07eb0c042..b3fa91a2e70 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dms", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["async-upnp-client==0.36.1"], + "requirements": ["async-upnp-client==0.36.2"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index a3f35b65555..48bdb7083b4 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -39,7 +39,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.6.0", "wakeonlan==2.1.0", - "async-upnp-client==0.36.1" + "async-upnp-client==0.36.2" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 21f0036aabd..bf48b44e5dc 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_push", "loggers": ["async_upnp_client"], "quality_scale": "internal", - "requirements": ["async-upnp-client==0.36.1"] + "requirements": ["async-upnp-client==0.36.2"] } diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 1651dea6612..25f83e0dbf5 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.36.1", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.36.2", "getmac==0.8.2"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index ecb8c1f35d2..6c44736fa6d 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["async_upnp_client", "yeelight"], "quality_scale": "platinum", - "requirements": ["yeelight==0.7.13", "async-upnp-client==0.36.1"], + "requirements": ["yeelight==0.7.13", "async-upnp-client==0.36.2"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1295f177468..9bbed054257 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -2,7 +2,7 @@ aiodiscover==1.5.1 aiohttp==3.8.5 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.36.1 +async-upnp-client==0.36.2 atomicwrites-homeassistant==1.4.1 attrs==23.1.0 awesomeversion==23.8.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7cfa083d273..a4b7548384c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -458,7 +458,7 @@ async-interrupt==1.1.1 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.36.1 +async-upnp-client==0.36.2 # homeassistant.components.keyboard_remote asyncinotify==4.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 578a7750e85..871084d8630 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -412,7 +412,7 @@ async-interrupt==1.1.1 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.36.1 +async-upnp-client==0.36.2 # homeassistant.components.sleepiq asyncsleepiq==1.3.7 From d5862c350af20e1cb5405c3972dc12717be81450 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Oct 2023 13:24:52 +0200 Subject: [PATCH 81/81] Bumped version to 2023.10.5 --- 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 def053f90cd..6756b8f3dc7 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 = "4" +PATCH_VERSION: Final = "5" __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 a20ce5bff6c..d3754f7af52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.4" +version = "2023.10.5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"