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 01/36] 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 e9760a96aa4e1..834a9bbb1ebc8 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -317,6 +317,11 @@ def _update_cost(self) -> None: 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 f4a1f661f9bee..522bbe5af06cd 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 02/36] 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 7322c58ae04fb..a42687a355119 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 fda92edee3f80..c52eae68202b7 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 675cfa7c64628..be09931ff33f2 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 03/36] 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 a2ddde02ad464..a6dd40d599338 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 c52eae68202b7..197c6919d8a72 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 be09931ff33f2..9e74cb091ef51 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 04/36] 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 38dd81ac196a0..37250fa6447a5 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 05/36] 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 06/36] 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 16a7ee5009c45..926ab5025f65c 100644 --- a/homeassistant/components/homeassistant/exposed_entities.py +++ b/homeassistant/components/homeassistant/exposed_entities.py @@ -38,6 +38,7 @@ "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 07/36] Disable scenarios (scenes) for local API in Overkiz (#105153) --- homeassistant/components/overkiz/__init__.py | 22 ++++++++----------- homeassistant/components/overkiz/strings.json | 2 +- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/overkiz/__init__.py b/homeassistant/components/overkiz/__init__.py index ebc3f96a7f541..03a81f673084b 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 @@ 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, scenarios = await asyncio.gather( - *[ - client.get_setup(), - client.get_scenarios(), - ] - ) + setup = await client.get_setup() + + # 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 2a549f1c24d63..a756df4d0d68f 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 08/36] 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 dc2ed04b4ed8b..044c9bf203b37 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -67,7 +67,7 @@ LightColorMode.XY: ColorMode.XY, } -TS0601_EFFECTS = [ +XMAS_LIGHT_EFFECTS = [ "carnival", "collide", "fading", @@ -200,8 +200,8 @@ def __init__(self, device: _LightDeviceT, gateway: DeconzGateway) -> None: 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 357371e4853b8..d38c65526c2e2 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 09/36] 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 5ffbc2fb18659..e03fa28b7cecf 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 197c6919d8a72..9a211710cde7a 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 9e74cb091ef51..855d22595f0a5 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 10/36] 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 4f2a9a8d99b03..ed9029d1c2c1d 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -9,7 +9,7 @@ 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 @@ async def prepare_text_to_speech(self) -> None: 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 @@ async def text_to_speech(self, tts_input: str) -> str: 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 24a4a92536d85..882d3a80fb3d5 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 audio_data(): ) +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 11/36] =?UTF-8?q?Bump=20M=C3=A9t=C3=A9o-France=20to=201.3.?= =?UTF-8?q?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 3b6bb9c3518ec..567788ec479c2 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 9a211710cde7a..e77116b881394 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 855d22595f0a5..96f288d6c44a4 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 12/36] 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 4bdedebfff9ff..87f59f31e9b72 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -253,7 +253,7 @@ def _matched( 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 13/36] 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 dcc2f49db0f0f..8760065055102 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 14/36] 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 dadd105b60663..5fe9d692bc347 100644 --- a/homeassistant/components/ping/coordinator.py +++ b/homeassistant/components/ping/coordinator.py @@ -40,7 +40,7 @@ def __init__( 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 15/36] 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 5fe9d692bc347..f6bda9693b817 100644 --- a/homeassistant/components/ping/coordinator.py +++ b/homeassistant/components/ping/coordinator.py @@ -40,7 +40,7 @@ def __init__( 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 16/36] 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 d6128d2672358..dda323c2c2adb 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -373,14 +373,14 @@ def mute_volume(self, mute=True): 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 17/36] 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 16558d2c795b6..b97ca06a471da 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -497,14 +497,16 @@ def temperature_unit(self) -> str: """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 18/36] 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 261cdb031bf3e..144495ec0661a 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 @@ async def _verify_meter(self, phone_number: str) -> None: 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 @@ async def async_step_user( 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 @@ async def async_step_finish_smart_meter( """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 ca6759baeff88..9ce87d707ff6b 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 19/36] 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 d31ccd180c460..8dc51e5973826 100644 --- a/homeassistant/components/fritzbox/light.py +++ b/homeassistant/components/fritzbox/light.py @@ -38,11 +38,9 @@ def _add_entities() -> None: 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 @@ def __init__( 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 @@ async def async_turn_off(self, **kwargs: Any) -> None: """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 20/36] 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 9cc96db7a576c..2d1030c6b92fb 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -209,21 +209,26 @@ def is_exclude(self, day: str, now: date) -> bool: 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 7457d2e0ada28..a359d83d87db2 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 21/36] 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 83f99ecc76a20..228da7f1a3612 100644 --- a/homeassistant/components/asuswrt/bridge.py +++ b/homeassistant/components/asuswrt/bridge.py @@ -55,7 +55,9 @@ _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 @@ async def _wrapper(self: _AsusWrtBridgeT) -> dict[str, Any]: 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 0f29c84c82087..72cbc37d57167 100644 --- a/tests/components/asuswrt/conftest.py +++ b/tests/components/asuswrt/conftest.py @@ -14,9 +14,9 @@ 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 22/36] 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 ed878fbb82edf..9648492c2e4dd 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 6c5428741af26..5e596d7970f5c 100644 --- a/tests/components/discovergy/const.py +++ b/tests/components/discovergy/const.py @@ -67,7 +67,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_diagnostics.ambr b/tests/components/discovergy/snapshots/test_diagnostics.ambr index 2a7dd6903af25..e8d4eab1909b6 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 981d1119a932f..2473af5012a03 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 23/36] 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 036f6c077daff..35b303a62e3f4 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 ff0655922832e..d4692ee8bf07f 100644 --- a/homeassistant/components/homewizard/const.py +++ b/homeassistant/components/homewizard/const.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from datetime import timedelta +import logging from homewizard_energy.models import Data, Device, State, System @@ -11,6 +12,8 @@ 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 78cee9ee6fec9..d980e66e0e4cb 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 7dab8cfbb06d4..a4893c77f42f4 100644 --- a/tests/components/homewizard/test_init.py +++ b/tests/components/homewizard/test_init.py @@ -7,7 +7,9 @@ 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 24/36] 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 8115066d0fbf5..5b8d19e5aa180 100644 --- a/homeassistant/components/ourgroceries/todo.py +++ b/homeassistant/components/ourgroceries/todo.py @@ -89,7 +89,7 @@ async def async_update_todo_item(self, item: TodoItem) -> None: 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 65bbff0e6018d..8686c52d79b65 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 25/36] 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 ee6f6d055489a..045538aa2d1ec 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 e77116b881394..6dd2780073747 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 96f288d6c44a4..1c61589a85920 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 26/36] 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 540aaa9aeac09..7174683fd189a 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 caf65db115eea..0e8e5d62f4b7d 100644 --- a/homeassistant/components/wyoming/satellite.py +++ b/homeassistant/components/wyoming/satellite.py @@ -9,6 +9,7 @@ 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 @@ async def _run_once(self) -> None: end_stage=end_stage, tts_audio_output="wav", pipeline_id=pipeline_id, + device_id=self.device.device_id, ) ) @@ -321,6 +323,16 @@ def _event_callback(self, event: assist_pipeline.PipelineEvent) -> None: 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 6dd2780073747..839b437a90754 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 1c61589a85920..eb45435be85f0 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 784f89b2ab811..b45b7508b2819 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 06ae337a19cd2..50252007aa5bc 100644 --- a/tests/components/wyoming/test_satellite.py +++ b/tests/components/wyoming/test_satellite.py @@ -8,6 +8,7 @@ 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 @@ def __init__(self, responses: list[Event]) -> None: 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 @@ async def write_event(self, event: Event): 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 on_stopped(self): # 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 27/36] 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 3d2957f153d1b..c48ce2c0a80f8 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -406,6 +406,9 @@ def state_received(msg: ReceiveMessage) -> None: 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 82b0b3467f4c4..c5c24c3ae7991 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 28/36] =?UTF-8?q?Always=20set=20=5Fattr=5Fcurrent=5Foption?= =?UTF-8?q?=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 b386e1584202e..2708dd75ffeca 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 29/36] 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 af2ea6f914908..2a7ef1396d562 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 e8e45a9393e14..2ec4c6843876b 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 839b437a90754..f31d734a68c9c 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 eb45435be85f0..55a34af78c016 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 30/36] 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 31/36] 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 99ebb4b60b1a4..aae8f67e1d8f2 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -649,7 +649,7 @@ def _make_intent_context( 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 fe94e2d5425c2..c68ec30128060 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 32/36] 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 ac18d43727c65..1af35c604ebe4 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 b3bccf71b5886..ff0f1ed8392f3 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 33/36] 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 8267fd2939072..df96500103509 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ 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 b6bb8649b0315..112a03f5e5bd5 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 34/36] 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 3fdc6b2c0794a..c4e6db4c623a5 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -362,6 +362,8 @@ def _update(self) -> Any: 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 cb988d3f2d47c..cdc9a8e07a671 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -5,6 +5,7 @@ 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 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.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 35/36] 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 721ac8bd5bee6..df8b1c1e01978 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 79358ec588de9..ce9fd0c036c5d 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 36/36] 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 efad1b7466b9f..84e0619773b58 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -160,6 +160,16 @@ def hvac_action(self) -> HVACAction: # 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 1373ba40fa3e2..bb2b428bf197c 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 f31d734a68c9c..9094578dd65cd 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 55a34af78c016..ae0f9d25a0b1e 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 dacee20c64425..37566e1d39ed7 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 0000000000000..049845bc82853 --- /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 6e6da1aa272d3..279fe6b8a43bf 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 0000000000000..104a723e4637b --- /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 0000000000000..ffb8cf62575a2 --- /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 624547155a3f3..2e1063d14d377 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 0000000000000..f78b4cd38a936 --- /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 e8a72c9b3fbfd..81d60bed9d41a 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 0000000000000..f78b4cd38a936 --- /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 0000000000000..ffb8cf62575a2 --- /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 0000000000000..ffb8cf62575a2 --- /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 0e0b3c51a0702..0a47893c077ad 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 0000000000000..8af35165c7e3c --- /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 d503bd3a59d26..ecda80491632b 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 0000000000000..7b301f5092423 --- /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 8604aaae10e6c..6b1012b0d87be 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 0000000000000..b2c839ae9d301 --- /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 597b9710ec5e1..29f23a137fb07 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 c14fd802e3b9f..c5ab3a209c2b1 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