From 7dfb397aefc66b6b3b85b96d71a2d64a6ac2d669 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Fri, 6 Oct 2023 18:23:48 +0200 Subject: [PATCH 01/29] Fix ZHA device diagnostics error for unknown unsupported attributes (#101239) * Modify test to account for scenario of unknown unsupported attributes * Add error checking for finding unsupported attributes * Change comment to clarify zigpy misses an attribute def This should make it more clear that it's about an unknown attribute (where zigpy doesn't have an attribute definition). * Increase test coverage This increases test coverage by doing the following: - adding the `IasZone` to our test device, so we have a cluster which actually has some attribute definitions - adding not just an unknown unsupported attribute by id, but also by name - adding a known unsupported attribute by id and by name * Fix diagnostics logic --- homeassistant/components/zha/diagnostics.py | 20 ++++++++++++++------ tests/components/zha/test_diagnostics.py | 18 +++++++++++++++++- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zha/diagnostics.py b/homeassistant/components/zha/diagnostics.py index 0fa1de5ff0e..ae68e6d5cca 100644 --- a/homeassistant/components/zha/diagnostics.py +++ b/homeassistant/components/zha/diagnostics.py @@ -139,6 +139,19 @@ def get_endpoint_cluster_attr_data(zha_device: ZHADevice) -> dict: def get_cluster_attr_data(cluster: Cluster) -> dict: """Return cluster attribute data.""" + unsupported_attributes = {} + for u_attr in cluster.unsupported_attributes: + try: + u_attr_def = cluster.find_attribute(u_attr) + unsupported_attributes[f"0x{u_attr_def.id:04x}"] = { + ATTR_ATTRIBUTE_NAME: u_attr_def.name + } + except KeyError: + if isinstance(u_attr, int): + unsupported_attributes[f"0x{u_attr:04x}"] = {} + else: + unsupported_attributes[u_attr] = {} + return { ATTRIBUTES: { f"0x{attr_id:04x}": { @@ -148,10 +161,5 @@ def get_cluster_attr_data(cluster: Cluster) -> dict: for attr_id, attr_def in cluster.attributes.items() if (attr_value := cluster.get(attr_def.name)) is not None }, - UNSUPPORTED_ATTRIBUTES: { - f"0x{cluster.find_attribute(u_attr).id:04x}": { - ATTR_ATTRIBUTE_NAME: cluster.find_attribute(u_attr).name - } - for u_attr in cluster.unsupported_attributes - }, + UNSUPPORTED_ATTRIBUTES: unsupported_attributes, } diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index c13bb36c1c0..1f6a731d0fb 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -44,7 +44,7 @@ def zigpy_device(zigpy_device_mock): """Device tracker zigpy device.""" endpoints = { 1: { - SIG_EP_INPUT: [security.IasAce.cluster_id], + SIG_EP_INPUT: [security.IasAce.cluster_id, security.IasZone.cluster_id], SIG_EP_OUTPUT: [], SIG_EP_TYPE: zha.DeviceType.IAS_ANCILLARY_CONTROL, SIG_EP_PROFILE: zha.PROFILE_ID, @@ -93,6 +93,22 @@ async def test_diagnostics_for_device( ) -> None: """Test diagnostics for device.""" zha_device: ZHADevice = await zha_device_joined(zigpy_device) + + # add unknown unsupported attribute with id and name + zha_device.device.endpoints[1].in_clusters[ + security.IasAce.cluster_id + ].unsupported_attributes.update({0x1000, "unknown_attribute_name"}) + + # add known unsupported attributes with id and name + zha_device.device.endpoints[1].in_clusters[ + security.IasZone.cluster_id + ].unsupported_attributes.update( + { + security.IasZone.AttributeDefs.num_zone_sensitivity_levels_supported.id, + security.IasZone.AttributeDefs.current_zone_sensitivity_level.name, + } + ) + dev_reg = async_get(hass) device = dev_reg.async_get_device(identifiers={("zha", str(zha_device.ieee))}) assert device From 2345a2be5f91f72549a6f34451618d93adab0489 Mon Sep 17 00:00:00 2001 From: Marty Sun Date: Thu, 5 Oct 2023 13:14:14 +0800 Subject: [PATCH 02/29] Bump pyyardian to 1.1.1 (#101363) * Update Yardian Dependency * test requirements --- homeassistant/components/yardian/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yardian/manifest.json b/homeassistant/components/yardian/manifest.json index a20315278b4..ba6396e1f75 100644 --- a/homeassistant/components/yardian/manifest.json +++ b/homeassistant/components/yardian/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yardian", "iot_class": "local_polling", - "requirements": ["pyyardian==1.1.0"] + "requirements": ["pyyardian==1.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index cbf8738cb2e..9c39e3c9052 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2270,7 +2270,7 @@ pyws66i==1.1 pyxeoma==1.4.1 # homeassistant.components.yardian -pyyardian==1.1.0 +pyyardian==1.1.1 # homeassistant.components.qrcode pyzbar==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 083595f13aa..12c0bdc96a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1693,7 +1693,7 @@ pywizlight==0.5.14 pyws66i==1.1 # homeassistant.components.yardian -pyyardian==1.1.0 +pyyardian==1.1.1 # homeassistant.components.zerproc pyzerproc==0.4.8 From 10e43048bd7698d569a8bf3d5ab46c6604456ae2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 4 Oct 2023 16:45:13 +0200 Subject: [PATCH 03/29] Fix Withings translations (#101397) --- homeassistant/components/withings/sensor.py | 5 ++--- homeassistant/components/withings/strings.json | 3 +++ tests/components/withings/snapshots/test_sensor.ambr | 8 ++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 42f5ac18f2f..77a706dc55d 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -240,8 +240,7 @@ SENSORS = [ key=Measurement.SLEEP_HEART_RATE_MAX.value, measurement=Measurement.SLEEP_HEART_RATE_MAX, measure_type=GetSleepSummaryField.HR_MAX, - translation_key="fat_mass", - name="Maximum heart rate", + translation_key="maximum_heart_rate", native_unit_of_measurement=UOM_BEATS_PER_MINUTE, icon="mdi:heart-pulse", state_class=SensorStateClass.MEASUREMENT, @@ -251,7 +250,7 @@ SENSORS = [ key=Measurement.SLEEP_HEART_RATE_MIN.value, measurement=Measurement.SLEEP_HEART_RATE_MIN, measure_type=GetSleepSummaryField.HR_MIN, - translation_key="maximum_heart_rate", + translation_key="minimum_heart_rate", native_unit_of_measurement=UOM_BEATS_PER_MINUTE, icon="mdi:heart-pulse", state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index ea925f535e3..df948a2b593 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -89,6 +89,9 @@ "maximum_heart_rate": { "name": "Maximum heart rate" }, + "minimum_heart_rate": { + "name": "Minimum heart rate" + }, "light_sleep": { "name": "Light sleep" }, diff --git a/tests/components/withings/snapshots/test_sensor.ambr b/tests/components/withings/snapshots/test_sensor.ambr index 6aa9e5b3784..9733880b03a 100644 --- a/tests/components/withings/snapshots/test_sensor.ambr +++ b/tests/components/withings/snapshots/test_sensor.ambr @@ -211,13 +211,13 @@ # name: test_all_entities.21 StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Fat mass', + 'friendly_name': 'henk Maximum heart rate', 'icon': 'mdi:heart-pulse', 'state_class': , 'unit_of_measurement': 'bpm', }), 'context': , - 'entity_id': 'sensor.henk_fat_mass_2', + 'entity_id': 'sensor.henk_maximum_heart_rate', 'last_changed': , 'last_updated': , 'state': '165.0', @@ -226,13 +226,13 @@ # name: test_all_entities.22 StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Maximum heart rate', + 'friendly_name': 'henk Minimum heart rate', 'icon': 'mdi:heart-pulse', 'state_class': , 'unit_of_measurement': 'bpm', }), 'context': , - 'entity_id': 'sensor.henk_maximum_heart_rate', + 'entity_id': 'sensor.henk_minimum_heart_rate', 'last_changed': , 'last_updated': , 'state': '166.0', From 223f3a434b5c4a75ee7bf5b3cac688fdf5023c5f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 4 Oct 2023 19:36:34 +0200 Subject: [PATCH 04/29] Raise vol.Invalid for invalid mqtt device_tracker config (#101399) Raise vol.Invalid for invalid mqtt device_tracker --- .../components/mqtt/device_tracker.py | 2 +- tests/components/mqtt/test_discovery.py | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index 2270f2b4031..2557a2afb5d 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -52,7 +52,7 @@ DEFAULT_SOURCE_TYPE = SourceType.GPS def valid_config(config: ConfigType) -> ConfigType: """Check if there is a state topic or json_attributes_topic.""" if CONF_STATE_TOPIC not in config and CONF_JSON_ATTRS_TOPIC not in config: - raise vol.MultipleInvalid( + raise vol.Invalid( f"Invalid device tracker config, missing {CONF_STATE_TOPIC} or {CONF_JSON_ATTRS_TOPIC}, got: {config}" ) return config diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index c528687623b..4d0b8457049 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -122,6 +122,28 @@ async def test_invalid_json( assert not mock_dispatcher_send.called +@pytest.mark.no_fail_on_log_exception +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) +async def test_discovery_schema_error( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test unexpected error JSON config.""" + with patch( + "homeassistant.components.mqtt.binary_sensor.DISCOVERY_SCHEMA", + side_effect=AttributeError("Attribute abc not found"), + ): + await mqtt_mock_entry() + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{"name": "Beer", "state_topic": "ok"}', + ) + await hass.async_block_till_done() + assert "AttributeError: Attribute abc not found" in caplog.text + + async def test_only_valid_components( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, From a4f0da82865d0ffea10cc848964dc0c11730cf25 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Oct 2023 21:01:10 -0400 Subject: [PATCH 05/29] Bump dbus-fast to 2.11.1 (#101406) changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v2.11.0...v2.11.1 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 2e2d6fa45ed..04815dc8972 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -19,6 +19,6 @@ "bluetooth-adapters==0.16.1", "bluetooth-auto-recovery==1.2.3", "bluetooth-data-tools==1.12.0", - "dbus-fast==2.11.0" + "dbus-fast==2.11.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 51d03a40971..95ac592dadb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ bluetooth-data-tools==1.12.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.4 -dbus-fast==2.11.0 +dbus-fast==2.11.1 fnv-hash-fast==0.4.1 ha-av==10.1.1 hass-nabucasa==0.71.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9c39e3c9052..cdfd83b23b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -645,7 +645,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==2.11.0 +dbus-fast==2.11.1 # homeassistant.components.debugpy debugpy==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12c0bdc96a5..11c632367af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -528,7 +528,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==2.11.0 +dbus-fast==2.11.1 # homeassistant.components.debugpy debugpy==1.8.0 From e8c38fe99ef6d2334295258973e63fa8e44f36d9 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 5 Oct 2023 08:32:43 +0200 Subject: [PATCH 06/29] Add translation for Tamper binary sensor (#101416) --- homeassistant/components/binary_sensor/strings.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index b86c013f104..573b154e2a4 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -286,6 +286,13 @@ "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" } }, + "tamper": { + "name": "Tamper", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "Tampering detected" + } + }, "update": { "name": "Update", "state": { From f7ab00a8bfe3b25bd20b1f087bbeaf067a732b4f Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 6 Oct 2023 02:18:35 -0500 Subject: [PATCH 07/29] Add wake word cooldown to avoid duplicate wake-ups (#101417) --- .../components/assist_pipeline/__init__.py | 6 +- .../components/assist_pipeline/const.py | 9 ++ .../components/assist_pipeline/pipeline.py | 29 ++++++- .../assist_pipeline/websocket_api.py | 7 +- .../assist_pipeline/snapshots/test_init.ambr | 2 + .../snapshots/test_websocket.ambr | 62 ++++++++++++++ tests/components/assist_pipeline/test_init.py | 16 ++-- .../assist_pipeline/test_websocket.py | 85 ++++++++++++++++++- 8 files changed, 198 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/assist_pipeline/__init__.py b/homeassistant/components/assist_pipeline/__init__.py index 9a61346f673..fab4c3178bc 100644 --- a/homeassistant/components/assist_pipeline/__init__.py +++ b/homeassistant/components/assist_pipeline/__init__.py @@ -9,7 +9,7 @@ from homeassistant.components import stt from homeassistant.core import Context, HomeAssistant from homeassistant.helpers.typing import ConfigType -from .const import DATA_CONFIG, DOMAIN +from .const import CONF_DEBUG_RECORDING_DIR, DATA_CONFIG, DOMAIN from .error import PipelineNotFound from .pipeline import ( AudioSettings, @@ -45,7 +45,9 @@ __all__ = ( CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( - {vol.Optional("debug_recording_dir"): str}, + { + vol.Optional(CONF_DEBUG_RECORDING_DIR): str, + }, ) }, extra=vol.ALLOW_EXTRA, diff --git a/homeassistant/components/assist_pipeline/const.py b/homeassistant/components/assist_pipeline/const.py index e21d9003a69..84b49fc18fa 100644 --- a/homeassistant/components/assist_pipeline/const.py +++ b/homeassistant/components/assist_pipeline/const.py @@ -2,3 +2,12 @@ DOMAIN = "assist_pipeline" DATA_CONFIG = f"{DOMAIN}.config" + +DEFAULT_PIPELINE_TIMEOUT = 60 * 5 # seconds + +DEFAULT_WAKE_WORD_TIMEOUT = 3 # seconds + +CONF_DEBUG_RECORDING_DIR = "debug_recording_dir" + +DATA_LAST_WAKE_UP = f"{DOMAIN}.last_wake_up" +DEFAULT_WAKE_WORD_COOLDOWN = 2 # seconds diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 76444fb2436..6ec031baf3b 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -48,7 +48,13 @@ from homeassistant.util import ( ) from homeassistant.util.limited_size_dict import LimitedSizeDict -from .const import DATA_CONFIG, DOMAIN +from .const import ( + CONF_DEBUG_RECORDING_DIR, + DATA_CONFIG, + DATA_LAST_WAKE_UP, + DEFAULT_WAKE_WORD_COOLDOWN, + DOMAIN, +) from .error import ( IntentRecognitionError, PipelineError, @@ -399,6 +405,9 @@ class WakeWordSettings: audio_seconds_to_buffer: float = 0 """Seconds of audio to buffer before detection and forward to STT.""" + cooldown_seconds: float = DEFAULT_WAKE_WORD_COOLDOWN + """Seconds after a wake word detection where other detections are ignored.""" + @dataclass(frozen=True) class AudioSettings: @@ -603,6 +612,8 @@ class PipelineRun: ) ) + wake_word_settings = self.wake_word_settings or WakeWordSettings() + # Remove language since it doesn't apply to wake words yet metadata_dict.pop("language", None) @@ -612,6 +623,7 @@ class PipelineRun: { "entity_id": self.wake_word_entity_id, "metadata": metadata_dict, + "timeout": wake_word_settings.timeout or 0, }, ) ) @@ -619,8 +631,6 @@ class PipelineRun: if self.debug_recording_queue is not None: self.debug_recording_queue.put_nowait(f"00_wake-{self.wake_word_entity_id}") - wake_word_settings = self.wake_word_settings or WakeWordSettings() - wake_word_vad: VoiceActivityTimeout | None = None if (wake_word_settings.timeout is not None) and ( wake_word_settings.timeout > 0 @@ -670,6 +680,17 @@ class PipelineRun: if result is None: wake_word_output: dict[str, Any] = {} else: + # Avoid duplicate detections by checking cooldown + last_wake_up = self.hass.data.get(DATA_LAST_WAKE_UP) + if last_wake_up is not None: + sec_since_last_wake_up = time.monotonic() - last_wake_up + if sec_since_last_wake_up < wake_word_settings.cooldown_seconds: + _LOGGER.debug("Duplicate wake word detection occurred") + raise WakeWordDetectionAborted + + # Record last wake up time to block duplicate detections + self.hass.data[DATA_LAST_WAKE_UP] = time.monotonic() + if result.queued_audio: # Add audio that was pending at detection. # @@ -1032,7 +1053,7 @@ class PipelineRun: # Directory to save audio for each pipeline run. # Configured in YAML for assist_pipeline. if debug_recording_dir := self.hass.data[DATA_CONFIG].get( - "debug_recording_dir" + CONF_DEBUG_RECORDING_DIR ): if device_id is None: # // diff --git a/homeassistant/components/assist_pipeline/websocket_api.py b/homeassistant/components/assist_pipeline/websocket_api.py index 798843ea6e3..fda3e266490 100644 --- a/homeassistant/components/assist_pipeline/websocket_api.py +++ b/homeassistant/components/assist_pipeline/websocket_api.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.util import language as language_util -from .const import DOMAIN +from .const import DEFAULT_PIPELINE_TIMEOUT, DEFAULT_WAKE_WORD_TIMEOUT, DOMAIN from .error import PipelineNotFound from .pipeline import ( AudioSettings, @@ -30,9 +30,6 @@ from .pipeline import ( async_get_pipeline, ) -DEFAULT_TIMEOUT = 60 * 5 # seconds -DEFAULT_WAKE_WORD_TIMEOUT = 3 # seconds - _LOGGER = logging.getLogger(__name__) @@ -117,7 +114,7 @@ async def websocket_run( ) return - timeout = msg.get("timeout", DEFAULT_TIMEOUT) + timeout = msg.get("timeout", DEFAULT_PIPELINE_TIMEOUT) start_stage = PipelineStage(msg["start_stage"]) end_stage = PipelineStage(msg["end_stage"]) handler_id: int | None = None diff --git a/tests/components/assist_pipeline/snapshots/test_init.ambr b/tests/components/assist_pipeline/snapshots/test_init.ambr index 3f0582f2bfb..e822759d208 100644 --- a/tests/components/assist_pipeline/snapshots/test_init.ambr +++ b/tests/components/assist_pipeline/snapshots/test_init.ambr @@ -285,6 +285,7 @@ 'format': , 'sample_rate': , }), + 'timeout': 0, }), 'type': , }), @@ -396,6 +397,7 @@ 'format': , 'sample_rate': , }), + 'timeout': 0, }), 'type': , }), diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 7cecf9fed40..b8c668f3fd0 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -373,6 +373,7 @@ 'format': 'wav', 'sample_rate': 16000, }), + 'timeout': 0, }) # --- # name: test_audio_pipeline_with_wake_word_no_timeout.2 @@ -474,6 +475,7 @@ 'format': 'wav', 'sample_rate': 16000, }), + 'timeout': 1, }) # --- # name: test_audio_pipeline_with_wake_word_timeout.2 @@ -655,3 +657,63 @@ # name: test_tts_failed.2 None # --- +# name: test_wake_word_cooldown + dict({ + 'language': 'en', + 'pipeline': , + 'runner_data': dict({ + 'stt_binary_handler_id': 1, + 'timeout': 300, + }), + }) +# --- +# name: test_wake_word_cooldown.1 + dict({ + 'language': 'en', + 'pipeline': , + 'runner_data': dict({ + 'stt_binary_handler_id': 1, + 'timeout': 300, + }), + }) +# --- +# name: test_wake_word_cooldown.2 + dict({ + 'entity_id': 'wake_word.test', + 'metadata': dict({ + 'bit_rate': 16, + 'channel': 1, + 'codec': 'pcm', + 'format': 'wav', + 'sample_rate': 16000, + }), + 'timeout': 3, + }) +# --- +# name: test_wake_word_cooldown.3 + dict({ + 'entity_id': 'wake_word.test', + 'metadata': dict({ + 'bit_rate': 16, + 'channel': 1, + 'codec': 'pcm', + 'format': 'wav', + 'sample_rate': 16000, + }), + 'timeout': 3, + }) +# --- +# name: test_wake_word_cooldown.4 + dict({ + 'wake_word_output': dict({ + 'timestamp': 0, + 'wake_word_id': 'test_ww', + }), + }) +# --- +# name: test_wake_word_cooldown.5 + dict({ + 'code': 'wake_word_detection_aborted', + 'message': '', + }) +# --- diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index 98ecae628f1..a98858a1bce 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -10,6 +10,10 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components import assist_pipeline, stt +from homeassistant.components.assist_pipeline.const import ( + CONF_DEBUG_RECORDING_DIR, + DOMAIN, +) from homeassistant.core import Context, HomeAssistant from homeassistant.setup import async_setup_component @@ -395,8 +399,8 @@ async def test_pipeline_save_audio( temp_dir = Path(temp_dir_str) assert await async_setup_component( hass, - "assist_pipeline", - {"assist_pipeline": {"debug_recording_dir": temp_dir_str}}, + DOMAIN, + {DOMAIN: {CONF_DEBUG_RECORDING_DIR: temp_dir_str}}, ) pipeline = assist_pipeline.async_get_pipeline(hass) @@ -476,8 +480,8 @@ async def test_pipeline_saved_audio_with_device_id( temp_dir = Path(temp_dir_str) assert await async_setup_component( hass, - "assist_pipeline", - {"assist_pipeline": {"debug_recording_dir": temp_dir_str}}, + DOMAIN, + {DOMAIN: {CONF_DEBUG_RECORDING_DIR: temp_dir_str}}, ) def event_callback(event: assist_pipeline.PipelineEvent): @@ -529,8 +533,8 @@ async def test_pipeline_saved_audio_write_error( temp_dir = Path(temp_dir_str) assert await async_setup_component( hass, - "assist_pipeline", - {"assist_pipeline": {"debug_recording_dir": temp_dir_str}}, + DOMAIN, + {DOMAIN: {CONF_DEBUG_RECORDING_DIR: temp_dir_str}}, ) def event_callback(event: assist_pipeline.PipelineEvent): diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index f995a0d3577..28b31e5b19c 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -9,6 +9,8 @@ from homeassistant.components.assist_pipeline.pipeline import Pipeline, Pipeline from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from .conftest import MockWakeWordEntity + from tests.typing import WebSocketGenerator @@ -266,7 +268,7 @@ async def test_audio_pipeline_with_wake_word_no_timeout( events.append(msg["event"]) # "audio" - await client.send_bytes(bytes([1]) + b"wake word") + await client.send_bytes(bytes([handler_id]) + b"wake word") msg = await client.receive_json() assert msg["event"]["type"] == "wake_word-end" @@ -1805,3 +1807,84 @@ async def test_audio_pipeline_with_enhancements( msg = await client.receive_json() assert msg["success"] assert msg["result"] == {"events": events} + + +async def test_wake_word_cooldown( + hass: HomeAssistant, + init_components, + mock_wake_word_provider_entity: MockWakeWordEntity, + hass_ws_client: WebSocketGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test that duplicate wake word detections are blocked during the cooldown period.""" + client_1 = await hass_ws_client(hass) + client_2 = await hass_ws_client(hass) + + await client_1.send_json_auto_id( + { + "type": "assist_pipeline/run", + "start_stage": "wake_word", + "end_stage": "tts", + "input": { + "sample_rate": 16000, + "no_vad": True, + "no_chunking": True, + }, + } + ) + + await client_2.send_json_auto_id( + { + "type": "assist_pipeline/run", + "start_stage": "wake_word", + "end_stage": "tts", + "input": { + "sample_rate": 16000, + "no_vad": True, + "no_chunking": True, + }, + } + ) + + # result + msg = await client_1.receive_json() + assert msg["success"], msg + + msg = await client_2.receive_json() + assert msg["success"], msg + + # run start + msg = await client_1.receive_json() + assert msg["event"]["type"] == "run-start" + msg["event"]["data"]["pipeline"] = ANY + handler_id_1 = msg["event"]["data"]["runner_data"]["stt_binary_handler_id"] + assert msg["event"]["data"] == snapshot + + msg = await client_2.receive_json() + assert msg["event"]["type"] == "run-start" + msg["event"]["data"]["pipeline"] = ANY + handler_id_2 = msg["event"]["data"]["runner_data"]["stt_binary_handler_id"] + assert msg["event"]["data"] == snapshot + + # wake_word + msg = await client_1.receive_json() + assert msg["event"]["type"] == "wake_word-start" + assert msg["event"]["data"] == snapshot + + msg = await client_2.receive_json() + assert msg["event"]["type"] == "wake_word-start" + assert msg["event"]["data"] == snapshot + + # Wake both up at the same time + await client_1.send_bytes(bytes([handler_id_1]) + b"wake word") + await client_2.send_bytes(bytes([handler_id_2]) + b"wake word") + + # Get response events + msg = await client_1.receive_json() + event_type_1 = msg["event"]["type"] + + msg = await client_2.receive_json() + event_type_2 = msg["event"]["type"] + + # One should be a wake up, one should be an error + assert {event_type_1, event_type_2} == {"wake_word-end", "error"} From f0cb2ba0055e3dd75e33e272af1319f7b4363e54 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Fri, 6 Oct 2023 01:41:01 -0600 Subject: [PATCH 08/29] Adjust WeatherFlow wind sensors to appropriately match native unit and library field (#101418) --- homeassistant/components/weatherflow/sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/weatherflow/sensor.py b/homeassistant/components/weatherflow/sensor.py index dfc8e585f1b..cd648fda360 100644 --- a/homeassistant/components/weatherflow/sensor.py +++ b/homeassistant/components/weatherflow/sensor.py @@ -245,7 +245,7 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( translation_key="wind_gust", icon="mdi:weather-windy", device_class=SensorDeviceClass.WIND_SPEED, - native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=2, raw_data_conv_fn=lambda raw_data: raw_data.magnitude, @@ -255,7 +255,7 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( translation_key="wind_lull", icon="mdi:weather-windy", device_class=SensorDeviceClass.WIND_SPEED, - native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=2, raw_data_conv_fn=lambda raw_data: raw_data.magnitude, @@ -265,17 +265,17 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.WIND_SPEED, icon="mdi:weather-windy", event_subscriptions=[EVENT_RAPID_WIND, EVENT_OBSERVATION], - native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=2, raw_data_conv_fn=lambda raw_data: raw_data.magnitude, ), WeatherFlowSensorEntityDescription( - key="wind_speed_average", + key="wind_average", translation_key="wind_speed_average", icon="mdi:weather-windy", device_class=SensorDeviceClass.WIND_SPEED, - native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=2, raw_data_conv_fn=lambda raw_data: raw_data.magnitude, From f8c7d502dfd5acaafe721d60dd6e030e1a8975af Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Thu, 5 Oct 2023 01:11:17 -0400 Subject: [PATCH 09/29] Bump env_canada to v0.5.37 (#101435) --- homeassistant/components/environment_canada/manifest.json | 2 +- pyproject.toml | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 0575ac132d4..4946c1900ea 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.5.36"] + "requirements": ["env-canada==0.5.37"] } diff --git a/pyproject.toml b/pyproject.toml index 9e153b6cc4f..81668c361a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -437,7 +437,7 @@ filterwarnings = [ # -- design choice 3rd party # https://github.com/gwww/elkm1/blob/2.2.5/elkm1_lib/util.py#L8-L19 "ignore:ssl.TLSVersion.TLSv1 is deprecated:DeprecationWarning:elkm1_lib.util", - # https://github.com/michaeldavie/env_canada/blob/v0.5.36/env_canada/ec_cache.py + # https://github.com/michaeldavie/env_canada/blob/v0.5.37/env_canada/ec_cache.py "ignore:Inheritance class CacheClientSession from ClientSession is discouraged:DeprecationWarning:env_canada.ec_cache", # https://github.com/bachya/regenmaschine/blob/2023.08.0/regenmaschine/client.py#L51 "ignore:ssl.TLSVersion.SSLv3 is deprecated:DeprecationWarning:regenmaschine.client", diff --git a/requirements_all.txt b/requirements_all.txt index cdfd83b23b7..50720d426fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -749,7 +749,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.5.36 +env-canada==0.5.37 # homeassistant.components.season ephem==4.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 11c632367af..7ca2065c52b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -605,7 +605,7 @@ energyzero==0.5.0 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.5.36 +env-canada==0.5.37 # homeassistant.components.season ephem==4.1.2 From a506ba94d11fc4ed81aac43cb2ee15f0831f8053 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Fri, 6 Oct 2023 18:23:18 +0200 Subject: [PATCH 10/29] Fix device_class.capitalize() in Point (#101440) --- homeassistant/components/point/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 130ea116cc1..9fe63bf1d55 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -277,7 +277,8 @@ class MinutPointEntity(Entity): sw_version=device["firmware"]["installed"], via_device=(DOMAIN, device["home"]), ) - self._attr_name = f"{self._name} {device_class.capitalize()}" + if device_class: + self._attr_name = f"{self._name} {device_class.capitalize()}" def __str__(self): """Return string representation of device.""" From 37cfa5efb772b9ea32c31cb67518c36aab765288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Fri, 6 Oct 2023 09:13:39 +0200 Subject: [PATCH 11/29] SMA add missing entity descriptions (#101462) --- homeassistant/components/sma/sensor.py | 38 ++++++++++++++++++++++++++ tests/components/sma/test_sensor.py | 20 ++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 11ed720b51c..f0fc475e0db 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -139,6 +139,13 @@ SENSOR_ENTITIES: dict[str, SensorEntityDescription] = { device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, ), + "pv_isolation_resistance": SensorEntityDescription( + key="pv_isolation_resistance", + name="PV Isolation Resistance", + native_unit_of_measurement="kOhms", + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), "insulation_residual_current": SensorEntityDescription( key="insulation_residual_current", name="Insulation Residual Current", @@ -147,6 +154,13 @@ SENSOR_ENTITIES: dict[str, SensorEntityDescription] = { device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, ), + "pv_power": SensorEntityDescription( + key="pv_power", + name="PV Power", + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + ), "grid_power": SensorEntityDescription( key="grid_power", name="Grid Power", @@ -479,6 +493,30 @@ SENSOR_ENTITIES: dict[str, SensorEntityDescription] = { state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, ), + "sps_voltage": SensorEntityDescription( + key="sps_voltage", + name="Secure Power Supply Voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + entity_registry_enabled_default=False, + ), + "sps_current": SensorEntityDescription( + key="sps_current", + name="Secure Power Supply Current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + ), + "sps_power": SensorEntityDescription( + key="sps_power", + name="Secure Power Supply Power", + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + ), "optimizer_power": SensorEntityDescription( key="optimizer_power", name="Optimizer Power", diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py index a79588f4800..acc26a8bf90 100644 --- a/tests/components/sma/test_sensor.py +++ b/tests/components/sma/test_sensor.py @@ -1,4 +1,12 @@ """Test the sma sensor platform.""" +from pysma.const import ( + ENERGY_METER_VIA_INVERTER, + GENERIC_SENSORS, + OPTIMIZERS_VIA_INVERTER, +) +from pysma.definitions import sensor_map + +from homeassistant.components.sma.sensor import SENSOR_ENTITIES from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfPower from homeassistant.core import HomeAssistant @@ -8,3 +16,15 @@ async def test_sensors(hass: HomeAssistant, init_integration) -> None: state = hass.states.get("sensor.sma_device_grid_power") assert state assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT + + +async def test_sensor_entities(hass: HomeAssistant, init_integration) -> None: + """Test SENSOR_ENTITIES contains a SensorEntityDescription for each pysma sensor.""" + pysma_sensor_definitions = ( + sensor_map[GENERIC_SENSORS] + + sensor_map[OPTIMIZERS_VIA_INVERTER] + + sensor_map[ENERGY_METER_VIA_INVERTER] + ) + + for sensor in pysma_sensor_definitions: + assert sensor.name in SENSOR_ENTITIES From c5585b070676aa713c1d9087dd85749b6c06a355 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 5 Oct 2023 22:17:09 +0200 Subject: [PATCH 12/29] Fix Trafikverket Camera if no location data (#101463) --- .../trafikverket_camera/config_flow.py | 8 +++- .../trafikverket_camera/conftest.py | 21 +++++++++++ .../trafikverket_camera/test_config_flow.py | 37 +++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/trafikverket_camera/config_flow.py b/homeassistant/components/trafikverket_camera/config_flow.py index e1f8220c4ff..d4a282cb344 100644 --- a/homeassistant/components/trafikverket_camera/config_flow.py +++ b/homeassistant/components/trafikverket_camera/config_flow.py @@ -35,6 +35,7 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Validate input from user input.""" errors: dict[str, str] = {} camera_info: CameraInfo | None = None + camera_location: str | None = None web_session = async_get_clientsession(self.hass) camera_api = TrafikverketCamera(web_session, sensor_api) @@ -49,7 +50,12 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except UnknownError: errors["base"] = "cannot_connect" - camera_location = camera_info.location if camera_info else None + if camera_info: + if _location := camera_info.location: + camera_location = _location + else: + camera_location = camera_info.camera_name + return (errors, camera_location) async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: diff --git a/tests/components/trafikverket_camera/conftest.py b/tests/components/trafikverket_camera/conftest.py index fc6d70ae704..95c145bbeb3 100644 --- a/tests/components/trafikverket_camera/conftest.py +++ b/tests/components/trafikverket_camera/conftest.py @@ -67,3 +67,24 @@ def fixture_get_camera() -> CameraInfo: status="Running", camera_type="Road", ) + + +@pytest.fixture(name="get_camera_no_location") +def fixture_get_camera_no_location() -> CameraInfo: + """Construct Camera Mock.""" + + return CameraInfo( + camera_name="Test Camera", + camera_id="1234", + active=True, + deleted=False, + description="Test Camera for testing", + direction="180", + fullsizephoto=True, + location=None, + modified=datetime(2022, 4, 4, 4, 4, 4, tzinfo=dt_util.UTC), + phototime=datetime(2022, 4, 4, 4, 4, 4, tzinfo=dt_util.UTC), + photourl="https://www.testurl.com/test_photo.jpg", + status="Running", + camera_type="Road", + ) diff --git a/tests/components/trafikverket_camera/test_config_flow.py b/tests/components/trafikverket_camera/test_config_flow.py index aa6122b7efe..ae3410d20b3 100644 --- a/tests/components/trafikverket_camera/test_config_flow.py +++ b/tests/components/trafikverket_camera/test_config_flow.py @@ -56,6 +56,43 @@ async def test_form(hass: HomeAssistant, get_camera: CameraInfo) -> None: assert result2["result"].unique_id == "trafikverket_camera-Test location" +async def test_form_no_location_data( + hass: HomeAssistant, get_camera_no_location: CameraInfo +) -> None: + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_camera", + return_value=get_camera_no_location, + ), patch( + "homeassistant.components.trafikverket_camera.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "1234567890", + CONF_LOCATION: "Test Cam", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Test Camera" + assert result2["data"] == { + "api_key": "1234567890", + "location": "Test Camera", + } + assert len(mock_setup_entry.mock_calls) == 1 + assert result2["result"].unique_id == "trafikverket_camera-Test Camera" + + @pytest.mark.parametrize( ("side_effect", "error_key", "base_error"), [ From 2210db4ca6bd2b280b5939dcc1ce7dc5ec51203b Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 5 Oct 2023 19:25:48 +0200 Subject: [PATCH 13/29] Update frontend to 20231005.0 (#101480) --- 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 40339e955f9..0d1c1659471 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==20231002.0"] + "requirements": ["home-assistant-frontend==20231005.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 95ac592dadb..9fa36c633bd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ ha-av==10.1.1 hass-nabucasa==0.71.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20231002.0 +home-assistant-frontend==20231005.0 home-assistant-intents==2023.10.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 50720d426fb..429f100d747 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -997,7 +997,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20231002.0 +home-assistant-frontend==20231005.0 # homeassistant.components.conversation home-assistant-intents==2023.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ca2065c52b..9825460c198 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -786,7 +786,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20231002.0 +home-assistant-frontend==20231005.0 # homeassistant.components.conversation home-assistant-intents==2023.10.2 From eadc70ede048b90044ad85b79a8d22c612078a09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Oct 2023 12:50:35 -0500 Subject: [PATCH 14/29] Bump zeroconf to 0.115.2 (#101482) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 53475588cfe..4c76a0c46ef 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.115.1"] + "requirements": ["zeroconf==0.115.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9fa36c633bd..005a6735e03 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -52,7 +52,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 yarl==1.9.2 -zeroconf==0.115.1 +zeroconf==0.115.2 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 429f100d747..7d852f07538 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2784,7 +2784,7 @@ zamg==0.3.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.115.1 +zeroconf==0.115.2 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9825460c198..d623c72c829 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2078,7 +2078,7 @@ yt-dlp==2023.9.24 zamg==0.3.0 # homeassistant.components.zeroconf -zeroconf==0.115.1 +zeroconf==0.115.2 # homeassistant.components.zeversolar zeversolar==0.3.1 From 948bbdd2bf8a303cb1e1e48705526d9b45f6f836 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Thu, 5 Oct 2023 22:07:27 +0200 Subject: [PATCH 15/29] bump pywaze to 0.5.1 sets timeout to 60s (#101487) --- homeassistant/components/waze_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index 1a4be798367..728a91e4933 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "iot_class": "cloud_polling", "loggers": ["pywaze", "homeassistant.helpers.location"], - "requirements": ["pywaze==0.5.0"] + "requirements": ["pywaze==0.5.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7d852f07538..92b42864dee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2246,7 +2246,7 @@ pyvlx==0.2.20 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==0.5.0 +pywaze==0.5.1 # homeassistant.components.weatherflow pyweatherflowudp==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d623c72c829..a127f857223 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1672,7 +1672,7 @@ pyvizio==0.1.61 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==0.5.0 +pywaze==0.5.1 # homeassistant.components.weatherflow pyweatherflowudp==1.4.3 From 26c7ba38d0203f58ac02fbb7070fc1402e76ce32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Oct 2023 02:18:05 -0500 Subject: [PATCH 16/29] Fix caching of latest short term stats after insertion of external stats (#101490) --- .../components/recorder/statistics.py | 27 ++++++++++++++++--- .../components/recorder/test_websocket_api.py | 17 ++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 24fb209ae07..0ea16e09df4 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -1924,7 +1924,13 @@ def get_latest_short_term_statistics( for metadata_id in missing_metadata_ids if ( latest_id := cache_latest_short_term_statistic_id_for_metadata_id( - run_cache, session, metadata_id + # orm_rows=False is used here because we are in + # a read-only session, and there will never be + # any pending inserts in the session. + run_cache, + session, + metadata_id, + orm_rows=False, ) ) is not None @@ -2310,8 +2316,14 @@ def _import_statistics_with_session( # We just inserted new short term statistics, so we need to update the # ShortTermStatisticsRunCache with the latest id for the metadata_id run_cache = get_short_term_statistics_run_cache(instance.hass) + # + # Because we are in the same session and we want to read rows + # that have not been flushed yet, we need to pass orm_rows=True + # to cache_latest_short_term_statistic_id_for_metadata_id + # to ensure that it gets the rows that were just inserted + # cache_latest_short_term_statistic_id_for_metadata_id( - run_cache, session, metadata_id + run_cache, session, metadata_id, orm_rows=True ) return True @@ -2326,7 +2338,10 @@ def get_short_term_statistics_run_cache( def cache_latest_short_term_statistic_id_for_metadata_id( - run_cache: ShortTermStatisticsRunCache, session: Session, metadata_id: int + run_cache: ShortTermStatisticsRunCache, + session: Session, + metadata_id: int, + orm_rows: bool, ) -> int | None: """Cache the latest short term statistic for a given metadata_id. @@ -2339,7 +2354,11 @@ def cache_latest_short_term_statistic_id_for_metadata_id( execute_stmt_lambda_element( session, _find_latest_short_term_statistic_for_metadata_id_stmt(metadata_id), - orm_rows=False, + orm_rows=orm_rows + # _import_statistics_with_session needs to be able + # to read back the rows it just inserted without + # a flush so we have to pass orm_rows so we get + # back the latest data. ), ): id_: int = latest[0].id diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 38b657945f7..969fdd63ae5 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -14,6 +14,7 @@ from homeassistant.components.recorder.db_schema import Statistics, StatisticsSh from homeassistant.components.recorder.statistics import ( async_add_external_statistics, get_last_statistics, + get_latest_short_term_statistics, get_metadata, get_short_term_statistics_run_cache, list_statistic_ids, @@ -635,6 +636,22 @@ async def test_statistic_during_period( "change": (imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"]) * 1000, } + stats = get_latest_short_term_statistics( + hass, {"sensor.test"}, {"last_reset", "max", "mean", "min", "state", "sum"} + ) + start = imported_stats_5min[-1]["start"].timestamp() + end = start + (5 * 60) + assert stats == { + "sensor.test": [ + { + "end": end, + "last_reset": None, + "start": start, + "state": None, + "sum": 38.0, + } + ] + } @pytest.mark.freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.UTC)) From 9725a0daf980fa22e1b98050b73a9838ba371c30 Mon Sep 17 00:00:00 2001 From: Ian Date: Thu, 5 Oct 2023 16:11:15 -0400 Subject: [PATCH 17/29] Fix key error in config flow when duplicate stop names exist (#101491) --- homeassistant/components/nextbus/config_flow.py | 2 +- tests/components/nextbus/conftest.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nextbus/config_flow.py b/homeassistant/components/nextbus/config_flow.py index d7149bcc9f4..000dd86eb52 100644 --- a/homeassistant/components/nextbus/config_flow.py +++ b/homeassistant/components/nextbus/config_flow.py @@ -58,7 +58,7 @@ def _get_stop_tags( # Append directions for stops with shared titles for tag, title in tags.items(): if title_counts[title] > 1: - tags[tag] = f"{title} ({stop_directions[tag]})" + tags[tag] = f"{title} ({stop_directions.get(tag, tag)})" return tags diff --git a/tests/components/nextbus/conftest.py b/tests/components/nextbus/conftest.py index a38f3fd850e..0940118c13a 100644 --- a/tests/components/nextbus/conftest.py +++ b/tests/components/nextbus/conftest.py @@ -19,6 +19,8 @@ def mock_nextbus_lists(mock_nextbus: MagicMock) -> MagicMock: "stop": [ {"tag": "5650", "title": "Market St & 7th St"}, {"tag": "5651", "title": "Market St & 7th St"}, + # Error case test. Duplicate title with no unique direction + {"tag": "5652", "title": "Market St & 7th St"}, ], "direction": [ { From d46962685542447c6adea1fb22e8338663f14389 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 6 Oct 2023 00:19:21 -0700 Subject: [PATCH 18/29] Fix bug in calendar state where alarms due to alarms not scheduled (#101510) --- homeassistant/components/calendar/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 1622f568a2d..5f6b54824fd 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -540,9 +540,9 @@ class CalendarEntity(Entity): @callback def update(_: datetime.datetime) -> None: - """Run when the active or upcoming event starts or ends.""" + """Update state and reschedule next alarms.""" _LOGGER.debug("Running %s update", self.entity_id) - self._async_write_ha_state() + self.async_write_ha_state() if now < event.start_datetime_local: self._alarm_unsubs.append( From d14934861e12f968a9266d1991d9c5938f36e86f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 6 Oct 2023 00:16:06 -0700 Subject: [PATCH 19/29] Fix for rainbird unique id (#101512) --- homeassistant/components/rainbird/calendar.py | 2 +- homeassistant/components/rainbird/number.py | 2 +- homeassistant/components/rainbird/sensor.py | 2 +- homeassistant/components/rainbird/switch.py | 7 ++-- tests/components/rainbird/test_switch.py | 34 ++++++++++++++++++- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/rainbird/calendar.py b/homeassistant/components/rainbird/calendar.py index 356f7d7cc4e..2001a14ac93 100644 --- a/homeassistant/components/rainbird/calendar.py +++ b/homeassistant/components/rainbird/calendar.py @@ -61,7 +61,7 @@ class RainBirdCalendarEntity( """Create the Calendar event device.""" super().__init__(coordinator) self._event: CalendarEvent | None = None - if unique_id: + if unique_id is not None: self._attr_unique_id = unique_id self._attr_device_info = device_info else: diff --git a/homeassistant/components/rainbird/number.py b/homeassistant/components/rainbird/number.py index 1e72fabafcd..dd9664222b2 100644 --- a/homeassistant/components/rainbird/number.py +++ b/homeassistant/components/rainbird/number.py @@ -51,7 +51,7 @@ class RainDelayNumber(CoordinatorEntity[RainbirdUpdateCoordinator], NumberEntity ) -> None: """Initialize the Rain Bird sensor.""" super().__init__(coordinator) - if coordinator.unique_id: + if coordinator.unique_id is not None: self._attr_unique_id = f"{coordinator.unique_id}-rain-delay" self._attr_device_info = coordinator.device_info else: diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index d44e7156cb5..84bf8cadb7b 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -52,7 +52,7 @@ class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], SensorEntity) """Initialize the Rain Bird sensor.""" super().__init__(coordinator) self.entity_description = description - if coordinator.unique_id: + if coordinator.unique_id is not None: self._attr_unique_id = f"{coordinator.unique_id}-{description.key}" self._attr_device_info = coordinator.device_info else: diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 62b3b0e9a8c..da3979a27fd 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -65,17 +65,18 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity) """Initialize a Rain Bird Switch Device.""" super().__init__(coordinator) self._zone = zone - if coordinator.unique_id: + _LOGGER.debug("coordinator.unique_id=%s", coordinator.unique_id) + if coordinator.unique_id is not None: self._attr_unique_id = f"{coordinator.unique_id}-{zone}" device_name = f"{MANUFACTURER} Sprinkler {zone}" if imported_name: self._attr_name = imported_name self._attr_has_entity_name = False else: - self._attr_name = None if coordinator.unique_id else device_name + self._attr_name = None if coordinator.unique_id is not None else device_name self._attr_has_entity_name = True self._duration_minutes = duration_minutes - if coordinator.unique_id and self._attr_unique_id: + if coordinator.unique_id is not None and self._attr_unique_id is not None: self._attr_device_info = DeviceInfo( name=device_name, identifiers={(DOMAIN, self._attr_unique_id)}, diff --git a/tests/components/rainbird/test_switch.py b/tests/components/rainbird/test_switch.py index 46a875e8928..31b64dded99 100644 --- a/tests/components/rainbird/test_switch.py +++ b/tests/components/rainbird/test_switch.py @@ -17,6 +17,7 @@ from .conftest import ( PASSWORD, RAIN_DELAY_OFF, RAIN_SENSOR_OFF, + SERIAL_NUMBER, ZONE_3_ON_RESPONSE, ZONE_5_ON_RESPONSE, ZONE_OFF_RESPONSE, @@ -286,7 +287,7 @@ async def test_switch_error( @pytest.mark.parametrize( ("config_entry_unique_id"), [ - None, + (None), ], ) async def test_no_unique_id( @@ -307,3 +308,34 @@ async def test_no_unique_id( entity_entry = entity_registry.async_get("switch.rain_bird_sprinkler_3") assert entity_entry is None + + +@pytest.mark.parametrize( + ("config_entry_unique_id", "entity_unique_id"), + [ + (SERIAL_NUMBER, "1263613994342-3"), + # Some existing config entries may have a "0" serial number but preserve + # their unique id + (0, "0-3"), + ], +) +async def test_has_unique_id( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, + responses: list[AiohttpClientMockResponse], + entity_registry: er.EntityRegistry, + entity_unique_id: str, +) -> None: + """Test an irrigation switch with no unique id.""" + + assert await setup_integration() + + zone = hass.states.get("switch.rain_bird_sprinkler_3") + assert zone is not None + assert zone.attributes.get("friendly_name") == "Rain Bird Sprinkler 3" + assert zone.state == "off" + + entity_entry = entity_registry.async_get("switch.rain_bird_sprinkler_3") + assert entity_entry + assert entity_entry.unique_id == entity_unique_id From 7f6506cfcfd894a59c64ce69828a7f69f6d236f7 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Fri, 6 Oct 2023 13:23:32 +0200 Subject: [PATCH 20/29] Limit waze_travel_time to 0.5call/s over all entries (#101514) --- homeassistant/components/waze_travel_time/__init__.py | 6 ++++++ homeassistant/components/waze_travel_time/const.py | 1 + homeassistant/components/waze_travel_time/sensor.py | 11 ++++++++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index 806672b3608..beaa2ecc69a 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -1,13 +1,19 @@ """The waze_travel_time component.""" +import asyncio + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from .const import DOMAIN, SEMAPHORE + PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Load the saved entities.""" + if SEMAPHORE not in hass.data.setdefault(DOMAIN, {}): + hass.data.setdefault(DOMAIN, {})[SEMAPHORE] = asyncio.Semaphore(1) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/waze_travel_time/const.py b/homeassistant/components/waze_travel_time/const.py index 698ba5a63b2..572676e1966 100644 --- a/homeassistant/components/waze_travel_time/const.py +++ b/homeassistant/components/waze_travel_time/const.py @@ -2,6 +2,7 @@ from __future__ import annotations DOMAIN = "waze_travel_time" +SEMAPHORE = "semaphore" CONF_DESTINATION = "destination" CONF_ORIGIN = "origin" diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index bf3544de8a9..b54d723f95d 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -43,6 +43,7 @@ from .const import ( DEFAULT_NAME, DOMAIN, IMPERIAL_UNITS, + SEMAPHORE, ) _LOGGER = logging.getLogger(__name__) @@ -51,7 +52,7 @@ SCAN_INTERVAL = timedelta(minutes=5) PARALLEL_UPDATES = 1 -MS_BETWEEN_API_CALLS = 0.5 +SECONDS_BETWEEN_API_CALLS = 0.5 async def async_setup_entry( @@ -148,8 +149,12 @@ class WazeTravelTime(SensorEntity): _LOGGER.debug("Fetching Route for %s", self._attr_name) self._waze_data.origin = find_coordinates(self.hass, self._origin) self._waze_data.destination = find_coordinates(self.hass, self._destination) - await self._waze_data.async_update() - await asyncio.sleep(MS_BETWEEN_API_CALLS) + await self.hass.data[DOMAIN][SEMAPHORE].acquire() + try: + await self._waze_data.async_update() + await asyncio.sleep(SECONDS_BETWEEN_API_CALLS) + finally: + self.hass.data[DOMAIN][SEMAPHORE].release() class WazeTravelTimeData: From 81f582eeb7094167fadcf046b465126640fedcb8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 6 Oct 2023 12:02:53 +0200 Subject: [PATCH 21/29] Use config flow in color extractor tests (#101524) --- tests/components/color_extractor/conftest.py | 21 +++++++++ .../fixtures/color_extractor_file.txt | 0 .../fixtures/color_extractor_url.txt | 0 tests/components/color_extractor/test_init.py | 17 +++++++ .../color_extractor/test_service.py | 46 ++++++++----------- 5 files changed, 57 insertions(+), 27 deletions(-) create mode 100644 tests/components/color_extractor/conftest.py rename tests/{ => components/color_extractor}/fixtures/color_extractor_file.txt (100%) rename tests/{ => components/color_extractor}/fixtures/color_extractor_url.txt (100%) create mode 100644 tests/components/color_extractor/test_init.py diff --git a/tests/components/color_extractor/conftest.py b/tests/components/color_extractor/conftest.py new file mode 100644 index 00000000000..299c8019f94 --- /dev/null +++ b/tests/components/color_extractor/conftest.py @@ -0,0 +1,21 @@ +"""Common fixtures for the Color extractor tests.""" +import pytest + +from homeassistant.components.color_extractor.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture +async def config_entry() -> MockConfigEntry: + """Mock config entry.""" + return MockConfigEntry(domain=DOMAIN, data={}) + + +@pytest.fixture +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Add config entry for color extractor.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/fixtures/color_extractor_file.txt b/tests/components/color_extractor/fixtures/color_extractor_file.txt similarity index 100% rename from tests/fixtures/color_extractor_file.txt rename to tests/components/color_extractor/fixtures/color_extractor_file.txt diff --git a/tests/fixtures/color_extractor_url.txt b/tests/components/color_extractor/fixtures/color_extractor_url.txt similarity index 100% rename from tests/fixtures/color_extractor_url.txt rename to tests/components/color_extractor/fixtures/color_extractor_url.txt diff --git a/tests/components/color_extractor/test_init.py b/tests/components/color_extractor/test_init.py new file mode 100644 index 00000000000..797eaf291fe --- /dev/null +++ b/tests/components/color_extractor/test_init.py @@ -0,0 +1,17 @@ +"""Test Color extractor component setup process.""" +from homeassistant.components.color_extractor import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +async def test_legacy_migration(hass: HomeAssistant) -> None: + """Test migration from yaml to config flow.""" + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + issue_registry = ir.async_get(hass) + assert len(issue_registry.issues) == 1 diff --git a/tests/components/color_extractor/test_service.py b/tests/components/color_extractor/test_service.py index ae3e799e9d2..361127c332b 100644 --- a/tests/components/color_extractor/test_service.py +++ b/tests/components/color_extractor/test_service.py @@ -1,6 +1,7 @@ """Tests for color_extractor component service calls.""" import base64 import io +from typing import Any from unittest.mock import Mock, mock_open, patch import aiohttp @@ -92,15 +93,8 @@ async def setup_light(hass: HomeAssistant): assert state.state == STATE_OFF -async def test_missing_url_and_path(hass: HomeAssistant) -> None: +async def test_missing_url_and_path(hass: HomeAssistant, setup_integration) -> None: """Test that nothing happens when url and path are missing.""" - # Load our color_extractor component - await async_setup_component( - hass, - DOMAIN, - {}, - ) - await hass.async_block_till_done() # Validate pre service call state = hass.states.get(LIGHT_ENTITY) @@ -124,15 +118,7 @@ async def test_missing_url_and_path(hass: HomeAssistant) -> None: assert state.state == STATE_OFF -async def _async_load_color_extractor_url(hass, service_data): - # Load our color_extractor component - await async_setup_component( - hass, - DOMAIN, - {}, - ) - await hass.async_block_till_done() - +async def _async_execute_service(hass: HomeAssistant, service_data: dict[str, Any]): # Validate pre service call state = hass.states.get(LIGHT_ENTITY) assert state @@ -145,7 +131,7 @@ async def _async_load_color_extractor_url(hass, service_data): async def test_url_success( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_integration ) -> None: """Test that a successful image GET translate to light RGB.""" service_data = { @@ -158,13 +144,15 @@ async def test_url_success( # Mock the HTTP Response with a base64 encoded 1x1 pixel aioclient_mock.get( url=service_data[ATTR_URL], - content=base64.b64decode(load_fixture("color_extractor_url.txt")), + content=base64.b64decode( + load_fixture("color_extractor/color_extractor_url.txt") + ), ) # Allow access to this URL using the proper mechanism hass.config.allowlist_external_urls.add("http://example.com/images/") - await _async_load_color_extractor_url(hass, service_data) + await _async_execute_service(hass, service_data) state = hass.states.get(LIGHT_ENTITY) assert state @@ -180,7 +168,7 @@ async def test_url_success( async def test_url_not_allowed( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_integration ) -> None: """Test that a not allowed external URL fails to turn light on.""" service_data = { @@ -188,7 +176,7 @@ async def test_url_not_allowed( ATTR_ENTITY_ID: LIGHT_ENTITY, } - await _async_load_color_extractor_url(hass, service_data) + await _async_execute_service(hass, service_data) # Light has not been modified due to failure state = hass.states.get(LIGHT_ENTITY) @@ -197,7 +185,7 @@ async def test_url_not_allowed( async def test_url_exception( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_integration ) -> None: """Test that a HTTPError fails to turn light on.""" service_data = { @@ -211,7 +199,7 @@ async def test_url_exception( # Mock the HTTP Response with an HTTPError aioclient_mock.get(url=service_data[ATTR_URL], exc=aiohttp.ClientError) - await _async_load_color_extractor_url(hass, service_data) + await _async_execute_service(hass, service_data) # Light has not been modified due to failure state = hass.states.get(LIGHT_ENTITY) @@ -220,7 +208,7 @@ async def test_url_exception( async def test_url_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, setup_integration ) -> None: """Test that a HTTP Error (non 200) doesn't turn light on.""" service_data = { @@ -234,7 +222,7 @@ async def test_url_error( # Mock the HTTP Response with a 400 Bad Request error aioclient_mock.get(url=service_data[ATTR_URL], status=400) - await _async_load_color_extractor_url(hass, service_data) + await _async_execute_service(hass, service_data) # Light has not been modified due to failure state = hass.states.get(LIGHT_ENTITY) @@ -244,7 +232,11 @@ async def test_url_error( @patch( "builtins.open", - mock_open(read_data=base64.b64decode(load_fixture("color_extractor_file.txt"))), + mock_open( + read_data=base64.b64decode( + load_fixture("color_extractor/color_extractor_file.txt") + ) + ), create=True, ) def _get_file_mock(file_path): From 76f78e249bb2c803712ec26114743d038ab02a05 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 6 Oct 2023 13:19:39 +0200 Subject: [PATCH 22/29] Delete existing Withings cloudhook (#101527) --- homeassistant/components/withings/__init__.py | 10 +++++++--- tests/components/withings/test_init.py | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 246bcc134d0..597517693c0 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at from __future__ import annotations from collections.abc import Awaitable, Callable +import contextlib from typing import Any from aiohttp.hdrs import METH_HEAD, METH_POST @@ -214,9 +215,12 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_cloudhook_generate_url(hass: HomeAssistant, entry: ConfigEntry) -> str: """Generate the full URL for a webhook_id.""" if CONF_CLOUDHOOK_URL not in entry.data: - webhook_url = await cloud.async_create_cloudhook( - hass, entry.data[CONF_WEBHOOK_ID] - ) + webhook_id = entry.data[CONF_WEBHOOK_ID] + # Some users already have their webhook as cloudhook. + # We remove them to be sure we can create a new one. + with contextlib.suppress(ValueError): + await cloud.async_delete_cloudhook(hass, webhook_id) + webhook_url = await cloud.async_create_cloudhook(hass, webhook_id) data = {**entry.data, CONF_CLOUDHOOK_URL: webhook_url} hass.config_entries.async_update_entry(entry, data=data) return webhook_url diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index a3918a6ff19..1c562182ae7 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -421,6 +421,7 @@ async def test_setup_with_cloud( assert hass.components.cloud.async_active_subscription() is True assert hass.components.cloud.async_is_connected() is True fake_create_cloudhook.assert_called_once() + fake_delete_cloudhook.assert_called_once() assert ( hass.config_entries.async_entries("withings")[0].data["cloudhook_url"] @@ -432,7 +433,7 @@ async def test_setup_with_cloud( for config_entry in hass.config_entries.async_entries("withings"): await hass.config_entries.async_remove(config_entry.entry_id) - fake_delete_cloudhook.assert_called_once() + fake_delete_cloudhook.call_count == 2 await hass.async_block_till_done() assert not hass.config_entries.async_entries(DOMAIN) From 4a5b0222ab440d16a8a00663bb9bbbc35c8a7c5d Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 6 Oct 2023 14:10:10 +0200 Subject: [PATCH 23/29] Modbus, wrong length when reading strings (#101529) --- homeassistant/components/modbus/validators.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index ca08ace853a..bef58b3fa56 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -84,7 +84,7 @@ DEFAULT_STRUCT_FORMAT = { DataType.INT64: ENTRY("q", 4, PARM_IS_LEGAL(False, False, True, True, True)), DataType.UINT64: ENTRY("Q", 4, PARM_IS_LEGAL(False, False, True, True, True)), DataType.FLOAT64: ENTRY("d", 4, PARM_IS_LEGAL(False, False, True, True, True)), - DataType.STRING: ENTRY("s", 1, PARM_IS_LEGAL(True, False, False, False, False)), + DataType.STRING: ENTRY("s", -1, PARM_IS_LEGAL(True, False, False, False, False)), DataType.CUSTOM: ENTRY("?", 0, PARM_IS_LEGAL(True, True, False, False, False)), } @@ -143,7 +143,8 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: f"{name}: Size of structure is {size} bytes but `{CONF_COUNT}: {count}` is {bytecount} bytes" ) else: - config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count + if data_type != DataType.STRING: + config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count if slave_count: structure = ( f">{slave_count + 1}{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" From 6c2d1e2142a1d6c5788c6a0b0ed0ff8d761a216a Mon Sep 17 00:00:00 2001 From: Mike Woudenberg Date: Fri, 6 Oct 2023 15:37:18 +0200 Subject: [PATCH 24/29] Update LoqedAPI to handle invalid transitions better (#101534) --- homeassistant/components/loqed/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/loqed/manifest.json b/homeassistant/components/loqed/manifest.json index 25d1f15486d..7c682b3189d 100644 --- a/homeassistant/components/loqed/manifest.json +++ b/homeassistant/components/loqed/manifest.json @@ -7,7 +7,7 @@ "dependencies": ["webhook"], "documentation": "https://www.home-assistant.io/integrations/loqed", "iot_class": "local_push", - "requirements": ["loqedAPI==2.1.7"], + "requirements": ["loqedAPI==2.1.8"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 92b42864dee..c91d8260050 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1165,7 +1165,7 @@ logi-circle==0.2.3 london-tube-status==0.5 # homeassistant.components.loqed -loqedAPI==2.1.7 +loqedAPI==2.1.8 # homeassistant.components.luftdaten luftdaten==0.7.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a127f857223..5609bc56223 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -903,7 +903,7 @@ logi-circle==0.2.3 london-tube-status==0.5 # homeassistant.components.loqed -loqedAPI==2.1.7 +loqedAPI==2.1.8 # homeassistant.components.luftdaten luftdaten==0.7.4 From 7369ae8c9fea65a676e679875d93158208a16d06 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 6 Oct 2023 17:10:19 +0200 Subject: [PATCH 25/29] Cancel callbacks on Withings entry unload (#101536) --- homeassistant/components/withings/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 597517693c0..25965b30ce2 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -161,7 +161,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: webhook_name = "Withings" if entry.title != DEFAULT_TITLE: - webhook_name = " ".join([DEFAULT_TITLE, entry.title]) + webhook_name = f"{DEFAULT_TITLE} {entry.title}" webhook_register( hass, @@ -183,14 +183,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if state is cloud.CloudConnectionState.CLOUD_DISCONNECTED: await unregister_webhook(None) - async_call_later(hass, 30, register_webhook) + entry.async_on_unload(async_call_later(hass, 30, register_webhook)) if cloud.async_active_subscription(hass): if cloud.async_is_connected(hass): await register_webhook(None) - cloud.async_listen_connection_change(hass, manage_cloudhook) + entry.async_on_unload( + cloud.async_listen_connection_change(hass, manage_cloudhook) + ) else: - async_at_started(hass, register_webhook) + entry.async_on_unload(async_at_started(hass, register_webhook)) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) From d26b1b370a9d1eaf78e0d1c9943ede06791533b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Oct 2023 05:06:18 -1000 Subject: [PATCH 26/29] Bump HAP-python to 4.8.0 (#101538) --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 6f3067d7a78..67f99ad5f8b 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["pyhap"], "requirements": [ - "HAP-python==4.7.1", + "HAP-python==4.8.0", "fnv-hash-fast==0.4.1", "PyQRCode==1.2.1", "base36==0.1.1" diff --git a/requirements_all.txt b/requirements_all.txt index c91d8260050..e1c8091b2d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -26,7 +26,7 @@ CO2Signal==0.4.2 DoorBirdPy==2.1.0 # homeassistant.components.homekit -HAP-python==4.7.1 +HAP-python==4.8.0 # homeassistant.components.tasmota HATasmota==0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5609bc56223..952cb0f6fa2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -25,7 +25,7 @@ CO2Signal==0.4.2 DoorBirdPy==2.1.0 # homeassistant.components.homekit -HAP-python==4.7.1 +HAP-python==4.8.0 # homeassistant.components.tasmota HATasmota==0.7.3 From 42b53c6349e9b72c69561c99bb410271b5abf000 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 6 Oct 2023 18:21:06 +0200 Subject: [PATCH 27/29] Add Withings webhooks after a slight delay (#101542) --- homeassistant/components/withings/__init__.py | 5 +- tests/components/withings/__init__.py | 13 ++++- .../components/withings/test_binary_sensor.py | 5 +- tests/components/withings/test_init.py | 55 ++++++------------- tests/components/withings/test_sensor.py | 4 +- 5 files changed, 38 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 25965b30ce2..810ad49171c 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -38,7 +38,6 @@ from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.event import async_call_later from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType from .api import ConfigEntryWithingsApi @@ -187,12 +186,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if cloud.async_active_subscription(hass): if cloud.async_is_connected(hass): - await register_webhook(None) + entry.async_on_unload(async_call_later(hass, 1, register_webhook)) entry.async_on_unload( cloud.async_listen_connection_change(hass, manage_cloudhook) ) else: - entry.async_on_unload(async_at_started(hass, register_webhook)) + entry.async_on_unload(async_call_later(hass, 1, register_webhook)) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/tests/components/withings/__init__.py b/tests/components/withings/__init__.py index 459deaae4c5..4d9a0e841b7 100644 --- a/tests/components/withings/__init__.py +++ b/tests/components/withings/__init__.py @@ -1,15 +1,17 @@ """Tests for the withings component.""" from dataclasses import dataclass +from datetime import timedelta from typing import Any from urllib.parse import urlparse from aiohttp.test_utils import TestClient +from freezegun.api import FrozenDateTimeFactory from homeassistant.components.webhook import async_generate_url from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @dataclass @@ -53,3 +55,12 @@ async def setup_integration( ) await hass.config_entries.async_setup(config_entry.entry_id) + + +async def prepare_webhook_setup( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: + """Prepare webhooks are registered by waiting a second.""" + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() diff --git a/tests/components/withings/test_binary_sensor.py b/tests/components/withings/test_binary_sensor.py index d258986bdaf..aa757486f86 100644 --- a/tests/components/withings/test_binary_sensor.py +++ b/tests/components/withings/test_binary_sensor.py @@ -2,13 +2,14 @@ from unittest.mock import AsyncMock from aiohttp.client_exceptions import ClientResponseError +from freezegun.api import FrozenDateTimeFactory import pytest from withings_api.common import NotifyAppli from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant -from . import call_webhook, setup_integration +from . import call_webhook, prepare_webhook_setup, setup_integration from .conftest import USER_ID, WEBHOOK_ID from tests.common import MockConfigEntry @@ -20,9 +21,11 @@ async def test_binary_sensor( withings: AsyncMock, webhook_config_entry: MockConfigEntry, hass_client_no_auth: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, ) -> None: """Test binary sensor.""" await setup_integration(hass, webhook_config_entry) + await prepare_webhook_setup(hass, freezer) client = await hass_client_no_auth() diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index 1c562182ae7..dd112671945 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -15,16 +15,11 @@ from homeassistant.components.cloud import CloudNotAvailable from homeassistant.components.webhook import async_generate_url from homeassistant.components.withings import CONFIG_SCHEMA, async_setup from homeassistant.components.withings.const import CONF_USE_WEBHOOK, DOMAIN -from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_WEBHOOK_ID, - EVENT_HOMEASSISTANT_STARTED, -) -from homeassistant.core import CoreState, HomeAssistant +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_WEBHOOK_ID +from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util -from . import call_webhook, setup_integration +from . import call_webhook, prepare_webhook_setup, setup_integration from .conftest import USER_ID, WEBHOOK_ID from tests.common import ( @@ -197,9 +192,12 @@ async def test_webhooks_request_data( withings: AsyncMock, webhook_config_entry: MockConfigEntry, hass_client_no_auth: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, ) -> None: """Test calling a webhook requests data.""" await setup_integration(hass, webhook_config_entry) + await prepare_webhook_setup(hass, freezer) + client = await hass_client_no_auth() assert withings.async_measure_get_meas.call_count == 1 @@ -213,35 +211,6 @@ async def test_webhooks_request_data( assert withings.async_measure_get_meas.call_count == 2 -async def test_delayed_startup( - hass: HomeAssistant, - withings: AsyncMock, - webhook_config_entry: MockConfigEntry, - hass_client_no_auth: ClientSessionGenerator, - freezer: FrozenDateTimeFactory, -) -> None: - """Test delayed start up.""" - hass.state = CoreState.not_running - await setup_integration(hass, webhook_config_entry) - - withings.async_notify_subscribe.assert_not_called() - client = await hass_client_no_auth() - - assert withings.async_measure_get_meas.call_count == 1 - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - await call_webhook( - hass, - WEBHOOK_ID, - {"userid": USER_ID, "appli": NotifyAppli.WEIGHT}, - client, - ) - assert withings.async_measure_get_meas.call_count == 2 - - @pytest.mark.parametrize( "error", [ @@ -395,7 +364,10 @@ async def test_removing_entry_with_cloud_unavailable( async def test_setup_with_cloud( - hass: HomeAssistant, webhook_config_entry: MockConfigEntry, withings: AsyncMock + hass: HomeAssistant, + webhook_config_entry: MockConfigEntry, + withings: AsyncMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test if set up with active cloud subscription.""" await mock_cloud(hass) @@ -418,6 +390,8 @@ async def test_setup_with_cloud( "homeassistant.components.withings.webhook_generate_url" ): await setup_integration(hass, webhook_config_entry) + await prepare_webhook_setup(hass, freezer) + assert hass.components.cloud.async_active_subscription() is True assert hass.components.cloud.async_is_connected() is True fake_create_cloudhook.assert_called_once() @@ -444,6 +418,7 @@ async def test_setup_without_https( webhook_config_entry: MockConfigEntry, withings: AsyncMock, caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, ) -> None: """Test if set up with cloud link and without https.""" hass.config.components.add("cloud") @@ -457,6 +432,7 @@ async def test_setup_without_https( ) as mock_async_generate_url: mock_async_generate_url.return_value = "http://example.com" await setup_integration(hass, webhook_config_entry) + await prepare_webhook_setup(hass, freezer) await hass.async_block_till_done() mock_async_generate_url.assert_called_once() @@ -492,6 +468,7 @@ async def test_cloud_disconnect( "homeassistant.components.withings.webhook_generate_url" ): await setup_integration(hass, webhook_config_entry) + await prepare_webhook_setup(hass, freezer) assert hass.components.cloud.async_active_subscription() is True assert hass.components.cloud.async_is_connected() is True @@ -537,9 +514,11 @@ async def test_webhook_post( body: dict[str, Any], expected_code: int, current_request_with_host: None, + freezer: FrozenDateTimeFactory, ) -> None: """Test webhook callback.""" await setup_integration(hass, webhook_config_entry) + await prepare_webhook_setup(hass, freezer) client = await hass_client_no_auth() webhook_url = async_generate_url(hass, WEBHOOK_ID) diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index fe640e315a0..44ae10b6a94 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry -from . import call_webhook, setup_integration +from . import call_webhook, prepare_webhook_setup, setup_integration from .conftest import USER_ID, WEBHOOK_ID from tests.common import MockConfigEntry, async_fire_time_changed @@ -96,9 +96,11 @@ async def test_sensor_default_enabled_entities( withings: AsyncMock, webhook_config_entry: MockConfigEntry, hass_client_no_auth: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, ) -> None: """Test entities enabled by default.""" await setup_integration(hass, webhook_config_entry) + await prepare_webhook_setup(hass, freezer) entity_registry: EntityRegistry = er.async_get(hass) client = await hass_client_no_auth() From 5925b6b9125b9126bac4f7231b5f795e7ef2896c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 6 Oct 2023 13:18:44 +0200 Subject: [PATCH 28/29] Only import color extractor when domain is in config (#101522) --- .../components/color_extractor/__init__.py | 14 +++++++++----- tests/components/color_extractor/test_init.py | 2 +- tests/components/color_extractor/test_service.py | 10 ++-------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/color_extractor/__init__.py b/homeassistant/components/color_extractor/__init__.py index af460f819cd..2cc3e206958 100644 --- a/homeassistant/components/color_extractor/__init__.py +++ b/homeassistant/components/color_extractor/__init__.py @@ -24,7 +24,10 @@ from .const import ATTR_PATH, ATTR_URL, DOMAIN, SERVICE_TURN_ON _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) +CONFIG_SCHEMA = vol.Schema( + {vol.Optional(DOMAIN): {}}, + extra=vol.ALLOW_EXTRA, +) # Extend the existing light.turn_on service schema SERVICE_SCHEMA = vol.All( @@ -62,11 +65,12 @@ def _get_color(file_handler) -> tuple: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Color extractor component.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data={} + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={} + ) ) - ) return True diff --git a/tests/components/color_extractor/test_init.py b/tests/components/color_extractor/test_init.py index 797eaf291fe..b4874b575e8 100644 --- a/tests/components/color_extractor/test_init.py +++ b/tests/components/color_extractor/test_init.py @@ -8,7 +8,7 @@ from homeassistant.setup import async_setup_component async def test_legacy_migration(hass: HomeAssistant) -> None: """Test migration from yaml to config flow.""" - assert await async_setup_component(hass, DOMAIN, {}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 diff --git a/tests/components/color_extractor/test_service.py b/tests/components/color_extractor/test_service.py index 361127c332b..647d945f158 100644 --- a/tests/components/color_extractor/test_service.py +++ b/tests/components/color_extractor/test_service.py @@ -254,7 +254,7 @@ def _get_file_mock(file_path): @patch("os.path.isfile", Mock(return_value=True)) @patch("os.access", Mock(return_value=True)) -async def test_file(hass: HomeAssistant) -> None: +async def test_file(hass: HomeAssistant, setup_integration) -> None: """Test that the file only service reads a file and translates to light RGB.""" service_data = { ATTR_PATH: "/opt/image.png", @@ -266,9 +266,6 @@ async def test_file(hass: HomeAssistant) -> None: # Add our /opt/ path to the allowed list of paths hass.config.allowlist_external_dirs.add("/opt/") - await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - # Verify pre service check state = hass.states.get(LIGHT_ENTITY) assert state @@ -295,7 +292,7 @@ async def test_file(hass: HomeAssistant) -> None: @patch("os.path.isfile", Mock(return_value=True)) @patch("os.access", Mock(return_value=True)) -async def test_file_denied_dir(hass: HomeAssistant) -> None: +async def test_file_denied_dir(hass: HomeAssistant, setup_integration) -> None: """Test that the file only service fails to read an image in a dir not explicitly allowed.""" service_data = { ATTR_PATH: "/path/to/a/dir/not/allowed/image.png", @@ -304,9 +301,6 @@ async def test_file_denied_dir(hass: HomeAssistant) -> None: ATTR_BRIGHTNESS_PCT: 100, } - await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - # Verify pre service check state = hass.states.get(LIGHT_ENTITY) assert state From b3080ae0053c3c063af509e1c15f293d89d93668 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 6 Oct 2023 19:38:00 +0200 Subject: [PATCH 29/29] Bumped version to 2023.10.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 c027875eae1..c2accfa83b5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 10 -PATCH_VERSION: Final = "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 81668c361a7..86e3d7a5e63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.10.0" +version = "2023.10.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"