From 95275482072c84c75be55a887f096651860bfb17 Mon Sep 17 00:00:00 2001 From: Excentyl <35411484+Excentyl@users.noreply.github.com> Date: Fri, 8 Dec 2023 16:46:08 +0000 Subject: [PATCH 001/106] Initialize energy_state without price (#97031) Co-authored-by: Erik --- homeassistant/components/energy/sensor.py | 5 + tests/components/energy/test_sensor.py | 108 ++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index e9760a96aa4..834a9bbb1eb 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -317,6 +317,11 @@ class EnergyCostSensor(SensorEntity): try: energy_price = float(energy_price_state.state) except ValueError: + if self._last_energy_sensor_state is None: + # Initialize as it's the first time all required entities except + # price are in place. This means that the cost will update the first + # time the energy is updated after the price entity is in place. + self._reset(energy_state) return energy_price_unit: str | None = energy_price_state.attributes.get( diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index f4a1f661f9b..522bbe5af06 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -877,6 +877,114 @@ async def test_cost_sensor_handle_price_units( assert state.state == "20.0" +async def test_cost_sensor_handle_late_price_sensor( + setup_integration, + hass: HomeAssistant, + hass_storage: dict[str, Any], +) -> None: + """Test energy cost where the price sensor is not immediately available.""" + energy_attributes = { + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + } + price_attributes = { + ATTR_UNIT_OF_MEASUREMENT: f"EUR/{UnitOfEnergy.KILO_WATT_HOUR}", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + } + energy_data = data.EnergyManager.default_preferences() + energy_data["energy_sources"].append( + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.energy_consumption", + "stat_cost": None, + "entity_energy_price": "sensor.energy_price", + "number_energy_price": None, + } + ], + "flow_to": [], + "cost_adjustment_day": 0, + } + ) + + hass_storage[data.STORAGE_KEY] = { + "version": 1, + "data": energy_data, + } + + # Initial state: 10kWh, price sensor not yet available + hass.states.async_set("sensor.energy_price", "unknown", price_attributes) + hass.states.async_set( + "sensor.energy_consumption", + 10, + energy_attributes, + ) + + await setup_integration(hass) + + state = hass.states.get("sensor.energy_consumption_cost") + assert state.state == "0.0" + + # Energy use bumped by 10 kWh, price sensor still not yet available + hass.states.async_set( + "sensor.energy_consumption", + 20, + energy_attributes, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_consumption_cost") + assert state.state == "0.0" + + # Energy use bumped by 10 kWh, price sensor now available + hass.states.async_set("sensor.energy_price", "1", price_attributes) + hass.states.async_set( + "sensor.energy_consumption", + 30, + energy_attributes, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_consumption_cost") + assert state.state == "20.0" + + # Energy use bumped by 10 kWh, price sensor available + hass.states.async_set( + "sensor.energy_consumption", + 40, + energy_attributes, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_consumption_cost") + assert state.state == "30.0" + + # Energy use bumped by 10 kWh, price sensor no longer available + hass.states.async_set("sensor.energy_price", "unknown", price_attributes) + hass.states.async_set( + "sensor.energy_consumption", + 50, + energy_attributes, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_consumption_cost") + assert state.state == "30.0" + + # Energy use bumped by 10 kWh, price sensor again available + hass.states.async_set("sensor.energy_price", "2", price_attributes) + hass.states.async_set( + "sensor.energy_consumption", + 60, + energy_attributes, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_consumption_cost") + assert state.state == "70.0" + + @pytest.mark.parametrize( "unit", (UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS), From 24f0e927f343eae3efe3d75dd20bb90b4dd8662e Mon Sep 17 00:00:00 2001 From: Matrix Date: Thu, 7 Dec 2023 14:30:27 +0800 Subject: [PATCH 002/106] Bump yolink-api to 0.3.4 (#105124) * Bump yolink-api to 0.3.3 * bump yolink api to 0.3.4 --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index 7322c58ae04..a42687a3551 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.3.1"] + "requirements": ["yolink-api==0.3.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index fda92edee3f..c52eae68202 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2792,7 +2792,7 @@ yeelight==0.7.14 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.3.1 +yolink-api==0.3.4 # homeassistant.components.youless youless-api==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 675cfa7c646..be09931ff33 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2090,7 +2090,7 @@ yalexs==1.10.0 yeelight==0.7.14 # homeassistant.components.yolink -yolink-api==0.3.1 +yolink-api==0.3.4 # homeassistant.components.youless youless-api==1.0.1 From 53497e3fadb5c0d91c1b6cd2c3bb3df9d2ff05b7 Mon Sep 17 00:00:00 2001 From: TJ Horner Date: Wed, 6 Dec 2023 09:36:46 -0800 Subject: [PATCH 003/106] Bump apple_weatherkit to 1.1.2 (#105140) --- homeassistant/components/weatherkit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weatherkit/manifest.json b/homeassistant/components/weatherkit/manifest.json index a2ddde02ad4..a6dd40d5993 100644 --- a/homeassistant/components/weatherkit/manifest.json +++ b/homeassistant/components/weatherkit/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/weatherkit", "iot_class": "cloud_polling", - "requirements": ["apple_weatherkit==1.1.1"] + "requirements": ["apple_weatherkit==1.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index c52eae68202..197c6919d8a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -437,7 +437,7 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.weatherkit -apple_weatherkit==1.1.1 +apple_weatherkit==1.1.2 # homeassistant.components.apprise apprise==1.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be09931ff33..9e74cb091ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -401,7 +401,7 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.weatherkit -apple_weatherkit==1.1.1 +apple_weatherkit==1.1.2 # homeassistant.components.apprise apprise==1.6.0 From 3972d8fc0099780b73b0d18ad39b9a0276adfaec Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 7 Dec 2023 09:35:22 +0100 Subject: [PATCH 004/106] Correct smtp error message string (#105148) --- homeassistant/components/smtp/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/smtp/strings.json b/homeassistant/components/smtp/strings.json index 38dd81ac196..37250fa6447 100644 --- a/homeassistant/components/smtp/strings.json +++ b/homeassistant/components/smtp/strings.json @@ -7,7 +7,7 @@ }, "exceptions": { "remote_path_not_allowed": { - "message": "Cannot send email with attachment '{file_name} form directory '{file_path} which is not secure to load data from. Only folders added to `{allow_list}` are accessible. See {url} for more information." + "message": "Cannot send email with attachment \"{file_name}\" from directory \"{file_path}\" which is not secure to load data from. Only folders added to `{allow_list}` are accessible. See {url} for more information." } } } From b977fd6ab201868fc530e2f9a9d6e2add185b024 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 7 Dec 2023 09:35:22 +0100 Subject: [PATCH 005/106] Correct smtp error message string (#105148) From 47032d055c6b1f3af4b92d0235b1edf99115bdd3 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 7 Dec 2023 02:22:03 -0600 Subject: [PATCH 006/106] Expose todo entities to Assist by default (#105150) --- homeassistant/components/homeassistant/exposed_entities.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/homeassistant/exposed_entities.py b/homeassistant/components/homeassistant/exposed_entities.py index 16a7ee5009c..926ab5025f6 100644 --- a/homeassistant/components/homeassistant/exposed_entities.py +++ b/homeassistant/components/homeassistant/exposed_entities.py @@ -38,6 +38,7 @@ DEFAULT_EXPOSED_DOMAINS = { "scene", "script", "switch", + "todo", "vacuum", "water_heater", } From a2f9ffe50fa029058039718e3e115e89f819c0d1 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Thu, 7 Dec 2023 09:33:33 +0100 Subject: [PATCH 007/106] Disable scenarios (scenes) for local API in Overkiz (#105153) --- homeassistant/components/overkiz/__init__.py | 20 ++++++++----------- homeassistant/components/overkiz/strings.json | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/overkiz/__init__.py b/homeassistant/components/overkiz/__init__.py index ebc3f96a7f5..03a81f67308 100644 --- a/homeassistant/components/overkiz/__init__.py +++ b/homeassistant/components/overkiz/__init__.py @@ -1,10 +1,8 @@ """The Overkiz (by Somfy) integration.""" from __future__ import annotations -import asyncio from collections import defaultdict from dataclasses import dataclass -from typing import cast from aiohttp import ClientError from pyoverkiz.client import OverkizClient @@ -16,7 +14,7 @@ from pyoverkiz.exceptions import ( NotSuchTokenException, TooManyRequestsException, ) -from pyoverkiz.models import Device, OverkizServer, Scenario, Setup +from pyoverkiz.models import Device, OverkizServer, Scenario from pyoverkiz.utils import generate_local_server from homeassistant.config_entries import ConfigEntry @@ -82,13 +80,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await client.login() + setup = await client.get_setup() - setup, scenarios = await asyncio.gather( - *[ - client.get_setup(), - client.get_scenarios(), - ] - ) + # Local API does expose scenarios, but they are not functional. + # Tracked in https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode/issues/21 + if api_type == APIType.CLOUD: + scenarios = await client.get_scenarios() + else: + scenarios = [] except (BadCredentialsException, NotSuchTokenException) as exception: raise ConfigEntryAuthFailed("Invalid authentication") from exception except TooManyRequestsException as exception: @@ -98,9 +97,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except MaintenanceException as exception: raise ConfigEntryNotReady("Server is down for maintenance") from exception - setup = cast(Setup, setup) - scenarios = cast(list[Scenario], scenarios) - coordinator = OverkizDataUpdateCoordinator( hass, LOGGER, diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json index 2a549f1c24d..a756df4d0d6 100644 --- a/homeassistant/components/overkiz/strings.json +++ b/homeassistant/components/overkiz/strings.json @@ -9,7 +9,7 @@ } }, "local_or_cloud": { - "description": "Choose between local or cloud API. Local API supports TaHoma Connexoon, TaHoma v2, and TaHoma Switch. Climate devices are not supported in local API.", + "description": "Choose between local or cloud API. Local API supports TaHoma Connexoon, TaHoma v2, and TaHoma Switch. Climate devices and scenarios are not supported in local API.", "data": { "api_type": "API type" } From cfa85956e1a323f087dcb2c8a342330b48b907b7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 7 Dec 2023 09:19:38 +0100 Subject: [PATCH 008/106] Improve LIDL christmas light detection in deCONZ (#105155) --- homeassistant/components/deconz/light.py | 6 +++--- tests/components/deconz/test_light.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index dc2ed04b4ed..044c9bf203b 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -67,7 +67,7 @@ DECONZ_TO_COLOR_MODE = { LightColorMode.XY: ColorMode.XY, } -TS0601_EFFECTS = [ +XMAS_LIGHT_EFFECTS = [ "carnival", "collide", "fading", @@ -200,8 +200,8 @@ class DeconzBaseLight(DeconzDevice[_LightDeviceT], LightEntity): if device.effect is not None: self._attr_supported_features |= LightEntityFeature.EFFECT self._attr_effect_list = [EFFECT_COLORLOOP] - if device.model_id == "TS0601": - self._attr_effect_list += TS0601_EFFECTS + if device.model_id in ("HG06467", "TS0601"): + self._attr_effect_list = XMAS_LIGHT_EFFECTS @property def color_mode(self) -> str | None: diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 357371e4853..d38c65526c2 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -186,7 +186,6 @@ async def test_no_lights_or_groups( "state": STATE_ON, "attributes": { ATTR_EFFECT_LIST: [ - EFFECT_COLORLOOP, "carnival", "collide", "fading", From b832a692d9b1c57cf297df9e6e95a43e54a5d8c0 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 7 Dec 2023 07:39:37 +0100 Subject: [PATCH 009/106] Bump reolink_aio to 0.8.2 (#105157) --- 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 5ffbc2fb186..e03fa28b7ce 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.8.1"] + "requirements": ["reolink-aio==0.8.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 197c6919d8a..9a211710cde 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2338,7 +2338,7 @@ renault-api==0.2.0 renson-endura-delta==1.6.0 # homeassistant.components.reolink -reolink-aio==0.8.1 +reolink-aio==0.8.2 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e74cb091ef..855d22595f0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1750,7 +1750,7 @@ renault-api==0.2.0 renson-endura-delta==1.6.0 # homeassistant.components.reolink -reolink-aio==0.8.1 +reolink-aio==0.8.2 # homeassistant.components.rflink rflink==0.0.65 From 614e9069c2c79e96e10a5eb77fd6889fed1d8b99 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 7 Dec 2023 14:28:04 -0600 Subject: [PATCH 010/106] Don't return TTS URL in Assist pipeline (#105164) * Don't return TTS URL * Add test for empty queue --- .../components/assist_pipeline/pipeline.py | 10 +-- tests/components/assist_pipeline/test_init.py | 64 +++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 4f2a9a8d99b..ed9029d1c2c 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -9,7 +9,7 @@ from dataclasses import asdict, dataclass, field from enum import StrEnum import logging from pathlib import Path -from queue import Queue +from queue import Empty, Queue from threading import Thread import time from typing import TYPE_CHECKING, Any, Final, cast @@ -1010,8 +1010,8 @@ class PipelineRun: self.tts_engine = engine self.tts_options = tts_options - async def text_to_speech(self, tts_input: str) -> str: - """Run text-to-speech portion of pipeline. Returns URL of TTS audio.""" + async def text_to_speech(self, tts_input: str) -> None: + """Run text-to-speech portion of pipeline.""" self.process_event( PipelineEvent( PipelineEventType.TTS_START, @@ -1058,8 +1058,6 @@ class PipelineRun: PipelineEvent(PipelineEventType.TTS_END, {"tts_output": tts_output}) ) - return tts_media.url - def _capture_chunk(self, audio_bytes: bytes | None) -> None: """Forward audio chunk to various capturing mechanisms.""" if self.debug_recording_queue is not None: @@ -1246,6 +1244,8 @@ def _pipeline_debug_recording_thread_proc( # Chunk of 16-bit mono audio at 16Khz if wav_writer is not None: wav_writer.writeframes(message) + except Empty: + pass # occurs when pipeline has unexpected error except Exception: # pylint: disable=broad-exception-caught _LOGGER.exception("Unexpected error in debug recording thread") finally: diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index 24a4a92536d..882d3a80fb3 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -1,4 +1,5 @@ """Test Voice Assistant init.""" +import asyncio from dataclasses import asdict import itertools as it from pathlib import Path @@ -569,6 +570,69 @@ async def test_pipeline_saved_audio_write_error( ) +async def test_pipeline_saved_audio_empty_queue( + hass: HomeAssistant, + mock_stt_provider: MockSttProvider, + mock_wake_word_provider_entity: MockWakeWordEntity, + init_supporting_components, + snapshot: SnapshotAssertion, +) -> None: + """Test that saved audio thread closes WAV file even if there's an empty queue.""" + with tempfile.TemporaryDirectory() as temp_dir_str: + # Enable audio recording to temporary directory + temp_dir = Path(temp_dir_str) + assert await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {CONF_DEBUG_RECORDING_DIR: temp_dir_str}}, + ) + + def event_callback(event: assist_pipeline.PipelineEvent): + if event.type == "run-end": + # Verify WAV file exists, but contains no data + pipeline_dirs = list(temp_dir.iterdir()) + run_dirs = list(pipeline_dirs[0].iterdir()) + wav_path = next(run_dirs[0].iterdir()) + with wave.open(str(wav_path), "rb") as wav_file: + assert wav_file.getnframes() == 0 + + async def audio_data(): + # Force timeout in _pipeline_debug_recording_thread_proc + await asyncio.sleep(1) + yield b"not used" + + # Wrap original function to time out immediately + _pipeline_debug_recording_thread_proc = ( + assist_pipeline.pipeline._pipeline_debug_recording_thread_proc + ) + + def proc_wrapper(run_recording_dir, queue): + _pipeline_debug_recording_thread_proc( + run_recording_dir, queue, message_timeout=0 + ) + + with patch( + "homeassistant.components.assist_pipeline.pipeline._pipeline_debug_recording_thread_proc", + proc_wrapper, + ): + await assist_pipeline.async_pipeline_from_audio_stream( + hass, + context=Context(), + event_callback=event_callback, + stt_metadata=stt.SpeechMetadata( + language="", + format=stt.AudioFormats.WAV, + codec=stt.AudioCodecs.PCM, + bit_rate=stt.AudioBitRates.BITRATE_16, + sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, + channel=stt.AudioChannels.CHANNEL_MONO, + ), + stt_stream=audio_data(), + start_stage=assist_pipeline.PipelineStage.WAKE_WORD, + end_stage=assist_pipeline.PipelineStage.STT, + ) + + async def test_wake_word_detection_aborted( hass: HomeAssistant, mock_stt_provider: MockSttProvider, From 054ede96631fecd837c9707d7f73170dd76f5814 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 7 Dec 2023 09:18:34 +0100 Subject: [PATCH 011/106] =?UTF-8?q?Bump=20M=C3=A9t=C3=A9o-France=20to=201.?= =?UTF-8?q?3.0=20(#105170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/meteo_france/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index 3b6bb9c3518..567788ec479 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/meteo_france", "iot_class": "cloud_polling", "loggers": ["meteofrance_api"], - "requirements": ["meteofrance-api==1.2.0"] + "requirements": ["meteofrance-api==1.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9a211710cde..e77116b8813 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1234,7 +1234,7 @@ messagebird==1.2.0 meteoalertapi==0.3.0 # homeassistant.components.meteo_france -meteofrance-api==1.2.0 +meteofrance-api==1.3.0 # homeassistant.components.mfi mficlient==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 855d22595f0..96f288d6c44 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -958,7 +958,7 @@ medcom-ble==0.1.1 melnor-bluetooth==0.0.25 # homeassistant.components.meteo_france -meteofrance-api==1.2.0 +meteofrance-api==1.3.0 # homeassistant.components.mfi mficlient==0.3.0 From c035ffb06e9837ee9c5d0dacc5e5184800d189c5 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 7 Dec 2023 07:15:31 +0100 Subject: [PATCH 012/106] Fix ZHA quirk ID custom entities matching all devices (#105184) --- homeassistant/components/zha/core/registries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 4bdedebfff9..87f59f31e9b 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -253,7 +253,7 @@ class MatchRule: else: matches.append(model in self.models) - if self.quirk_ids and quirk_id: + if self.quirk_ids: if callable(self.quirk_ids): matches.append(self.quirk_ids(quirk_id)) else: From 688fab49c35d69faf006731cceeace26965cd77c Mon Sep 17 00:00:00 2001 From: lunmay <28674102+lunmay@users.noreply.github.com> Date: Thu, 7 Dec 2023 07:12:27 +0100 Subject: [PATCH 013/106] Fix missing apostrophe in smtp (#105189) Fix missing apostrophe --- homeassistant/components/smtp/notify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index dcc2f49db0f..87600650551 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -263,8 +263,8 @@ def _attach_file(hass, atch_name, content_id=""): file_name = os.path.basename(atch_name) url = "https://www.home-assistant.io/docs/configuration/basic/" raise ServiceValidationError( - f"Cannot send email with attachment '{file_name} " - f"from directory '{file_path} which is not secure to load data from. " + f"Cannot send email with attachment '{file_name}' " + f"from directory '{file_path}' which is not secure to load data from. " f"Only folders added to `{allow_list}` are accessible. " f"See {url} for more information.", translation_domain=DOMAIN, From 8ffb1479263e1951f725431b4de417eb40ab3960 Mon Sep 17 00:00:00 2001 From: Sebastian Nohn Date: Thu, 7 Dec 2023 07:44:19 +0100 Subject: [PATCH 014/106] Set ping interval to 15 seconds instead of 5 minutes (#105191) set ping interval to a more sane value of 15 seconds instead of 5 minutes. fixes home-assistant/core#105163 --- homeassistant/components/ping/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ping/coordinator.py b/homeassistant/components/ping/coordinator.py index dadd105b606..5fe9d692bc3 100644 --- a/homeassistant/components/ping/coordinator.py +++ b/homeassistant/components/ping/coordinator.py @@ -40,7 +40,7 @@ class PingUpdateCoordinator(DataUpdateCoordinator[PingResult]): hass, _LOGGER, name=f"Ping {ping.ip_address}", - update_interval=timedelta(minutes=5), + update_interval=timedelta(seconds=15), ) async def _async_update_data(self) -> PingResult: From c5d1a0fbe1ecfd5b90eed442a0240c0a46669fdb Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 7 Dec 2023 09:50:21 +0100 Subject: [PATCH 015/106] Increase ping update interval to 30 seconds (#105199) --- homeassistant/components/ping/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ping/coordinator.py b/homeassistant/components/ping/coordinator.py index 5fe9d692bc3..f6bda9693b8 100644 --- a/homeassistant/components/ping/coordinator.py +++ b/homeassistant/components/ping/coordinator.py @@ -40,7 +40,7 @@ class PingUpdateCoordinator(DataUpdateCoordinator[PingResult]): hass, _LOGGER, name=f"Ping {ping.ip_address}", - update_interval=timedelta(seconds=15), + update_interval=timedelta(seconds=30), ) async def _async_update_data(self) -> PingResult: From 119c9c3a6b2ebc2cb73a2b04c03328581ac0214c Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Thu, 7 Dec 2023 10:00:26 +0000 Subject: [PATCH 016/106] Fix bug in roon incremental volume control. (#105201) --- homeassistant/components/roon/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index d6128d26723..dda323c2c2a 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -373,14 +373,14 @@ class RoonDevice(MediaPlayerEntity): def volume_up(self) -> None: """Send new volume_level to device.""" if self._volume_incremental: - self._server.roonapi.change_volume_raw(self.output_id, 1, "relative_step") + self._server.roonapi.change_volume_raw(self.output_id, 1, "relative") else: self._server.roonapi.change_volume_percent(self.output_id, 3) def volume_down(self) -> None: """Send new volume_level to device.""" if self._volume_incremental: - self._server.roonapi.change_volume_raw(self.output_id, -1, "relative_step") + self._server.roonapi.change_volume_raw(self.output_id, -1, "relative") else: self._server.roonapi.change_volume_percent(self.output_id, -3) From f1169e0a0d690f471ee1e311b8aeaa6420f0509b Mon Sep 17 00:00:00 2001 From: haimn Date: Thu, 7 Dec 2023 19:03:07 +0200 Subject: [PATCH 017/106] fix supportedFanOscillationModes is null (#105205) * fix supportedFanOscillationModes is null * set default supported_swings to None * return None if no fan oscillation modes listed --- homeassistant/components/smartthings/climate.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 16558d2c795..b97ca06a471 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -497,14 +497,16 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): """Return the unit of measurement.""" return UNIT_MAP[self._device.status.attributes[Attribute.temperature].unit] - def _determine_swing_modes(self) -> list[str]: + def _determine_swing_modes(self) -> list[str] | None: """Return the list of available swing modes.""" + supported_swings = None supported_modes = self._device.status.attributes[ Attribute.supported_fan_oscillation_modes ][0] - supported_swings = [ - FAN_OSCILLATION_TO_SWING.get(m, SWING_OFF) for m in supported_modes - ] + if supported_modes is not None: + supported_swings = [ + FAN_OSCILLATION_TO_SWING.get(m, SWING_OFF) for m in supported_modes + ] return supported_swings async def async_set_swing_mode(self, swing_mode: str) -> None: From d679764d3b699934e231d41ebef52ee6db74719c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Dec 2023 13:25:23 +0100 Subject: [PATCH 018/106] Disable config flow progress in peco config flow (#105222) --- homeassistant/components/peco/config_flow.py | 18 ++---------- tests/components/peco/test_config_flow.py | 30 -------------------- 2 files changed, 2 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/peco/config_flow.py b/homeassistant/components/peco/config_flow.py index 261cdb031bf..144495ec066 100644 --- a/homeassistant/components/peco/config_flow.py +++ b/homeassistant/components/peco/config_flow.py @@ -33,7 +33,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - meter_verification: bool = False meter_data: dict[str, str] = {} meter_error: dict[str, str] = {} @@ -53,17 +52,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except HttpError: self.meter_error = {"phone_number": "http_error", "type": "error"} - self.hass.async_create_task( - self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) - ) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the initial step.""" - if self.meter_verification is True: - return self.async_show_progress_done(next_step_id="finish_smart_meter") - if user_input is None: return self.async_show_form( step_id="user", @@ -86,20 +78,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(f"{county}-{phone_number}") self._abort_if_unique_id_configured() - self.meter_verification = True - if self.meter_error is not None: # Clear any previous errors, since the user may have corrected them self.meter_error = {} - self.hass.async_create_task(self._verify_meter(phone_number)) + await self._verify_meter(phone_number) self.meter_data = user_input - return self.async_show_progress( - step_id="user", - progress_action="verifying_meter", - ) + return await self.async_step_finish_smart_meter() async def async_step_finish_smart_meter( self, user_input: dict[str, Any] | None = None @@ -107,7 +94,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the finish smart meter step.""" if "phone_number" in self.meter_error: if self.meter_error["type"] == "error": - self.meter_verification = False return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, diff --git a/tests/components/peco/test_config_flow.py b/tests/components/peco/test_config_flow.py index ca6759baeff..9ce87d707ff 100644 --- a/tests/components/peco/test_config_flow.py +++ b/tests/components/peco/test_config_flow.py @@ -78,12 +78,6 @@ async def test_meter_value_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "user" - assert result["progress_action"] == "verifying_meter" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"phone_number": "invalid_phone_number"} @@ -107,12 +101,6 @@ async def test_incompatible_meter_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "user" - assert result["progress_action"] == "verifying_meter" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT assert result["reason"] == "incompatible_meter" @@ -135,12 +123,6 @@ async def test_unresponsive_meter_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "user" - assert result["progress_action"] == "verifying_meter" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"phone_number": "unresponsive_meter"} @@ -164,12 +146,6 @@ async def test_meter_http_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "user" - assert result["progress_action"] == "verifying_meter" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"phone_number": "http_error"} @@ -193,12 +169,6 @@ async def test_smart_meter(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "user" - assert result["progress_action"] == "verifying_meter" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Philadelphia - 1234567890" assert result["data"]["phone_number"] == "1234567890" From c6187ed9144180224b4fccdd3e51bb0bd25497c5 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 8 Dec 2023 09:33:24 +0100 Subject: [PATCH 019/106] Fix Fritzbox light setup (#105232) --- homeassistant/components/fritzbox/light.py | 46 ++++++++++++---------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py index d31ccd180c4..8dc51e59738 100644 --- a/homeassistant/components/fritzbox/light.py +++ b/homeassistant/components/fritzbox/light.py @@ -38,11 +38,9 @@ async def async_setup_entry( FritzboxLight( coordinator, ain, - device.get_colors(), - device.get_color_temps(), ) for ain in coordinator.new_devices - if (device := coordinator.data.devices[ain]).has_lightbulb + if (coordinator.data.devices[ain]).has_lightbulb ) entry.async_on_unload(coordinator.async_add_listener(_add_entities)) @@ -57,27 +55,10 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity): self, coordinator: FritzboxDataUpdateCoordinator, ain: str, - supported_colors: dict, - supported_color_temps: list[int], ) -> None: """Initialize the FritzboxLight entity.""" super().__init__(coordinator, ain, None) - - if supported_color_temps: - # only available for color bulbs - self._attr_max_color_temp_kelvin = int(max(supported_color_temps)) - self._attr_min_color_temp_kelvin = int(min(supported_color_temps)) - - # Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each. - # Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup self._supported_hs: dict[int, list[int]] = {} - for values in supported_colors.values(): - hue = int(values[0][0]) - self._supported_hs[hue] = [ - int(values[0][1]), - int(values[1][1]), - int(values[2][1]), - ] @property def is_on(self) -> bool: @@ -173,3 +154,28 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity): """Turn the light off.""" await self.hass.async_add_executor_job(self.data.set_state_off) await self.coordinator.async_refresh() + + async def async_added_to_hass(self) -> None: + """Get light attributes from device after entity is added to hass.""" + await super().async_added_to_hass() + supported_colors = await self.hass.async_add_executor_job( + self.coordinator.data.devices[self.ain].get_colors + ) + supported_color_temps = await self.hass.async_add_executor_job( + self.coordinator.data.devices[self.ain].get_color_temps + ) + + if supported_color_temps: + # only available for color bulbs + self._attr_max_color_temp_kelvin = int(max(supported_color_temps)) + self._attr_min_color_temp_kelvin = int(min(supported_color_temps)) + + # Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each. + # Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup + for values in supported_colors.values(): + hue = int(values[0][0]) + self._supported_hs[hue] = [ + int(values[0][1]), + int(values[1][1]), + int(values[2][1]), + ] From 3a10ea18921eb22069d3fb9cb0cfaf8c01e921fb Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 7 Dec 2023 20:04:39 +0100 Subject: [PATCH 020/106] Fix check_date service in workday (#105241) * Fix check_date service in workday * Add test --- .../components/workday/binary_sensor.py | 21 ++++++++++++------- .../components/workday/test_binary_sensor.py | 12 +++++++++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 9cc96db7a57..2d1030c6b92 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -209,21 +209,26 @@ class IsWorkdaySensor(BinarySensorEntity): async def async_update(self) -> None: """Get date and look whether it is a holiday.""" + self._attr_is_on = self.date_is_workday(dt_util.now()) + + async def check_date(self, check_date: date) -> ServiceResponse: + """Service to check if date is workday or not.""" + return {"workday": self.date_is_workday(check_date)} + + def date_is_workday(self, check_date: date) -> bool: + """Check if date is workday.""" # Default is no workday - self._attr_is_on = False + is_workday = False # Get ISO day of the week (1 = Monday, 7 = Sunday) - adjusted_date = dt_util.now() + timedelta(days=self._days_offset) + adjusted_date = check_date + timedelta(days=self._days_offset) day = adjusted_date.isoweekday() - 1 day_of_week = ALLOWED_DAYS[day] if self.is_include(day_of_week, adjusted_date): - self._attr_is_on = True + is_workday = True if self.is_exclude(day_of_week, adjusted_date): - self._attr_is_on = False + is_workday = False - async def check_date(self, check_date: date) -> ServiceResponse: - """Check if date is workday or not.""" - holiday_date = check_date in self._obj_holidays - return {"workday": not holiday_date} + return is_workday diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index 7457d2e0ada..a359d83d87d 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -316,6 +316,18 @@ async def test_check_date_service( ) assert response == {"binary_sensor.workday_sensor": {"workday": True}} + response = await hass.services.async_call( + DOMAIN, + SERVICE_CHECK_DATE, + { + "entity_id": "binary_sensor.workday_sensor", + "check_date": date(2022, 12, 17), # Saturday (no workday) + }, + blocking=True, + return_response=True, + ) + assert response == {"binary_sensor.workday_sensor": {"workday": False}} + async def test_language_difference_english_language( hass: HomeAssistant, From f8d9c4c3adc5466f4de2a72ac79de6dbef57b707 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Fri, 8 Dec 2023 16:01:22 +0800 Subject: [PATCH 021/106] Fix AsusWrt invalid data type with tuple type (#105247) --- homeassistant/components/asuswrt/bridge.py | 6 ++++-- tests/components/asuswrt/conftest.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/asuswrt/bridge.py b/homeassistant/components/asuswrt/bridge.py index 83f99ecc76a..228da7f1a36 100644 --- a/homeassistant/components/asuswrt/bridge.py +++ b/homeassistant/components/asuswrt/bridge.py @@ -55,7 +55,9 @@ _LOGGER = logging.getLogger(__name__) _AsusWrtBridgeT = TypeVar("_AsusWrtBridgeT", bound="AsusWrtBridge") -_FuncType = Callable[[_AsusWrtBridgeT], Awaitable[list[Any] | dict[str, Any]]] +_FuncType = Callable[ + [_AsusWrtBridgeT], Awaitable[list[Any] | tuple[Any] | dict[str, Any]] +] _ReturnFuncType = Callable[[_AsusWrtBridgeT], Coroutine[Any, Any, dict[str, Any]]] @@ -81,7 +83,7 @@ def handle_errors_and_zip( if isinstance(data, dict): return dict(zip(keys, list(data.values()))) - if not isinstance(data, list): + if not isinstance(data, (list, tuple)): raise UpdateFailed("Received invalid data type") return dict(zip(keys, data)) diff --git a/tests/components/asuswrt/conftest.py b/tests/components/asuswrt/conftest.py index 0f29c84c820..72cbc37d571 100644 --- a/tests/components/asuswrt/conftest.py +++ b/tests/components/asuswrt/conftest.py @@ -14,9 +14,9 @@ from .common import ASUSWRT_BASE, MOCK_MACS, ROUTER_MAC_ADDR, new_device ASUSWRT_HTTP_LIB = f"{ASUSWRT_BASE}.bridge.AsusWrtHttp" ASUSWRT_LEGACY_LIB = f"{ASUSWRT_BASE}.bridge.AsusWrtLegacy" -MOCK_BYTES_TOTAL = [60000000000, 50000000000] +MOCK_BYTES_TOTAL = 60000000000, 50000000000 MOCK_BYTES_TOTAL_HTTP = dict(enumerate(MOCK_BYTES_TOTAL)) -MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000] +MOCK_CURRENT_TRANSFER_RATES = 20000000, 10000000 MOCK_CURRENT_TRANSFER_RATES_HTTP = dict(enumerate(MOCK_CURRENT_TRANSFER_RATES)) MOCK_LOAD_AVG_HTTP = {"load_avg_1": 1.1, "load_avg_5": 1.2, "load_avg_15": 1.3} MOCK_LOAD_AVG = list(MOCK_LOAD_AVG_HTTP.values()) From 38e01b248f1dfbeb2eb71064ac838f48011f6747 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 7 Dec 2023 19:47:14 +0100 Subject: [PATCH 022/106] Explicit check for None in Discovergy entity if condition (#105248) Fix checking for None in Discovergy --- homeassistant/components/discovergy/sensor.py | 1 + tests/components/discovergy/const.py | 2 +- tests/components/discovergy/snapshots/test_diagnostics.ambr | 2 +- tests/components/discovergy/snapshots/test_sensor.ambr | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/discovergy/sensor.py b/homeassistant/components/discovergy/sensor.py index ed878fbb82e..9648492c2e4 100644 --- a/homeassistant/components/discovergy/sensor.py +++ b/homeassistant/components/discovergy/sensor.py @@ -183,6 +183,7 @@ async def async_setup_entry( for description in sensors for value_key in {description.key, *description.alternative_keys} if description.value_fn(coordinator.data, value_key, description.scale) + is not None ) async_add_entities(entities) diff --git a/tests/components/discovergy/const.py b/tests/components/discovergy/const.py index 6c5428741af..5e596d7970f 100644 --- a/tests/components/discovergy/const.py +++ b/tests/components/discovergy/const.py @@ -67,7 +67,7 @@ LAST_READING = Reading( "energyOut": 55048723044000.0, "energyOut1": 0.0, "energyOut2": 0.0, - "power": 531750.0, + "power": 0.0, "power1": 142680.0, "power2": 138010.0, "power3": 251060.0, diff --git a/tests/components/discovergy/snapshots/test_diagnostics.ambr b/tests/components/discovergy/snapshots/test_diagnostics.ambr index 2a7dd6903af..e8d4eab1909 100644 --- a/tests/components/discovergy/snapshots/test_diagnostics.ambr +++ b/tests/components/discovergy/snapshots/test_diagnostics.ambr @@ -61,7 +61,7 @@ 'energyOut': 55048723044000.0, 'energyOut1': 0.0, 'energyOut2': 0.0, - 'power': 531750.0, + 'power': 0.0, 'power1': 142680.0, 'power2': 138010.0, 'power3': 251060.0, diff --git a/tests/components/discovergy/snapshots/test_sensor.ambr b/tests/components/discovergy/snapshots/test_sensor.ambr index 981d1119a93..2473af5012a 100644 --- a/tests/components/discovergy/snapshots/test_sensor.ambr +++ b/tests/components/discovergy/snapshots/test_sensor.ambr @@ -132,7 +132,7 @@ 'entity_id': 'sensor.electricity_teststrasse_1_total_power', 'last_changed': , 'last_updated': , - 'state': '531.75', + 'state': '0.0', }) # --- # name: test_sensor[gas last transmitted] From 4953a36da8bc6eddf956327e73744026e6dae293 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Fri, 8 Dec 2023 09:39:39 +0100 Subject: [PATCH 023/106] Add migration for old HomeWizard sensors (#105251) Co-authored-by: Franck Nijhof --- .../components/homewizard/__init__.py | 55 +++++++++- homeassistant/components/homewizard/const.py | 3 + homeassistant/components/homewizard/sensor.py | 1 - tests/components/homewizard/test_init.py | 103 ++++++++++++++++++ 4 files changed, 159 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homewizard/__init__.py b/homeassistant/components/homewizard/__init__.py index 036f6c077da..35b303a62e3 100644 --- a/homeassistant/components/homewizard/__init__.py +++ b/homeassistant/components/homewizard/__init__.py @@ -1,12 +1,61 @@ """The Homewizard integration.""" from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import entity_registry as er -from .const import DOMAIN, PLATFORMS +from .const import DOMAIN, LOGGER, PLATFORMS from .coordinator import HWEnergyDeviceUpdateCoordinator as Coordinator +async def _async_migrate_entries( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + """Migrate old entry. + + The HWE-SKT had no total_power_*_kwh in 2023.11, in 2023.12 it does. + But simultaneously, the total_power_*_t1_kwh was removed for HWE-SKT. + + This migration migrates the old unique_id to the new one, if possible. + + Migration can be removed after 2024.6 + """ + entity_registry = er.async_get(hass) + + @callback + def update_unique_id(entry: er.RegistryEntry) -> dict[str, str] | None: + replacements = { + "total_power_import_t1_kwh": "total_power_import_kwh", + "total_power_export_t1_kwh": "total_power_export_kwh", + } + + for old_id, new_id in replacements.items(): + if entry.unique_id.endswith(old_id): + new_unique_id = entry.unique_id.replace(old_id, new_id) + if existing_entity_id := entity_registry.async_get_entity_id( + entry.domain, entry.platform, new_unique_id + ): + LOGGER.debug( + "Cannot migrate to unique_id '%s', already exists for '%s'", + new_unique_id, + existing_entity_id, + ) + return None + LOGGER.debug( + "Migrating entity '%s' unique_id from '%s' to '%s'", + entry.entity_id, + entry.unique_id, + new_unique_id, + ) + return { + "new_unique_id": new_unique_id, + } + + return None + + await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Homewizard from a config entry.""" coordinator = Coordinator(hass) @@ -21,6 +70,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise + await _async_migrate_entries(hass, entry) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator # Abort reauth config flow if active diff --git a/homeassistant/components/homewizard/const.py b/homeassistant/components/homewizard/const.py index ff065592283..d4692ee8bf0 100644 --- a/homeassistant/components/homewizard/const.py +++ b/homeassistant/components/homewizard/const.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta +import logging from homewizard_energy.models import Data, Device, State, System @@ -11,6 +12,8 @@ from homeassistant.const import Platform DOMAIN = "homewizard" PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] +LOGGER = logging.getLogger(__package__) + # Platform config. CONF_API_ENABLED = "api_enabled" CONF_DATA = "data" diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index 78cee9ee6fe..d980e66e0e4 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -436,7 +436,6 @@ async def async_setup_entry( ) -> None: """Initialize sensors.""" coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities( HomeWizardSensorEntity(coordinator, description) for description in SENSORS diff --git a/tests/components/homewizard/test_init.py b/tests/components/homewizard/test_init.py index 7dab8cfbb06..a4893c77f42 100644 --- a/tests/components/homewizard/test_init.py +++ b/tests/components/homewizard/test_init.py @@ -7,7 +7,9 @@ import pytest from homeassistant.components.homewizard.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry @@ -118,3 +120,104 @@ async def test_load_handles_homewizardenergy_exception( ConfigEntryState.SETUP_RETRY, ConfigEntryState.SETUP_ERROR, ) + + +@pytest.mark.parametrize( + ("device_fixture", "old_unique_id", "new_unique_id"), + [ + ( + "HWE-SKT", + "aabbccddeeff_total_power_import_t1_kwh", + "aabbccddeeff_total_power_import_kwh", + ), + ( + "HWE-SKT", + "aabbccddeeff_total_power_export_t1_kwh", + "aabbccddeeff_total_power_export_kwh", + ), + ], +) +@pytest.mark.usefixtures("mock_homewizardenergy") +async def test_sensor_migration( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + old_unique_id: str, + new_unique_id: str, +) -> None: + """Test total power T1 sensors are migrated.""" + mock_config_entry.add_to_hass(hass) + + entity: er.RegistryEntry = entity_registry.async_get_or_create( + domain=Platform.SENSOR, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=mock_config_entry, + ) + + assert entity.unique_id == old_unique_id + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_registry.async_get(entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == new_unique_id + assert entity_migrated.previous_unique_id == old_unique_id + + +@pytest.mark.parametrize( + ("device_fixture", "old_unique_id", "new_unique_id"), + [ + ( + "HWE-SKT", + "aabbccddeeff_total_power_import_t1_kwh", + "aabbccddeeff_total_power_import_kwh", + ), + ( + "HWE-SKT", + "aabbccddeeff_total_power_export_t1_kwh", + "aabbccddeeff_total_power_export_kwh", + ), + ], +) +@pytest.mark.usefixtures("mock_homewizardenergy") +async def test_sensor_migration_does_not_trigger( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + old_unique_id: str, + new_unique_id: str, +) -> None: + """Test total power T1 sensors are not migrated when not possible.""" + mock_config_entry.add_to_hass(hass) + + old_entity: er.RegistryEntry = entity_registry.async_get_or_create( + domain=Platform.SENSOR, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=mock_config_entry, + ) + + new_entity: er.RegistryEntry = entity_registry.async_get_or_create( + domain=Platform.SENSOR, + platform=DOMAIN, + unique_id=new_unique_id, + config_entry=mock_config_entry, + ) + + assert old_entity.unique_id == old_unique_id + assert new_entity.unique_id == new_unique_id + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + entity = entity_registry.async_get(old_entity.entity_id) + assert entity + assert entity.unique_id == old_unique_id + assert entity.previous_unique_id is None + + entity = entity_registry.async_get(new_entity.entity_id) + assert entity + assert entity.unique_id == new_unique_id + assert entity.previous_unique_id is None From d89f6b5eb060db02ad8ba0f54d46112eb11f390d Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 7 Dec 2023 23:11:08 +0200 Subject: [PATCH 024/106] Fix update of uncategorized OurGroceries items (#105255) * Fix update of uncategorized OurGroceries items * Address code review comments --- homeassistant/components/ourgroceries/todo.py | 2 +- tests/components/ourgroceries/test_todo.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ourgroceries/todo.py b/homeassistant/components/ourgroceries/todo.py index 8115066d0fb..5b8d19e5aa1 100644 --- a/homeassistant/components/ourgroceries/todo.py +++ b/homeassistant/components/ourgroceries/todo.py @@ -89,7 +89,7 @@ class OurGroceriesTodoListEntity( if item.summary: api_items = self.coordinator.data[self._list_id]["list"]["items"] category = next( - api_item["categoryId"] + api_item.get("categoryId") for api_item in api_items if api_item["id"] == item.uid ) diff --git a/tests/components/ourgroceries/test_todo.py b/tests/components/ourgroceries/test_todo.py index 65bbff0e601..8686c52d79b 100644 --- a/tests/components/ourgroceries/test_todo.py +++ b/tests/components/ourgroceries/test_todo.py @@ -142,12 +142,20 @@ async def test_update_todo_item_status( @pytest.mark.parametrize( - ("items"), [[{"id": "12345", "name": "Soda", "categoryId": "test_category"}]] + ("items", "category"), + [ + ( + [{"id": "12345", "name": "Soda", "categoryId": "test_category"}], + "test_category", + ), + ([{"id": "12345", "name": "Uncategorized"}], None), + ], ) async def test_update_todo_item_summary( hass: HomeAssistant, setup_integration: None, ourgroceries: AsyncMock, + category: str | None, ) -> None: """Test for updating an item summary.""" @@ -171,7 +179,7 @@ async def test_update_todo_item_summary( ) assert ourgroceries.change_item_on_list args = ourgroceries.change_item_on_list.call_args - assert args.args == ("test_list", "12345", "test_category", "Milk") + assert args.args == ("test_list", "12345", category, "Milk") @pytest.mark.parametrize( From f3bb832b1951c33ba381cddd31ac606c38473911 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Dec 2023 22:00:23 -1000 Subject: [PATCH 025/106] Bump pyunifiprotect to 4.22.0 (#105265) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index ee6f6d05548..045538aa2d1 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.21.0", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.22.0", "unifi-discovery==1.1.7"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index e77116b8813..6dd27800737 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2245,7 +2245,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.21.0 +pyunifiprotect==4.22.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96f288d6c44..1c61589a859 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1681,7 +1681,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.21.0 +pyunifiprotect==4.22.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 53cbde8dcaa49cb431d824b01ad4d3bfb8b24e65 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 7 Dec 2023 19:44:43 -0600 Subject: [PATCH 026/106] Set device id and forward errors to Wyoming satellites (#105266) * Set device id and forward errors * Fix tests --- .../components/wyoming/manifest.json | 2 +- homeassistant/components/wyoming/satellite.py | 12 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../wyoming/snapshots/test_stt.ambr | 2 +- tests/components/wyoming/test_satellite.py | 50 ++++++++++++++++++- 6 files changed, 65 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/wyoming/manifest.json b/homeassistant/components/wyoming/manifest.json index 540aaa9aeac..7174683fd18 100644 --- a/homeassistant/components/wyoming/manifest.json +++ b/homeassistant/components/wyoming/manifest.json @@ -6,6 +6,6 @@ "dependencies": ["assist_pipeline"], "documentation": "https://www.home-assistant.io/integrations/wyoming", "iot_class": "local_push", - "requirements": ["wyoming==1.3.0"], + "requirements": ["wyoming==1.4.0"], "zeroconf": ["_wyoming._tcp.local."] } diff --git a/homeassistant/components/wyoming/satellite.py b/homeassistant/components/wyoming/satellite.py index caf65db115e..0e8e5d62f4b 100644 --- a/homeassistant/components/wyoming/satellite.py +++ b/homeassistant/components/wyoming/satellite.py @@ -9,6 +9,7 @@ import wave from wyoming.asr import Transcribe, Transcript from wyoming.audio import AudioChunk, AudioChunkConverter, AudioStart, AudioStop from wyoming.client import AsyncTcpClient +from wyoming.error import Error from wyoming.pipeline import PipelineStage, RunPipeline from wyoming.satellite import RunSatellite from wyoming.tts import Synthesize, SynthesizeVoice @@ -227,6 +228,7 @@ class WyomingSatellite: end_stage=end_stage, tts_audio_output="wav", pipeline_id=pipeline_id, + device_id=self.device.device_id, ) ) @@ -321,6 +323,16 @@ class WyomingSatellite: if event.data and (tts_output := event.data["tts_output"]): media_id = tts_output["media_id"] self.hass.add_job(self._stream_tts(media_id)) + elif event.type == assist_pipeline.PipelineEventType.ERROR: + # Pipeline error + if event.data: + self.hass.add_job( + self._client.write_event( + Error( + text=event.data["message"], code=event.data["code"] + ).event() + ) + ) async def _connect(self) -> None: """Connect to satellite over TCP.""" diff --git a/requirements_all.txt b/requirements_all.txt index 6dd27800737..839b437a907 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2750,7 +2750,7 @@ wled==0.17.0 wolf-smartset==0.1.11 # homeassistant.components.wyoming -wyoming==1.3.0 +wyoming==1.4.0 # homeassistant.components.xbox xbox-webapi==2.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c61589a859..eb45435be85 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2054,7 +2054,7 @@ wled==0.17.0 wolf-smartset==0.1.11 # homeassistant.components.wyoming -wyoming==1.3.0 +wyoming==1.4.0 # homeassistant.components.xbox xbox-webapi==2.0.11 diff --git a/tests/components/wyoming/snapshots/test_stt.ambr b/tests/components/wyoming/snapshots/test_stt.ambr index 784f89b2ab8..b45b7508b28 100644 --- a/tests/components/wyoming/snapshots/test_stt.ambr +++ b/tests/components/wyoming/snapshots/test_stt.ambr @@ -6,7 +6,7 @@ 'language': 'en', }), 'payload': None, - 'type': 'transcibe', + 'type': 'transcribe', }), dict({ 'data': dict({ diff --git a/tests/components/wyoming/test_satellite.py b/tests/components/wyoming/test_satellite.py index 06ae337a19c..50252007aa5 100644 --- a/tests/components/wyoming/test_satellite.py +++ b/tests/components/wyoming/test_satellite.py @@ -8,6 +8,7 @@ import wave from wyoming.asr import Transcribe, Transcript from wyoming.audio import AudioChunk, AudioStart, AudioStop +from wyoming.error import Error from wyoming.event import Event from wyoming.pipeline import PipelineStage, RunPipeline from wyoming.satellite import RunSatellite @@ -96,6 +97,9 @@ class SatelliteAsyncTcpClient(MockAsyncTcpClient): self.tts_audio_stop_event = asyncio.Event() self.tts_audio_chunk: AudioChunk | None = None + self.error_event = asyncio.Event() + self.error: Error | None = None + self._mic_audio_chunk = AudioChunk( rate=16000, width=2, channels=1, audio=b"chunk" ).event() @@ -135,6 +139,9 @@ class SatelliteAsyncTcpClient(MockAsyncTcpClient): self.tts_audio_chunk_event.set() elif AudioStop.is_type(event.type): self.tts_audio_stop_event.set() + elif Error.is_type(event.type): + self.error = Error.from_event(event) + self.error_event.set() async def read_event(self) -> Event | None: """Receive.""" @@ -175,8 +182,9 @@ async def test_satellite_pipeline(hass: HomeAssistant) -> None: await mock_client.connect_event.wait() await mock_client.run_satellite_event.wait() - mock_run_pipeline.assert_called() + mock_run_pipeline.assert_called_once() event_callback = mock_run_pipeline.call_args.kwargs["event_callback"] + assert mock_run_pipeline.call_args.kwargs.get("device_id") == device.device_id # Start detecting wake word event_callback( @@ -458,3 +466,43 @@ async def test_satellite_disconnect_during_pipeline(hass: HomeAssistant) -> None # Sensor should have been turned off assert not device.is_active + + +async def test_satellite_error_during_pipeline(hass: HomeAssistant) -> None: + """Test satellite error occurring during pipeline run.""" + events = [ + RunPipeline( + start_stage=PipelineStage.WAKE, end_stage=PipelineStage.TTS + ).event(), + ] # no audio chunks after RunPipeline + + with patch( + "homeassistant.components.wyoming.data.load_wyoming_info", + return_value=SATELLITE_INFO, + ), patch( + "homeassistant.components.wyoming.satellite.AsyncTcpClient", + SatelliteAsyncTcpClient(events), + ) as mock_client, patch( + "homeassistant.components.wyoming.satellite.assist_pipeline.async_pipeline_from_audio_stream", + ) as mock_run_pipeline: + await setup_config_entry(hass) + + async with asyncio.timeout(1): + await mock_client.connect_event.wait() + await mock_client.run_satellite_event.wait() + + mock_run_pipeline.assert_called_once() + event_callback = mock_run_pipeline.call_args.kwargs["event_callback"] + event_callback( + assist_pipeline.PipelineEvent( + assist_pipeline.PipelineEventType.ERROR, + {"code": "test code", "message": "test message"}, + ) + ) + + async with asyncio.timeout(1): + await mock_client.error_event.wait() + + assert mock_client.error is not None + assert mock_client.error.text == "test message" + assert mock_client.error.code == "test code" From 892a7c36ca64415417d5948be0b54c771432ee18 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 8 Dec 2023 16:54:02 +0100 Subject: [PATCH 027/106] Fix mqtt json light state updates using deprecated color handling (#105283) --- .../components/mqtt/light/schema_json.py | 3 + tests/components/mqtt/test_light_json.py | 87 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 3d2957f153d..c48ce2c0a80 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -406,6 +406,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): values["color_temp"], self.entity_id, ) + # Allow to switch back to color_temp + if "color" not in values: + self._attr_hs_color = None if self.supported_features and LightEntityFeature.EFFECT: with suppress(KeyError): diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 82b0b3467f4..c5c24c3ae79 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -725,6 +725,93 @@ async def test_controlling_state_via_topic2( ) +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "json", + "name": "test", + "command_topic": "test_light_rgb/set", + "state_topic": "test_light_rgb/set", + "rgb": True, + "color_temp": True, + "brightness": True, + } + } + } + ], +) +async def test_controlling_the_state_with_legacy_color_handling( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test state updates for lights with a legacy color handling.""" + supported_color_modes = ["color_temp", "hs"] + await mqtt_mock_entry() + + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_mode") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("supported_color_modes") == supported_color_modes + assert state.attributes.get("xy_color") is None + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + for _ in range(0, 2): + # Returned state after the light was turned on + # Receiving legacy color mode: rgb. + async_fire_mqtt_message( + hass, + "test_light_rgb/set", + '{ "state": "ON", "brightness": 255, "level": 100, "hue": 16,' + '"saturation": 100, "color": { "r": 255, "g": 67, "b": 0 }, ' + '"bulb_mode": "color", "color_mode": "rgb" }', + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 255 + assert state.attributes.get("color_mode") == "hs" + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") == (15.765, 100.0) + assert state.attributes.get("rgb_color") == (255, 67, 0) + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("xy_color") == (0.674, 0.322) + + # Returned state after the lights color mode was changed + # Receiving legacy color mode: color_temp + async_fire_mqtt_message( + hass, + "test_light_rgb/set", + '{ "state": "ON", "brightness": 255, "level": 100, ' + '"kelvin": 92, "color_temp": 353, "bulb_mode": "white", ' + '"color_mode": "color_temp" }', + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 255 + assert state.attributes.get("color_mode") == "color_temp" + assert state.attributes.get("color_temp") == 353 + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") == (28.125, 61.661) + assert state.attributes.get("rgb_color") == (255, 171, 97) + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("xy_color") == (0.513, 0.386) + + @pytest.mark.parametrize( "hass_config", [ From c24af97514e46bb6eaed9abb277ebfa768d7543f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Fri, 8 Dec 2023 14:42:42 +0100 Subject: [PATCH 028/106] =?UTF-8?q?Always=20set=20=5Fattr=5Fcurrent=5Fopti?= =?UTF-8?q?on=20in=20Nob=C3=B8=20Hub=20select=20entities=20(#105289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Always set _attr_current_option in select entities. --- homeassistant/components/nobo_hub/select.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nobo_hub/select.py b/homeassistant/components/nobo_hub/select.py index b386e158420..2708dd75ffe 100644 --- a/homeassistant/components/nobo_hub/select.py +++ b/homeassistant/components/nobo_hub/select.py @@ -59,7 +59,7 @@ class NoboGlobalSelector(SelectEntity): nobo.API.OVERRIDE_MODE_ECO: "eco", } _attr_options = list(_modes.values()) - _attr_current_option: str + _attr_current_option: str | None = None def __init__(self, hub: nobo, override_type) -> None: """Initialize the global override selector.""" @@ -117,7 +117,7 @@ class NoboProfileSelector(SelectEntity): _attr_should_poll = False _profiles: dict[int, str] = {} _attr_options: list[str] = [] - _attr_current_option: str + _attr_current_option: str | None = None def __init__(self, zone_id: str, hub: nobo) -> None: """Initialize the week profile selector.""" From 1777f6b9356dcbb5d12a1f6d65b51ea8491b7d49 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 8 Dec 2023 16:45:34 +0100 Subject: [PATCH 029/106] Update frontend to 20231208.2 (#105299) --- homeassistant/components/frontend/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/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index af2ea6f9149..2a7ef1396d5 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20231206.0"] + "requirements": ["home-assistant-frontend==20231208.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e8e45a9393e..2ec4c684387 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,7 +26,7 @@ ha-ffmpeg==3.1.0 hass-nabucasa==0.74.0 hassil==1.5.1 home-assistant-bluetooth==1.10.4 -home-assistant-frontend==20231206.0 +home-assistant-frontend==20231208.2 home-assistant-intents==2023.12.05 httpx==0.25.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 839b437a907..f31d734a68c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1014,7 +1014,7 @@ hole==0.8.0 holidays==0.36 # homeassistant.components.frontend -home-assistant-frontend==20231206.0 +home-assistant-frontend==20231208.2 # homeassistant.components.conversation home-assistant-intents==2023.12.05 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb45435be85..55a34af78c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -801,7 +801,7 @@ hole==0.8.0 holidays==0.36 # homeassistant.components.frontend -home-assistant-frontend==20231206.0 +home-assistant-frontend==20231208.2 # homeassistant.components.conversation home-assistant-intents==2023.12.05 From 1e3c154fdf653d95e2c62801de982634358c4f84 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 8 Dec 2023 16:51:07 +0100 Subject: [PATCH 030/106] Add test for energy cost sensor for late price sensor (#105312) From d9b31e984199cfd66d0b5910076a44faf1b30c2b Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 8 Dec 2023 10:05:21 -0600 Subject: [PATCH 031/106] Use area id for context instead of name (#105313) --- .../components/conversation/default_agent.py | 2 +- tests/components/conversation/test_default_agent.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 99ebb4b60b1..aae8f67e1d8 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -649,7 +649,7 @@ class DefaultAgent(AbstractConversationAgent): if device_area is None: return None - return {"area": device_area.name} + return {"area": device_area.id} def _get_error_text( self, response_type: ResponseType, lang_intents: LanguageIntents | None diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index fe94e2d5425..c68ec301280 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -307,8 +307,8 @@ async def test_device_area_context( turn_on_calls = async_mock_service(hass, "light", "turn_on") turn_off_calls = async_mock_service(hass, "light", "turn_off") - area_kitchen = area_registry.async_get_or_create("kitchen") - area_bedroom = area_registry.async_get_or_create("bedroom") + area_kitchen = area_registry.async_get_or_create("Kitchen") + area_bedroom = area_registry.async_get_or_create("Bedroom") # Create 2 lights in each area area_lights = defaultdict(list) @@ -323,7 +323,7 @@ async def test_device_area_context( "off", attributes={ATTR_FRIENDLY_NAME: f"{area.name} light {i}"}, ) - area_lights[area.name].append(light_entity) + area_lights[area.id].append(light_entity) # Create voice satellites in each area entry = MockConfigEntry() @@ -354,6 +354,8 @@ async def test_device_area_context( ) await hass.async_block_till_done() assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.intent is not None + assert result.response.intent.slots["area"]["value"] == area_kitchen.id # Verify only kitchen lights were targeted assert {s.entity_id for s in result.response.matched_states} == { @@ -375,6 +377,8 @@ async def test_device_area_context( ) await hass.async_block_till_done() assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.intent is not None + assert result.response.intent.slots["area"]["value"] == area_bedroom.id # Verify only bedroom lights were targeted assert {s.entity_id for s in result.response.matched_states} == { @@ -396,6 +400,8 @@ async def test_device_area_context( ) await hass.async_block_till_done() assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.intent is not None + assert result.response.intent.slots["area"]["value"] == area_bedroom.id # Verify only bedroom lights were targeted assert {s.entity_id for s in result.response.matched_states} == { From 35954128ad4ad2967f89bced3d13ef69af26a9f5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 8 Dec 2023 18:13:34 +0100 Subject: [PATCH 032/106] Add workaround for orjson not handling subclasses of str (#105314) Co-authored-by: Franck Nijhof --- homeassistant/util/json.py | 14 +++++++++++--- tests/util/test_json.py | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index ac18d43727c..1af35c604eb 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -33,9 +33,17 @@ class SerializationError(HomeAssistantError): """Error serializing the data to JSON.""" -json_loads: Callable[[bytes | bytearray | memoryview | str], JsonValueType] -json_loads = orjson.loads -"""Parse JSON data.""" +def json_loads(__obj: bytes | bytearray | memoryview | str) -> JsonValueType: + """Parse JSON data. + + This adds a workaround for orjson not handling subclasses of str, + https://github.com/ijl/orjson/issues/445. + """ + if type(__obj) in (bytes, bytearray, memoryview, str): + return orjson.loads(__obj) # type:ignore[no-any-return] + if isinstance(__obj, str): + return orjson.loads(str(__obj)) # type:ignore[no-any-return] + return orjson.loads(__obj) # type:ignore[no-any-return] def json_loads_array(__obj: bytes | bytearray | memoryview | str) -> JsonArrayType: diff --git a/tests/util/test_json.py b/tests/util/test_json.py index b3bccf71b58..ff0f1ed8392 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -1,10 +1,12 @@ """Test Home Assistant json utility functions.""" from pathlib import Path +import orjson import pytest from homeassistant.exceptions import HomeAssistantError from homeassistant.util.json import ( + json_loads, json_loads_array, json_loads_object, load_json, @@ -153,3 +155,20 @@ async def test_deprecated_save_json( save_json(fname, TEST_JSON_A) assert "uses save_json from homeassistant.util.json" in caplog.text assert "should be updated to use homeassistant.helpers.json module" in caplog.text + + +async def test_loading_derived_class(): + """Test loading data from classes derived from str.""" + + class MyStr(str): + pass + + class MyBytes(bytes): + pass + + assert json_loads('"abc"') == "abc" + assert json_loads(MyStr('"abc"')) == "abc" + + assert json_loads(b'"abc"') == "abc" + with pytest.raises(orjson.JSONDecodeError): + assert json_loads(MyBytes(b'"abc"')) == "abc" From 9aaff618e206baec5ea6d58dfd6d055bcb228bc9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 8 Dec 2023 19:08:47 +0100 Subject: [PATCH 033/106] Bump version to 2023.12.1 --- 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 8267fd29390..df965001035 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 = 12 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index b6bb8649b03..112a03f5e5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.12.0" +version = "2023.12.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 629731e2ddacdc534679ebeeb26c786b9c4b320e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 8 Dec 2023 21:13:37 +0100 Subject: [PATCH 034/106] Add rollback on exception that needs rollback in SQL (#104948) --- homeassistant/components/sql/sensor.py | 2 ++ tests/components/sql/test_sensor.py | 48 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 3fdc6b2c079..c4e6db4c623 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -362,6 +362,8 @@ class SQLSensor(ManualTriggerSensorEntity): self._query, redact_credentials(str(err)), ) + sess.rollback() + sess.close() return for res in result.mappings(): diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index cb988d3f2d4..cdc9a8e07a6 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -5,6 +5,7 @@ from datetime import timedelta from typing import Any from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from sqlalchemy import text as sql_text from sqlalchemy.exc import SQLAlchemyError @@ -12,6 +13,7 @@ from sqlalchemy.exc import SQLAlchemyError from homeassistant.components.recorder import Recorder from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.sql.const import CONF_QUERY, DOMAIN +from homeassistant.components.sql.sensor import _generate_lambda_stmt from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_ICON, @@ -21,6 +23,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir +from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -570,3 +573,48 @@ async def test_attributes_from_entry_config( assert state.attributes["unit_of_measurement"] == "MiB" assert "device_class" not in state.attributes assert "state_class" not in state.attributes + + +async def test_query_recover_from_rollback( + recorder_mock: Recorder, + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the SQL sensor.""" + config = { + "db_url": "sqlite://", + "query": "SELECT 5 as value", + "column": "value", + "name": "Select value SQL query", + "unique_id": "very_unique_id", + } + await init_integration(hass, config) + platforms = async_get_platforms(hass, "sql") + sql_entity = platforms[0].entities["sensor.select_value_sql_query"] + + state = hass.states.get("sensor.select_value_sql_query") + assert state.state == "5" + assert state.attributes["value"] == 5 + + with patch.object( + sql_entity, + "_lambda_stmt", + _generate_lambda_stmt("Faulty syntax create operational issue"), + ): + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert "sqlite3.OperationalError" in caplog.text + + state = hass.states.get("sensor.select_value_sql_query") + assert state.state == "5" + assert state.attributes.get("value") is None + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("sensor.select_value_sql_query") + assert state.state == "5" + assert state.attributes.get("value") == 5 From 5220afa856031f345a03ea5bda85c9c7ac03977b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 8 Dec 2023 20:57:53 +0100 Subject: [PATCH 035/106] Workaround `to_json` template filter in parsing dict key (#105327) * Work-a-round orjson for `to_json` fiter in case dict key is str subclass * Add option instead * Remove json.dumps work-a-round * Update homeassistant/helpers/template.py * Fix test --------- Co-authored-by: Erik Montnemery --- homeassistant/helpers/template.py | 4 ++++ tests/helpers/test_template.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 721ac8bd5be..df8b1c1e019 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -2125,6 +2125,10 @@ def to_json( option = ( ORJSON_PASSTHROUGH_OPTIONS + # OPT_NON_STR_KEYS is added as a workaround to + # ensure subclasses of str are allowed as dict keys + # See: https://github.com/ijl/orjson/issues/445 + | orjson.OPT_NON_STR_KEYS | (orjson.OPT_INDENT_2 if pretty_print else 0) | (orjson.OPT_SORT_KEYS if sort_keys else 0) ) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 79358ec588d..ce9fd0c036c 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1233,6 +1233,22 @@ def test_to_json(hass: HomeAssistant) -> None: with pytest.raises(TemplateError): template.Template("{{ {'Foo': now()} | to_json }}", hass).async_render() + # Test special case where substring class cannot be rendered + # See: https://github.com/ijl/orjson/issues/445 + class MyStr(str): + pass + + expected_result = '{"mykey1":11.0,"mykey2":"myvalue2","mykey3":["opt3b","opt3a"]}' + test_dict = { + MyStr("mykey2"): "myvalue2", + MyStr("mykey1"): 11.0, + MyStr("mykey3"): ["opt3b", "opt3a"], + } + actual_result = template.Template( + "{{ test_dict | to_json(sort_keys=True) }}", hass + ).async_render(parse_result=False, variables={"test_dict": test_dict}) + assert actual_result == expected_result + def test_to_json_ensure_ascii(hass: HomeAssistant) -> None: """Test the object to JSON string filter.""" From 47dc48ca6695cd8784dabc98795baa8fbe6671df Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 8 Dec 2023 21:15:33 +0100 Subject: [PATCH 036/106] Bump plugwise to v0.34.5 (#105330) --- homeassistant/components/plugwise/climate.py | 10 +++++++ .../components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../plugwise/fixtures/adam_jip/all_data.json | 5 ++++ .../fixtures/adam_jip/device_list.json | 13 ++++++++++ .../all_data.json | 1 + .../device_list.json | 20 ++++++++++++++ .../anna_heatpump_heating/device_list.json | 5 ++++ .../fixtures/m_adam_cooling/all_data.json | 3 +++ .../fixtures/m_adam_cooling/device_list.json | 8 ++++++ .../fixtures/m_adam_heating/all_data.json | 26 +++++++++++++++++++ .../fixtures/m_adam_heating/device_list.json | 8 ++++++ .../m_anna_heatpump_cooling/device_list.json | 5 ++++ .../m_anna_heatpump_idle/device_list.json | 5 ++++ .../fixtures/p1v3_full_option/all_data.json | 1 + .../p1v3_full_option/device_list.json | 1 + .../fixtures/p1v4_442_triple/all_data.json | 1 + .../fixtures/p1v4_442_triple/device_list.json | 1 + .../fixtures/stretch_v31/all_data.json | 1 + .../fixtures/stretch_v31/device_list.json | 10 +++++++ .../plugwise/snapshots/test_diagnostics.ambr | 1 + tests/components/plugwise/test_climate.py | 8 +++--- 23 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 tests/components/plugwise/fixtures/adam_jip/device_list.json create mode 100644 tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/device_list.json create mode 100644 tests/components/plugwise/fixtures/anna_heatpump_heating/device_list.json create mode 100644 tests/components/plugwise/fixtures/m_adam_cooling/device_list.json create mode 100644 tests/components/plugwise/fixtures/m_adam_heating/device_list.json create mode 100644 tests/components/plugwise/fixtures/m_anna_heatpump_cooling/device_list.json create mode 100644 tests/components/plugwise/fixtures/m_anna_heatpump_idle/device_list.json create mode 100644 tests/components/plugwise/fixtures/p1v3_full_option/device_list.json create mode 100644 tests/components/plugwise/fixtures/p1v4_442_triple/device_list.json create mode 100644 tests/components/plugwise/fixtures/stretch_v31/device_list.json diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index efad1b7466b..84e0619773b 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -160,6 +160,16 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): # Keep track of the previous action-mode self._previous_action_mode(self.coordinator) + # Adam provides the hvac_action for each thermostat + if (control_state := self.device.get("control_state")) == "cooling": + return HVACAction.COOLING + if control_state == "heating": + return HVACAction.HEATING + if control_state == "preheating": + return HVACAction.PREHEATING + if control_state == "off": + return HVACAction.IDLE + heater: str = self.coordinator.data.gateway["heater_id"] heater_data = self.coordinator.data.devices[heater] if heater_data["binary_sensors"]["heating_state"]: diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 1373ba40fa3..bb2b428bf19 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["crcmod", "plugwise"], - "requirements": ["plugwise==0.34.3"], + "requirements": ["plugwise==0.34.5"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index f31d734a68c..9094578dd65 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1476,7 +1476,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.34.3 +plugwise==0.34.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55a34af78c0..ae0f9d25a0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1134,7 +1134,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.34.3 +plugwise==0.34.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/fixtures/adam_jip/all_data.json b/tests/components/plugwise/fixtures/adam_jip/all_data.json index dacee20c644..37566e1d39e 100644 --- a/tests/components/plugwise/fixtures/adam_jip/all_data.json +++ b/tests/components/plugwise/fixtures/adam_jip/all_data.json @@ -4,6 +4,7 @@ "active_preset": "no_frost", "available": true, "available_schedules": ["None"], + "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", "hardware": "255", @@ -99,6 +100,7 @@ "active_preset": "home", "available": true, "available_schedules": ["None"], + "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", "hardware": "255", @@ -155,6 +157,7 @@ "active_preset": "home", "available": true, "available_schedules": ["None"], + "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", "hardware": "255", @@ -265,6 +268,7 @@ "active_preset": "home", "available": true, "available_schedules": ["None"], + "control_state": "off", "dev_class": "zone_thermometer", "firmware": "2020-09-01T02:00:00+02:00", "hardware": "1", @@ -300,6 +304,7 @@ "cooling_present": false, "gateway_id": "b5c2386c6f6342669e50fe49dd05b188", "heater_id": "e4684553153b44afbef2200885f379dc", + "item_count": 219, "notifications": {}, "smile_name": "Adam" } diff --git a/tests/components/plugwise/fixtures/adam_jip/device_list.json b/tests/components/plugwise/fixtures/adam_jip/device_list.json new file mode 100644 index 00000000000..049845bc828 --- /dev/null +++ b/tests/components/plugwise/fixtures/adam_jip/device_list.json @@ -0,0 +1,13 @@ +[ + "b5c2386c6f6342669e50fe49dd05b188", + "e4684553153b44afbef2200885f379dc", + "a6abc6a129ee499c88a4d420cc413b47", + "1346fbd8498d4dbcab7e18d51b771f3d", + "833de10f269c4deab58fb9df69901b4e", + "6f3e9d7084214c21b9dfa46f6eeb8700", + "f61f1a2535f54f52ad006a3d18e459ca", + "d4496250d0e942cfa7aea3476e9070d5", + "356b65335e274d769c338223e7af9c33", + "1da4d325838e4ad8aac12177214505c9", + "457ce8414de24596a2d5e7dbc9c7682f" +] diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json index 6e6da1aa272..279fe6b8a43 100644 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json @@ -468,6 +468,7 @@ "cooling_present": false, "gateway_id": "fe799307f1624099878210aa0b9f1475", "heater_id": "90986d591dcd426cae3ec3e8111ff730", + "item_count": 315, "notifications": { "af82e4ccf9c548528166d38e560662a4": { "warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device." diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/device_list.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/device_list.json new file mode 100644 index 00000000000..104a723e463 --- /dev/null +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/device_list.json @@ -0,0 +1,20 @@ +[ + "fe799307f1624099878210aa0b9f1475", + "90986d591dcd426cae3ec3e8111ff730", + "df4a4a8169904cdb9c03d61a21f42140", + "b310b72a0e354bfab43089919b9a88bf", + "a2c3583e0a6349358998b760cea82d2a", + "b59bcebaf94b499ea7d46e4a66fb62d8", + "d3da73bde12a47d5a6b8f9dad971f2ec", + "21f2b542c49845e6bb416884c55778d6", + "78d1126fc4c743db81b61c20e88342a7", + "cd0ddb54ef694e11ac18ed1cbce5dbbd", + "4a810418d5394b3f82727340b91ba740", + "02cf28bfec924855854c544690a609ef", + "a28f588dc4a049a483fd03a30361ad3a", + "6a3bf693d05e48e0b460c815a4fdd09d", + "680423ff840043738f42cc7f1ff97a36", + "f1fee6043d3642a9b0a65297455f008e", + "675416a629f343c495449970e2ca37b5", + "e7693eb9582644e5b865dba8d4447cf1" +] diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/device_list.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/device_list.json new file mode 100644 index 00000000000..ffb8cf62575 --- /dev/null +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/device_list.json @@ -0,0 +1,5 @@ +[ + "015ae9ea3f964e668e490fa39da3870b", + "1cbf783bb11e4a7c8a6843dee3a86927", + "3cb70739631c4d17a86b8b12e8a5161b" +] diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json index 624547155a3..2e1063d14d3 100644 --- a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json @@ -53,6 +53,7 @@ "active_preset": "asleep", "available": true, "available_schedules": ["Weekschema", "Badkamer", "Test"], + "control_state": "cooling", "dev_class": "thermostat", "location": "f2bf9048bef64cc5b6d5110154e33c81", "mode": "cool", @@ -102,6 +103,7 @@ "active_preset": "home", "available": true, "available_schedules": ["Weekschema", "Badkamer", "Test"], + "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-10T02:00:00+02:00", "hardware": "255", @@ -148,6 +150,7 @@ "cooling_present": true, "gateway_id": "da224107914542988a88561b4452b0f6", "heater_id": "056ee145a816487eaa69243c3280f8bf", + "item_count": 145, "notifications": {}, "smile_name": "Adam" } diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/device_list.json b/tests/components/plugwise/fixtures/m_adam_cooling/device_list.json new file mode 100644 index 00000000000..f78b4cd38a9 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_adam_cooling/device_list.json @@ -0,0 +1,8 @@ +[ + "da224107914542988a88561b4452b0f6", + "056ee145a816487eaa69243c3280f8bf", + "ad4838d7d35c4d6ea796ee12ae5aedf8", + "1772a4ea304041adb83f357b751341ff", + "e2f4322d57924fa090fbbc48b3a140dc", + "e8ef2a01ed3b4139a53bf749204fe6b4" +] diff --git a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json index e8a72c9b3fb..81d60bed9d4 100644 --- a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json @@ -1,5 +1,28 @@ { "devices": { + "01234567890abcdefghijklmnopqrstu": { + "available": false, + "dev_class": "thermo_sensor", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "f871b8c4d63549319221e294e4f88074", + "model": "Tom/Floor", + "name": "Tom Badkamer", + "sensors": { + "battery": 99, + "temperature": 18.6, + "temperature_difference": 2.3, + "valve_position": 0.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01" + }, "056ee145a816487eaa69243c3280f8bf": { "available": true, "binary_sensors": { @@ -58,6 +81,7 @@ "active_preset": "asleep", "available": true, "available_schedules": ["Weekschema", "Badkamer", "Test"], + "control_state": "preheating", "dev_class": "thermostat", "location": "f2bf9048bef64cc5b6d5110154e33c81", "mode": "heat", @@ -101,6 +125,7 @@ "active_preset": "home", "available": true, "available_schedules": ["Weekschema", "Badkamer", "Test"], + "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-10T02:00:00+02:00", "hardware": "255", @@ -147,6 +172,7 @@ "cooling_present": false, "gateway_id": "da224107914542988a88561b4452b0f6", "heater_id": "056ee145a816487eaa69243c3280f8bf", + "item_count": 145, "notifications": {}, "smile_name": "Adam" } diff --git a/tests/components/plugwise/fixtures/m_adam_heating/device_list.json b/tests/components/plugwise/fixtures/m_adam_heating/device_list.json new file mode 100644 index 00000000000..f78b4cd38a9 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_adam_heating/device_list.json @@ -0,0 +1,8 @@ +[ + "da224107914542988a88561b4452b0f6", + "056ee145a816487eaa69243c3280f8bf", + "ad4838d7d35c4d6ea796ee12ae5aedf8", + "1772a4ea304041adb83f357b751341ff", + "e2f4322d57924fa090fbbc48b3a140dc", + "e8ef2a01ed3b4139a53bf749204fe6b4" +] diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/device_list.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/device_list.json new file mode 100644 index 00000000000..ffb8cf62575 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/device_list.json @@ -0,0 +1,5 @@ +[ + "015ae9ea3f964e668e490fa39da3870b", + "1cbf783bb11e4a7c8a6843dee3a86927", + "3cb70739631c4d17a86b8b12e8a5161b" +] diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/device_list.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/device_list.json new file mode 100644 index 00000000000..ffb8cf62575 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/device_list.json @@ -0,0 +1,5 @@ +[ + "015ae9ea3f964e668e490fa39da3870b", + "1cbf783bb11e4a7c8a6843dee3a86927", + "3cb70739631c4d17a86b8b12e8a5161b" +] diff --git a/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json b/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json index 0e0b3c51a07..0a47893c077 100644 --- a/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json +++ b/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json @@ -42,6 +42,7 @@ }, "gateway": { "gateway_id": "cd3e822288064775a7c4afcdd70bdda2", + "item_count": 31, "notifications": {}, "smile_name": "Smile P1" } diff --git a/tests/components/plugwise/fixtures/p1v3_full_option/device_list.json b/tests/components/plugwise/fixtures/p1v3_full_option/device_list.json new file mode 100644 index 00000000000..8af35165c7e --- /dev/null +++ b/tests/components/plugwise/fixtures/p1v3_full_option/device_list.json @@ -0,0 +1 @@ +["cd3e822288064775a7c4afcdd70bdda2", "e950c7d5e1ee407a858e2a8b5016c8b3"] diff --git a/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json b/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json index d503bd3a59d..ecda8049163 100644 --- a/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json +++ b/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json @@ -51,6 +51,7 @@ }, "gateway": { "gateway_id": "03e65b16e4b247a29ae0d75a78cb492e", + "item_count": 40, "notifications": { "97a04c0c263049b29350a660b4cdd01e": { "warning": "The Smile P1 is not connected to a smart meter." diff --git a/tests/components/plugwise/fixtures/p1v4_442_triple/device_list.json b/tests/components/plugwise/fixtures/p1v4_442_triple/device_list.json new file mode 100644 index 00000000000..7b301f50924 --- /dev/null +++ b/tests/components/plugwise/fixtures/p1v4_442_triple/device_list.json @@ -0,0 +1 @@ +["03e65b16e4b247a29ae0d75a78cb492e", "b82b6b3322484f2ea4e25e0bd5f3d61f"] diff --git a/tests/components/plugwise/fixtures/stretch_v31/all_data.json b/tests/components/plugwise/fixtures/stretch_v31/all_data.json index 8604aaae10e..6b1012b0d87 100644 --- a/tests/components/plugwise/fixtures/stretch_v31/all_data.json +++ b/tests/components/plugwise/fixtures/stretch_v31/all_data.json @@ -135,6 +135,7 @@ }, "gateway": { "gateway_id": "0000aaaa0000aaaa0000aaaa0000aa00", + "item_count": 83, "notifications": {}, "smile_name": "Stretch" } diff --git a/tests/components/plugwise/fixtures/stretch_v31/device_list.json b/tests/components/plugwise/fixtures/stretch_v31/device_list.json new file mode 100644 index 00000000000..b2c839ae9d3 --- /dev/null +++ b/tests/components/plugwise/fixtures/stretch_v31/device_list.json @@ -0,0 +1,10 @@ +[ + "0000aaaa0000aaaa0000aaaa0000aa00", + "5871317346d045bc9f6b987ef25ee638", + "e1c884e7dede431dadee09506ec4f859", + "aac7b735042c4832ac9ff33aae4f453b", + "cfe95cf3de1948c0b8955125bf754614", + "059e4d03c7a34d278add5c7a4a781d19", + "d950b314e9d8499f968e6db8d82ef78c", + "d03738edfcc947f7b8f4573571d90d2d" +] diff --git a/tests/components/plugwise/snapshots/test_diagnostics.ambr b/tests/components/plugwise/snapshots/test_diagnostics.ambr index 597b9710ec5..29f23a137fb 100644 --- a/tests/components/plugwise/snapshots/test_diagnostics.ambr +++ b/tests/components/plugwise/snapshots/test_diagnostics.ambr @@ -500,6 +500,7 @@ 'cooling_present': False, 'gateway_id': 'fe799307f1624099878210aa0b9f1475', 'heater_id': '90986d591dcd426cae3ec3e8111ff730', + 'item_count': 315, 'notifications': dict({ 'af82e4ccf9c548528166d38e560662a4': dict({ 'warning': "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device.", diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index c14fd802e3b..c5ab3a209c2 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -65,7 +65,7 @@ async def test_adam_2_climate_entity_attributes( state = hass.states.get("climate.anna") assert state assert state.state == HVACMode.HEAT - assert state.attributes["hvac_action"] == "heating" + assert state.attributes["hvac_action"] == "preheating" assert state.attributes["hvac_modes"] == [ HVACMode.OFF, HVACMode.AUTO, @@ -75,7 +75,7 @@ async def test_adam_2_climate_entity_attributes( state = hass.states.get("climate.lisa_badkamer") assert state assert state.state == HVACMode.AUTO - assert state.attributes["hvac_action"] == "heating" + assert state.attributes["hvac_action"] == "idle" assert state.attributes["hvac_modes"] == [ HVACMode.OFF, HVACMode.AUTO, @@ -101,7 +101,7 @@ async def test_adam_3_climate_entity_attributes( data.devices["da224107914542988a88561b4452b0f6"][ "select_regulation_mode" ] = "heating" - data.devices["ad4838d7d35c4d6ea796ee12ae5aedf8"]["mode"] = "heat" + data.devices["ad4838d7d35c4d6ea796ee12ae5aedf8"]["control_state"] = "heating" data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ "cooling_state" ] = False @@ -124,7 +124,7 @@ async def test_adam_3_climate_entity_attributes( data.devices["da224107914542988a88561b4452b0f6"][ "select_regulation_mode" ] = "cooling" - data.devices["ad4838d7d35c4d6ea796ee12ae5aedf8"]["mode"] = "cool" + data.devices["ad4838d7d35c4d6ea796ee12ae5aedf8"]["control_state"] = "cooling" data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ "cooling_state" ] = True From 0fa3ce763e7fad0e0863dadae02fa02f83a2b66c Mon Sep 17 00:00:00 2001 From: Alex Thompson Date: Mon, 11 Dec 2023 10:27:02 -0500 Subject: [PATCH 037/106] Fix Lyric LCC thermostats auto mode (#104853) --- homeassistant/components/lyric/climate.py | 109 +++++++++++++--------- 1 file changed, 66 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index f01e4c4fe55..e2504232c68 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import enum import logging from time import localtime, strftime, time from typing import Any @@ -151,6 +152,13 @@ async def async_setup_entry( ) +class LyricThermostatType(enum.Enum): + """Lyric thermostats are classified as TCC or LCC devices.""" + + TCC = enum.auto() + LCC = enum.auto() + + class LyricClimate(LyricDeviceEntity, ClimateEntity): """Defines a Honeywell Lyric climate entity.""" @@ -201,8 +209,10 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): # Setup supported features if device.changeableValues.thermostatSetpointStatus: self._attr_supported_features = SUPPORT_FLAGS_LCC + self._attr_thermostat_type = LyricThermostatType.LCC else: self._attr_supported_features = SUPPORT_FLAGS_TCC + self._attr_thermostat_type = LyricThermostatType.TCC # Setup supported fan modes if device_fan_modes := device.settings.attributes.get("fan", {}).get( @@ -365,56 +375,69 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): """Set hvac mode.""" _LOGGER.debug("HVAC mode: %s", hvac_mode) try: - if LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL: - # If the system is off, turn it to Heat first then to Auto, - # otherwise it turns to. - # Auto briefly and then reverts to Off (perhaps related to - # heatCoolMode). This is the behavior that happens with the - # native app as well, so likely a bug in the api itself - if HVAC_MODES[self.device.changeableValues.mode] == HVACMode.OFF: - _LOGGER.debug( - "HVAC mode passed to lyric: %s", - HVAC_MODES[LYRIC_HVAC_MODE_COOL], - ) - await self._update_thermostat( - self.location, - self.device, - mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], - autoChangeoverActive=False, - ) - # Sleep 3 seconds before proceeding - await asyncio.sleep(3) - _LOGGER.debug( - "HVAC mode passed to lyric: %s", - HVAC_MODES[LYRIC_HVAC_MODE_HEAT], - ) - await self._update_thermostat( - self.location, - self.device, - mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], - autoChangeoverActive=True, - ) - else: - _LOGGER.debug( - "HVAC mode passed to lyric: %s", - HVAC_MODES[self.device.changeableValues.mode], - ) - await self._update_thermostat( - self.location, self.device, autoChangeoverActive=True - ) - else: + match self._attr_thermostat_type: + case LyricThermostatType.TCC: + await self._async_set_hvac_mode_tcc(hvac_mode) + case LyricThermostatType.LCC: + await self._async_set_hvac_mode_lcc(hvac_mode) + except LYRIC_EXCEPTIONS as exception: + _LOGGER.error(exception) + await self.coordinator.async_refresh() + + async def _async_set_hvac_mode_tcc(self, hvac_mode: HVACMode) -> None: + if LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL: + # If the system is off, turn it to Heat first then to Auto, + # otherwise it turns to. + # Auto briefly and then reverts to Off (perhaps related to + # heatCoolMode). This is the behavior that happens with the + # native app as well, so likely a bug in the api itself + if HVAC_MODES[self.device.changeableValues.mode] == HVACMode.OFF: _LOGGER.debug( - "HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode] + "HVAC mode passed to lyric: %s", + HVAC_MODES[LYRIC_HVAC_MODE_COOL], ) await self._update_thermostat( self.location, self.device, - mode=LYRIC_HVAC_MODES[hvac_mode], + mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], autoChangeoverActive=False, ) - except LYRIC_EXCEPTIONS as exception: - _LOGGER.error(exception) - await self.coordinator.async_refresh() + # Sleep 3 seconds before proceeding + await asyncio.sleep(3) + _LOGGER.debug( + "HVAC mode passed to lyric: %s", + HVAC_MODES[LYRIC_HVAC_MODE_HEAT], + ) + await self._update_thermostat( + self.location, + self.device, + mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], + autoChangeoverActive=True, + ) + else: + _LOGGER.debug( + "HVAC mode passed to lyric: %s", + HVAC_MODES[self.device.changeableValues.mode], + ) + await self._update_thermostat( + self.location, self.device, autoChangeoverActive=True + ) + else: + _LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode]) + await self._update_thermostat( + self.location, + self.device, + mode=LYRIC_HVAC_MODES[hvac_mode], + autoChangeoverActive=False, + ) + + async def _async_set_hvac_mode_lcc(self, hvac_mode: HVACMode) -> None: + _LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode]) + await self._update_thermostat( + self.location, + self.device, + mode=LYRIC_HVAC_MODES[hvac_mode], + ) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset (PermanentHold, HoldUntil, NoHold, VacationHold) mode.""" From 7c0a5baf77ae6e90e99455e8e040397f99c9799f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Dec 2023 00:51:34 -1000 Subject: [PATCH 038/106] Bump zeroconf to 0.128.0 (#104936) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 5eb77b0c41c..8351212f0b8 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.127.0"] + "requirements": ["zeroconf==0.128.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2ec4c684387..7116cdae98a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -57,7 +57,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 yarl==1.9.2 -zeroconf==0.127.0 +zeroconf==0.128.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 9094578dd65..021d1389f6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2810,7 +2810,7 @@ zamg==0.3.3 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.127.0 +zeroconf==0.128.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae0f9d25a0b..2ab6f470703 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2105,7 +2105,7 @@ yt-dlp==2023.11.16 zamg==0.3.3 # homeassistant.components.zeroconf -zeroconf==0.127.0 +zeroconf==0.128.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From c6339cb0c0087dbf93fd37804d089b3e108ae196 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 8 Dec 2023 14:35:01 +0100 Subject: [PATCH 039/106] Fix CI test_invalid_rrule_fix test by freezing the time (#105294) --- tests/components/google/test_calendar.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index a70cd8aee9f..8466f5ad4eb 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -1301,6 +1301,7 @@ async def test_event_differs_timezone( } +@pytest.mark.freeze_time("2023-11-30 12:15:00 +00:00") async def test_invalid_rrule_fix( hass: HomeAssistant, hass_client: ClientSessionGenerator, From 2d1c7164446e033413e0e8f9c48ba4dcd027ff70 Mon Sep 17 00:00:00 2001 From: mkmer Date: Fri, 8 Dec 2023 15:30:41 -0500 Subject: [PATCH 040/106] Add missing configuration for services.yaml in blink (#105310) --- homeassistant/components/blink/services.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml index 95f4d33f91f..f6420e7f004 100644 --- a/homeassistant/components/blink/services.yaml +++ b/homeassistant/components/blink/services.yaml @@ -8,6 +8,10 @@ trigger_camera: domain: camera save_video: + target: + entity: + integration: blink + domain: camera fields: name: required: true @@ -21,6 +25,10 @@ save_video: text: save_recent_clips: + target: + entity: + integration: blink + domain: camera fields: name: required: true @@ -34,6 +42,10 @@ save_recent_clips: text: send_pin: + target: + entity: + integration: blink + domain: camera fields: pin: example: "abc123" From ef17950d76c4e4eb4ba7445c6e5c58aa2590ae81 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Sat, 9 Dec 2023 04:01:40 -0400 Subject: [PATCH 041/106] Bump pyschlage to 2023.12.0 (#105349) Co-authored-by: J. Nick Koston --- homeassistant/components/schlage/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json index 1eb7cb2ab0f..e14a5bc706e 100644 --- a/homeassistant/components/schlage/manifest.json +++ b/homeassistant/components/schlage/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/schlage", "iot_class": "cloud_polling", - "requirements": ["pyschlage==2023.11.0"] + "requirements": ["pyschlage==2023.12.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 021d1389f6f..7fa1835fa7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2026,7 +2026,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2023.11.0 +pyschlage==2023.12.0 # homeassistant.components.sensibo pysensibo==1.0.36 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ab6f470703..c4251ebc218 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1531,7 +1531,7 @@ pyrympro==0.0.7 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2023.11.0 +pyschlage==2023.12.0 # homeassistant.components.sensibo pysensibo==1.0.36 From 15cf732463791e157d9ece8c906506cc741151ce Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 9 Dec 2023 13:49:32 +0100 Subject: [PATCH 042/106] Fix preset modes error in Smartthings (#105375) --- homeassistant/components/smartthings/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index b97ca06a471..f07c293939a 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -528,10 +528,10 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): def _determine_preset_modes(self) -> list[str] | None: """Return a list of available preset modes.""" - supported_modes = self._device.status.attributes[ + supported_modes: list | None = self._device.status.attributes[ "supportedAcOptionalMode" ].value - if WINDFREE in supported_modes: + if supported_modes and WINDFREE in supported_modes: return [WINDFREE] return None From a02faaf489d9f43c178250f7c08929f0680d49dc Mon Sep 17 00:00:00 2001 From: mkmer Date: Sat, 9 Dec 2023 13:18:59 -0500 Subject: [PATCH 043/106] Fix service missing key in Blink (#105387) * fix update service refactor service yaml * Remove leftover target --- homeassistant/components/blink/services.py | 7 +++- homeassistant/components/blink/services.yaml | 44 +++++++++++++------- homeassistant/components/blink/strings.json | 28 ++++++++++++- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/blink/services.py b/homeassistant/components/blink/services.py index 12ac0d3b859..dae2f0ad951 100644 --- a/homeassistant/components/blink/services.py +++ b/homeassistant/components/blink/services.py @@ -25,6 +25,11 @@ from .const import ( ) from .coordinator import BlinkUpdateCoordinator +SERVICE_UPDATE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), + } +) SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema( { vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), @@ -152,7 +157,7 @@ def setup_services(hass: HomeAssistant) -> None: # Register all the above services service_mapping = [ - (blink_refresh, SERVICE_REFRESH, None), + (blink_refresh, SERVICE_REFRESH, SERVICE_UPDATE_SCHEMA), ( async_handle_save_video_service, SERVICE_SAVE_VIDEO, diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml index f6420e7f004..aaecde64353 100644 --- a/homeassistant/components/blink/services.yaml +++ b/homeassistant/components/blink/services.yaml @@ -1,18 +1,28 @@ # Describes the format for available Blink services blink_update: + fields: + device_id: + required: true + selector: + device: + integration: blink + trigger_camera: - target: - entity: - integration: blink - domain: camera + fields: + device_id: + required: true + selector: + device: + integration: blink save_video: - target: - entity: - integration: blink - domain: camera fields: + device_id: + required: true + selector: + device: + integration: blink name: required: true example: "Living Room" @@ -25,11 +35,12 @@ save_video: text: save_recent_clips: - target: - entity: - integration: blink - domain: camera fields: + device_id: + required: true + selector: + device: + integration: blink name: required: true example: "Living Room" @@ -42,11 +53,12 @@ save_recent_clips: text: send_pin: - target: - entity: - integration: blink - domain: camera fields: + device_id: + required: true + selector: + device: + integration: blink pin: example: "abc123" selector: diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index f47f72acb9c..fc0450dc8ea 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -57,11 +57,23 @@ "services": { "blink_update": { "name": "Update", - "description": "Forces a refresh." + "description": "Forces a refresh.", + "fields": { + "device_id": { + "name": "Device ID", + "description": "The Blink device id." + } + } }, "trigger_camera": { "name": "Trigger camera", - "description": "Requests camera to take new image." + "description": "Requests camera to take new image.", + "fields": { + "device_id": { + "name": "Device ID", + "description": "The Blink device id." + } + } }, "save_video": { "name": "Save video", @@ -74,6 +86,10 @@ "filename": { "name": "File name", "description": "Filename to writable path (directory may need to be included in allowlist_external_dirs in config)." + }, + "device_id": { + "name": "Device ID", + "description": "The Blink device id." } } }, @@ -88,6 +104,10 @@ "file_path": { "name": "Output directory", "description": "Directory name of writable path (directory may need to be included in allowlist_external_dirs in config)." + }, + "device_id": { + "name": "Device ID", + "description": "The Blink device id." } } }, @@ -98,6 +118,10 @@ "pin": { "name": "Pin", "description": "PIN received from blink. Leave empty if you only received a verification email." + }, + "device_id": { + "name": "Device ID", + "description": "The Blink device id." } } } From 84c713894b1fb5f968b6f002a64ac504182a71d2 Mon Sep 17 00:00:00 2001 From: Florian B Date: Sun, 10 Dec 2023 17:23:05 +0100 Subject: [PATCH 044/106] Fix adding/updating todo items with due date in CalDAV integration (#105435) * refactor: return date/datetime for due date * fix: explicitly set due date on vTODO component Using `set_due` automatically handles converting the Python-native date/datetime values to the correct representation required by RFC5545. * fix: fix tests with changed due date handling * fix: item.due may not be a str * refactor: keep local timezone of due datetime * refactor: reorder import statement To make ruff happy. * fix: fix false-positive mypy error --- homeassistant/components/caldav/todo.py | 10 +++++----- tests/components/caldav/test_todo.py | 13 +++++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/caldav/todo.py b/homeassistant/components/caldav/todo.py index 1bd24dc542a..b7089c3da65 100644 --- a/homeassistant/components/caldav/todo.py +++ b/homeassistant/components/caldav/todo.py @@ -98,10 +98,7 @@ def _to_ics_fields(item: TodoItem) -> dict[str, Any]: if status := item.status: item_data["status"] = TODO_STATUS_MAP_INV.get(status, "NEEDS-ACTION") if due := item.due: - if isinstance(due, datetime): - item_data["due"] = dt_util.as_utc(due).strftime("%Y%m%dT%H%M%SZ") - else: - item_data["due"] = due.strftime("%Y%m%d") + item_data["due"] = due if description := item.description: item_data["description"] = description return item_data @@ -162,7 +159,10 @@ class WebDavTodoListEntity(TodoListEntity): except (requests.ConnectionError, DAVError) as err: raise HomeAssistantError(f"CalDAV lookup error: {err}") from err vtodo = todo.icalendar_component # type: ignore[attr-defined] - vtodo.update(**_to_ics_fields(item)) + updated_fields = _to_ics_fields(item) + if "due" in updated_fields: + todo.set_due(updated_fields.pop("due")) # type: ignore[attr-defined] + vtodo.update(**updated_fields) try: await self.hass.async_add_executor_job( partial( diff --git a/tests/components/caldav/test_todo.py b/tests/components/caldav/test_todo.py index 6e92f211463..a90529297be 100644 --- a/tests/components/caldav/test_todo.py +++ b/tests/components/caldav/test_todo.py @@ -1,4 +1,5 @@ """The tests for the webdav todo component.""" +from datetime import UTC, date, datetime from typing import Any from unittest.mock import MagicMock, Mock @@ -200,12 +201,16 @@ async def test_supported_components( ), ( {"due_date": "2023-11-18"}, - {"status": "NEEDS-ACTION", "summary": "Cheese", "due": "20231118"}, + {"status": "NEEDS-ACTION", "summary": "Cheese", "due": date(2023, 11, 18)}, {**RESULT_ITEM, "due": "2023-11-18"}, ), ( {"due_datetime": "2023-11-18T08:30:00-06:00"}, - {"status": "NEEDS-ACTION", "summary": "Cheese", "due": "20231118T143000Z"}, + { + "status": "NEEDS-ACTION", + "summary": "Cheese", + "due": datetime(2023, 11, 18, 14, 30, 00, tzinfo=UTC), + }, {**RESULT_ITEM, "due": "2023-11-18T08:30:00-06:00"}, ), ( @@ -311,13 +316,13 @@ async def test_add_item_failure( ), ( {"due_date": "2023-11-18"}, - ["SUMMARY:Cheese", "DUE:20231118"], + ["SUMMARY:Cheese", "DUE;VALUE=DATE:20231118"], "1", {**RESULT_ITEM, "due": "2023-11-18"}, ), ( {"due_datetime": "2023-11-18T08:30:00-06:00"}, - ["SUMMARY:Cheese", "DUE:20231118T143000Z"], + ["SUMMARY:Cheese", "DUE;TZID=America/Regina:20231118T083000"], "1", {**RESULT_ITEM, "due": "2023-11-18T08:30:00-06:00"}, ), From a0e9772f52caae89d70dec1fcb4557d39a77325b Mon Sep 17 00:00:00 2001 From: Jan Schneider Date: Sun, 10 Dec 2023 23:59:54 +0100 Subject: [PATCH 045/106] Check if heat area exists when setting up valve opening and battery sensors in moehlenhoff alpha2 (#105437) Check whether the referenced heat area exists when setting up valve opening and battery sensors --- homeassistant/components/moehlenhoff_alpha2/binary_sensor.py | 1 + homeassistant/components/moehlenhoff_alpha2/sensor.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/moehlenhoff_alpha2/binary_sensor.py b/homeassistant/components/moehlenhoff_alpha2/binary_sensor.py index 8acc88d8314..5cdca72fa55 100644 --- a/homeassistant/components/moehlenhoff_alpha2/binary_sensor.py +++ b/homeassistant/components/moehlenhoff_alpha2/binary_sensor.py @@ -27,6 +27,7 @@ async def async_setup_entry( Alpha2IODeviceBatterySensor(coordinator, io_device_id) for io_device_id, io_device in coordinator.data["io_devices"].items() if io_device["_HEATAREA_ID"] + and io_device["_HEATAREA_ID"] in coordinator.data["heat_areas"] ) diff --git a/homeassistant/components/moehlenhoff_alpha2/sensor.py b/homeassistant/components/moehlenhoff_alpha2/sensor.py index e41c6b041f6..2c2e44f451d 100644 --- a/homeassistant/components/moehlenhoff_alpha2/sensor.py +++ b/homeassistant/components/moehlenhoff_alpha2/sensor.py @@ -25,7 +25,7 @@ async def async_setup_entry( Alpha2HeatControlValveOpeningSensor(coordinator, heat_control_id) for heat_control_id, heat_control in coordinator.data["heat_controls"].items() if heat_control["INUSE"] - and heat_control["_HEATAREA_ID"] + and heat_control["_HEATAREA_ID"] in coordinator.data["heat_areas"] and heat_control.get("ACTOR_PERCENT") is not None ) From 164d0d2085f5ee4c6bbe9c947aa5c3ded0f21c83 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 10 Dec 2023 23:16:06 +0100 Subject: [PATCH 046/106] Bump hatasmota to 0.8.0 (#105440) * Bump hatasmota to 0.8.0 * Keep devices with deep sleep support always available * Add tests --- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/mixins.py | 9 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/tasmota/test_binary_sensor.py | 29 +++++ tests/components/tasmota/test_common.py | 120 ++++++++++++++++++ tests/components/tasmota/test_cover.py | 36 ++++++ tests/components/tasmota/test_fan.py | 27 ++++ tests/components/tasmota/test_light.py | 27 ++++ tests/components/tasmota/test_sensor.py | 38 ++++++ tests/components/tasmota/test_switch.py | 25 ++++ 11 files changed, 313 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 42fc849a2cf..2ce81772774 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.7.3"] + "requirements": ["HATasmota==0.8.0"] } diff --git a/homeassistant/components/tasmota/mixins.py b/homeassistant/components/tasmota/mixins.py index 21030b8c14b..48dbe51fd67 100644 --- a/homeassistant/components/tasmota/mixins.py +++ b/homeassistant/components/tasmota/mixins.py @@ -112,8 +112,11 @@ class TasmotaAvailability(TasmotaEntity): def __init__(self, **kwds: Any) -> None: """Initialize the availability mixin.""" - self._available = False super().__init__(**kwds) + if self._tasmota_entity.deep_sleep_enabled: + self._available = True + else: + self._available = False async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" @@ -122,6 +125,8 @@ class TasmotaAvailability(TasmotaEntity): async_subscribe_connection_status(self.hass, self.async_mqtt_connected) ) await super().async_added_to_hass() + if self._tasmota_entity.deep_sleep_enabled: + await self._tasmota_entity.poll_status() async def availability_updated(self, available: bool) -> None: """Handle updated availability.""" @@ -135,6 +140,8 @@ class TasmotaAvailability(TasmotaEntity): if not self.hass.is_stopping: if not mqtt_connected(self.hass): self._available = False + elif self._tasmota_entity.deep_sleep_enabled: + self._available = True self.async_write_ha_state() @property diff --git a/requirements_all.txt b/requirements_all.txt index 7fa1835fa7e..dc463541355 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -28,7 +28,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.7.3 +HATasmota==0.8.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4251ebc218..2def893d495 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -25,7 +25,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.7.3 +HATasmota==0.8.0 # homeassistant.components.doods # homeassistant.components.generic diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py index 2bfb4a9d5e2..d5f1e4d7101 100644 --- a/tests/components/tasmota/test_binary_sensor.py +++ b/tests/components/tasmota/test_binary_sensor.py @@ -31,6 +31,8 @@ from .test_common import ( help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -313,6 +315,21 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["swc"][0] = 1 + config["swn"][0] = "Test" + await help_test_deep_sleep_availability_when_connection_lost( + hass, mqtt_client_mock, mqtt_mock, Platform.BINARY_SENSOR, config + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -323,6 +340,18 @@ async def test_availability( await help_test_availability(hass, mqtt_mock, Platform.BINARY_SENSOR, config) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability when deep sleep is enabled.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["swc"][0] = 1 + config["swn"][0] = "Test" + await help_test_deep_sleep_availability( + hass, mqtt_mock, Platform.BINARY_SENSOR, config + ) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index a184f650fae..1f414cb4e5a 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -4,6 +4,7 @@ import json from unittest.mock import ANY from hatasmota.const import ( + CONF_DEEP_SLEEP, CONF_MAC, CONF_OFFLINE, CONF_ONLINE, @@ -188,6 +189,76 @@ async def help_test_availability_when_connection_lost( assert state.state != STATE_UNAVAILABLE +async def help_test_deep_sleep_availability_when_connection_lost( + hass, + mqtt_client_mock, + mqtt_mock, + domain, + config, + sensor_config=None, + object_id="tasmota_test", +): + """Test availability after MQTT disconnection when deep sleep is enabled. + + This is a test helper for the TasmotaAvailability mixin. + """ + config[CONF_DEEP_SLEEP] = 1 + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + if sensor_config: + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors", + json.dumps(sensor_config), + ) + await hass.async_block_till_done() + + # Device online + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + # Disconnected from MQTT server -> state changed to unavailable + mqtt_mock.connected = False + await hass.async_add_executor_job(mqtt_client_mock.on_disconnect, None, None, 0) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state == STATE_UNAVAILABLE + + # Reconnected to MQTT server -> state no longer unavailable + mqtt_mock.connected = True + await hass.async_add_executor_job(mqtt_client_mock.on_connect, None, None, None, 0) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + # Receive LWT again + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_online(config), + ) + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_offline(config), + ) + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + async def help_test_availability( hass, mqtt_mock, @@ -236,6 +307,55 @@ async def help_test_availability( assert state.state == STATE_UNAVAILABLE +async def help_test_deep_sleep_availability( + hass, + mqtt_mock, + domain, + config, + sensor_config=None, + object_id="tasmota_test", +): + """Test availability when deep sleep is enabled. + + This is a test helper for the TasmotaAvailability mixin. + """ + config[CONF_DEEP_SLEEP] = 1 + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + if sensor_config: + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors", + json.dumps(sensor_config), + ) + await hass.async_block_till_done() + + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_online(config), + ) + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_offline(config), + ) + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + async def help_test_availability_discovery_update( hass, mqtt_mock, diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index e2bdc8b2ca7..cae65521e21 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -22,6 +22,8 @@ from .test_common import ( help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -663,6 +665,27 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["rl"][0] = 3 + config["rl"][1] = 3 + await help_test_deep_sleep_availability_when_connection_lost( + hass, + mqtt_client_mock, + mqtt_mock, + Platform.COVER, + config, + object_id="test_cover_1", + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -676,6 +699,19 @@ async def test_availability( ) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["rl"][0] = 3 + config["rl"][1] = 3 + await help_test_deep_sleep_availability( + hass, mqtt_mock, Platform.COVER, config, object_id="test_cover_1" + ) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index 2a50e2d43b5..05e3151be2e 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -22,6 +22,8 @@ from .test_common import ( help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -232,6 +234,20 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["if"] = 1 + await help_test_deep_sleep_availability_when_connection_lost( + hass, mqtt_client_mock, mqtt_mock, Platform.FAN, config, object_id="tasmota" + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -243,6 +259,17 @@ async def test_availability( ) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["if"] = 1 + await help_test_deep_sleep_availability( + hass, mqtt_mock, Platform.FAN, config, object_id="tasmota" + ) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 27b7bd1a82a..50f11fb7757 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -22,6 +22,8 @@ from .test_common import ( help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -1669,6 +1671,21 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 1 # 1 channel light (Dimmer) + await help_test_deep_sleep_availability_when_connection_lost( + hass, mqtt_client_mock, mqtt_mock, Platform.LIGHT, config + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -1679,6 +1696,16 @@ async def test_availability( await help_test_availability(hass, mqtt_mock, Platform.LIGHT, config) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 1 # 1 channel light (Dimmer) + await help_test_deep_sleep_availability(hass, mqtt_mock, Platform.LIGHT, config) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 2f50a84ffdd..dc4820779a6 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -28,6 +28,8 @@ from .test_common import ( help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -1222,6 +1224,26 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG) + await help_test_deep_sleep_availability_when_connection_lost( + hass, + mqtt_client_mock, + mqtt_mock, + Platform.SENSOR, + config, + sensor_config, + "tasmota_dht11_temperature", + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -1238,6 +1260,22 @@ async def test_availability( ) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG) + await help_test_deep_sleep_availability( + hass, + mqtt_mock, + Platform.SENSOR, + config, + sensor_config, + "tasmota_dht11_temperature", + ) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: diff --git a/tests/components/tasmota/test_switch.py b/tests/components/tasmota/test_switch.py index 54d94b46fe8..1a16f372fc9 100644 --- a/tests/components/tasmota/test_switch.py +++ b/tests/components/tasmota/test_switch.py @@ -20,6 +20,8 @@ from .test_common import ( help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -158,6 +160,20 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 1 + await help_test_deep_sleep_availability_when_connection_lost( + hass, mqtt_client_mock, mqtt_mock, Platform.SWITCH, config + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -167,6 +183,15 @@ async def test_availability( await help_test_availability(hass, mqtt_mock, Platform.SWITCH, config) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 1 + await help_test_deep_sleep_availability(hass, mqtt_mock, Platform.SWITCH, config) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: From 0023d66c80b5ba4f2848ca0bde0360de0fed6846 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:29:50 +0100 Subject: [PATCH 047/106] Bump plugwise to v0.35.3 (#105442) --- homeassistant/components/plugwise/const.py | 19 ++++++++++++++++++- .../components/plugwise/manifest.json | 2 +- homeassistant/components/plugwise/number.py | 3 +-- homeassistant/components/plugwise/select.py | 3 +-- .../components/plugwise/strings.json | 5 ++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../all_data.json | 19 ++++++++++++------- .../anna_heatpump_heating/all_data.json | 2 +- .../fixtures/m_adam_cooling/all_data.json | 4 ++-- .../fixtures/m_adam_heating/all_data.json | 4 ++-- .../m_anna_heatpump_cooling/all_data.json | 2 +- .../m_anna_heatpump_idle/all_data.json | 2 +- .../plugwise/snapshots/test_diagnostics.ambr | 9 +++++++-- 14 files changed, 53 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index 34bb5c926ae..f5677c0b4a9 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Final +from typing import Final, Literal from homeassistant.const import Platform @@ -36,6 +36,23 @@ ZEROCONF_MAP: Final[dict[str, str]] = { "stretch": "Stretch", } +NumberType = Literal[ + "maximum_boiler_temperature", + "max_dhw_temperature", + "temperature_offset", +] + +SelectType = Literal[ + "select_dhw_mode", + "select_regulation_mode", + "select_schedule", +] +SelectOptionsType = Literal[ + "dhw_modes", + "regulation_modes", + "available_schedules", +] + # Default directives DEFAULT_MAX_TEMP: Final = 30 DEFAULT_MIN_TEMP: Final = 4 diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index bb2b428bf19..92923e98d2c 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["crcmod", "plugwise"], - "requirements": ["plugwise==0.34.5"], + "requirements": ["plugwise==0.35.3"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index 2c87edddf04..c21ecbd94c7 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -5,7 +5,6 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from plugwise import Smile -from plugwise.constants import NumberType from homeassistant.components.number import ( NumberDeviceClass, @@ -18,7 +17,7 @@ from homeassistant.const import EntityCategory, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DOMAIN, NumberType from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index c12ca671554..eef873703c1 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -5,7 +5,6 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from plugwise import Smile -from plugwise.constants import SelectOptionsType, SelectType from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry @@ -13,7 +12,7 @@ from homeassistant.const import STATE_ON, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DOMAIN, SelectOptionsType, SelectType from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 5348a1dc484..addd1ceadb1 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -108,7 +108,10 @@ } }, "select_schedule": { - "name": "Thermostat schedule" + "name": "Thermostat schedule", + "state": { + "off": "Off" + } } }, "sensor": { diff --git a/requirements_all.txt b/requirements_all.txt index dc463541355..e0f764c4817 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1476,7 +1476,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.34.5 +plugwise==0.35.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2def893d495..7f3e4910c1a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1134,7 +1134,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.34.5 +plugwise==0.35.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json index 279fe6b8a43..f97182782e6 100644 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json @@ -112,7 +112,8 @@ "Bios Schema met Film Avond", "GF7 Woonkamer", "Badkamer Schema", - "CV Jessie" + "CV Jessie", + "off" ], "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", @@ -251,7 +252,8 @@ "Bios Schema met Film Avond", "GF7 Woonkamer", "Badkamer Schema", - "CV Jessie" + "CV Jessie", + "off" ], "dev_class": "zone_thermostat", "firmware": "2016-08-02T02:00:00+02:00", @@ -334,7 +336,8 @@ "Bios Schema met Film Avond", "GF7 Woonkamer", "Badkamer Schema", - "CV Jessie" + "CV Jessie", + "off" ], "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", @@ -344,7 +347,7 @@ "model": "Lisa", "name": "Zone Lisa Bios", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "select_schedule": "None", + "select_schedule": "off", "sensors": { "battery": 67, "setpoint": 13.0, @@ -373,7 +376,8 @@ "Bios Schema met Film Avond", "GF7 Woonkamer", "Badkamer Schema", - "CV Jessie" + "CV Jessie", + "off" ], "dev_class": "thermostatic_radiator_valve", "firmware": "2019-03-27T01:00:00+01:00", @@ -383,7 +387,7 @@ "model": "Tom/Floor", "name": "CV Kraan Garage", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "select_schedule": "None", + "select_schedule": "off", "sensors": { "battery": 68, "setpoint": 5.5, @@ -414,7 +418,8 @@ "Bios Schema met Film Avond", "GF7 Woonkamer", "Badkamer Schema", - "CV Jessie" + "CV Jessie", + "off" ], "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json index 9ef93d63bdd..d655f95c79b 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json @@ -59,7 +59,7 @@ }, "3cb70739631c4d17a86b8b12e8a5161b": { "active_preset": "home", - "available_schedules": ["standaard"], + "available_schedules": ["standaard", "off"], "dev_class": "thermostat", "firmware": "2018-02-08T11:15:53+01:00", "hardware": "6539-1301-5002", diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json index 2e1063d14d3..7b570a6cf61 100644 --- a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json @@ -52,7 +52,7 @@ "ad4838d7d35c4d6ea796ee12ae5aedf8": { "active_preset": "asleep", "available": true, - "available_schedules": ["Weekschema", "Badkamer", "Test"], + "available_schedules": ["Weekschema", "Badkamer", "Test", "off"], "control_state": "cooling", "dev_class": "thermostat", "location": "f2bf9048bef64cc5b6d5110154e33c81", @@ -102,7 +102,7 @@ "e2f4322d57924fa090fbbc48b3a140dc": { "active_preset": "home", "available": true, - "available_schedules": ["Weekschema", "Badkamer", "Test"], + "available_schedules": ["Weekschema", "Badkamer", "Test", "off"], "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-10T02:00:00+02:00", diff --git a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json index 81d60bed9d4..57259047698 100644 --- a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json @@ -80,7 +80,7 @@ "ad4838d7d35c4d6ea796ee12ae5aedf8": { "active_preset": "asleep", "available": true, - "available_schedules": ["Weekschema", "Badkamer", "Test"], + "available_schedules": ["Weekschema", "Badkamer", "Test", "off"], "control_state": "preheating", "dev_class": "thermostat", "location": "f2bf9048bef64cc5b6d5110154e33c81", @@ -124,7 +124,7 @@ "e2f4322d57924fa090fbbc48b3a140dc": { "active_preset": "home", "available": true, - "available_schedules": ["Weekschema", "Badkamer", "Test"], + "available_schedules": ["Weekschema", "Badkamer", "Test", "off"], "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-10T02:00:00+02:00", diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json index 844eae4c2f7..92c95f6c5a9 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json @@ -59,7 +59,7 @@ }, "3cb70739631c4d17a86b8b12e8a5161b": { "active_preset": "home", - "available_schedules": ["standaard"], + "available_schedules": ["standaard", "off"], "dev_class": "thermostat", "firmware": "2018-02-08T11:15:53+01:00", "hardware": "6539-1301-5002", diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json index f6be6f35188..be400b9bc98 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json @@ -59,7 +59,7 @@ }, "3cb70739631c4d17a86b8b12e8a5161b": { "active_preset": "home", - "available_schedules": ["standaard"], + "available_schedules": ["standaard", "off"], "dev_class": "thermostat", "firmware": "2018-02-08T11:15:53+01:00", "hardware": "6539-1301-5002", diff --git a/tests/components/plugwise/snapshots/test_diagnostics.ambr b/tests/components/plugwise/snapshots/test_diagnostics.ambr index 29f23a137fb..c2bbea9418a 100644 --- a/tests/components/plugwise/snapshots/test_diagnostics.ambr +++ b/tests/components/plugwise/snapshots/test_diagnostics.ambr @@ -115,6 +115,7 @@ 'GF7 Woonkamer', 'Badkamer Schema', 'CV Jessie', + 'off', ]), 'dev_class': 'zone_thermostat', 'firmware': '2016-10-27T02:00:00+02:00', @@ -260,6 +261,7 @@ 'GF7 Woonkamer', 'Badkamer Schema', 'CV Jessie', + 'off', ]), 'dev_class': 'zone_thermostat', 'firmware': '2016-08-02T02:00:00+02:00', @@ -349,6 +351,7 @@ 'GF7 Woonkamer', 'Badkamer Schema', 'CV Jessie', + 'off', ]), 'dev_class': 'zone_thermostat', 'firmware': '2016-10-27T02:00:00+02:00', @@ -364,7 +367,7 @@ 'vacation', 'no_frost', ]), - 'select_schedule': 'None', + 'select_schedule': 'off', 'sensors': dict({ 'battery': 67, 'setpoint': 13.0, @@ -394,6 +397,7 @@ 'GF7 Woonkamer', 'Badkamer Schema', 'CV Jessie', + 'off', ]), 'dev_class': 'thermostatic_radiator_valve', 'firmware': '2019-03-27T01:00:00+01:00', @@ -409,7 +413,7 @@ 'vacation', 'no_frost', ]), - 'select_schedule': 'None', + 'select_schedule': 'off', 'sensors': dict({ 'battery': 68, 'setpoint': 5.5, @@ -441,6 +445,7 @@ 'GF7 Woonkamer', 'Badkamer Schema', 'CV Jessie', + 'off', ]), 'dev_class': 'zone_thermostat', 'firmware': '2016-10-27T02:00:00+02:00', From 399f98a726ff1a945ed24b045a7caed77b6c9ecb Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 10 Dec 2023 17:27:01 +0100 Subject: [PATCH 048/106] Fix alexa calling not featured cover services (#105444) * Fix alexa calls not supported cover services * Follow up comment and additional tests --- homeassistant/components/alexa/handlers.py | 19 +- tests/components/alexa/test_smart_home.py | 443 ++++++++++++++++----- 2 files changed, 354 insertions(+), 108 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index f99b0231e4d..2796c10795b 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1304,13 +1304,14 @@ async def async_api_set_range( service = None data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} range_value = directive.payload["rangeValue"] + supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) # Cover Position if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": range_value = int(range_value) - if range_value == 0: + if supported & cover.CoverEntityFeature.CLOSE and range_value == 0: service = cover.SERVICE_CLOSE_COVER - elif range_value == 100: + elif supported & cover.CoverEntityFeature.OPEN and range_value == 100: service = cover.SERVICE_OPEN_COVER else: service = cover.SERVICE_SET_COVER_POSITION @@ -1319,9 +1320,9 @@ async def async_api_set_range( # Cover Tilt elif instance == f"{cover.DOMAIN}.tilt": range_value = int(range_value) - if range_value == 0: + if supported & cover.CoverEntityFeature.CLOSE_TILT and range_value == 0: service = cover.SERVICE_CLOSE_COVER_TILT - elif range_value == 100: + elif supported & cover.CoverEntityFeature.OPEN_TILT and range_value == 100: service = cover.SERVICE_OPEN_COVER_TILT else: service = cover.SERVICE_SET_COVER_TILT_POSITION @@ -1332,13 +1333,11 @@ async def async_api_set_range( range_value = int(range_value) if range_value == 0: service = fan.SERVICE_TURN_OFF + elif supported & fan.FanEntityFeature.SET_SPEED: + service = fan.SERVICE_SET_PERCENTAGE + data[fan.ATTR_PERCENTAGE] = range_value else: - supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if supported and fan.FanEntityFeature.SET_SPEED: - service = fan.SERVICE_SET_PERCENTAGE - data[fan.ATTR_PERCENTAGE] = range_value - else: - service = fan.SERVICE_TURN_ON + service = fan.SERVICE_TURN_ON # Humidifier target humidity elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}": diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 7a1abe96110..0a5b1f79f72 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -6,7 +6,7 @@ import pytest from homeassistant.components.alexa import smart_home, state_report import homeassistant.components.camera as camera -from homeassistant.components.cover import CoverDeviceClass +from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature from homeassistant.components.media_player import MediaPlayerEntityFeature from homeassistant.components.vacuum import VacuumEntityFeature from homeassistant.config import async_process_ha_core_config @@ -1884,8 +1884,199 @@ async def test_group(hass: HomeAssistant) -> None: ) -async def test_cover_position_range(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("position", "position_attr_in_service_call", "supported_features", "service_call"), + [ + ( + 30, + 30, + CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE, + "cover.set_cover_position", + ), + ( + 0, + None, + CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE, + "cover.close_cover", + ), + ( + 99, + 99, + CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE, + "cover.set_cover_position", + ), + ( + 100, + None, + CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE, + "cover.open_cover", + ), + ( + 0, + 0, + CoverEntityFeature.SET_POSITION, + "cover.set_cover_position", + ), + ( + 60, + 60, + CoverEntityFeature.SET_POSITION, + "cover.set_cover_position", + ), + ( + 100, + 100, + CoverEntityFeature.SET_POSITION, + "cover.set_cover_position", + ), + ( + 0, + 0, + CoverEntityFeature.SET_POSITION | CoverEntityFeature.OPEN, + "cover.set_cover_position", + ), + ( + 100, + 100, + CoverEntityFeature.SET_POSITION | CoverEntityFeature.CLOSE, + "cover.set_cover_position", + ), + ], + ids=[ + "position_30_open_close", + "position_0_open_close", + "position_99_open_close", + "position_100_open_close", + "position_0_no_open_close", + "position_60_no_open_close", + "position_100_no_open_close", + "position_0_no_close", + "position_100_no_open", + ], +) +async def test_cover_position( + hass: HomeAssistant, + position: int, + position_attr_in_service_call: int | None, + supported_features: CoverEntityFeature, + service_call: str, +) -> None: """Test cover discovery and position using rangeController.""" + device = ( + "cover.test_range", + "open", + { + "friendly_name": "Test cover range", + "device_class": "blind", + "supported_features": supported_features, + "position": position, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "cover#test_range" + assert appliance["displayCategories"][0] == "INTERIOR_BLIND" + assert appliance["friendlyName"] == "Test cover range" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PowerController", + "Alexa.RangeController", + "Alexa.EndpointHealth", + "Alexa", + ) + + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "cover.position" + + properties = range_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "rangeValue"} in properties["supported"] + + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "text", + "value": {"text": "Position", "locale": "en-US"}, + } in capability_resources["friendlyNames"] + + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.Opening"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + assert configuration["unitOfMeasure"] == "Alexa.Unit.Percent" + + supported_range = configuration["supportedRange"] + assert supported_range["minimumValue"] == 0 + assert supported_range["maximumValue"] == 100 + assert supported_range["precision"] == 1 + + # Assert for Position Semantics + position_semantics = range_capability["semantics"] + assert position_semantics is not None + + position_action_mappings = position_semantics["actionMappings"] + assert position_action_mappings is not None + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Lower", "Alexa.Actions.Close"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, + } in position_action_mappings + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Raise", "Alexa.Actions.Open"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, + } in position_action_mappings + + position_state_mappings = position_semantics["stateMappings"] + assert position_state_mappings is not None + assert { + "@type": "StatesToValue", + "states": ["Alexa.States.Closed"], + "value": 0, + } in position_state_mappings + assert { + "@type": "StatesToRange", + "states": ["Alexa.States.Open"], + "range": {"minimumValue": 1, "maximumValue": 100}, + } in position_state_mappings + + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "cover#test_range", + service_call, + hass, + payload={"rangeValue": position}, + instance="cover.position", + ) + assert call.data.get("position") == position_attr_in_service_call + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == position + + +async def test_cover_position_range( + hass: HomeAssistant, +) -> None: + """Test cover discovery and position range using rangeController. + + Also tests an invalid cover position being handled correctly. + """ + device = ( "cover.test_range", "open", @@ -1969,59 +2160,6 @@ async def test_cover_position_range(hass: HomeAssistant) -> None: "range": {"minimumValue": 1, "maximumValue": 100}, } in position_state_mappings - call, _ = await assert_request_calls_service( - "Alexa.RangeController", - "SetRangeValue", - "cover#test_range", - "cover.set_cover_position", - hass, - payload={"rangeValue": 50}, - instance="cover.position", - ) - assert call.data["position"] == 50 - - call, msg = await assert_request_calls_service( - "Alexa.RangeController", - "SetRangeValue", - "cover#test_range", - "cover.close_cover", - hass, - payload={"rangeValue": 0}, - instance="cover.position", - ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 0 - - call, msg = await assert_request_calls_service( - "Alexa.RangeController", - "SetRangeValue", - "cover#test_range", - "cover.open_cover", - hass, - payload={"rangeValue": 100}, - instance="cover.position", - ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 100 - - call, msg = await assert_request_calls_service( - "Alexa.RangeController", - "AdjustRangeValue", - "cover#test_range", - "cover.open_cover", - hass, - payload={"rangeValueDelta": 99, "rangeValueDeltaDefault": False}, - instance="cover.position", - ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 100 - call, msg = await assert_request_calls_service( "Alexa.RangeController", "AdjustRangeValue", @@ -3435,8 +3573,159 @@ async def test_presence_sensor(hass: HomeAssistant) -> None: assert {"name": "humanPresenceDetectionState"} in properties["supported"] -async def test_cover_tilt_position_range(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ( + "tilt_position", + "tilt_position_attr_in_service_call", + "supported_features", + "service_call", + ), + [ + ( + 30, + 30, + CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT, + "cover.set_cover_tilt_position", + ), + ( + 0, + None, + CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT, + "cover.close_cover_tilt", + ), + ( + 99, + 99, + CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT, + "cover.set_cover_tilt_position", + ), + ( + 100, + None, + CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT, + "cover.open_cover_tilt", + ), + ( + 0, + 0, + CoverEntityFeature.SET_TILT_POSITION, + "cover.set_cover_tilt_position", + ), + ( + 60, + 60, + CoverEntityFeature.SET_TILT_POSITION, + "cover.set_cover_tilt_position", + ), + ( + 100, + 100, + CoverEntityFeature.SET_TILT_POSITION, + "cover.set_cover_tilt_position", + ), + ( + 0, + 0, + CoverEntityFeature.SET_TILT_POSITION | CoverEntityFeature.OPEN_TILT, + "cover.set_cover_tilt_position", + ), + ( + 100, + 100, + CoverEntityFeature.SET_TILT_POSITION | CoverEntityFeature.CLOSE_TILT, + "cover.set_cover_tilt_position", + ), + ], + ids=[ + "tilt_position_30_open_close", + "tilt_position_0_open_close", + "tilt_position_99_open_close", + "tilt_position_100_open_close", + "tilt_position_0_no_open_close", + "tilt_position_60_no_open_close", + "tilt_position_100_no_open_close", + "tilt_position_0_no_close", + "tilt_position_100_no_open", + ], +) +async def test_cover_tilt_position( + hass: HomeAssistant, + tilt_position: int, + tilt_position_attr_in_service_call: int | None, + supported_features: CoverEntityFeature, + service_call: str, +) -> None: """Test cover discovery and tilt position using rangeController.""" + device = ( + "cover.test_tilt_range", + "open", + { + "friendly_name": "Test cover tilt range", + "device_class": "blind", + "supported_features": supported_features, + "tilt_position": tilt_position, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "cover#test_tilt_range" + assert appliance["displayCategories"][0] == "INTERIOR_BLIND" + assert appliance["friendlyName"] == "Test cover tilt range" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PowerController", + "Alexa.RangeController", + "Alexa.EndpointHealth", + "Alexa", + ) + + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "cover.tilt" + + semantics = range_capability["semantics"] + assert semantics is not None + + action_mappings = semantics["actionMappings"] + assert action_mappings is not None + + state_mappings = semantics["stateMappings"] + assert state_mappings is not None + + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "cover#test_tilt_range", + service_call, + hass, + payload={"rangeValue": tilt_position}, + instance="cover.tilt", + ) + assert call.data.get("tilt_position") == tilt_position_attr_in_service_call + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == tilt_position + + +async def test_cover_tilt_position_range(hass: HomeAssistant) -> None: + """Test cover discovery and tilt position range using rangeController. + + Also tests and invalid tilt position being handled correctly. + """ device = ( "cover.test_tilt_range", "open", @@ -3485,48 +3774,6 @@ async def test_cover_tilt_position_range(hass: HomeAssistant) -> None: ) assert call.data["tilt_position"] == 50 - call, msg = await assert_request_calls_service( - "Alexa.RangeController", - "SetRangeValue", - "cover#test_tilt_range", - "cover.close_cover_tilt", - hass, - payload={"rangeValue": 0}, - instance="cover.tilt", - ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 0 - - call, msg = await assert_request_calls_service( - "Alexa.RangeController", - "SetRangeValue", - "cover#test_tilt_range", - "cover.open_cover_tilt", - hass, - payload={"rangeValue": 100}, - instance="cover.tilt", - ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 100 - - call, msg = await assert_request_calls_service( - "Alexa.RangeController", - "AdjustRangeValue", - "cover#test_tilt_range", - "cover.open_cover_tilt", - hass, - payload={"rangeValueDelta": 99, "rangeValueDeltaDefault": False}, - instance="cover.tilt", - ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 100 - call, msg = await assert_request_calls_service( "Alexa.RangeController", "AdjustRangeValue", From 0683b8be21d5ab95ec2262b7dc7ef21a78fde418 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 10 Dec 2023 14:09:58 -0800 Subject: [PATCH 049/106] Fix fitbit oauth reauth debug logging (#105450) --- homeassistant/components/fitbit/application_credentials.py | 5 ++++- tests/components/fitbit/test_init.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fitbit/application_credentials.py b/homeassistant/components/fitbit/application_credentials.py index caf0384eca2..caa47351f45 100644 --- a/homeassistant/components/fitbit/application_credentials.py +++ b/homeassistant/components/fitbit/application_credentials.py @@ -60,7 +60,10 @@ class FitbitOAuth2Implementation(AuthImplementation): resp.raise_for_status() except aiohttp.ClientResponseError as err: if _LOGGER.isEnabledFor(logging.DEBUG): - error_body = await resp.text() if not session.closed else "" + try: + error_body = await resp.text() + except aiohttp.ClientError: + error_body = "" _LOGGER.debug( "Client response error status=%s, body=%s", err.status, error_body ) diff --git a/tests/components/fitbit/test_init.py b/tests/components/fitbit/test_init.py index b6bf75c1c69..3ed3695ff3d 100644 --- a/tests/components/fitbit/test_init.py +++ b/tests/components/fitbit/test_init.py @@ -107,18 +107,21 @@ async def test_token_refresh_success( @pytest.mark.parametrize("token_expiration_time", [12345]) +@pytest.mark.parametrize("closing", [True, False]) async def test_token_requires_reauth( hass: HomeAssistant, integration_setup: Callable[[], Awaitable[bool]], config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, setup_credentials: None, + closing: bool, ) -> None: """Test where token is expired and the refresh attempt requires reauth.""" aioclient_mock.post( OAUTH2_TOKEN, status=HTTPStatus.UNAUTHORIZED, + closing=closing, ) assert not await integration_setup() From 3b7add533b5e75895491fb8030d5f19eb24e3b2c Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:22:10 +0100 Subject: [PATCH 050/106] Write Enphase Envoy data to log when in debug mode (#105456) --- homeassistant/components/enphase_envoy/coordinator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index 75f2ef39289..02a9d2f2491 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -144,7 +144,10 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): if not self._setup_complete: await self._async_setup_and_authenticate() self._async_mark_setup_complete() - return (await envoy.update()).raw + # dump all received data in debug mode to assist troubleshooting + envoy_data = await envoy.update() + _LOGGER.debug("Envoy data: %s", envoy_data) + return envoy_data.raw except INVALID_AUTH_ERRORS as err: if self._setup_complete and tries == 0: # token likely expired or firmware changed, try to re-authenticate From 557ce965cf928fda32dfaf0b953d805ae8985293 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 10 Dec 2023 15:46:27 -0800 Subject: [PATCH 051/106] Bump ical to 6.1.1 (#105462) --- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index d7b16ee3bef..f5a24e07b0c 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==6.1.0"] + "requirements": ["ical==6.1.1"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 4c3a8e10a62..335a89eab3c 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==6.1.0"] + "requirements": ["ical==6.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index e0f764c4817..89ad5de43bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1054,7 +1054,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==6.1.0 +ical==6.1.1 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f3e4910c1a..b35c8b19882 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -832,7 +832,7 @@ ibeacon-ble==1.0.1 # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==6.1.0 +ical==6.1.1 # homeassistant.components.ping icmplib==3.0 From ba84af93743417033e0b688a46bbbd696beb0a3e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Dec 2023 20:47:53 -1000 Subject: [PATCH 052/106] Bump zeroconf to 0.128.4 (#105465) * Bump zeroconf to 0.128.3 significant bug fixes changelog: https://github.com/python-zeroconf/python-zeroconf/compare/0.128.0...0.128.3 * .4 --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 8351212f0b8..6738431b304 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.128.0"] + "requirements": ["zeroconf==0.128.4"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7116cdae98a..7d5f53f8f56 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -57,7 +57,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 yarl==1.9.2 -zeroconf==0.128.0 +zeroconf==0.128.4 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 89ad5de43bb..e4355ec9161 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2810,7 +2810,7 @@ zamg==0.3.3 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.128.0 +zeroconf==0.128.4 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b35c8b19882..0962ef50a52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2105,7 +2105,7 @@ yt-dlp==2023.11.16 zamg==0.3.3 # homeassistant.components.zeroconf -zeroconf==0.128.0 +zeroconf==0.128.4 # homeassistant.components.zeversolar zeversolar==0.3.1 From 8a0b1637b1a482b81e090bf657b1e42cdf6a1d75 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 11 Dec 2023 12:09:43 +0100 Subject: [PATCH 053/106] Remove Aftership import issue when entry already exists (#105476) --- .../components/aftership/config_flow.py | 23 +++---------------- .../components/aftership/strings.json | 4 ---- .../components/aftership/test_config_flow.py | 10 +++++--- 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/aftership/config_flow.py b/homeassistant/components/aftership/config_flow.py index 3da6ac9e3d5..94578091501 100644 --- a/homeassistant/components/aftership/config_flow.py +++ b/homeassistant/components/aftership/config_flow.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -51,25 +51,6 @@ class AfterShipConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import(self, config: dict[str, Any]) -> FlowResult: """Import configuration from yaml.""" - try: - self._async_abort_entries_match({CONF_API_KEY: config[CONF_API_KEY]}) - except AbortFlow as err: - async_create_issue( - self.hass, - DOMAIN, - "deprecated_yaml_import_issue_already_configured", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_import_issue_already_configured", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "AfterShip", - }, - ) - raise err - async_create_issue( self.hass, HOMEASSISTANT_DOMAIN, @@ -84,6 +65,8 @@ class AfterShipConfigFlow(ConfigFlow, domain=DOMAIN): "integration_title": "AfterShip", }, ) + + self._async_abort_entries_match({CONF_API_KEY: config[CONF_API_KEY]}) return self.async_create_entry( title=config.get(CONF_NAME, "AfterShip"), data={CONF_API_KEY: config[CONF_API_KEY]}, diff --git a/homeassistant/components/aftership/strings.json b/homeassistant/components/aftership/strings.json index b49c19976a6..ace8eb6d2d3 100644 --- a/homeassistant/components/aftership/strings.json +++ b/homeassistant/components/aftership/strings.json @@ -49,10 +49,6 @@ } }, "issues": { - "deprecated_yaml_import_issue_already_configured": { - "title": "The {integration_title} YAML configuration import failed", - "description": "Configuring {integration_title} using YAML is being removed but the YAML configuration was already imported.\n\nRemove the YAML configuration and restart Home Assistant." - }, "deprecated_yaml_import_issue_cannot_connect": { "title": "The {integration_title} YAML configuration import failed", "description": "Configuring {integration_title} using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." diff --git a/tests/components/aftership/test_config_flow.py b/tests/components/aftership/test_config_flow.py index 2ac5919a555..4668e7a61e4 100644 --- a/tests/components/aftership/test_config_flow.py +++ b/tests/components/aftership/test_config_flow.py @@ -77,7 +77,9 @@ async def test_flow_cannot_connect(hass: HomeAssistant, mock_setup_entry) -> Non } -async def test_import_flow(hass: HomeAssistant, mock_setup_entry) -> None: +async def test_import_flow( + hass: HomeAssistant, issue_registry: ir.IssueRegistry, mock_setup_entry +) -> None: """Test importing yaml config.""" with patch( @@ -95,11 +97,12 @@ async def test_import_flow(hass: HomeAssistant, mock_setup_entry) -> None: assert result["data"] == { CONF_API_KEY: "yaml-api-key", } - issue_registry = ir.async_get(hass) assert len(issue_registry.issues) == 1 -async def test_import_flow_already_exists(hass: HomeAssistant) -> None: +async def test_import_flow_already_exists( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: """Test importing yaml config where entry already exists.""" entry = MockConfigEntry(domain=DOMAIN, data={CONF_API_KEY: "yaml-api-key"}) entry.add_to_hass(hass) @@ -108,3 +111,4 @@ async def test_import_flow_already_exists(hass: HomeAssistant) -> None: ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" + assert len(issue_registry.issues) == 1 From 48498844678126c1c1472758155170a5cf919ca6 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 11 Dec 2023 10:18:46 -0600 Subject: [PATCH 054/106] Disconnect before reconnecting to satellite (#105500) Disconnect before reconnecting --- homeassistant/components/wyoming/satellite.py | 26 ++++++++++++++++--- tests/components/wyoming/test_satellite.py | 23 ++++++++++++---- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/wyoming/satellite.py b/homeassistant/components/wyoming/satellite.py index 0e8e5d62f4b..45d882dd1e2 100644 --- a/homeassistant/components/wyoming/satellite.py +++ b/homeassistant/components/wyoming/satellite.py @@ -70,11 +70,11 @@ class WyomingSatellite: while self.is_running: try: # Check if satellite has been disabled - if not self.device.is_enabled: + while not self.device.is_enabled: await self.on_disabled() if not self.is_running: # Satellite was stopped while waiting to be enabled - break + return # Connect and run pipeline loop await self._run_once() @@ -86,7 +86,7 @@ class WyomingSatellite: # Ensure sensor is off self.device.set_is_active(False) - await self.on_stopped() + await self.on_stopped() def stop(self) -> None: """Signal satellite task to stop running.""" @@ -129,6 +129,7 @@ class WyomingSatellite: self._audio_queue.put_nowait(None) self._enabled_changed_event.set() + self._enabled_changed_event.clear() def _pipeline_changed(self) -> None: """Run when device pipeline changes.""" @@ -243,9 +244,17 @@ class WyomingSatellite: chunk = AudioChunk.from_event(client_event) chunk = self._chunk_converter.convert(chunk) self._audio_queue.put_nowait(chunk.audio) + elif AudioStop.is_type(client_event.type): + # Stop pipeline + _LOGGER.debug("Client requested pipeline to stop") + self._audio_queue.put_nowait(b"") + break else: _LOGGER.debug("Unexpected event from satellite: %s", client_event) + # Ensure task finishes + await _pipeline_task + _LOGGER.debug("Pipeline finished") def _event_callback(self, event: assist_pipeline.PipelineEvent) -> None: @@ -336,12 +345,23 @@ class WyomingSatellite: async def _connect(self) -> None: """Connect to satellite over TCP.""" + await self._disconnect() + _LOGGER.debug( "Connecting to satellite at %s:%s", self.service.host, self.service.port ) self._client = AsyncTcpClient(self.service.host, self.service.port) await self._client.connect() + async def _disconnect(self) -> None: + """Disconnect if satellite is currently connected.""" + if self._client is None: + return + + _LOGGER.debug("Disconnecting from satellite") + await self._client.disconnect() + self._client = None + async def _stream_tts(self, media_id: str) -> None: """Stream TTS WAV audio to satellite in chunks.""" assert self._client is not None diff --git a/tests/components/wyoming/test_satellite.py b/tests/components/wyoming/test_satellite.py index 50252007aa5..83e4d98d971 100644 --- a/tests/components/wyoming/test_satellite.py +++ b/tests/components/wyoming/test_satellite.py @@ -322,11 +322,12 @@ async def test_satellite_disabled(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry, service: WyomingService ): satellite = original_make_satellite(hass, config_entry, service) - satellite.device.is_enabled = False + satellite.device.set_is_enabled(False) return satellite async def on_disabled(self): + self.device.set_is_enabled(True) on_disabled_event.set() with patch( @@ -368,11 +369,19 @@ async def test_satellite_restart(hass: HomeAssistant) -> None: async def test_satellite_reconnect(hass: HomeAssistant) -> None: """Test satellite reconnect call after connection refused.""" - on_reconnect_event = asyncio.Event() + num_reconnects = 0 + reconnect_event = asyncio.Event() + stopped_event = asyncio.Event() async def on_reconnect(self): - self.stop() - on_reconnect_event.set() + nonlocal num_reconnects + num_reconnects += 1 + if num_reconnects >= 2: + reconnect_event.set() + self.stop() + + async def on_stopped(self): + stopped_event.set() with patch( "homeassistant.components.wyoming.data.load_wyoming_info", @@ -383,10 +392,14 @@ async def test_satellite_reconnect(hass: HomeAssistant) -> None: ), patch( "homeassistant.components.wyoming.satellite.WyomingSatellite.on_reconnect", on_reconnect, + ), patch( + "homeassistant.components.wyoming.satellite.WyomingSatellite.on_stopped", + on_stopped, ): await setup_config_entry(hass) async with asyncio.timeout(1): - await on_reconnect_event.wait() + await reconnect_event.wait() + await stopped_event.wait() async def test_satellite_disconnect_before_pipeline(hass: HomeAssistant) -> None: From 9d0fed85394c8628ae114bb39f06f3af2bf82bc6 Mon Sep 17 00:00:00 2001 From: "Julien \"_FrnchFrgg_\" Rivaud" Date: Tue, 12 Dec 2023 04:42:52 +0100 Subject: [PATCH 055/106] Bump caldav to 1.3.8 (#105508) * Bump caldav to 1.3.8 1.3.8 fixes a bug where duplicate STATUS properties would be emitted for a single VTODO depending on the case of the arguments used. That bug meant that even though that is the intended API usage, passing lowercase for the status argument name would be rejected by caldav servers checking conformance with the spec which forbids duplicate STATUS. This in turn prevented HomeAssistant to add new items to a caldav todo list. Bump the requirements to 1.3.8 to repair that feature * Update global requirements --- homeassistant/components/caldav/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index a7365515758..619523ae7a1 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/caldav", "iot_class": "cloud_polling", "loggers": ["caldav", "vobject"], - "requirements": ["caldav==1.3.6"] + "requirements": ["caldav==1.3.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index e4355ec9161..cb9e04ea638 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -604,7 +604,7 @@ btsmarthub-devicelist==0.2.3 buienradar==1.0.5 # homeassistant.components.caldav -caldav==1.3.6 +caldav==1.3.8 # homeassistant.components.circuit circuit-webhook==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0962ef50a52..f6817910405 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -503,7 +503,7 @@ bthome-ble==3.2.0 buienradar==1.0.5 # homeassistant.components.caldav -caldav==1.3.6 +caldav==1.3.8 # homeassistant.components.coinbase coinbase==2.1.0 From fbb315b0a89583899199618ed33994775fd282c2 Mon Sep 17 00:00:00 2001 From: Khole <29937485+KJonline@users.noreply.github.com> Date: Tue, 12 Dec 2023 07:38:12 +0000 Subject: [PATCH 056/106] Bump pyhiveapi to v0.5.16 (#105513) Co-authored-by: Khole Jones --- homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 67da3617b44..870223f8fe6 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -9,5 +9,5 @@ }, "iot_class": "cloud_polling", "loggers": ["apyhiveapi"], - "requirements": ["pyhiveapi==0.5.14"] + "requirements": ["pyhiveapi==0.5.16"] } diff --git a/requirements_all.txt b/requirements_all.txt index cb9e04ea638..3ec2df6ae86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1775,7 +1775,7 @@ pyhaversion==22.8.0 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.14 +pyhiveapi==0.5.16 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6817910405..1610539940d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1340,7 +1340,7 @@ pyhaversion==22.8.0 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.14 +pyhiveapi==0.5.16 # homeassistant.components.homematic pyhomematic==0.1.77 From 8b3ba452bc9e073143fdb1fd474d1fe28ee5f798 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 12 Dec 2023 20:43:09 +0100 Subject: [PATCH 057/106] Fix setup Fast.com (#105580) * Fix setup fastdotcom * Add if --- homeassistant/components/fastdotcom/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index 2fe5b3ccafc..56f9ba4fd5f 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -35,15 +35,16 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup_platform(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Fast.com component. (deprecated).""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config[DOMAIN], + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config[DOMAIN], + ) ) - ) return True From 8812ca13d9af64bcdc13bdede64d24c378cb9707 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 12 Dec 2023 21:54:15 -0600 Subject: [PATCH 058/106] Add name slot to HassClimateGetTemperature intent (#105585) --- homeassistant/components/climate/intent.py | 16 +++++++++++++++- tests/components/climate/test_intent.py | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/intent.py b/homeassistant/components/climate/intent.py index 23cc3d5bcd2..4152fb5ee2d 100644 --- a/homeassistant/components/climate/intent.py +++ b/homeassistant/components/climate/intent.py @@ -21,7 +21,7 @@ class GetTemperatureIntent(intent.IntentHandler): """Handle GetTemperature intents.""" intent_type = INTENT_GET_TEMPERATURE - slot_schema = {vol.Optional("area"): str} + slot_schema = {vol.Optional("area"): str, vol.Optional("name"): str} async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the intent.""" @@ -49,6 +49,20 @@ class GetTemperatureIntent(intent.IntentHandler): if climate_state is None: raise intent.IntentHandleError(f"No climate entity in area {area_name}") + climate_entity = component.get_entity(climate_state.entity_id) + elif "name" in slots: + # Filter by name + entity_name = slots["name"]["value"] + + for maybe_climate in intent.async_match_states( + hass, name=entity_name, domains=[DOMAIN] + ): + climate_state = maybe_climate + break + + if climate_state is None: + raise intent.IntentHandleError(f"No climate entity named {entity_name}") + climate_entity = component.get_entity(climate_state.entity_id) else: # First entity diff --git a/tests/components/climate/test_intent.py b/tests/components/climate/test_intent.py index eaf7029d303..6473eca1b88 100644 --- a/tests/components/climate/test_intent.py +++ b/tests/components/climate/test_intent.py @@ -153,7 +153,7 @@ async def test_get_temperature( state = response.matched_states[0] assert state.attributes["current_temperature"] == 10.0 - # Select by area instead (climate_2) + # Select by area (climate_2) response = await intent.async_handle( hass, "test", @@ -166,6 +166,19 @@ async def test_get_temperature( state = response.matched_states[0] assert state.attributes["current_temperature"] == 22.0 + # Select by name (climate_2) + response = await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"name": {"value": "Climate 2"}}, + ) + assert response.response_type == intent.IntentResponseType.QUERY_ANSWER + assert len(response.matched_states) == 1 + assert response.matched_states[0].entity_id == climate_2.entity_id + state = response.matched_states[0] + assert state.attributes["current_temperature"] == 22.0 + async def test_get_temperature_no_entities( hass: HomeAssistant, From 797af140120ba25ee76b96594cf39dd63f16833b Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 12 Dec 2023 23:21:16 -0600 Subject: [PATCH 059/106] Skip TTS events entirely with empty text (#105617) --- .../components/assist_pipeline/pipeline.py | 60 ++++++++++--------- .../snapshots/test_websocket.ambr | 28 +++++++-- .../assist_pipeline/test_websocket.py | 11 ++-- 3 files changed, 59 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index ed9029d1c2c..26d599da836 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -369,6 +369,7 @@ class PipelineStage(StrEnum): STT = "stt" INTENT = "intent" TTS = "tts" + END = "end" PIPELINE_STAGE_ORDER = [ @@ -1024,35 +1025,32 @@ class PipelineRun: ) ) - if tts_input := tts_input.strip(): - try: - # Synthesize audio and get URL - tts_media_id = tts_generate_media_source_id( - self.hass, - tts_input, - engine=self.tts_engine, - language=self.pipeline.tts_language, - options=self.tts_options, - ) - tts_media = await media_source.async_resolve_media( - self.hass, - tts_media_id, - None, - ) - except Exception as src_error: - _LOGGER.exception("Unexpected error during text-to-speech") - raise TextToSpeechError( - code="tts-failed", - message="Unexpected error during text-to-speech", - ) from src_error + try: + # Synthesize audio and get URL + tts_media_id = tts_generate_media_source_id( + self.hass, + tts_input, + engine=self.tts_engine, + language=self.pipeline.tts_language, + options=self.tts_options, + ) + tts_media = await media_source.async_resolve_media( + self.hass, + tts_media_id, + None, + ) + except Exception as src_error: + _LOGGER.exception("Unexpected error during text-to-speech") + raise TextToSpeechError( + code="tts-failed", + message="Unexpected error during text-to-speech", + ) from src_error - _LOGGER.debug("TTS result %s", tts_media) - tts_output = { - "media_id": tts_media_id, - **asdict(tts_media), - } - else: - tts_output = {} + _LOGGER.debug("TTS result %s", tts_media) + tts_output = { + "media_id": tts_media_id, + **asdict(tts_media), + } self.process_event( PipelineEvent(PipelineEventType.TTS_END, {"tts_output": tts_output}) @@ -1345,7 +1343,11 @@ class PipelineInput: self.conversation_id, self.device_id, ) - current_stage = PipelineStage.TTS + if tts_input.strip(): + current_stage = PipelineStage.TTS + else: + # Skip TTS + current_stage = PipelineStage.END if self.run.end_stage != PipelineStage.INTENT: # text-to-speech diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 072b1ff730a..c165675a6ff 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -662,15 +662,33 @@ # --- # name: test_pipeline_empty_tts_output.1 dict({ - 'engine': 'test', - 'language': 'en-US', - 'tts_input': '', - 'voice': 'james_earl_jones', + 'conversation_id': None, + 'device_id': None, + 'engine': 'homeassistant', + 'intent_input': 'never mind', + 'language': 'en', }) # --- # name: test_pipeline_empty_tts_output.2 dict({ - 'tts_output': dict({ + 'intent_output': dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + }), + }), }), }) # --- diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index 0e2a3ad538c..458320a9a90 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -2467,10 +2467,10 @@ async def test_pipeline_empty_tts_output( await client.send_json_auto_id( { "type": "assist_pipeline/run", - "start_stage": "tts", + "start_stage": "intent", "end_stage": "tts", "input": { - "text": "", + "text": "never mind", }, } ) @@ -2486,16 +2486,15 @@ async def test_pipeline_empty_tts_output( assert msg["event"]["data"] == snapshot events.append(msg["event"]) - # text-to-speech + # intent msg = await client.receive_json() - assert msg["event"]["type"] == "tts-start" + assert msg["event"]["type"] == "intent-start" assert msg["event"]["data"] == snapshot events.append(msg["event"]) msg = await client.receive_json() - assert msg["event"]["type"] == "tts-end" + assert msg["event"]["type"] == "intent-end" assert msg["event"]["data"] == snapshot - assert not msg["event"]["data"]["tts_output"] events.append(msg["event"]) # run end From 36c56eb32abc856c1e4a53ca2b0a48b8e120b0f5 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 13 Dec 2023 02:09:22 -0600 Subject: [PATCH 060/106] Rename "satellite enabled" to "mute" (#105619) --- homeassistant/components/wyoming/devices.py | 28 ++++++------ homeassistant/components/wyoming/satellite.py | 44 +++++++++---------- homeassistant/components/wyoming/strings.json | 4 +- homeassistant/components/wyoming/switch.py | 18 ++++---- tests/components/wyoming/test_devices.py | 20 ++++----- tests/components/wyoming/test_satellite.py | 26 +++++------ tests/components/wyoming/test_switch.py | 24 +++++----- 7 files changed, 81 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/wyoming/devices.py b/homeassistant/components/wyoming/devices.py index 90dad889707..47a5cdc7eb8 100644 --- a/homeassistant/components/wyoming/devices.py +++ b/homeassistant/components/wyoming/devices.py @@ -17,11 +17,11 @@ class SatelliteDevice: satellite_id: str device_id: str is_active: bool = False - is_enabled: bool = True + is_muted: bool = False pipeline_name: str | None = None _is_active_listener: Callable[[], None] | None = None - _is_enabled_listener: Callable[[], None] | None = None + _is_muted_listener: Callable[[], None] | None = None _pipeline_listener: Callable[[], None] | None = None @callback @@ -33,12 +33,12 @@ class SatelliteDevice: self._is_active_listener() @callback - def set_is_enabled(self, enabled: bool) -> None: - """Set enabled state.""" - if enabled != self.is_enabled: - self.is_enabled = enabled - if self._is_enabled_listener is not None: - self._is_enabled_listener() + def set_is_muted(self, muted: bool) -> None: + """Set muted state.""" + if muted != self.is_muted: + self.is_muted = muted + if self._is_muted_listener is not None: + self._is_muted_listener() @callback def set_pipeline_name(self, pipeline_name: str) -> None: @@ -54,9 +54,9 @@ class SatelliteDevice: self._is_active_listener = is_active_listener @callback - def set_is_enabled_listener(self, is_enabled_listener: Callable[[], None]) -> None: - """Listen for updates to is_enabled.""" - self._is_enabled_listener = is_enabled_listener + def set_is_muted_listener(self, is_muted_listener: Callable[[], None]) -> None: + """Listen for updates to muted status.""" + self._is_muted_listener = is_muted_listener @callback def set_pipeline_listener(self, pipeline_listener: Callable[[], None]) -> None: @@ -70,11 +70,11 @@ class SatelliteDevice: "binary_sensor", DOMAIN, f"{self.satellite_id}-assist_in_progress" ) - def get_satellite_enabled_entity_id(self, hass: HomeAssistant) -> str | None: - """Return entity id for satellite enabled switch.""" + def get_muted_entity_id(self, hass: HomeAssistant) -> str | None: + """Return entity id for satellite muted switch.""" ent_reg = er.async_get(hass) return ent_reg.async_get_entity_id( - "switch", DOMAIN, f"{self.satellite_id}-satellite_enabled" + "switch", DOMAIN, f"{self.satellite_id}-mute" ) def get_pipeline_entity_id(self, hass: HomeAssistant) -> str | None: diff --git a/homeassistant/components/wyoming/satellite.py b/homeassistant/components/wyoming/satellite.py index 45d882dd1e2..16240cb625b 100644 --- a/homeassistant/components/wyoming/satellite.py +++ b/homeassistant/components/wyoming/satellite.py @@ -49,7 +49,6 @@ class WyomingSatellite: self.hass = hass self.service = service self.device = device - self.is_enabled = True self.is_running = True self._client: AsyncTcpClient | None = None @@ -57,9 +56,9 @@ class WyomingSatellite: self._is_pipeline_running = False self._audio_queue: asyncio.Queue[bytes | None] = asyncio.Queue() self._pipeline_id: str | None = None - self._enabled_changed_event = asyncio.Event() + self._muted_changed_event = asyncio.Event() - self.device.set_is_enabled_listener(self._enabled_changed) + self.device.set_is_muted_listener(self._muted_changed) self.device.set_pipeline_listener(self._pipeline_changed) async def run(self) -> None: @@ -69,11 +68,11 @@ class WyomingSatellite: try: while self.is_running: try: - # Check if satellite has been disabled - while not self.device.is_enabled: - await self.on_disabled() + # Check if satellite has been muted + while self.device.is_muted: + await self.on_muted() if not self.is_running: - # Satellite was stopped while waiting to be enabled + # Satellite was stopped while waiting to be unmuted return # Connect and run pipeline loop @@ -92,8 +91,8 @@ class WyomingSatellite: """Signal satellite task to stop running.""" self.is_running = False - # Unblock waiting for enabled - self._enabled_changed_event.set() + # Unblock waiting for unmuted + self._muted_changed_event.set() async def on_restart(self) -> None: """Block until pipeline loop will be restarted.""" @@ -111,9 +110,9 @@ class WyomingSatellite: ) await asyncio.sleep(_RECONNECT_SECONDS) - async def on_disabled(self) -> None: - """Block until device may be enabled again.""" - await self._enabled_changed_event.wait() + async def on_muted(self) -> None: + """Block until device may be unmated again.""" + await self._muted_changed_event.wait() async def on_stopped(self) -> None: """Run when run() has fully stopped.""" @@ -121,15 +120,14 @@ class WyomingSatellite: # ------------------------------------------------------------------------- - def _enabled_changed(self) -> None: - """Run when device enabled status changes.""" - - if not self.device.is_enabled: + def _muted_changed(self) -> None: + """Run when device muted status changes.""" + if self.device.is_muted: # Cancel any running pipeline self._audio_queue.put_nowait(None) - self._enabled_changed_event.set() - self._enabled_changed_event.clear() + self._muted_changed_event.set() + self._muted_changed_event.clear() def _pipeline_changed(self) -> None: """Run when device pipeline changes.""" @@ -141,7 +139,7 @@ class WyomingSatellite: """Run pipelines until an error occurs.""" self.device.set_is_active(False) - while self.is_running and self.is_enabled: + while self.is_running and (not self.device.is_muted): try: await self._connect() break @@ -151,7 +149,7 @@ class WyomingSatellite: assert self._client is not None _LOGGER.debug("Connected to satellite") - if (not self.is_running) or (not self.is_enabled): + if (not self.is_running) or self.device.is_muted: # Run was cancelled or satellite was disabled during connection return @@ -160,7 +158,7 @@ class WyomingSatellite: # Wait until we get RunPipeline event run_pipeline: RunPipeline | None = None - while self.is_running and self.is_enabled: + while self.is_running and (not self.device.is_muted): run_event = await self._client.read_event() if run_event is None: raise ConnectionResetError("Satellite disconnected") @@ -174,7 +172,7 @@ class WyomingSatellite: assert run_pipeline is not None _LOGGER.debug("Received run information: %s", run_pipeline) - if (not self.is_running) or (not self.is_enabled): + if (not self.is_running) or self.device.is_muted: # Run was cancelled or satellite was disabled while waiting for # RunPipeline event. return @@ -189,7 +187,7 @@ class WyomingSatellite: raise ValueError(f"Invalid end stage: {end_stage}") # Each loop is a pipeline run - while self.is_running and self.is_enabled: + while self.is_running and (not self.device.is_muted): # Use select to get pipeline each time in case it's changed pipeline_id = pipeline_select.get_chosen_pipeline( self.hass, diff --git a/homeassistant/components/wyoming/strings.json b/homeassistant/components/wyoming/strings.json index 19b6a513d4b..c7ae63e7b95 100644 --- a/homeassistant/components/wyoming/strings.json +++ b/homeassistant/components/wyoming/strings.json @@ -42,8 +42,8 @@ } }, "switch": { - "satellite_enabled": { - "name": "Satellite enabled" + "mute": { + "name": "Mute" } } } diff --git a/homeassistant/components/wyoming/switch.py b/homeassistant/components/wyoming/switch.py index 2bc43122588..7366a52efab 100644 --- a/homeassistant/components/wyoming/switch.py +++ b/homeassistant/components/wyoming/switch.py @@ -29,17 +29,17 @@ async def async_setup_entry( # Setup is only forwarded for satellites assert item.satellite is not None - async_add_entities([WyomingSatelliteEnabledSwitch(item.satellite.device)]) + async_add_entities([WyomingSatelliteMuteSwitch(item.satellite.device)]) -class WyomingSatelliteEnabledSwitch( +class WyomingSatelliteMuteSwitch( WyomingSatelliteEntity, restore_state.RestoreEntity, SwitchEntity ): - """Entity to represent if satellite is enabled.""" + """Entity to represent if satellite is muted.""" entity_description = SwitchEntityDescription( - key="satellite_enabled", - translation_key="satellite_enabled", + key="mute", + translation_key="mute", entity_category=EntityCategory.CONFIG, ) @@ -49,17 +49,17 @@ class WyomingSatelliteEnabledSwitch( state = await self.async_get_last_state() - # Default to on - self._attr_is_on = (state is None) or (state.state == STATE_ON) + # Default to off + self._attr_is_on = (state is not None) and (state.state == STATE_ON) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on.""" self._attr_is_on = True self.async_write_ha_state() - self._device.set_is_enabled(True) + self._device.set_is_muted(True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off.""" self._attr_is_on = False self.async_write_ha_state() - self._device.set_is_enabled(False) + self._device.set_is_muted(False) diff --git a/tests/components/wyoming/test_devices.py b/tests/components/wyoming/test_devices.py index 549f76f20f1..0273a7da275 100644 --- a/tests/components/wyoming/test_devices.py +++ b/tests/components/wyoming/test_devices.py @@ -5,7 +5,7 @@ from homeassistant.components.assist_pipeline.select import OPTION_PREFERRED from homeassistant.components.wyoming import DOMAIN from homeassistant.components.wyoming.devices import SatelliteDevice from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -34,11 +34,11 @@ async def test_device_registry_info( assert assist_in_progress_state is not None assert assist_in_progress_state.state == STATE_OFF - satellite_enabled_id = satellite_device.get_satellite_enabled_entity_id(hass) - assert satellite_enabled_id - satellite_enabled_state = hass.states.get(satellite_enabled_id) - assert satellite_enabled_state is not None - assert satellite_enabled_state.state == STATE_ON + muted_id = satellite_device.get_muted_entity_id(hass) + assert muted_id + muted_state = hass.states.get(muted_id) + assert muted_state is not None + assert muted_state.state == STATE_OFF pipeline_entity_id = satellite_device.get_pipeline_entity_id(hass) assert pipeline_entity_id @@ -59,9 +59,9 @@ async def test_remove_device_registry_entry( assert assist_in_progress_id assert hass.states.get(assist_in_progress_id) is not None - satellite_enabled_id = satellite_device.get_satellite_enabled_entity_id(hass) - assert satellite_enabled_id - assert hass.states.get(satellite_enabled_id) is not None + muted_id = satellite_device.get_muted_entity_id(hass) + assert muted_id + assert hass.states.get(muted_id) is not None pipeline_entity_id = satellite_device.get_pipeline_entity_id(hass) assert pipeline_entity_id @@ -74,5 +74,5 @@ async def test_remove_device_registry_entry( # Everything should be gone assert hass.states.get(assist_in_progress_id) is None - assert hass.states.get(satellite_enabled_id) is None + assert hass.states.get(muted_id) is None assert hass.states.get(pipeline_entity_id) is None diff --git a/tests/components/wyoming/test_satellite.py b/tests/components/wyoming/test_satellite.py index 83e4d98d971..07a6aa8925e 100644 --- a/tests/components/wyoming/test_satellite.py +++ b/tests/components/wyoming/test_satellite.py @@ -196,7 +196,7 @@ async def test_satellite_pipeline(hass: HomeAssistant) -> None: await mock_client.detect_event.wait() assert not device.is_active - assert device.is_enabled + assert not device.is_muted # Wake word is detected event_callback( @@ -312,36 +312,36 @@ async def test_satellite_pipeline(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def test_satellite_disabled(hass: HomeAssistant) -> None: - """Test callback for a satellite that has been disabled.""" - on_disabled_event = asyncio.Event() +async def test_satellite_muted(hass: HomeAssistant) -> None: + """Test callback for a satellite that has been muted.""" + on_muted_event = asyncio.Event() original_make_satellite = wyoming._make_satellite - def make_disabled_satellite( + def make_muted_satellite( hass: HomeAssistant, config_entry: ConfigEntry, service: WyomingService ): satellite = original_make_satellite(hass, config_entry, service) - satellite.device.set_is_enabled(False) + satellite.device.set_is_muted(True) return satellite - async def on_disabled(self): - self.device.set_is_enabled(True) - on_disabled_event.set() + async def on_muted(self): + self.device.set_is_muted(False) + on_muted_event.set() with patch( "homeassistant.components.wyoming.data.load_wyoming_info", return_value=SATELLITE_INFO, ), patch( - "homeassistant.components.wyoming._make_satellite", make_disabled_satellite + "homeassistant.components.wyoming._make_satellite", make_muted_satellite ), patch( - "homeassistant.components.wyoming.satellite.WyomingSatellite.on_disabled", - on_disabled, + "homeassistant.components.wyoming.satellite.WyomingSatellite.on_muted", + on_muted, ): await setup_config_entry(hass) async with asyncio.timeout(1): - await on_disabled_event.wait() + await on_muted_event.wait() async def test_satellite_restart(hass: HomeAssistant) -> None: diff --git a/tests/components/wyoming/test_switch.py b/tests/components/wyoming/test_switch.py index 0b05724d761..fc5a8689ceb 100644 --- a/tests/components/wyoming/test_switch.py +++ b/tests/components/wyoming/test_switch.py @@ -5,28 +5,28 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -async def test_satellite_enabled( +async def test_muted( hass: HomeAssistant, satellite_config_entry: ConfigEntry, satellite_device: SatelliteDevice, ) -> None: - """Test satellite enabled.""" - satellite_enabled_id = satellite_device.get_satellite_enabled_entity_id(hass) - assert satellite_enabled_id + """Test satellite muted.""" + muted_id = satellite_device.get_muted_entity_id(hass) + assert muted_id - state = hass.states.get(satellite_enabled_id) + state = hass.states.get(muted_id) assert state is not None - assert state.state == STATE_ON - assert satellite_device.is_enabled + assert state.state == STATE_OFF + assert not satellite_device.is_muted await hass.services.async_call( "switch", - "turn_off", - {"entity_id": satellite_enabled_id}, + "turn_on", + {"entity_id": muted_id}, blocking=True, ) - state = hass.states.get(satellite_enabled_id) + state = hass.states.get(muted_id) assert state is not None - assert state.state == STATE_OFF - assert not satellite_device.is_enabled + assert state.state == STATE_ON + assert satellite_device.is_muted From 8744b054687cad23eb25db79d681f373d1da979b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 13 Dec 2023 14:53:10 +0100 Subject: [PATCH 061/106] Bump version to 2023.12.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 df965001035..fcebd21eafd 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 = 12 -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 112a03f5e5b..7b06c7a6506 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.12.1" +version = "2023.12.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 896ca8ce83ff2c945b62e424028beb81e64b7c1d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Dec 2023 16:48:46 +0100 Subject: [PATCH 062/106] Fix timing issue in Withings (#105203) --- homeassistant/components/withings/__init__.py | 85 +++++++++++-------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 701f7f444cf..f42fb7a57b9 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -192,52 +192,67 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = withings_data + register_lock = asyncio.Lock() + webhooks_registered = False + async def unregister_webhook( _: Any, ) -> None: - LOGGER.debug("Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID]) - webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) - await async_unsubscribe_webhooks(client) - for coordinator in withings_data.coordinators: - coordinator.webhook_subscription_listener(False) + nonlocal webhooks_registered + async with register_lock: + LOGGER.debug( + "Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID] + ) + webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) + await async_unsubscribe_webhooks(client) + for coordinator in withings_data.coordinators: + coordinator.webhook_subscription_listener(False) + webhooks_registered = False async def register_webhook( _: Any, ) -> None: - if cloud.async_active_subscription(hass): - webhook_url = await _async_cloudhook_generate_url(hass, entry) - else: - webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID]) - url = URL(webhook_url) - if url.scheme != "https" or url.port != 443: - LOGGER.warning( - "Webhook not registered - " - "https and port 443 is required to register the webhook" + nonlocal webhooks_registered + async with register_lock: + if webhooks_registered: + return + if cloud.async_active_subscription(hass): + webhook_url = await _async_cloudhook_generate_url(hass, entry) + else: + webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID]) + url = URL(webhook_url) + if url.scheme != "https" or url.port != 443: + LOGGER.warning( + "Webhook not registered - " + "https and port 443 is required to register the webhook" + ) + return + + webhook_name = "Withings" + if entry.title != DEFAULT_TITLE: + webhook_name = f"{DEFAULT_TITLE} {entry.title}" + + webhook_register( + hass, + DOMAIN, + webhook_name, + entry.data[CONF_WEBHOOK_ID], + get_webhook_handler(withings_data), + allowed_methods=[METH_POST], ) - return + LOGGER.debug("Registered Withings webhook at hass: %s", webhook_url) - webhook_name = "Withings" - if entry.title != DEFAULT_TITLE: - webhook_name = f"{DEFAULT_TITLE} {entry.title}" - - webhook_register( - hass, - DOMAIN, - webhook_name, - entry.data[CONF_WEBHOOK_ID], - get_webhook_handler(withings_data), - allowed_methods=[METH_POST], - ) - - await async_subscribe_webhooks(client, webhook_url) - for coordinator in withings_data.coordinators: - 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) - ) + await async_subscribe_webhooks(client, webhook_url) + for coordinator in withings_data.coordinators: + coordinator.webhook_subscription_listener(True) + LOGGER.debug("Registered Withings webhook at Withings: %s", webhook_url) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) + ) + webhooks_registered = True async def manage_cloudhook(state: cloud.CloudConnectionState) -> None: + LOGGER.debug("Cloudconnection state changed to %s", state) if state is cloud.CloudConnectionState.CLOUD_CONNECTED: await register_webhook(None) From 75bdd84c80cf7c4dca2e083fcaf0b1afe81b4341 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 11 Dec 2023 17:04:07 +0100 Subject: [PATCH 063/106] Update pylint to 3.0.3 (#105491) --- homeassistant/components/improv_ble/config_flow.py | 2 +- homeassistant/components/mqtt/__init__.py | 1 - homeassistant/components/zha/__init__.py | 2 +- requirements_test.txt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/improv_ble/config_flow.py b/homeassistant/components/improv_ble/config_flow.py index bfc86ac0162..762f37ef5d4 100644 --- a/homeassistant/components/improv_ble/config_flow.py +++ b/homeassistant/components/improv_ble/config_flow.py @@ -405,7 +405,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): raise AbortFlow("characteristic_missing") from err except improv_ble_errors.CommandFailed: raise - except Exception as err: # pylint: disable=broad-except + except Exception as err: _LOGGER.exception("Unexpected exception") raise AbortFlow("unknown") from err diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 16f584db011..593d5bbd202 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -247,7 +247,6 @@ async def async_check_config_schema( schema(config) except vol.Invalid as exc: integration = await async_get_integration(hass, DOMAIN) - # pylint: disable-next=protected-access message = conf_util.format_schema_error( hass, exc, domain, config, integration.documentation ) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 2046070d6a5..340e0db40a6 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -182,7 +182,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) from exc except TransientConnectionError as exc: raise ConfigEntryNotReady from exc - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: _LOGGER.debug( "Couldn't start coordinator (attempt %s of %s)", attempt + 1, diff --git a/requirements_test.txt b/requirements_test.txt index d880fecaca5..ee45a757669 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,7 +14,7 @@ mock-open==1.4.0 mypy==1.7.1 pre-commit==3.5.0 pydantic==1.10.12 -pylint==3.0.2 +pylint==3.0.3 pylint-per-file-ignores==1.2.1 pipdeptree==2.11.0 pytest-asyncio==0.21.0 From db52b9b0e1d16fbf7087c869f589415735849017 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:24:26 -0500 Subject: [PATCH 064/106] Reload ZHA integration on any error, not just recoverable ones (#105659) --- homeassistant/components/zha/__init__.py | 75 +++++++++------------- homeassistant/components/zha/core/const.py | 3 - tests/components/zha/conftest.py | 2 +- tests/components/zha/test_repairs.py | 4 +- 4 files changed, 34 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 340e0db40a6..1eb3369c1be 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -37,8 +37,6 @@ from .core.const import ( DOMAIN, PLATFORMS, SIGNAL_ADD_ENTITIES, - STARTUP_FAILURE_DELAY_S, - STARTUP_RETRIES, RadioType, ) from .core.device import get_device_automation_triggers @@ -161,49 +159,40 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b _LOGGER.debug("Trigger cache: %s", zha_data.device_trigger_cache) - # Retry setup a few times before giving up to deal with missing serial ports in VMs - for attempt in range(STARTUP_RETRIES): - try: - zha_gateway = await ZHAGateway.async_from_config( - hass=hass, - config=zha_data.yaml_config, - config_entry=config_entry, - ) - break - except NetworkSettingsInconsistent as exc: - await warn_on_inconsistent_network_settings( - hass, - config_entry=config_entry, - old_state=exc.old_state, - new_state=exc.new_state, - ) - raise ConfigEntryError( - "Network settings do not match most recent backup" - ) from exc - except TransientConnectionError as exc: - raise ConfigEntryNotReady from exc - except Exception as exc: - _LOGGER.debug( - "Couldn't start coordinator (attempt %s of %s)", - attempt + 1, - STARTUP_RETRIES, - exc_info=exc, - ) + try: + zha_gateway = await ZHAGateway.async_from_config( + hass=hass, + config=zha_data.yaml_config, + config_entry=config_entry, + ) + except NetworkSettingsInconsistent as exc: + await warn_on_inconsistent_network_settings( + hass, + config_entry=config_entry, + old_state=exc.old_state, + new_state=exc.new_state, + ) + raise ConfigEntryError( + "Network settings do not match most recent backup" + ) from exc + except TransientConnectionError as exc: + raise ConfigEntryNotReady from exc + except Exception as exc: + _LOGGER.debug("Failed to set up ZHA", exc_info=exc) + device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] - if attempt < STARTUP_RETRIES - 1: - await asyncio.sleep(STARTUP_FAILURE_DELAY_S) - continue + if ( + not device_path.startswith("socket://") + and RadioType[config_entry.data[CONF_RADIO_TYPE]] == RadioType.ezsp + ): + try: + # Ignore all exceptions during probing, they shouldn't halt setup + if await warn_on_wrong_silabs_firmware(hass, device_path): + raise ConfigEntryError("Incorrect firmware installed") from exc + except AlreadyRunningEZSP as ezsp_exc: + raise ConfigEntryNotReady from ezsp_exc - if RadioType[config_entry.data[CONF_RADIO_TYPE]] == RadioType.ezsp: - try: - # Ignore all exceptions during probing, they shouldn't halt setup - await warn_on_wrong_silabs_firmware( - hass, config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] - ) - except AlreadyRunningEZSP as ezsp_exc: - raise ConfigEntryNotReady from ezsp_exc - - raise + raise ConfigEntryNotReady from exc repairs.async_delete_blocking_issues(hass) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index f89ed8d9a52..ecbd347a621 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -409,9 +409,6 @@ class Strobe(t.enum8): Strobe = 0x01 -STARTUP_FAILURE_DELAY_S = 3 -STARTUP_RETRIES = 3 - EZSP_OVERWRITE_EUI64 = ( "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" ) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 1b3a536007a..55405d0a51c 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -46,7 +46,7 @@ def disable_request_retry_delay(): with patch( "homeassistant.components.zha.core.cluster_handlers.RETRYABLE_REQUEST_DECORATOR", zigpy.util.retryable_request(tries=3, delay=0), - ), patch("homeassistant.components.zha.STARTUP_FAILURE_DELAY_S", 0.01): + ): yield diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index d168e2e57b1..0efff5ecb52 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -95,7 +95,6 @@ def test_detect_radio_hardware_failure(hass: HomeAssistant) -> None: assert _detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.OTHER -@patch("homeassistant.components.zha.STARTUP_RETRIES", new=1) @pytest.mark.parametrize( ("detected_hardware", "expected_learn_more_url"), [ @@ -176,7 +175,7 @@ async def test_multipan_firmware_no_repair_on_probe_failure( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state == ConfigEntryState.SETUP_RETRY await hass.config_entries.async_unload(config_entry.entry_id) @@ -189,7 +188,6 @@ async def test_multipan_firmware_no_repair_on_probe_failure( assert issue is None -@patch("homeassistant.components.zha.STARTUP_RETRIES", new=1) async def test_multipan_firmware_retry_on_probe_ezsp( hass: HomeAssistant, config_entry: MockConfigEntry, From ebb54d7a6cd796218e1457e566840795ac94dcc6 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 13 Dec 2023 11:06:46 -0500 Subject: [PATCH 065/106] Bump ZHA dependencies (#105661) --- homeassistant/components/zha/core/const.py | 1 - homeassistant/components/zha/core/gateway.py | 10 ----- homeassistant/components/zha/manifest.json | 8 ++-- homeassistant/components/zha/radio_manager.py | 2 - requirements_all.txt | 8 ++-- requirements_test_all.txt | 8 ++-- tests/components/zha/test_gateway.py | 45 +------------------ 7 files changed, 13 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index ecbd347a621..7e591a596e5 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -139,7 +139,6 @@ CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" CONF_ENABLE_QUIRKS = "enable_quirks" CONF_RADIO_TYPE = "radio_type" CONF_USB_PATH = "usb_path" -CONF_USE_THREAD = "use_thread" CONF_ZIGPY = "zigpy_config" CONF_CONSIDER_UNAVAILABLE_MAINS = "consider_unavailable_mains" diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 5c038a2d7f8..6c461ac45c3 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -46,7 +46,6 @@ from .const import ( ATTR_SIGNATURE, ATTR_TYPE, CONF_RADIO_TYPE, - CONF_USE_THREAD, CONF_ZIGPY, DEBUG_COMP_BELLOWS, DEBUG_COMP_ZHA, @@ -158,15 +157,6 @@ class ZHAGateway: if CONF_NWK_VALIDATE_SETTINGS not in app_config: app_config[CONF_NWK_VALIDATE_SETTINGS] = True - # The bellows UART thread sometimes propagates a cancellation into the main Core - # event loop, when a connection to a TCP coordinator fails in a specific way - if ( - CONF_USE_THREAD not in app_config - and radio_type is RadioType.ezsp - and app_config[CONF_DEVICE][CONF_DEVICE_PATH].startswith("socket://") - ): - app_config[CONF_USE_THREAD] = False - # Local import to avoid circular dependencies # pylint: disable-next=import-outside-toplevel from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 4c8a58a12cf..fe58ff044cd 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,13 +21,13 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.37.1", + "bellows==0.37.3", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.107", - "zigpy-deconz==0.22.0", - "zigpy==0.60.0", - "zigpy-xbee==0.20.0", + "zigpy-deconz==0.22.2", + "zigpy==0.60.1", + "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.0", "universal-silabs-flasher==0.0.15", diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index d3ca03de8d8..92a90e0e13a 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -10,7 +10,6 @@ import logging import os from typing import Any, Self -from bellows.config import CONF_USE_THREAD import voluptuous as vol from zigpy.application import ControllerApplication import zigpy.backups @@ -175,7 +174,6 @@ class ZhaRadioManager: app_config[CONF_DATABASE] = database_path app_config[CONF_DEVICE] = self.device_settings app_config[CONF_NWK_BACKUP_ENABLED] = False - app_config[CONF_USE_THREAD] = False app_config = self.radio_type.controller.SCHEMA(app_config) app = await self.radio_type.controller.new( diff --git a/requirements_all.txt b/requirements_all.txt index 3ec2df6ae86..27a14e0b137 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -523,7 +523,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.37.1 +bellows==0.37.3 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -2825,10 +2825,10 @@ zhong-hong-hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.22.0 +zigpy-deconz==0.22.2 # homeassistant.components.zha -zigpy-xbee==0.20.0 +zigpy-xbee==0.20.1 # homeassistant.components.zha zigpy-zigate==0.12.0 @@ -2837,7 +2837,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.0 # homeassistant.components.zha -zigpy==0.60.0 +zigpy==0.60.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1610539940d..cae0417ff0a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -445,7 +445,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.37.1 +bellows==0.37.3 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -2114,10 +2114,10 @@ zeversolar==0.3.1 zha-quirks==0.0.107 # homeassistant.components.zha -zigpy-deconz==0.22.0 +zigpy-deconz==0.22.2 # homeassistant.components.zha -zigpy-xbee==0.20.0 +zigpy-xbee==0.20.1 # homeassistant.components.zha zigpy-zigate==0.12.0 @@ -2126,7 +2126,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.0 # homeassistant.components.zha -zigpy==0.60.0 +zigpy==0.60.1 # homeassistant.components.zwave_js zwave-js-server-python==0.54.0 diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 4f520920704..1d9042daa4a 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,9 +1,8 @@ """Test ZHA Gateway.""" import asyncio -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest -from zigpy.application import ControllerApplication import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting @@ -223,48 +222,6 @@ async def test_gateway_create_group_with_id( assert zha_group.group_id == 0x1234 -@patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_devices", - MagicMock(), -) -@patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_groups", - MagicMock(), -) -@pytest.mark.parametrize( - ("device_path", "thread_state", "config_override"), - [ - ("/dev/ttyUSB0", True, {}), - ("socket://192.168.1.123:9999", False, {}), - ("socket://192.168.1.123:9999", True, {"use_thread": True}), - ], -) -async def test_gateway_initialize_bellows_thread( - device_path: str, - thread_state: bool, - config_override: dict, - hass: HomeAssistant, - zigpy_app_controller: ControllerApplication, - config_entry: MockConfigEntry, -) -> None: - """Test ZHA disabling the UART thread when connecting to a TCP coordinator.""" - config_entry.data = dict(config_entry.data) - config_entry.data["device"]["path"] = device_path - config_entry.add_to_hass(hass) - - zha_gateway = ZHAGateway(hass, {"zigpy_config": config_override}, config_entry) - - with patch( - "bellows.zigbee.application.ControllerApplication.new", - return_value=zigpy_app_controller, - ) as mock_new: - await zha_gateway.async_initialize() - - mock_new.mock_calls[-1].kwargs["config"]["use_thread"] is thread_state - - await zha_gateway.shutdown() - - @pytest.mark.parametrize( ("device_path", "config_override", "expected_channel"), [ From bfdadd12e98ffd0c6b34c554dca3508fb04e2fc4 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 13 Dec 2023 18:07:29 +0100 Subject: [PATCH 066/106] Ensure platform setup for all AVM FRITZ!SmartHome devices (#105515) --- .../components/fritzbox/binary_sensor.py | 10 ++++++---- homeassistant/components/fritzbox/button.py | 12 ++++++------ homeassistant/components/fritzbox/climate.py | 10 ++++++---- homeassistant/components/fritzbox/cover.py | 10 ++++++---- homeassistant/components/fritzbox/light.py | 17 ++++++++--------- homeassistant/components/fritzbox/sensor.py | 10 ++++++---- homeassistant/components/fritzbox/switch.py | 10 ++++++---- 7 files changed, 44 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 2460635351e..e766a53518a 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -70,20 +70,22 @@ async def async_setup_entry( coordinator = get_coordinator(hass, entry.entry_id) @callback - def _add_entities() -> None: + def _add_entities(devices: set[str] | None = None) -> None: """Add devices.""" - if not coordinator.new_devices: + if devices is None: + devices = coordinator.new_devices + if not devices: return async_add_entities( FritzboxBinarySensor(coordinator, ain, description) - for ain in coordinator.new_devices + for ain in devices for description in BINARY_SENSOR_TYPES if description.suitable(coordinator.data.devices[ain]) ) entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + _add_entities(set(coordinator.data.devices.keys())) class FritzboxBinarySensor(FritzBoxDeviceEntity, BinarySensorEntity): diff --git a/homeassistant/components/fritzbox/button.py b/homeassistant/components/fritzbox/button.py index 732c41bfb7d..e56ebc1e3b0 100644 --- a/homeassistant/components/fritzbox/button.py +++ b/homeassistant/components/fritzbox/button.py @@ -19,17 +19,17 @@ async def async_setup_entry( coordinator = get_coordinator(hass, entry.entry_id) @callback - def _add_entities() -> None: + def _add_entities(templates: set[str] | None = None) -> None: """Add templates.""" - if not coordinator.new_templates: + if templates is None: + templates = coordinator.new_templates + if not templates: return - async_add_entities( - FritzBoxTemplate(coordinator, ain) for ain in coordinator.new_templates - ) + async_add_entities(FritzBoxTemplate(coordinator, ain) for ain in templates) entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + _add_entities(set(coordinator.data.templates.keys())) class FritzBoxTemplate(FritzBoxEntity, ButtonEntity): diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 70359d9b2af..6ce885a3fdb 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -52,19 +52,21 @@ async def async_setup_entry( coordinator = get_coordinator(hass, entry.entry_id) @callback - def _add_entities() -> None: + def _add_entities(devices: set[str] | None = None) -> None: """Add devices.""" - if not coordinator.new_devices: + if devices is None: + devices = coordinator.new_devices + if not devices: return async_add_entities( FritzboxThermostat(coordinator, ain) - for ain in coordinator.new_devices + for ain in devices if coordinator.data.devices[ain].has_thermostat ) entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + _add_entities(set(coordinator.data.devices.keys())) class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): diff --git a/homeassistant/components/fritzbox/cover.py b/homeassistant/components/fritzbox/cover.py index 7d27356fdf9..d3d4c9080ea 100644 --- a/homeassistant/components/fritzbox/cover.py +++ b/homeassistant/components/fritzbox/cover.py @@ -24,19 +24,21 @@ async def async_setup_entry( coordinator = get_coordinator(hass, entry.entry_id) @callback - def _add_entities() -> None: + def _add_entities(devices: set[str] | None = None) -> None: """Add devices.""" - if not coordinator.new_devices: + if devices is None: + devices = coordinator.new_devices + if not devices: return async_add_entities( FritzboxCover(coordinator, ain) - for ain in coordinator.new_devices + for ain in devices if coordinator.data.devices[ain].has_blind ) entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + _add_entities(set(coordinator.data.devices.keys())) class FritzboxCover(FritzBoxDeviceEntity, CoverEntity): diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py index 8dc51e59738..88d32fe33a5 100644 --- a/homeassistant/components/fritzbox/light.py +++ b/homeassistant/components/fritzbox/light.py @@ -30,22 +30,21 @@ async def async_setup_entry( coordinator = get_coordinator(hass, entry.entry_id) @callback - def _add_entities() -> None: + def _add_entities(devices: set[str] | None = None) -> None: """Add devices.""" - if not coordinator.new_devices: + if devices is None: + devices = coordinator.new_devices + if not devices: return async_add_entities( - FritzboxLight( - coordinator, - ain, - ) - for ain in coordinator.new_devices - if (coordinator.data.devices[ain]).has_lightbulb + FritzboxLight(coordinator, ain) + for ain in devices + if coordinator.data.devices[ain].has_lightbulb ) entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + _add_entities(set(coordinator.data.devices.keys())) class FritzboxLight(FritzBoxDeviceEntity, LightEntity): diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 1e5d7754934..fda8b239859 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -215,20 +215,22 @@ async def async_setup_entry( coordinator = get_coordinator(hass, entry.entry_id) @callback - def _add_entities() -> None: + def _add_entities(devices: set[str] | None = None) -> None: """Add devices.""" - if not coordinator.new_devices: + if devices is None: + devices = coordinator.new_devices + if not devices: return async_add_entities( FritzBoxSensor(coordinator, ain, description) - for ain in coordinator.new_devices + for ain in devices for description in SENSOR_TYPES if description.suitable(coordinator.data.devices[ain]) ) entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + _add_entities(set(coordinator.data.devices.keys())) class FritzBoxSensor(FritzBoxDeviceEntity, SensorEntity): diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 617a5242c5b..4a2960a18ea 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -19,19 +19,21 @@ async def async_setup_entry( coordinator = get_coordinator(hass, entry.entry_id) @callback - def _add_entities() -> None: + def _add_entities(devices: set[str] | None = None) -> None: """Add devices.""" - if not coordinator.new_devices: + if devices is None: + devices = coordinator.new_devices + if not devices: return async_add_entities( FritzboxSwitch(coordinator, ain) - for ain in coordinator.new_devices + for ain in devices if coordinator.data.devices[ain].has_switch ) entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities() + _add_entities(set(coordinator.data.devices.keys())) class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity): From d5ecc55f894f0ca06055f08497f2aefe4c363acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 14 Dec 2023 18:59:04 +0100 Subject: [PATCH 067/106] Update AEMET-OpenData to v0.4.7 (#105676) --- homeassistant/components/aemet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index 544931b50b5..2bc30860803 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aemet", "iot_class": "cloud_polling", "loggers": ["aemet_opendata"], - "requirements": ["AEMET-OpenData==0.4.6"] + "requirements": ["AEMET-OpenData==0.4.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 27a14e0b137..5a2307ce5a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,7 +4,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.4.6 +AEMET-OpenData==0.4.7 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.58 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cae0417ff0a..ae4bbcc1f95 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.aemet -AEMET-OpenData==0.4.6 +AEMET-OpenData==0.4.7 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.58 From 3adda6b11024edff427a6522d2fb9e4c1b440db0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 13 Dec 2023 22:35:41 +0100 Subject: [PATCH 068/106] Fix restoring UniFi clients with old unique id (#105691) Fix restoring UniFi clients with bad unique id --- homeassistant/components/unifi/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 035cf66a983..a941e836ae2 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -260,8 +260,8 @@ class UniFiController: 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]) + if entry.domain == Platform.DEVICE_TRACKER and "-" in entry.unique_id: + macs.append(entry.unique_id.split("-", 1)[1]) for mac in self.option_supported_clients + self.option_block_clients + macs: if mac not in self.api.clients and mac in self.api.clients_all: From 73e234dfa5461918c2d785373eb17c6f14a9f21b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 13 Dec 2023 12:14:07 -1000 Subject: [PATCH 069/106] Bump zeroconf to 0.128.5 (#105694) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 6738431b304..d78f33f0d91 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.128.4"] + "requirements": ["zeroconf==0.128.5"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7d5f53f8f56..ba511ba7e40 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -57,7 +57,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 yarl==1.9.2 -zeroconf==0.128.4 +zeroconf==0.128.5 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 5a2307ce5a2..cbd2bdbf87b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2810,7 +2810,7 @@ zamg==0.3.3 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.128.4 +zeroconf==0.128.5 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae4bbcc1f95..d62114a8f6e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2105,7 +2105,7 @@ yt-dlp==2023.11.16 zamg==0.3.3 # homeassistant.components.zeroconf -zeroconf==0.128.4 +zeroconf==0.128.5 # homeassistant.components.zeversolar zeversolar==0.3.1 From f8e92ddcb3c48904ede4febb90bc98894c1b8d2f Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 14 Dec 2023 15:20:34 +0100 Subject: [PATCH 070/106] Add missing rest_command reload service to services.yaml (#105714) * Add missing rest_command reload service to services.yaml * Add missing strings.json * retrigger stale CI --- homeassistant/components/rest_command/services.yaml | 1 + homeassistant/components/rest_command/strings.json | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 homeassistant/components/rest_command/strings.json diff --git a/homeassistant/components/rest_command/services.yaml b/homeassistant/components/rest_command/services.yaml index e69de29bb2d..c983a105c93 100644 --- a/homeassistant/components/rest_command/services.yaml +++ b/homeassistant/components/rest_command/services.yaml @@ -0,0 +1 @@ +reload: diff --git a/homeassistant/components/rest_command/strings.json b/homeassistant/components/rest_command/strings.json new file mode 100644 index 00000000000..15f59ec8e29 --- /dev/null +++ b/homeassistant/components/rest_command/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reload": { + "name": "[%key:common::action::reload%]", + "description": "Reloads RESTful commands from the YAML-configuration." + } + } +} From 25bfe7ec82025dd0598d8252927182539dfd3a5e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Dec 2023 12:54:03 +0100 Subject: [PATCH 071/106] Fix issue clearing renault schedules (#105719) * Fix issue clearing renault schedules * Adjust --- .../components/renault/manifest.json | 2 +- homeassistant/components/renault/services.py | 16 ++++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/renault/test_services.py | 22 +++++++++++++------ 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index e5470259aa4..98e1c8b1e7c 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["renault_api"], "quality_scale": "platinum", - "requirements": ["renault-api==0.2.0"] + "requirements": ["renault-api==0.2.1"] } diff --git a/homeassistant/components/renault/services.py b/homeassistant/components/renault/services.py index d25b73cafc2..d2c7d451844 100644 --- a/homeassistant/components/renault/services.py +++ b/homeassistant/components/renault/services.py @@ -43,13 +43,15 @@ SERVICE_CHARGE_SET_SCHEDULE_SCHEMA = vol.Schema( { vol.Required("id"): cv.positive_int, vol.Optional("activated"): cv.boolean, - vol.Optional("monday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA), - vol.Optional("tuesday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA), - vol.Optional("wednesday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA), - vol.Optional("thursday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA), - vol.Optional("friday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA), - vol.Optional("saturday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA), - vol.Optional("sunday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA), + vol.Optional("monday"): vol.Any(None, SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA), + vol.Optional("tuesday"): vol.Any(None, SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA), + vol.Optional("wednesday"): vol.Any( + None, SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA + ), + vol.Optional("thursday"): vol.Any(None, SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA), + vol.Optional("friday"): vol.Any(None, SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA), + vol.Optional("saturday"): vol.Any(None, SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA), + vol.Optional("sunday"): vol.Any(None, SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA), } ) SERVICE_CHARGE_SET_SCHEDULES_SCHEMA = SERVICE_VEHICLE_SCHEMA.extend( diff --git a/requirements_all.txt b/requirements_all.txt index cbd2bdbf87b..eb5034fdfd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2332,7 +2332,7 @@ raspyrfm-client==1.2.8 regenmaschine==2023.06.0 # homeassistant.components.renault -renault-api==0.2.0 +renault-api==0.2.1 # homeassistant.components.renson renson-endura-delta==1.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d62114a8f6e..593bea301cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1744,7 +1744,7 @@ rapt-ble==0.1.2 regenmaschine==2023.06.0 # homeassistant.components.renault -renault-api==0.2.0 +renault-api==0.2.1 # homeassistant.components.renson renson-endura-delta==1.6.0 diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index 58d51eca537..7f5cb9a8184 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -203,13 +203,12 @@ async def test_service_set_charge_schedule_multi( { "id": 2, "activated": True, - "monday": {"startTime": "T12:00Z", "duration": 15}, - "tuesday": {"startTime": "T12:00Z", "duration": 15}, - "wednesday": {"startTime": "T12:00Z", "duration": 15}, - "thursday": {"startTime": "T12:00Z", "duration": 15}, - "friday": {"startTime": "T12:00Z", "duration": 15}, - "saturday": {"startTime": "T12:00Z", "duration": 15}, - "sunday": {"startTime": "T12:00Z", "duration": 15}, + "monday": {"startTime": "T12:00Z", "duration": 30}, + "tuesday": {"startTime": "T12:00Z", "duration": 30}, + "wednesday": None, + "friday": {"startTime": "T12:00Z", "duration": 30}, + "saturday": {"startTime": "T12:00Z", "duration": 30}, + "sunday": {"startTime": "T12:00Z", "duration": 30}, }, {"id": 3}, ] @@ -238,6 +237,15 @@ async def test_service_set_charge_schedule_multi( mock_call_data: list[ChargeSchedule] = mock_action.mock_calls[0][1][0] assert mock_action.mock_calls[0][1] == (mock_call_data,) + # Monday updated with new values + assert mock_call_data[1].monday.startTime == "T12:00Z" + assert mock_call_data[1].monday.duration == 30 + # Wednesday has original values cleared + assert mock_call_data[1].wednesday is None + # Thursday keeps original values + assert mock_call_data[1].thursday.startTime == "T23:30Z" + assert mock_call_data[1].thursday.duration == 15 + async def test_service_invalid_device_id( hass: HomeAssistant, config_entry: ConfigEntry From 4aa03b33f65e552c4639ced55e092ff6c6e1213d Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Thu, 14 Dec 2023 12:59:37 -0500 Subject: [PATCH 072/106] Fix Fully Kiosk Browser MQTT event callbacks with non-standard event topics (#105735) --- .../components/fully_kiosk/entity.py | 4 +++- tests/components/fully_kiosk/test_switch.py | 24 +++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fully_kiosk/entity.py b/homeassistant/components/fully_kiosk/entity.py index 5fd9f75a6a0..b053508ae41 100644 --- a/homeassistant/components/fully_kiosk/entity.py +++ b/homeassistant/components/fully_kiosk/entity.py @@ -74,7 +74,8 @@ class FullyKioskEntity(CoordinatorEntity[FullyKioskDataUpdateCoordinator], Entit @callback def message_callback(message: mqtt.ReceiveMessage) -> None: payload = json.loads(message.payload) - event_callback(**payload) + if "event" in payload and payload["event"] == event: + event_callback(**payload) topic_template = data["settings"]["mqttEventTopic"] topic = ( @@ -82,4 +83,5 @@ class FullyKioskEntity(CoordinatorEntity[FullyKioskDataUpdateCoordinator], Entit .replace("$event", event) .replace("$deviceId", data["deviceID"]) ) + return await mqtt.async_subscribe(self.hass, topic, message_callback) diff --git a/tests/components/fully_kiosk/test_switch.py b/tests/components/fully_kiosk/test_switch.py index 20b5ed11998..3c0874384c2 100644 --- a/tests/components/fully_kiosk/test_switch.py +++ b/tests/components/fully_kiosk/test_switch.py @@ -107,19 +107,35 @@ async def test_switches_mqtt_update( assert entity assert entity.state == "on" - async_fire_mqtt_message(hass, "fully/event/onScreensaverStart/abcdef-123456", "{}") + async_fire_mqtt_message( + hass, + "fully/event/onScreensaverStart/abcdef-123456", + '{"deviceId": "abcdef-123456","event": "onScreensaverStart"}', + ) entity = hass.states.get("switch.amazon_fire_screensaver") assert entity.state == "on" - async_fire_mqtt_message(hass, "fully/event/onScreensaverStop/abcdef-123456", "{}") + async_fire_mqtt_message( + hass, + "fully/event/onScreensaverStop/abcdef-123456", + '{"deviceId": "abcdef-123456","event": "onScreensaverStop"}', + ) entity = hass.states.get("switch.amazon_fire_screensaver") assert entity.state == "off" - async_fire_mqtt_message(hass, "fully/event/screenOff/abcdef-123456", "{}") + async_fire_mqtt_message( + hass, + "fully/event/screenOff/abcdef-123456", + '{"deviceId": "abcdef-123456","event": "screenOff"}', + ) entity = hass.states.get("switch.amazon_fire_screen") assert entity.state == "off" - async_fire_mqtt_message(hass, "fully/event/screenOn/abcdef-123456", "{}") + async_fire_mqtt_message( + hass, + "fully/event/screenOn/abcdef-123456", + '{"deviceId": "abcdef-123456","event": "screenOn"}', + ) entity = hass.states.get("switch.amazon_fire_screen") assert entity.state == "on" From dbfc5ea8f96bde6cd165892f5a6a6f9a65731c76 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Dec 2023 20:28:08 +0100 Subject: [PATCH 073/106] Disable user profiles on login screen (#105749) --- homeassistant/components/auth/login_flow.py | 21 ----------- homeassistant/components/person/__init__.py | 36 +++---------------- tests/components/auth/test_login_flow.py | 13 +------ tests/components/person/test_init.py | 39 ++------------------- 4 files changed, 7 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 96255f59c7b..9b96e57dbd3 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -91,7 +91,6 @@ from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView from homeassistant.core import HomeAssistant from homeassistant.helpers.network import is_cloud_connection -from homeassistant.util.network import is_local from . import indieauth @@ -165,8 +164,6 @@ class AuthProvidersView(HomeAssistantView): providers = [] for provider in hass.auth.auth_providers: - additional_data = {} - if provider.type == "trusted_networks": if cloud_connection: # Skip quickly as trusted networks are not available on cloud @@ -179,30 +176,12 @@ class AuthProvidersView(HomeAssistantView): except InvalidAuthError: # Not a trusted network, so we don't expose that trusted_network authenticator is setup continue - elif ( - provider.type == "homeassistant" - and not cloud_connection - and is_local(remote_address) - and "person" in hass.config.components - ): - # We are local, return user id and username - users = await provider.store.async_get_users() - additional_data["users"] = { - user.id: credentials.data["username"] - for user in users - for credentials in user.credentials - if ( - credentials.auth_provider_type == provider.type - and credentials.auth_provider_id == provider.id - ) - } providers.append( { "name": provider.name, "id": provider.id, "type": provider.type, - **additional_data, } ) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index b6f8b5b2db6..c796cb8d843 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations from http import HTTPStatus -from ipaddress import ip_address import logging from typing import Any @@ -51,12 +50,10 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.network import is_cloud_connection from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from homeassistant.util.network import is_local _LOGGER = logging.getLogger(__name__) @@ -588,33 +585,8 @@ class ListPersonsView(HomeAssistantView): async def get(self, request: web.Request) -> web.Response: """Return a list of persons if request comes from a local IP.""" - try: - remote_address = ip_address(request.remote) # type: ignore[arg-type] - except ValueError: - return self.json_message( - message="Invalid remote IP", - status_code=HTTPStatus.BAD_REQUEST, - message_code="invalid_remote_ip", - ) - - hass: HomeAssistant = request.app["hass"] - if is_cloud_connection(hass) or not is_local(remote_address): - return self.json_message( - message="Not local", - status_code=HTTPStatus.BAD_REQUEST, - message_code="not_local", - ) - - yaml, storage, _ = hass.data[DOMAIN] - persons = [*yaml.async_items(), *storage.async_items()] - - return self.json( - { - person[ATTR_USER_ID]: { - ATTR_NAME: person[ATTR_NAME], - CONF_PICTURE: person.get(CONF_PICTURE), - } - for person in persons - if person.get(ATTR_USER_ID) - } + return self.json_message( + message="Not local", + status_code=HTTPStatus.BAD_REQUEST, + message_code="not_local", ) diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py index 639bbb9a9cb..27652ca2be4 100644 --- a/tests/components/auth/test_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -1,12 +1,10 @@ """Tests for the login flow.""" -from collections.abc import Callable from http import HTTPStatus from typing import Any from unittest.mock import patch import pytest -from homeassistant.auth.models import User from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -67,22 +65,16 @@ async def _test_fetch_auth_providers_home_assistant( hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, ip: str, - additional_expected_fn: Callable[[User], dict[str, Any]], ) -> None: """Test fetching auth providers for homeassistant auth provider.""" client = await async_setup_auth( hass, aiohttp_client, [{"type": "homeassistant"}], custom_ip=ip ) - provider = hass.auth.auth_providers[0] - credentials = await provider.async_get_or_create_credentials({"username": "hello"}) - user = await hass.auth.async_get_or_create_user(credentials) - expected = { "name": "Home Assistant Local", "type": "homeassistant", "id": None, - **additional_expected_fn(user), } resp = await client.get("/auth/providers") @@ -105,9 +97,7 @@ async def test_fetch_auth_providers_home_assistant_person_not_loaded( ip: str, ) -> None: """Test fetching auth providers for homeassistant auth provider, where person integration is not loaded.""" - await _test_fetch_auth_providers_home_assistant( - hass, aiohttp_client, ip, lambda _: {} - ) + await _test_fetch_auth_providers_home_assistant(hass, aiohttp_client, ip) @pytest.mark.parametrize( @@ -134,7 +124,6 @@ async def test_fetch_auth_providers_home_assistant_person_loaded( hass, aiohttp_client, ip, - lambda user: {"users": {user.id: user.name}} if is_local else {}, ) diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 4d7781a095f..1866f682b55 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -1,5 +1,4 @@ """The tests for the person component.""" -from collections.abc import Callable from http import HTTPStatus from typing import Any from unittest.mock import patch @@ -31,7 +30,6 @@ from homeassistant.setup import async_setup_component from .conftest import DEVICE_TRACKER, DEVICE_TRACKER_2 from tests.common import MockUser, mock_component, mock_restore_cache -from tests.test_util import mock_real_ip from tests.typing import ClientSessionGenerator, WebSocketGenerator @@ -852,42 +850,10 @@ async def test_entities_in_person(hass: HomeAssistant) -> None: ] -@pytest.mark.parametrize( - ("ip", "status_code", "expected_fn"), - [ - ( - "192.168.0.10", - HTTPStatus.OK, - lambda user: { - user["user_id"]: {"name": user["name"], "picture": user["picture"]} - }, - ), - ( - "::ffff:192.168.0.10", - HTTPStatus.OK, - lambda user: { - user["user_id"]: {"name": user["name"], "picture": user["picture"]} - }, - ), - ( - "1.2.3.4", - HTTPStatus.BAD_REQUEST, - lambda _: {"code": "not_local", "message": "Not local"}, - ), - ( - "2001:db8::1", - HTTPStatus.BAD_REQUEST, - lambda _: {"code": "not_local", "message": "Not local"}, - ), - ], -) async def test_list_persons( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, hass_admin_user: MockUser, - ip: str, - status_code: HTTPStatus, - expected_fn: Callable[[dict[str, Any]], dict[str, Any]], ) -> None: """Test listing persons from a not local ip address.""" @@ -902,11 +868,10 @@ async def test_list_persons( assert await async_setup_component(hass, DOMAIN, config) await async_setup_component(hass, "api", {}) - mock_real_ip(hass.http.app)(ip) client = await hass_client_no_auth() resp = await client.get("/api/person/list") - assert resp.status == status_code + assert resp.status == HTTPStatus.BAD_REQUEST result = await resp.json() - assert result == expected_fn(admin) + assert result == {"code": "not_local", "message": "Not local"} From 07667a6aee92b6763607471be8fefd4ec9d3ff15 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Dec 2023 20:37:09 +0100 Subject: [PATCH 074/106] Bump version to 2023.12.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 fcebd21eafd..e4c6c2ed86a 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 = 12 -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 7b06c7a6506..c39c23819f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.12.2" +version = "2023.12.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 6720580a9e262d79892d0378fa4a2e56c9bb6071 Mon Sep 17 00:00:00 2001 From: vexofp Date: Tue, 19 Dec 2023 15:47:42 -0500 Subject: [PATCH 075/106] Pass timeout to httpx in RESTful Switch (#105364) Co-authored-by: J. Nick Koston --- homeassistant/components/rest/switch.py | 36 ++++++++++++------------- tests/components/rest/test_switch.py | 11 ++++---- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 102bb024924..991bfff7da0 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -202,22 +202,22 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity): rendered_headers = template.render_complex(self._headers, parse_result=False) rendered_params = template.render_complex(self._params) - async with asyncio.timeout(self._timeout): - req: httpx.Response = await getattr(websession, self._method)( - self._resource, - auth=self._auth, - content=bytes(body, "utf-8"), - headers=rendered_headers, - params=rendered_params, - ) - return req + req: httpx.Response = await getattr(websession, self._method)( + self._resource, + auth=self._auth, + content=bytes(body, "utf-8"), + headers=rendered_headers, + params=rendered_params, + timeout=self._timeout, + ) + return req async def async_update(self) -> None: """Get the current state, catching errors.""" req = None try: req = await self.get_device_state(self.hass) - except asyncio.TimeoutError: + except (asyncio.TimeoutError, httpx.TimeoutException): _LOGGER.exception("Timed out while fetching data") except httpx.RequestError as err: _LOGGER.exception("Error while fetching data: %s", err) @@ -233,14 +233,14 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity): rendered_headers = template.render_complex(self._headers, parse_result=False) rendered_params = template.render_complex(self._params) - async with asyncio.timeout(self._timeout): - req = await websession.get( - self._state_resource, - auth=self._auth, - headers=rendered_headers, - params=rendered_params, - ) - text = req.text + req = await websession.get( + self._state_resource, + auth=self._auth, + headers=rendered_headers, + params=rendered_params, + timeout=self._timeout, + ) + text = req.text if self._is_on_template is not None: text = self._is_on_template.async_render_with_possible_json_value( diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index df90af44e73..cc591573bd6 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -1,5 +1,4 @@ """The tests for the REST switch platform.""" -import asyncio from http import HTTPStatus import httpx @@ -84,7 +83,7 @@ async def test_setup_failed_connect( caplog: pytest.LogCaptureFixture, ) -> None: """Test setup when connection error occurs.""" - respx.get(RESOURCE).mock(side_effect=asyncio.TimeoutError()) + respx.get(RESOURCE).mock(side_effect=httpx.ConnectError("")) config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() @@ -98,7 +97,7 @@ async def test_setup_timeout( caplog: pytest.LogCaptureFixture, ) -> None: """Test setup when connection timeout occurs.""" - respx.get(RESOURCE).mock(side_effect=asyncio.TimeoutError()) + respx.get(RESOURCE).mock(side_effect=httpx.TimeoutException("")) config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() @@ -304,7 +303,7 @@ async def test_turn_on_timeout(hass: HomeAssistant) -> None: """Test turn_on when timeout occurs.""" await _async_setup_test_switch(hass) - respx.post(RESOURCE) % HTTPStatus.INTERNAL_SERVER_ERROR + respx.post(RESOURCE).mock(side_effect=httpx.TimeoutException("")) await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -364,7 +363,7 @@ async def test_turn_off_timeout(hass: HomeAssistant) -> None: """Test turn_off when timeout occurs.""" await _async_setup_test_switch(hass) - respx.post(RESOURCE).mock(side_effect=asyncio.TimeoutError()) + respx.post(RESOURCE).mock(side_effect=httpx.TimeoutException("")) await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -417,7 +416,7 @@ async def test_update_timeout(hass: HomeAssistant) -> None: """Test update when timeout occurs.""" await _async_setup_test_switch(hass) - respx.get(RESOURCE).mock(side_effect=asyncio.TimeoutError()) + respx.get(RESOURCE).mock(side_effect=httpx.TimeoutException("")) async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() From 8bfb6b57457242e6eabbc41578a3ed30ed7df106 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 11 Dec 2023 17:39:48 +0100 Subject: [PATCH 076/106] Add Raspberry Pi 5 specific container image (#105488) --- .github/workflows/builder.yml | 5 +++-- machine/raspberrypi5-64 | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 machine/raspberrypi5-64 diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 9d13c07301e..2fdde85c8cd 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -197,7 +197,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2023.09.0 + uses: home-assistant/builder@2023.12.0 with: args: | $BUILD_ARGS \ @@ -247,6 +247,7 @@ jobs: - raspberrypi3-64 - raspberrypi4 - raspberrypi4-64 + - raspberrypi5-64 - tinker - yellow - green @@ -273,7 +274,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2023.09.0 + uses: home-assistant/builder@2023.12.0 with: args: | $BUILD_ARGS \ diff --git a/machine/raspberrypi5-64 b/machine/raspberrypi5-64 new file mode 100644 index 00000000000..2ed3b3c8e44 --- /dev/null +++ b/machine/raspberrypi5-64 @@ -0,0 +1,8 @@ +ARG \ + BUILD_FROM + +FROM $BUILD_FROM + +RUN apk --no-cache add \ + raspberrypi-userland \ + raspberrypi-userland-libs From f1f3301edc2df92d4146620561a072a653205748 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 14 Dec 2023 15:53:22 -0600 Subject: [PATCH 077/106] Set todo item status in intent (#105743) --- homeassistant/components/todo/intent.py | 6 ++++-- tests/components/todo/test_init.py | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/todo/intent.py b/homeassistant/components/todo/intent.py index ba3545d8dfd..4cf62c6391d 100644 --- a/homeassistant/components/todo/intent.py +++ b/homeassistant/components/todo/intent.py @@ -6,7 +6,7 @@ from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent -from . import DOMAIN, TodoItem, TodoListEntity +from . import DOMAIN, TodoItem, TodoItemStatus, TodoListEntity INTENT_LIST_ADD_ITEM = "HassListAddItem" @@ -47,7 +47,9 @@ class ListAddItemIntent(intent.IntentHandler): assert target_list is not None # Add to list - await target_list.async_create_todo_item(TodoItem(item)) + await target_list.async_create_todo_item( + TodoItem(summary=item, status=TodoItemStatus.NEEDS_ACTION) + ) response = intent_obj.create_response() response.response_type = intent.IntentResponseType.ACTION_DONE diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 90b06858e00..0edca7a7ef6 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -1038,6 +1038,7 @@ async def test_add_item_intent( assert len(entity1.items) == 1 assert len(entity2.items) == 0 assert entity1.items[0].summary == "beer" + assert entity1.items[0].status == TodoItemStatus.NEEDS_ACTION entity1.items.clear() # Add to second list @@ -1052,6 +1053,7 @@ async def test_add_item_intent( assert len(entity1.items) == 0 assert len(entity2.items) == 1 assert entity2.items[0].summary == "cheese" + assert entity2.items[0].status == TodoItemStatus.NEEDS_ACTION # List name is case insensitive response = await intent.async_handle( @@ -1065,6 +1067,7 @@ async def test_add_item_intent( assert len(entity1.items) == 0 assert len(entity2.items) == 2 assert entity2.items[1].summary == "wine" + assert entity2.items[1].status == TodoItemStatus.NEEDS_ACTION # Missing list with pytest.raises(intent.IntentHandleError): From 0e2677396a851a276834621e986f1590ef6ab183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 15 Dec 2023 16:27:00 +0100 Subject: [PATCH 078/106] Bump aioairzone to v0.7.0 (#105807) --- 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 e9485f1b9d0..893316b5564 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.9"] + "requirements": ["aioairzone==0.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index eb5034fdfd0..2c2a74f0290 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,7 +191,7 @@ aioairq==0.3.1 aioairzone-cloud==0.3.6 # homeassistant.components.airzone -aioairzone==0.6.9 +aioairzone==0.7.0 # homeassistant.components.ambient_station aioambient==2023.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 593bea301cf..a9b82b9e20e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairq==0.3.1 aioairzone-cloud==0.3.6 # homeassistant.components.airzone -aioairzone==0.6.9 +aioairzone==0.7.0 # homeassistant.components.ambient_station aioambient==2023.04.0 From c5e3d922b03cf43aac3cfa79b3e77322fdecc21f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 16 Dec 2023 01:59:43 +0100 Subject: [PATCH 079/106] Update aioairzone to v0.7.2 (#105811) --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airzone/snapshots/test_diagnostics.ambr | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 893316b5564..20b8a452324 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.7.0"] + "requirements": ["aioairzone==0.7.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2c2a74f0290..705b2326885 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,7 +191,7 @@ aioairq==0.3.1 aioairzone-cloud==0.3.6 # homeassistant.components.airzone -aioairzone==0.7.0 +aioairzone==0.7.2 # homeassistant.components.ambient_station aioambient==2023.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a9b82b9e20e..155c886d650 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairq==0.3.1 aioairzone-cloud==0.3.6 # homeassistant.components.airzone -aioairzone==0.7.0 +aioairzone==0.7.2 # homeassistant.components.ambient_station aioambient==2023.04.0 diff --git a/tests/components/airzone/snapshots/test_diagnostics.ambr b/tests/components/airzone/snapshots/test_diagnostics.ambr index 9cb6e550711..7037faab8c6 100644 --- a/tests/components/airzone/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone/snapshots/test_diagnostics.ambr @@ -601,7 +601,7 @@ 1, ]), 'demand': False, - 'double-set-point': True, + 'double-set-point': False, 'full-name': 'Airzone [2:1] Airzone 2:1', 'heat-stage': 1, 'heat-stages': list([ From b58c8eaaa9653524267b6084b9e07a16e16802e4 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 15 Dec 2023 18:14:53 +0100 Subject: [PATCH 080/106] Fix HVAC mode duplication for Shelly Gen2 climate platform (#105812) Fix HVAC mode duplication --- homeassistant/components/shelly/climate.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 6a592c904f6..396fef5ac2e 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -419,7 +419,6 @@ class BlockSleepingClimate( class RpcClimate(ShellyRpcEntity, ClimateEntity): """Entity that controls a thermostat on RPC based Shelly devices.""" - _attr_hvac_modes = [HVACMode.OFF] _attr_icon = "mdi:thermostat" _attr_max_temp = RPC_THERMOSTAT_SETTINGS["max"] _attr_min_temp = RPC_THERMOSTAT_SETTINGS["min"] @@ -435,9 +434,9 @@ class RpcClimate(ShellyRpcEntity, ClimateEntity): "type", "heating" ) if self._thermostat_type == "cooling": - self._attr_hvac_modes.append(HVACMode.COOL) + self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL] else: - self._attr_hvac_modes.append(HVACMode.HEAT) + self._attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT] @property def target_temperature(self) -> float | None: From 7ae296b0f2d3ff0b0955b8f2a0b7d12096734512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 16 Dec 2023 18:42:58 +0100 Subject: [PATCH 081/106] Fix Airzone temperature range on new climate card (#105830) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * airzone: climate: fix double setpoint Signed-off-by: Álvaro Fernández Rojas * tests: airzone: fix double setpoint temperature Signed-off-by: Álvaro Fernández Rojas * tests: airzone: fix swapped double setpoint Signed-off-by: Álvaro Fernández Rojas --------- Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/climate.py | 3 ++- .../airzone/snapshots/test_diagnostics.ambr | 8 ++++---- tests/components/airzone/test_climate.py | 11 ++++++----- tests/components/airzone/util.py | 4 ++-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index 22172255b9b..f5a0e1b109e 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -248,7 +248,6 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): self._attr_hvac_mode = HVACMode.OFF self._attr_max_temp = self.get_airzone_value(AZD_TEMP_MAX) self._attr_min_temp = self.get_airzone_value(AZD_TEMP_MIN) - self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET) if self.supported_features & ClimateEntityFeature.FAN_MODE: self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED)) if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE: @@ -258,3 +257,5 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): self._attr_target_temperature_low = self.get_airzone_value( AZD_HEAT_TEMP_SET ) + else: + self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET) diff --git a/tests/components/airzone/snapshots/test_diagnostics.ambr b/tests/components/airzone/snapshots/test_diagnostics.ambr index 7037faab8c6..0eab9ffe81b 100644 --- a/tests/components/airzone/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone/snapshots/test_diagnostics.ambr @@ -188,7 +188,7 @@ 'coldStages': 0, 'coolmaxtemp': 90, 'coolmintemp': 64, - 'coolsetpoint': 73, + 'coolsetpoint': 77, 'errors': list([ ]), 'floor_demand': 0, @@ -196,7 +196,7 @@ 'heatStages': 0, 'heatmaxtemp': 86, 'heatmintemp': 50, - 'heatsetpoint': 77, + 'heatsetpoint': 73, 'humidity': 0, 'maxTemp': 90, 'minTemp': 64, @@ -644,7 +644,7 @@ 'cold-stage': 0, 'cool-temp-max': 90.0, 'cool-temp-min': 64.0, - 'cool-temp-set': 73.0, + 'cool-temp-set': 77.0, 'demand': True, 'double-set-point': True, 'floor-demand': False, @@ -652,7 +652,7 @@ 'heat-stage': 0, 'heat-temp-max': 86.0, 'heat-temp-min': 50.0, - 'heat-temp-set': 77.0, + 'heat-temp-set': 73.0, 'id': 1, 'master': True, 'mode': 7, diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py index 34844e34370..f33d1a8b28a 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -221,7 +221,8 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_MAX_TEMP) == 32.2 assert state.attributes.get(ATTR_MIN_TEMP) == 17.8 assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP - assert state.attributes.get(ATTR_TEMPERATURE) == 22.8 + assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25.0 + assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 22.8 HVAC_MOCK_CHANGED = copy.deepcopy(HVAC_MOCK) HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MAX_TEMP] = 25 @@ -594,8 +595,8 @@ async def test_airzone_climate_set_temp_range(hass: HomeAssistant) -> None: { API_SYSTEM_ID: 3, API_ZONE_ID: 1, - API_COOL_SET_POINT: 68.0, - API_HEAT_SET_POINT: 77.0, + API_COOL_SET_POINT: 77.0, + API_HEAT_SET_POINT: 68.0, } ] } @@ -618,5 +619,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) == 20.0 - assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 25.0 + assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25.0 + assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 20.0 diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index a3454549e05..f83eceaae9c 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -245,10 +245,10 @@ HVAC_MOCK = { API_ZONE_ID: 1, API_NAME: "DKN Plus", API_ON: 1, - API_COOL_SET_POINT: 73, + API_COOL_SET_POINT: 77, API_COOL_MAX_TEMP: 90, API_COOL_MIN_TEMP: 64, - API_HEAT_SET_POINT: 77, + API_HEAT_SET_POINT: 73, API_HEAT_MAX_TEMP: 86, API_HEAT_MIN_TEMP: 50, API_MAX_TEMP: 90, From 5ea24712606325bc0f3853c70e0878ab3b5cb195 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 Dec 2023 21:02:00 -1000 Subject: [PATCH 082/106] Bump pyunifiprotect to 4.22.3 (#105833) changelog: https://github.com/AngellusMortis/pyunifiprotect/compare/v4.22.0...v4.22.3 --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 045538aa2d1..cd38f50bf6d 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.22.0", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.22.3", "unifi-discovery==1.1.7"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 705b2326885..db384ac73df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2245,7 +2245,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.22.0 +pyunifiprotect==4.22.3 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 155c886d650..68054b77943 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1681,7 +1681,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.22.0 +pyunifiprotect==4.22.3 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From e16fb3a9d3c3d1be4cb58c6ed8a21a2d0a55211a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 16 Dec 2023 19:38:58 +0100 Subject: [PATCH 083/106] Address late review comments on AVM FRITZ!SmartHome (#105860) set copies dict keys by default --- homeassistant/components/fritzbox/binary_sensor.py | 2 +- homeassistant/components/fritzbox/button.py | 2 +- homeassistant/components/fritzbox/climate.py | 2 +- homeassistant/components/fritzbox/cover.py | 2 +- homeassistant/components/fritzbox/light.py | 2 +- homeassistant/components/fritzbox/sensor.py | 2 +- homeassistant/components/fritzbox/switch.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index e766a53518a..e36056d2fab 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -85,7 +85,7 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities(set(coordinator.data.devices.keys())) + _add_entities(set(coordinator.data.devices)) class FritzboxBinarySensor(FritzBoxDeviceEntity, BinarySensorEntity): diff --git a/homeassistant/components/fritzbox/button.py b/homeassistant/components/fritzbox/button.py index e56ebc1e3b0..6695c564331 100644 --- a/homeassistant/components/fritzbox/button.py +++ b/homeassistant/components/fritzbox/button.py @@ -29,7 +29,7 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities(set(coordinator.data.templates.keys())) + _add_entities(set(coordinator.data.templates)) class FritzBoxTemplate(FritzBoxEntity, ButtonEntity): diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 6ce885a3fdb..f648d4b3966 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -66,7 +66,7 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities(set(coordinator.data.devices.keys())) + _add_entities(set(coordinator.data.devices)) class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): diff --git a/homeassistant/components/fritzbox/cover.py b/homeassistant/components/fritzbox/cover.py index d3d4c9080ea..4c2ba76c377 100644 --- a/homeassistant/components/fritzbox/cover.py +++ b/homeassistant/components/fritzbox/cover.py @@ -38,7 +38,7 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities(set(coordinator.data.devices.keys())) + _add_entities(set(coordinator.data.devices)) class FritzboxCover(FritzBoxDeviceEntity, CoverEntity): diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py index 88d32fe33a5..cb0c8594695 100644 --- a/homeassistant/components/fritzbox/light.py +++ b/homeassistant/components/fritzbox/light.py @@ -44,7 +44,7 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities(set(coordinator.data.devices.keys())) + _add_entities(set(coordinator.data.devices)) class FritzboxLight(FritzBoxDeviceEntity, LightEntity): diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index fda8b239859..140ecaef331 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -230,7 +230,7 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities(set(coordinator.data.devices.keys())) + _add_entities(set(coordinator.data.devices)) class FritzBoxSensor(FritzBoxDeviceEntity, SensorEntity): diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 4a2960a18ea..4d93cddb617 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -33,7 +33,7 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities(set(coordinator.data.devices.keys())) + _add_entities(set(coordinator.data.devices)) class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity): From a6018c5f7e4ef025d861785ee9709e5420782994 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 21 Dec 2023 10:29:01 +0100 Subject: [PATCH 084/106] Set WiFi QR code entity to unknown when Fritzbox is not available (#105870) --- homeassistant/components/fritz/image.py | 10 +++++- tests/components/fritz/test_image.py | 43 ++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritz/image.py b/homeassistant/components/fritz/image.py index d14c562bd76..aa1ede5a185 100644 --- a/homeassistant/components/fritz/image.py +++ b/homeassistant/components/fritz/image.py @@ -5,6 +5,8 @@ from __future__ import annotations from io import BytesIO import logging +from requests.exceptions import RequestException + from homeassistant.components.image import ImageEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory @@ -78,7 +80,13 @@ class FritzGuestWifiQRImage(FritzBoxBaseEntity, ImageEntity): async def async_update(self) -> None: """Update the image entity data.""" - qr_bytes = await self._fetch_image() + try: + qr_bytes = await self._fetch_image() + except RequestException: + self._current_qr_bytes = None + self._attr_image_last_updated = None + self.async_write_ha_state() + return if self._current_qr_bytes != qr_bytes: dt_now = dt_util.utcnow() diff --git a/tests/components/fritz/test_image.py b/tests/components/fritz/test_image.py index cbcbded5692..da5b8a76d27 100644 --- a/tests/components/fritz/test_image.py +++ b/tests/components/fritz/test_image.py @@ -4,12 +4,13 @@ from http import HTTPStatus from unittest.mock import patch import pytest +from requests.exceptions import ReadTimeout from syrupy.assertion import SnapshotAssertion from homeassistant.components.fritz.const import DOMAIN from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import Platform +from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from homeassistant.setup import async_setup_component @@ -170,3 +171,43 @@ async def test_image_update( assert resp_body != resp_body_new assert resp_body_new == snapshot + + +@pytest.mark.parametrize(("fc_data"), [({**MOCK_FB_SERVICES, **GUEST_WIFI_ENABLED})]) +async def test_image_update_unavailable( + hass: HomeAssistant, + fc_class_mock, + fh_class_mock, +) -> None: + """Test image update when fritzbox is unavailable.""" + + # setup component with image platform only + with patch( + "homeassistant.components.fritz.PLATFORMS", + [Platform.IMAGE], + ): + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + state = hass.states.get("image.mock_title_guestwifi") + assert state + + # fritzbox becomes unavailable + fc_class_mock().call_action_side_effect(ReadTimeout) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done() + + state = hass.states.get("image.mock_title_guestwifi") + assert state.state == STATE_UNKNOWN + + # fritzbox is available again + fc_class_mock().call_action_side_effect(None) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done() + + state = hass.states.get("image.mock_title_guestwifi") + assert state.state != STATE_UNKNOWN From 03d7e9182b3d0b921d8dd90aad3207998411f48f Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 11 Dec 2023 17:37:15 +0100 Subject: [PATCH 085/106] Bump reolink_aio to 0.8.3 (#105489) --- 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 e03fa28b7ce..7dc81e83b53 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.8.2"] + "requirements": ["reolink-aio==0.8.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index db384ac73df..65771a3c241 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2338,7 +2338,7 @@ renault-api==0.2.1 renson-endura-delta==1.6.0 # homeassistant.components.reolink -reolink-aio==0.8.2 +reolink-aio==0.8.3 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 68054b77943..6b663987bd5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1750,7 +1750,7 @@ renault-api==0.2.1 renson-endura-delta==1.6.0 # homeassistant.components.reolink -reolink-aio==0.8.2 +reolink-aio==0.8.3 # homeassistant.components.rflink rflink==0.0.65 From 02919fc1a4d2902d17a538e56c6955f550d466bf Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 18 Dec 2023 10:37:22 +0100 Subject: [PATCH 086/106] Bump reolink_aio to 0.8.4 (#105946) --- 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 7dc81e83b53..e687fc5d9b1 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.8.3"] + "requirements": ["reolink-aio==0.8.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 65771a3c241..ade6570a86a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2338,7 +2338,7 @@ renault-api==0.2.1 renson-endura-delta==1.6.0 # homeassistant.components.reolink -reolink-aio==0.8.3 +reolink-aio==0.8.4 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6b663987bd5..4dbf78c04b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1750,7 +1750,7 @@ renault-api==0.2.1 renson-endura-delta==1.6.0 # homeassistant.components.reolink -reolink-aio==0.8.3 +reolink-aio==0.8.4 # homeassistant.components.rflink rflink==0.0.65 From 3b18eb214dac4e26a4699641a1ec22cf1038267a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 18 Dec 2023 11:30:44 +0100 Subject: [PATCH 087/106] Bump motionblinds to 0.6.19 (#105951) --- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index cc31ff42edf..f9115cd8146 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -21,5 +21,5 @@ "documentation": "https://www.home-assistant.io/integrations/motion_blinds", "iot_class": "local_push", "loggers": ["motionblinds"], - "requirements": ["motionblinds==0.6.18"] + "requirements": ["motionblinds==0.6.19"] } diff --git a/requirements_all.txt b/requirements_all.txt index ade6570a86a..01995284e2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1261,7 +1261,7 @@ moehlenhoff-alpha2==1.3.0 mopeka-iot-ble==0.5.0 # homeassistant.components.motion_blinds -motionblinds==0.6.18 +motionblinds==0.6.19 # homeassistant.components.motioneye motioneye-client==0.3.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4dbf78c04b0..ef42d0eec44 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -985,7 +985,7 @@ moehlenhoff-alpha2==1.3.0 mopeka-iot-ble==0.5.0 # homeassistant.components.motion_blinds -motionblinds==0.6.18 +motionblinds==0.6.19 # homeassistant.components.motioneye motioneye-client==0.3.14 From 03009a2bb98678a0a6112d612520344738548b74 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 18 Dec 2023 13:57:34 +0100 Subject: [PATCH 088/106] Fix unreachable Netatmo sensor returning false values (#105954) * Fix unreachable sensor returning false values * Clean up unnecessary code --- .../components/netatmo/manifest.json | 2 +- homeassistant/components/netatmo/sensor.py | 22 +++++++++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../netatmo/fixtures/getstationsdata.json | 14 ++---------- .../netatmo/snapshots/test_diagnostics.ambr | 6 +++-- tests/components/netatmo/test_sensor.py | 16 ++++++++++++-- 7 files changed, 36 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 7d84641874a..3860c70bbea 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["pyatmo"], - "requirements": ["pyatmo==7.6.0"] + "requirements": ["pyatmo==8.0.0"] } diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 10114a75f63..2f99b866cf2 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -447,17 +447,16 @@ class NetatmoWeatherSensor(NetatmoBase, SensorEntity): } ) - @property - def available(self) -> bool: - """Return entity availability.""" - return self.state is not None - @callback def async_update_callback(self) -> None: """Update the entity's state.""" if ( - state := getattr(self._module, self.entity_description.netatmo_name) - ) is None: + not self._module.reachable + or (state := getattr(self._module, self.entity_description.netatmo_name)) + is None + ): + if self.available: + self._attr_available = False return if self.entity_description.netatmo_name in { @@ -475,6 +474,7 @@ class NetatmoWeatherSensor(NetatmoBase, SensorEntity): else: self._attr_native_value = state + self._attr_available = True self.async_write_ha_state() @@ -519,7 +519,6 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity): if not self._module.reachable: if self.available: self._attr_available = False - self._attr_native_value = None return self._attr_available = True @@ -565,9 +564,15 @@ class NetatmoSensor(NetatmoBase, SensorEntity): @callback def async_update_callback(self) -> None: """Update the entity's state.""" + if not self._module.reachable: + if self.available: + self._attr_available = False + return + if (state := getattr(self._module, self.entity_description.key)) is None: return + self._attr_available = True self._attr_native_value = state self.async_write_ha_state() @@ -777,7 +782,6 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): self.entity_description.key, self._area_name, ) - self._attr_native_value = None self._attr_available = False return diff --git a/requirements_all.txt b/requirements_all.txt index 01995284e2e..ce71bcd1ca0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1619,7 +1619,7 @@ pyasuswrt==0.1.20 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.6.0 +pyatmo==8.0.0 # homeassistant.components.apple_tv pyatv==0.14.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef42d0eec44..95d8d6a3b0e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1235,7 +1235,7 @@ pyasuswrt==0.1.20 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.6.0 +pyatmo==8.0.0 # homeassistant.components.apple_tv pyatv==0.14.3 diff --git a/tests/components/netatmo/fixtures/getstationsdata.json b/tests/components/netatmo/fixtures/getstationsdata.json index 10c3ca85e06..b0da0820699 100644 --- a/tests/components/netatmo/fixtures/getstationsdata.json +++ b/tests/components/netatmo/fixtures/getstationsdata.json @@ -475,22 +475,12 @@ "last_setup": 1558709954, "data_type": ["Temperature", "Humidity"], "battery_percent": 27, - "reachable": true, + "reachable": false, "firmware": 50, "last_message": 1644582699, "last_seen": 1644582699, "rf_status": 68, - "battery_vp": 4678, - "dashboard_data": { - "time_utc": 1644582648, - "Temperature": 9.4, - "Humidity": 57, - "min_temp": 6.7, - "max_temp": 9.8, - "date_max_temp": 1644534223, - "date_min_temp": 1644569369, - "temp_trend": "up" - } + "battery_vp": 4678 }, { "_id": "12:34:56:80:c1:ea", diff --git a/tests/components/netatmo/snapshots/test_diagnostics.ambr b/tests/components/netatmo/snapshots/test_diagnostics.ambr index bd9005bd389..0dd424ec7d8 100644 --- a/tests/components/netatmo/snapshots/test_diagnostics.ambr +++ b/tests/components/netatmo/snapshots/test_diagnostics.ambr @@ -561,26 +561,28 @@ 'access_doorbell', 'access_presence', 'read_bubendorff', + 'read_bfi', 'read_camera', 'read_carbonmonoxidedetector', 'read_doorbell', 'read_homecoach', 'read_magellan', + 'read_mhs1', 'read_mx', 'read_presence', 'read_smarther', 'read_smokedetector', 'read_station', 'read_thermostat', - 'read_mhs1', 'write_bubendorff', + 'write_bfi', 'write_camera', 'write_magellan', + 'write_mhs1', 'write_mx', 'write_presence', 'write_smarther', 'write_thermostat', - 'write_mhs1', ]), 'type': 'Bearer', }), diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index 00cec6f8aa0..ce35873c3e5 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -10,8 +10,8 @@ from homeassistant.helpers import entity_registry as er from .common import TEST_TIME, selected_platforms -async def test_weather_sensor(hass: HomeAssistant, config_entry, netatmo_auth) -> None: - """Test weather sensor setup.""" +async def test_indoor_sensor(hass: HomeAssistant, config_entry, netatmo_auth) -> None: + """Test indoor sensor setup.""" with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -25,6 +25,18 @@ async def test_weather_sensor(hass: HomeAssistant, config_entry, netatmo_auth) - assert hass.states.get(f"{prefix}pressure").state == "1014.5" +async def test_weather_sensor(hass: HomeAssistant, config_entry, netatmo_auth) -> None: + """Test weather sensor unreachable.""" + with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + prefix = "sensor.villa_outdoor_" + + assert hass.states.get(f"{prefix}temperature").state == "unavailable" + + async def test_public_weather_sensor( hass: HomeAssistant, config_entry, netatmo_auth ) -> None: From 283f8f466fd08521c1b2c712729da4046da11f92 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 18 Dec 2023 19:31:37 +0100 Subject: [PATCH 089/106] Add Raspberry Pi 5 to version and hardware integration (#105992) --- homeassistant/components/hassio/__init__.py | 1 + homeassistant/components/raspberry_pi/hardware.py | 2 ++ homeassistant/components/version/const.py | 2 ++ 3 files changed, 5 insertions(+) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e7ab7aac3c8..3dd9b11ae64 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -270,6 +270,7 @@ HARDWARE_INTEGRATIONS = { "rpi3-64": "raspberry_pi", "rpi4": "raspberry_pi", "rpi4-64": "raspberry_pi", + "rpi5-64": "raspberry_pi", "yellow": "homeassistant_yellow", } diff --git a/homeassistant/components/raspberry_pi/hardware.py b/homeassistant/components/raspberry_pi/hardware.py index e90316ccb3c..2141ff6034d 100644 --- a/homeassistant/components/raspberry_pi/hardware.py +++ b/homeassistant/components/raspberry_pi/hardware.py @@ -17,6 +17,7 @@ BOARD_NAMES = { "rpi3-64": "Raspberry Pi 3", "rpi4": "Raspberry Pi 4 (32-bit)", "rpi4-64": "Raspberry Pi 4", + "rpi5-64": "Raspberry Pi 5", } MODELS = { @@ -28,6 +29,7 @@ MODELS = { "rpi3-64": "3", "rpi4": "4", "rpi4-64": "4", + "rpi5-64": "5", } diff --git a/homeassistant/components/version/const.py b/homeassistant/components/version/const.py index 2dcb0028b27..0b39ecee604 100644 --- a/homeassistant/components/version/const.py +++ b/homeassistant/components/version/const.py @@ -66,6 +66,7 @@ BOARD_MAP: Final[dict[str, str]] = { "RaspberryPi 3 64bit": "rpi3-64", "RaspberryPi 4": "rpi4", "RaspberryPi 4 64bit": "rpi4-64", + "RaspberryPi 5": "rpi5-64", "ASUS Tinkerboard": "tinker", "ODROID C2": "odroid-c2", "ODROID C4": "odroid-c4", @@ -112,6 +113,7 @@ VALID_IMAGES: Final = [ "raspberrypi3", "raspberrypi4-64", "raspberrypi4", + "raspberrypi5-64", "tinker", ] From 629146531327ad6148d200bda619b2d1fb039678 Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 19 Dec 2023 04:36:13 -0500 Subject: [PATCH 090/106] Bump blinkpy 0.22.4 (#105993) --- homeassistant/components/blink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index db3ab91de11..a1268919052 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/blink", "iot_class": "cloud_polling", "loggers": ["blinkpy"], - "requirements": ["blinkpy==0.22.3"] + "requirements": ["blinkpy==0.22.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index ce71bcd1ca0..08e02aa2a57 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -541,7 +541,7 @@ bleak==0.21.1 blebox-uniapi==2.2.0 # homeassistant.components.blink -blinkpy==0.22.3 +blinkpy==0.22.4 # homeassistant.components.bitcoin blockchain==1.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 95d8d6a3b0e..bef7bf8c7c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -460,7 +460,7 @@ bleak==0.21.1 blebox-uniapi==2.2.0 # homeassistant.components.blink -blinkpy==0.22.3 +blinkpy==0.22.4 # homeassistant.components.bluemaestro bluemaestro-ble==0.2.3 From 937b13dec623d185002d3c551eeccbc975f464af Mon Sep 17 00:00:00 2001 From: On Freund Date: Wed, 20 Dec 2023 11:35:42 +0200 Subject: [PATCH 091/106] Don't fetch unchanged OurGroceries lists (#105998) --- .../components/ourgroceries/__init__.py | 4 +-- .../components/ourgroceries/coordinator.py | 25 ++++++++------- tests/components/ourgroceries/__init__.py | 4 +-- tests/components/ourgroceries/conftest.py | 2 +- tests/components/ourgroceries/test_todo.py | 32 ++++++++++++++++++- 5 files changed, 48 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ourgroceries/__init__.py b/homeassistant/components/ourgroceries/__init__.py index d645b8617c2..ebb928e72d0 100644 --- a/homeassistant/components/ourgroceries/__init__.py +++ b/homeassistant/components/ourgroceries/__init__.py @@ -24,16 +24,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) data = entry.data og = OurGroceries(data[CONF_USERNAME], data[CONF_PASSWORD]) - lists = [] try: await og.login() - lists = (await og.get_my_lists())["shoppingLists"] except (AsyncIOTimeoutError, ClientError) as error: raise ConfigEntryNotReady from error except InvalidLoginException: return False - coordinator = OurGroceriesDataUpdateCoordinator(hass, og, lists) + coordinator = OurGroceriesDataUpdateCoordinator(hass, og) await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = coordinator diff --git a/homeassistant/components/ourgroceries/coordinator.py b/homeassistant/components/ourgroceries/coordinator.py index 636ebcc300a..c583fb4d5b1 100644 --- a/homeassistant/components/ourgroceries/coordinator.py +++ b/homeassistant/components/ourgroceries/coordinator.py @@ -20,13 +20,11 @@ _LOGGER = logging.getLogger(__name__) class OurGroceriesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): """Class to manage fetching OurGroceries data.""" - def __init__( - self, hass: HomeAssistant, og: OurGroceries, lists: list[dict] - ) -> None: + def __init__(self, hass: HomeAssistant, og: OurGroceries) -> None: """Initialize global OurGroceries data updater.""" self.og = og - self.lists = lists - self._ids = [sl["id"] for sl in lists] + self.lists: list[dict] = [] + self._cache: dict[str, dict] = {} interval = timedelta(seconds=SCAN_INTERVAL) super().__init__( hass, @@ -35,13 +33,16 @@ class OurGroceriesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): update_interval=interval, ) + async def _update_list(self, list_id: str, version_id: str) -> None: + old_version = self._cache.get(list_id, {}).get("list", {}).get("versionId", "") + if old_version == version_id: + return + self._cache[list_id] = await self.og.get_list_items(list_id=list_id) + async def _async_update_data(self) -> dict[str, dict]: """Fetch data from OurGroceries.""" - return dict( - zip( - self._ids, - await asyncio.gather( - *[self.og.get_list_items(list_id=id) for id in self._ids] - ), - ) + self.lists = (await self.og.get_my_lists())["shoppingLists"] + await asyncio.gather( + *[self._update_list(sl["id"], sl["versionId"]) for sl in self.lists] ) + return self._cache diff --git a/tests/components/ourgroceries/__init__.py b/tests/components/ourgroceries/__init__.py index 67fcb439908..6f90cb7ea1b 100644 --- a/tests/components/ourgroceries/__init__.py +++ b/tests/components/ourgroceries/__init__.py @@ -1,6 +1,6 @@ """Tests for the OurGroceries integration.""" -def items_to_shopping_list(items: list) -> dict[dict[list]]: +def items_to_shopping_list(items: list, version_id: str = "1") -> dict[dict[list]]: """Convert a list of items into a shopping list.""" - return {"list": {"items": items}} + return {"list": {"versionId": version_id, "items": items}} diff --git a/tests/components/ourgroceries/conftest.py b/tests/components/ourgroceries/conftest.py index 7f113da2633..c5fdec3ecb7 100644 --- a/tests/components/ourgroceries/conftest.py +++ b/tests/components/ourgroceries/conftest.py @@ -46,7 +46,7 @@ def mock_ourgroceries(items: list[dict]) -> AsyncMock: og = AsyncMock() og.login.return_value = True og.get_my_lists.return_value = { - "shoppingLists": [{"id": "test_list", "name": "Test List"}] + "shoppingLists": [{"id": "test_list", "name": "Test List", "versionId": "1"}] } og.get_list_items.return_value = items_to_shopping_list(items) return og diff --git a/tests/components/ourgroceries/test_todo.py b/tests/components/ourgroceries/test_todo.py index 8686c52d79b..649e86f2b05 100644 --- a/tests/components/ourgroceries/test_todo.py +++ b/tests/components/ourgroceries/test_todo.py @@ -17,6 +17,10 @@ from . import items_to_shopping_list from tests.common import async_fire_time_changed +def _mock_version_id(og: AsyncMock, version: int) -> None: + og.get_my_lists.return_value["shoppingLists"][0]["versionId"] = str(version) + + @pytest.mark.parametrize( ("items", "expected_state"), [ @@ -57,8 +61,10 @@ async def test_add_todo_list_item( ourgroceries.add_item_to_list = AsyncMock() # Fake API response when state is refreshed after create + _mock_version_id(ourgroceries, 2) ourgroceries.get_list_items.return_value = items_to_shopping_list( - [{"id": "12345", "name": "Soda"}] + [{"id": "12345", "name": "Soda"}], + version_id="2", ) await hass.services.async_call( @@ -95,6 +101,7 @@ async def test_update_todo_item_status( ourgroceries.toggle_item_crossed_off = AsyncMock() # Fake API response when state is refreshed after crossing off + _mock_version_id(ourgroceries, 2) ourgroceries.get_list_items.return_value = items_to_shopping_list( [{"id": "12345", "name": "Soda", "crossedOffAt": 1699107501}] ) @@ -118,6 +125,7 @@ async def test_update_todo_item_status( assert state.state == "0" # Fake API response when state is refreshed after reopen + _mock_version_id(ourgroceries, 2) ourgroceries.get_list_items.return_value = items_to_shopping_list( [{"id": "12345", "name": "Soda"}] ) @@ -166,6 +174,7 @@ async def test_update_todo_item_summary( ourgroceries.change_item_on_list = AsyncMock() # Fake API response when state is refreshed update + _mock_version_id(ourgroceries, 2) ourgroceries.get_list_items.return_value = items_to_shopping_list( [{"id": "12345", "name": "Milk"}] ) @@ -204,6 +213,7 @@ async def test_remove_todo_item( ourgroceries.remove_item_from_list = AsyncMock() # Fake API response when state is refreshed after remove + _mock_version_id(ourgroceries, 2) ourgroceries.get_list_items.return_value = items_to_shopping_list([]) await hass.services.async_call( @@ -224,6 +234,25 @@ async def test_remove_todo_item( assert state.state == "0" +async def test_version_id_optimization( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + setup_integration: None, + ourgroceries: AsyncMock, +) -> None: + """Test that list items aren't being retrieved if version id stays the same.""" + state = hass.states.get("todo.test_list") + assert state.state == "0" + assert ourgroceries.get_list_items.call_count == 1 + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("todo.test_list") + assert state.state == "0" + assert ourgroceries.get_list_items.call_count == 1 + + @pytest.mark.parametrize( ("exception"), [ @@ -242,6 +271,7 @@ async def test_coordinator_error( state = hass.states.get("todo.test_list") assert state.state == "0" + _mock_version_id(ourgroceries, 2) ourgroceries.get_list_items.side_effect = exception freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) From 59c50775577618436ae7bfc77b9b21a8e1d6853c Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 20 Dec 2023 20:32:03 +0100 Subject: [PATCH 092/106] Bump pyatmo to 8.0.1 (#106094) Fix missing NLFE --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 3860c70bbea..f5f2d67947f 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["pyatmo"], - "requirements": ["pyatmo==8.0.0"] + "requirements": ["pyatmo==8.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 08e02aa2a57..2de112025ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1619,7 +1619,7 @@ pyasuswrt==0.1.20 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==8.0.0 +pyatmo==8.0.1 # homeassistant.components.apple_tv pyatv==0.14.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bef7bf8c7c8..23d3e74498d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1235,7 +1235,7 @@ pyasuswrt==0.1.20 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==8.0.0 +pyatmo==8.0.1 # homeassistant.components.apple_tv pyatv==0.14.3 From 56c6de722362ea2c19c96330b7c987522d6c426c Mon Sep 17 00:00:00 2001 From: Ben <512997+benleb@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:46:54 +0100 Subject: [PATCH 093/106] Bump surepy to 0.9.0 (#106101) --- homeassistant/components/surepetcare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index 89e018b6635..bcfd10d2f02 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/surepetcare", "iot_class": "cloud_polling", "loggers": ["rich", "surepy"], - "requirements": ["surepy==0.8.0"] + "requirements": ["surepy==0.9.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2de112025ff..f781bd1414b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2537,7 +2537,7 @@ subarulink==0.7.9 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.8.0 +surepy==0.9.0 # homeassistant.components.swiss_hydrological_data swisshydrodata==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 23d3e74498d..833eb7ba259 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1898,7 +1898,7 @@ subarulink==0.7.9 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.8.0 +surepy==0.9.0 # homeassistant.components.switchbot_cloud switchbot-api==1.2.1 From 529d34235c0ebcfcfbcdc577c7adbb3147ec76cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 20 Dec 2023 14:10:56 -1000 Subject: [PATCH 094/106] Bump pyenphase to 1.15.2 (#106134) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index c49e1f143e6..4ae7760a56b 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.14.3"], + "requirements": ["pyenphase==1.15.2"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index f781bd1414b..125f8a75350 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1715,7 +1715,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.14.3 +pyenphase==1.15.2 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 833eb7ba259..eb8218648e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1295,7 +1295,7 @@ pyeconet==0.1.22 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.14.3 +pyenphase==1.15.2 # homeassistant.components.everlights pyeverlights==0.1.0 From e3cb90487c427418d5c5488e88de12d6bb6cba3a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 21 Dec 2023 01:44:36 -0500 Subject: [PATCH 095/106] Bump ZHA dependencies (#106147) --- homeassistant/components/zha/manifest.json | 10 +++++----- requirements_all.txt | 10 +++++----- requirements_test_all.txt | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index fe58ff044cd..a2965e782f4 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,15 +21,15 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.37.3", + "bellows==0.37.4", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.107", - "zigpy-deconz==0.22.2", - "zigpy==0.60.1", + "zha-quirks==0.0.108", + "zigpy-deconz==0.22.3", + "zigpy==0.60.2", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", - "zigpy-znp==0.12.0", + "zigpy-znp==0.12.1", "universal-silabs-flasher==0.0.15", "pyserial-asyncio-fast==0.11" ], diff --git a/requirements_all.txt b/requirements_all.txt index 125f8a75350..3917eaa919d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -523,7 +523,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.37.3 +bellows==0.37.4 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -2816,7 +2816,7 @@ zeroconf==0.128.5 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.107 +zha-quirks==0.0.108 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.9 @@ -2825,7 +2825,7 @@ zhong-hong-hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.22.2 +zigpy-deconz==0.22.3 # homeassistant.components.zha zigpy-xbee==0.20.1 @@ -2834,10 +2834,10 @@ zigpy-xbee==0.20.1 zigpy-zigate==0.12.0 # homeassistant.components.zha -zigpy-znp==0.12.0 +zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.1 +zigpy==0.60.2 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb8218648e9..2fe6eaa54a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -445,7 +445,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.37.3 +bellows==0.37.4 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -2111,10 +2111,10 @@ zeroconf==0.128.5 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.107 +zha-quirks==0.0.108 # homeassistant.components.zha -zigpy-deconz==0.22.2 +zigpy-deconz==0.22.3 # homeassistant.components.zha zigpy-xbee==0.20.1 @@ -2123,10 +2123,10 @@ zigpy-xbee==0.20.1 zigpy-zigate==0.12.0 # homeassistant.components.zha -zigpy-znp==0.12.0 +zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.1 +zigpy==0.60.2 # homeassistant.components.zwave_js zwave-js-server-python==0.54.0 From 0ca8e52e57c841dbe8f79599bd1b3a6c27ae09a2 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Thu, 21 Dec 2023 00:18:49 -0600 Subject: [PATCH 096/106] Bump life360 to 6.0.1 (#106149) * Bump life360 package to 6.0.1 Fix recent API issues. * Update requirements files --- homeassistant/components/life360/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index 18b83013d70..481d006809d 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/life360", "iot_class": "cloud_polling", "loggers": ["life360"], - "requirements": ["life360==6.0.0"] + "requirements": ["life360==6.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3917eaa919d..fd622b32f30 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1162,7 +1162,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==6.0.0 +life360==6.0.1 # homeassistant.components.osramlightify lightify==1.0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2fe6eaa54a7..fce1f7a4c5a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -913,7 +913,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==6.0.0 +life360==6.0.1 # homeassistant.components.linear_garage_door linear-garage-door==0.2.7 From 0beb47ac2c339b010396a60551edbed4027118c0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 23 Dec 2023 02:26:00 -0700 Subject: [PATCH 097/106] Fix bug with non-existent Notion bridge IDs (#106152) --- homeassistant/components/notion/__init__.py | 27 ++++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 036ef6e4f0e..406acd6aabd 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -290,17 +290,19 @@ class NotionEntity(CoordinatorEntity[DataUpdateCoordinator[NotionData]]): """Initialize the entity.""" super().__init__(coordinator) - bridge = self.coordinator.data.bridges[bridge_id] sensor = self.coordinator.data.sensors[sensor_id] + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, sensor.hardware_id)}, manufacturer="Silicon Labs", model=str(sensor.hardware_revision), name=str(sensor.name).capitalize(), sw_version=sensor.firmware_version, - via_device=(DOMAIN, bridge.hardware_id), ) + if bridge := self._async_get_bridge(bridge_id): + self._attr_device_info["via_device"] = (DOMAIN, bridge.hardware_id) + self._attr_extra_state_attributes = {} self._attr_unique_id = listener_id self._bridge_id = bridge_id @@ -322,6 +324,14 @@ class NotionEntity(CoordinatorEntity[DataUpdateCoordinator[NotionData]]): """Return the listener related to this entity.""" return self.coordinator.data.listeners[self._listener_id] + @callback + def _async_get_bridge(self, bridge_id: int) -> Bridge | None: + """Get a bridge by ID (if it exists).""" + if (bridge := self.coordinator.data.bridges.get(bridge_id)) is None: + LOGGER.debug("Entity references a non-existent bridge ID: %s", bridge_id) + return None + return bridge + @callback def _async_update_bridge_id(self) -> None: """Update the entity's bridge ID if it has changed. @@ -330,13 +340,12 @@ class NotionEntity(CoordinatorEntity[DataUpdateCoordinator[NotionData]]): """ sensor = self.coordinator.data.sensors[self._sensor_id] - # If the sensor's bridge ID is the same as what we had before or if it points - # to a bridge that doesn't exist (which can happen due to a Notion API bug), - # return immediately: - if ( - self._bridge_id == sensor.bridge.id - or sensor.bridge.id not in self.coordinator.data.bridges - ): + # If the bridge ID hasn't changed, return: + if self._bridge_id == sensor.bridge.id: + return + + # If the bridge doesn't exist, return: + if (bridge := self._async_get_bridge(sensor.bridge.id)) is None: return self._bridge_id = sensor.bridge.id From 5ab41c40a615e89a001eac3e01388528ad06176b Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 22 Dec 2023 07:37:57 +0100 Subject: [PATCH 098/106] Fix Netatmo light switching states by assuming state until next update (#106162) --- homeassistant/components/netatmo/light.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index e3bd8952b55..b796372fc20 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -186,11 +186,6 @@ class NetatmoLight(NetatmoBase, LightEntity): ] ) - @property - def is_on(self) -> bool: - """Return true if light is on.""" - return self._dimmer.on is True - async def async_turn_on(self, **kwargs: Any) -> None: """Turn light on.""" if ATTR_BRIGHTNESS in kwargs: @@ -211,6 +206,8 @@ class NetatmoLight(NetatmoBase, LightEntity): @callback def async_update_callback(self) -> None: """Update the entity's state.""" + self._attr_is_on = self._dimmer.on is True + if self._dimmer.brightness is not None: # Netatmo uses a range of [0, 100] to control brightness self._attr_brightness = round((self._dimmer.brightness / 100) * 255) From c76e1e8c80c7d9cce962ef1c7b8834a6a79f3159 Mon Sep 17 00:00:00 2001 From: fwestenberg <47930023+fwestenberg@users.noreply.github.com> Date: Thu, 21 Dec 2023 15:00:11 +0100 Subject: [PATCH 099/106] Bump Devialet to 1.4.4 (#106171) Bump Devialet==1.4.4 --- homeassistant/components/devialet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/devialet/manifest.json b/homeassistant/components/devialet/manifest.json index 286b9bfb112..e09485a8599 100644 --- a/homeassistant/components/devialet/manifest.json +++ b/homeassistant/components/devialet/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/devialet", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["devialet==1.4.3"], + "requirements": ["devialet==1.4.4"], "zeroconf": ["_devialet-http._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index fd622b32f30..f970c7cc254 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -681,7 +681,7 @@ demetriek==0.4.0 denonavr==0.11.4 # homeassistant.components.devialet -devialet==1.4.3 +devialet==1.4.4 # homeassistant.components.devolo_home_control devolo-home-control-api==0.18.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fce1f7a4c5a..72c56885b0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -556,7 +556,7 @@ demetriek==0.4.0 denonavr==0.11.4 # homeassistant.components.devialet -devialet==1.4.3 +devialet==1.4.4 # homeassistant.components.devolo_home_control devolo-home-control-api==0.18.2 From b98cb82f414060076f364a639e9a1ded3ca96b32 Mon Sep 17 00:00:00 2001 From: sdb9696 <51370195+sdb9696@users.noreply.github.com> Date: Thu, 21 Dec 2023 13:58:01 +0000 Subject: [PATCH 100/106] Bump ring-doorbell to 0.8.5 (#106178) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 36514fc8f35..85cab6f1763 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/ring", "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], - "requirements": ["ring-doorbell[listen]==0.8.3"] + "requirements": ["ring-doorbell[listen]==0.8.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index f970c7cc254..37a2b0305d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2347,7 +2347,7 @@ rfk101py==0.0.1 rflink==0.0.65 # homeassistant.components.ring -ring-doorbell[listen]==0.8.3 +ring-doorbell[listen]==0.8.5 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72c56885b0b..7497421f0c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1756,7 +1756,7 @@ reolink-aio==0.8.4 rflink==0.0.65 # homeassistant.components.ring -ring-doorbell[listen]==0.8.3 +ring-doorbell[listen]==0.8.5 # homeassistant.components.roku rokuecp==0.18.1 From d42048386934282d0c546049bb176988c5cdde76 Mon Sep 17 00:00:00 2001 From: fwestenberg <47930023+fwestenberg@users.noreply.github.com> Date: Thu, 21 Dec 2023 20:18:20 +0100 Subject: [PATCH 101/106] Bump Devialet to 1.4.5 (#106184) * Bump Devialet==1.4.4 * Bump Devialet to 1.4.5 --- homeassistant/components/devialet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/devialet/manifest.json b/homeassistant/components/devialet/manifest.json index e09485a8599..dd30f91c835 100644 --- a/homeassistant/components/devialet/manifest.json +++ b/homeassistant/components/devialet/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/devialet", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["devialet==1.4.4"], + "requirements": ["devialet==1.4.5"], "zeroconf": ["_devialet-http._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 37a2b0305d5..58233cb36e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -681,7 +681,7 @@ demetriek==0.4.0 denonavr==0.11.4 # homeassistant.components.devialet -devialet==1.4.4 +devialet==1.4.5 # homeassistant.components.devolo_home_control devolo-home-control-api==0.18.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7497421f0c4..2a95984f53a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -556,7 +556,7 @@ demetriek==0.4.0 denonavr==0.11.4 # homeassistant.components.devialet -devialet==1.4.4 +devialet==1.4.5 # homeassistant.components.devolo_home_control devolo-home-control-api==0.18.2 From baa18eb0bd8c3d684a309b350648f6a5938d3acb Mon Sep 17 00:00:00 2001 From: Alan Murray Date: Sat, 23 Dec 2023 01:23:39 +1100 Subject: [PATCH 102/106] Bump aiopulse to 0.4.4 (#106239) * Bump Rolease Acmeda version Bump aiopulse version to 0.4.4 to fix issue for blinds that use status structure to indicate blinds that are fully open or closed. * Update manifest.json * update requirements --- homeassistant/components/acmeda/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/acmeda/manifest.json b/homeassistant/components/acmeda/manifest.json index 94dcf3325ca..a8b3c7c829f 100644 --- a/homeassistant/components/acmeda/manifest.json +++ b/homeassistant/components/acmeda/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/acmeda", "iot_class": "local_push", "loggers": ["aiopulse"], - "requirements": ["aiopulse==0.4.3"] + "requirements": ["aiopulse==0.4.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 58233cb36e5..b65e6f60338 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -318,7 +318,7 @@ aioopenexchangerates==0.4.0 aiopegelonline==0.0.6 # homeassistant.components.acmeda -aiopulse==0.4.3 +aiopulse==0.4.4 # homeassistant.components.purpleair aiopurpleair==2022.12.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a95984f53a..cbe772165d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -291,7 +291,7 @@ aioopenexchangerates==0.4.0 aiopegelonline==0.0.6 # homeassistant.components.acmeda -aiopulse==0.4.3 +aiopulse==0.4.4 # homeassistant.components.purpleair aiopurpleair==2022.12.1 From 2bf5f8563ecd11e449859a2beef5088c8f45f517 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 22 Dec 2023 22:58:59 +0100 Subject: [PATCH 103/106] Fix Shelly consumption_types (#106273) --- homeassistant/components/shelly/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 6b5c59f28db..d4164c1dd7d 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -367,7 +367,9 @@ def is_block_channel_type_light(settings: dict[str, Any], channel: int) -> bool: def is_rpc_channel_type_light(config: dict[str, Any], channel: int) -> bool: """Return true if rpc channel consumption type is set to light.""" con_types = config["sys"].get("ui_data", {}).get("consumption_types") - return con_types is not None and con_types[channel].lower().startswith("light") + if con_types is None or len(con_types) <= channel: + return False + return cast(str, con_types[channel]).lower().startswith("light") def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]: From 690f68c571cbb460cf0600acb51f570f030a02d6 Mon Sep 17 00:00:00 2001 From: mkmer Date: Sat, 23 Dec 2023 21:11:51 -0500 Subject: [PATCH 104/106] Missing exception on relogin in Honeywell (#106324) Missing exception on relogin --- homeassistant/components/honeywell/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index dfac69b3aed..f76c78d52d2 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -507,6 +507,7 @@ class HoneywellUSThermostat(ClimateEntity): except ( AuthError, ClientConnectionError, + AscConnectionError, asyncio.TimeoutError, ): self._retry += 1 From 04c0dc7d91e043a7c0b9bf59cc9e30ad3c7ffc26 Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 26 Dec 2023 10:16:54 -0500 Subject: [PATCH 105/106] Redact unique id from diagnostics in blink (#106413) redact unique id --- homeassistant/components/blink/diagnostics.py | 2 +- tests/components/blink/conftest.py | 1 + tests/components/blink/snapshots/test_diagnostics.ambr | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/blink/diagnostics.py b/homeassistant/components/blink/diagnostics.py index f69c1721bf1..664d1421ac2 100644 --- a/homeassistant/components/blink/diagnostics.py +++ b/homeassistant/components/blink/diagnostics.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN -TO_REDACT = {"serial", "macaddress", "username", "password", "token"} +TO_REDACT = {"serial", "macaddress", "username", "password", "token", "unique_id"} async def async_get_config_entry_diagnostics( diff --git a/tests/components/blink/conftest.py b/tests/components/blink/conftest.py index 946840c23b9..d7deaf39bd9 100644 --- a/tests/components/blink/conftest.py +++ b/tests/components/blink/conftest.py @@ -87,6 +87,7 @@ def mock_config_fixture(): "device_id": "Home Assistant", "uid": "BlinkCamera_e1233333e2-0909-09cd-777a-123456789012", "token": "A_token", + "unique_id": "an_email@email.com", "host": "u034.immedia-semi.com", "region_id": "u034", "client_id": 123456, diff --git a/tests/components/blink/snapshots/test_diagnostics.ambr b/tests/components/blink/snapshots/test_diagnostics.ambr index 7fb13c97548..b572aae0a00 100644 --- a/tests/components/blink/snapshots/test_diagnostics.ambr +++ b/tests/components/blink/snapshots/test_diagnostics.ambr @@ -34,6 +34,7 @@ 'region_id': 'u034', 'token': '**REDACTED**', 'uid': 'BlinkCamera_e1233333e2-0909-09cd-777a-123456789012', + 'unique_id': '**REDACTED**', 'username': '**REDACTED**', }), 'disabled_by': None, From fd8a3cf78fa9b28a75ec8b22d8f229ba1faecece Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Dec 2023 09:45:19 +0100 Subject: [PATCH 106/106] Bump version to 2023.12.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 e4c6c2ed86a..b2538d0c87a 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 = 12 -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 c39c23819f4..a408e0de07e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.12.3" +version = "2023.12.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"