From 0fa3ce763e7fad0e0863dadae02fa02f83a2b66c Mon Sep 17 00:00:00 2001 From: Alex Thompson Date: Mon, 11 Dec 2023 10:27:02 -0500 Subject: [PATCH 01/29] Fix Lyric LCC thermostats auto mode (#104853) --- homeassistant/components/lyric/climate.py | 109 +++++++++++++--------- 1 file changed, 66 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index f01e4c4fe55..e2504232c68 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import enum import logging from time import localtime, strftime, time from typing import Any @@ -151,6 +152,13 @@ async def async_setup_entry( ) +class LyricThermostatType(enum.Enum): + """Lyric thermostats are classified as TCC or LCC devices.""" + + TCC = enum.auto() + LCC = enum.auto() + + class LyricClimate(LyricDeviceEntity, ClimateEntity): """Defines a Honeywell Lyric climate entity.""" @@ -201,8 +209,10 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): # Setup supported features if device.changeableValues.thermostatSetpointStatus: self._attr_supported_features = SUPPORT_FLAGS_LCC + self._attr_thermostat_type = LyricThermostatType.LCC else: self._attr_supported_features = SUPPORT_FLAGS_TCC + self._attr_thermostat_type = LyricThermostatType.TCC # Setup supported fan modes if device_fan_modes := device.settings.attributes.get("fan", {}).get( @@ -365,56 +375,69 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): """Set hvac mode.""" _LOGGER.debug("HVAC mode: %s", hvac_mode) try: - if LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL: - # If the system is off, turn it to Heat first then to Auto, - # otherwise it turns to. - # Auto briefly and then reverts to Off (perhaps related to - # heatCoolMode). This is the behavior that happens with the - # native app as well, so likely a bug in the api itself - if HVAC_MODES[self.device.changeableValues.mode] == HVACMode.OFF: - _LOGGER.debug( - "HVAC mode passed to lyric: %s", - HVAC_MODES[LYRIC_HVAC_MODE_COOL], - ) - await self._update_thermostat( - self.location, - self.device, - mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], - autoChangeoverActive=False, - ) - # Sleep 3 seconds before proceeding - await asyncio.sleep(3) - _LOGGER.debug( - "HVAC mode passed to lyric: %s", - HVAC_MODES[LYRIC_HVAC_MODE_HEAT], - ) - await self._update_thermostat( - self.location, - self.device, - mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], - autoChangeoverActive=True, - ) - else: - _LOGGER.debug( - "HVAC mode passed to lyric: %s", - HVAC_MODES[self.device.changeableValues.mode], - ) - await self._update_thermostat( - self.location, self.device, autoChangeoverActive=True - ) - else: + match self._attr_thermostat_type: + case LyricThermostatType.TCC: + await self._async_set_hvac_mode_tcc(hvac_mode) + case LyricThermostatType.LCC: + await self._async_set_hvac_mode_lcc(hvac_mode) + except LYRIC_EXCEPTIONS as exception: + _LOGGER.error(exception) + await self.coordinator.async_refresh() + + async def _async_set_hvac_mode_tcc(self, hvac_mode: HVACMode) -> None: + if LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL: + # If the system is off, turn it to Heat first then to Auto, + # otherwise it turns to. + # Auto briefly and then reverts to Off (perhaps related to + # heatCoolMode). This is the behavior that happens with the + # native app as well, so likely a bug in the api itself + if HVAC_MODES[self.device.changeableValues.mode] == HVACMode.OFF: _LOGGER.debug( - "HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode] + "HVAC mode passed to lyric: %s", + HVAC_MODES[LYRIC_HVAC_MODE_COOL], ) await self._update_thermostat( self.location, self.device, - mode=LYRIC_HVAC_MODES[hvac_mode], + mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], autoChangeoverActive=False, ) - except LYRIC_EXCEPTIONS as exception: - _LOGGER.error(exception) - await self.coordinator.async_refresh() + # Sleep 3 seconds before proceeding + await asyncio.sleep(3) + _LOGGER.debug( + "HVAC mode passed to lyric: %s", + HVAC_MODES[LYRIC_HVAC_MODE_HEAT], + ) + await self._update_thermostat( + self.location, + self.device, + mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], + autoChangeoverActive=True, + ) + else: + _LOGGER.debug( + "HVAC mode passed to lyric: %s", + HVAC_MODES[self.device.changeableValues.mode], + ) + await self._update_thermostat( + self.location, self.device, autoChangeoverActive=True + ) + else: + _LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode]) + await self._update_thermostat( + self.location, + self.device, + mode=LYRIC_HVAC_MODES[hvac_mode], + autoChangeoverActive=False, + ) + + async def _async_set_hvac_mode_lcc(self, hvac_mode: HVACMode) -> None: + _LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode]) + await self._update_thermostat( + self.location, + self.device, + mode=LYRIC_HVAC_MODES[hvac_mode], + ) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset (PermanentHold, HoldUntil, NoHold, VacationHold) mode.""" From 7c0a5baf77ae6e90e99455e8e040397f99c9799f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Dec 2023 00:51:34 -1000 Subject: [PATCH 02/29] Bump zeroconf to 0.128.0 (#104936) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 5eb77b0c41c..8351212f0b8 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.127.0"] + "requirements": ["zeroconf==0.128.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2ec4c684387..7116cdae98a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -57,7 +57,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 yarl==1.9.2 -zeroconf==0.127.0 +zeroconf==0.128.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 9094578dd65..021d1389f6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2810,7 +2810,7 @@ zamg==0.3.3 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.127.0 +zeroconf==0.128.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae0f9d25a0b..2ab6f470703 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2105,7 +2105,7 @@ yt-dlp==2023.11.16 zamg==0.3.3 # homeassistant.components.zeroconf -zeroconf==0.127.0 +zeroconf==0.128.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From c6339cb0c0087dbf93fd37804d089b3e108ae196 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 8 Dec 2023 14:35:01 +0100 Subject: [PATCH 03/29] Fix CI test_invalid_rrule_fix test by freezing the time (#105294) --- tests/components/google/test_calendar.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index a70cd8aee9f..8466f5ad4eb 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -1301,6 +1301,7 @@ async def test_event_differs_timezone( } +@pytest.mark.freeze_time("2023-11-30 12:15:00 +00:00") async def test_invalid_rrule_fix( hass: HomeAssistant, hass_client: ClientSessionGenerator, From 2d1c7164446e033413e0e8f9c48ba4dcd027ff70 Mon Sep 17 00:00:00 2001 From: mkmer Date: Fri, 8 Dec 2023 15:30:41 -0500 Subject: [PATCH 04/29] Add missing configuration for services.yaml in blink (#105310) --- homeassistant/components/blink/services.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml index 95f4d33f91f..f6420e7f004 100644 --- a/homeassistant/components/blink/services.yaml +++ b/homeassistant/components/blink/services.yaml @@ -8,6 +8,10 @@ trigger_camera: domain: camera save_video: + target: + entity: + integration: blink + domain: camera fields: name: required: true @@ -21,6 +25,10 @@ save_video: text: save_recent_clips: + target: + entity: + integration: blink + domain: camera fields: name: required: true @@ -34,6 +42,10 @@ save_recent_clips: text: send_pin: + target: + entity: + integration: blink + domain: camera fields: pin: example: "abc123" From ef17950d76c4e4eb4ba7445c6e5c58aa2590ae81 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Sat, 9 Dec 2023 04:01:40 -0400 Subject: [PATCH 05/29] Bump pyschlage to 2023.12.0 (#105349) Co-authored-by: J. Nick Koston --- homeassistant/components/schlage/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json index 1eb7cb2ab0f..e14a5bc706e 100644 --- a/homeassistant/components/schlage/manifest.json +++ b/homeassistant/components/schlage/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/schlage", "iot_class": "cloud_polling", - "requirements": ["pyschlage==2023.11.0"] + "requirements": ["pyschlage==2023.12.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 021d1389f6f..7fa1835fa7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2026,7 +2026,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2023.11.0 +pyschlage==2023.12.0 # homeassistant.components.sensibo pysensibo==1.0.36 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ab6f470703..c4251ebc218 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1531,7 +1531,7 @@ pyrympro==0.0.7 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2023.11.0 +pyschlage==2023.12.0 # homeassistant.components.sensibo pysensibo==1.0.36 From 15cf732463791e157d9ece8c906506cc741151ce Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 9 Dec 2023 13:49:32 +0100 Subject: [PATCH 06/29] Fix preset modes error in Smartthings (#105375) --- homeassistant/components/smartthings/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index b97ca06a471..f07c293939a 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -528,10 +528,10 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): def _determine_preset_modes(self) -> list[str] | None: """Return a list of available preset modes.""" - supported_modes = self._device.status.attributes[ + supported_modes: list | None = self._device.status.attributes[ "supportedAcOptionalMode" ].value - if WINDFREE in supported_modes: + if supported_modes and WINDFREE in supported_modes: return [WINDFREE] return None From a02faaf489d9f43c178250f7c08929f0680d49dc Mon Sep 17 00:00:00 2001 From: mkmer Date: Sat, 9 Dec 2023 13:18:59 -0500 Subject: [PATCH 07/29] Fix service missing key in Blink (#105387) * fix update service refactor service yaml * Remove leftover target --- homeassistant/components/blink/services.py | 7 +++- homeassistant/components/blink/services.yaml | 44 +++++++++++++------- homeassistant/components/blink/strings.json | 28 ++++++++++++- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/blink/services.py b/homeassistant/components/blink/services.py index 12ac0d3b859..dae2f0ad951 100644 --- a/homeassistant/components/blink/services.py +++ b/homeassistant/components/blink/services.py @@ -25,6 +25,11 @@ from .const import ( ) from .coordinator import BlinkUpdateCoordinator +SERVICE_UPDATE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), + } +) SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema( { vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), @@ -152,7 +157,7 @@ def setup_services(hass: HomeAssistant) -> None: # Register all the above services service_mapping = [ - (blink_refresh, SERVICE_REFRESH, None), + (blink_refresh, SERVICE_REFRESH, SERVICE_UPDATE_SCHEMA), ( async_handle_save_video_service, SERVICE_SAVE_VIDEO, diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml index f6420e7f004..aaecde64353 100644 --- a/homeassistant/components/blink/services.yaml +++ b/homeassistant/components/blink/services.yaml @@ -1,18 +1,28 @@ # Describes the format for available Blink services blink_update: + fields: + device_id: + required: true + selector: + device: + integration: blink + trigger_camera: - target: - entity: - integration: blink - domain: camera + fields: + device_id: + required: true + selector: + device: + integration: blink save_video: - target: - entity: - integration: blink - domain: camera fields: + device_id: + required: true + selector: + device: + integration: blink name: required: true example: "Living Room" @@ -25,11 +35,12 @@ save_video: text: save_recent_clips: - target: - entity: - integration: blink - domain: camera fields: + device_id: + required: true + selector: + device: + integration: blink name: required: true example: "Living Room" @@ -42,11 +53,12 @@ save_recent_clips: text: send_pin: - target: - entity: - integration: blink - domain: camera fields: + device_id: + required: true + selector: + device: + integration: blink pin: example: "abc123" selector: diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index f47f72acb9c..fc0450dc8ea 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -57,11 +57,23 @@ "services": { "blink_update": { "name": "Update", - "description": "Forces a refresh." + "description": "Forces a refresh.", + "fields": { + "device_id": { + "name": "Device ID", + "description": "The Blink device id." + } + } }, "trigger_camera": { "name": "Trigger camera", - "description": "Requests camera to take new image." + "description": "Requests camera to take new image.", + "fields": { + "device_id": { + "name": "Device ID", + "description": "The Blink device id." + } + } }, "save_video": { "name": "Save video", @@ -74,6 +86,10 @@ "filename": { "name": "File name", "description": "Filename to writable path (directory may need to be included in allowlist_external_dirs in config)." + }, + "device_id": { + "name": "Device ID", + "description": "The Blink device id." } } }, @@ -88,6 +104,10 @@ "file_path": { "name": "Output directory", "description": "Directory name of writable path (directory may need to be included in allowlist_external_dirs in config)." + }, + "device_id": { + "name": "Device ID", + "description": "The Blink device id." } } }, @@ -98,6 +118,10 @@ "pin": { "name": "Pin", "description": "PIN received from blink. Leave empty if you only received a verification email." + }, + "device_id": { + "name": "Device ID", + "description": "The Blink device id." } } } From 84c713894b1fb5f968b6f002a64ac504182a71d2 Mon Sep 17 00:00:00 2001 From: Florian B Date: Sun, 10 Dec 2023 17:23:05 +0100 Subject: [PATCH 08/29] Fix adding/updating todo items with due date in CalDAV integration (#105435) * refactor: return date/datetime for due date * fix: explicitly set due date on vTODO component Using `set_due` automatically handles converting the Python-native date/datetime values to the correct representation required by RFC5545. * fix: fix tests with changed due date handling * fix: item.due may not be a str * refactor: keep local timezone of due datetime * refactor: reorder import statement To make ruff happy. * fix: fix false-positive mypy error --- homeassistant/components/caldav/todo.py | 10 +++++----- tests/components/caldav/test_todo.py | 13 +++++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/caldav/todo.py b/homeassistant/components/caldav/todo.py index 1bd24dc542a..b7089c3da65 100644 --- a/homeassistant/components/caldav/todo.py +++ b/homeassistant/components/caldav/todo.py @@ -98,10 +98,7 @@ def _to_ics_fields(item: TodoItem) -> dict[str, Any]: if status := item.status: item_data["status"] = TODO_STATUS_MAP_INV.get(status, "NEEDS-ACTION") if due := item.due: - if isinstance(due, datetime): - item_data["due"] = dt_util.as_utc(due).strftime("%Y%m%dT%H%M%SZ") - else: - item_data["due"] = due.strftime("%Y%m%d") + item_data["due"] = due if description := item.description: item_data["description"] = description return item_data @@ -162,7 +159,10 @@ class WebDavTodoListEntity(TodoListEntity): except (requests.ConnectionError, DAVError) as err: raise HomeAssistantError(f"CalDAV lookup error: {err}") from err vtodo = todo.icalendar_component # type: ignore[attr-defined] - vtodo.update(**_to_ics_fields(item)) + updated_fields = _to_ics_fields(item) + if "due" in updated_fields: + todo.set_due(updated_fields.pop("due")) # type: ignore[attr-defined] + vtodo.update(**updated_fields) try: await self.hass.async_add_executor_job( partial( diff --git a/tests/components/caldav/test_todo.py b/tests/components/caldav/test_todo.py index 6e92f211463..a90529297be 100644 --- a/tests/components/caldav/test_todo.py +++ b/tests/components/caldav/test_todo.py @@ -1,4 +1,5 @@ """The tests for the webdav todo component.""" +from datetime import UTC, date, datetime from typing import Any from unittest.mock import MagicMock, Mock @@ -200,12 +201,16 @@ async def test_supported_components( ), ( {"due_date": "2023-11-18"}, - {"status": "NEEDS-ACTION", "summary": "Cheese", "due": "20231118"}, + {"status": "NEEDS-ACTION", "summary": "Cheese", "due": date(2023, 11, 18)}, {**RESULT_ITEM, "due": "2023-11-18"}, ), ( {"due_datetime": "2023-11-18T08:30:00-06:00"}, - {"status": "NEEDS-ACTION", "summary": "Cheese", "due": "20231118T143000Z"}, + { + "status": "NEEDS-ACTION", + "summary": "Cheese", + "due": datetime(2023, 11, 18, 14, 30, 00, tzinfo=UTC), + }, {**RESULT_ITEM, "due": "2023-11-18T08:30:00-06:00"}, ), ( @@ -311,13 +316,13 @@ async def test_add_item_failure( ), ( {"due_date": "2023-11-18"}, - ["SUMMARY:Cheese", "DUE:20231118"], + ["SUMMARY:Cheese", "DUE;VALUE=DATE:20231118"], "1", {**RESULT_ITEM, "due": "2023-11-18"}, ), ( {"due_datetime": "2023-11-18T08:30:00-06:00"}, - ["SUMMARY:Cheese", "DUE:20231118T143000Z"], + ["SUMMARY:Cheese", "DUE;TZID=America/Regina:20231118T083000"], "1", {**RESULT_ITEM, "due": "2023-11-18T08:30:00-06:00"}, ), From a0e9772f52caae89d70dec1fcb4557d39a77325b Mon Sep 17 00:00:00 2001 From: Jan Schneider Date: Sun, 10 Dec 2023 23:59:54 +0100 Subject: [PATCH 09/29] Check if heat area exists when setting up valve opening and battery sensors in moehlenhoff alpha2 (#105437) Check whether the referenced heat area exists when setting up valve opening and battery sensors --- homeassistant/components/moehlenhoff_alpha2/binary_sensor.py | 1 + homeassistant/components/moehlenhoff_alpha2/sensor.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/moehlenhoff_alpha2/binary_sensor.py b/homeassistant/components/moehlenhoff_alpha2/binary_sensor.py index 8acc88d8314..5cdca72fa55 100644 --- a/homeassistant/components/moehlenhoff_alpha2/binary_sensor.py +++ b/homeassistant/components/moehlenhoff_alpha2/binary_sensor.py @@ -27,6 +27,7 @@ async def async_setup_entry( Alpha2IODeviceBatterySensor(coordinator, io_device_id) for io_device_id, io_device in coordinator.data["io_devices"].items() if io_device["_HEATAREA_ID"] + and io_device["_HEATAREA_ID"] in coordinator.data["heat_areas"] ) diff --git a/homeassistant/components/moehlenhoff_alpha2/sensor.py b/homeassistant/components/moehlenhoff_alpha2/sensor.py index e41c6b041f6..2c2e44f451d 100644 --- a/homeassistant/components/moehlenhoff_alpha2/sensor.py +++ b/homeassistant/components/moehlenhoff_alpha2/sensor.py @@ -25,7 +25,7 @@ async def async_setup_entry( Alpha2HeatControlValveOpeningSensor(coordinator, heat_control_id) for heat_control_id, heat_control in coordinator.data["heat_controls"].items() if heat_control["INUSE"] - and heat_control["_HEATAREA_ID"] + and heat_control["_HEATAREA_ID"] in coordinator.data["heat_areas"] and heat_control.get("ACTOR_PERCENT") is not None ) From 164d0d2085f5ee4c6bbe9c947aa5c3ded0f21c83 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 10 Dec 2023 23:16:06 +0100 Subject: [PATCH 10/29] Bump hatasmota to 0.8.0 (#105440) * Bump hatasmota to 0.8.0 * Keep devices with deep sleep support always available * Add tests --- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/mixins.py | 9 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/tasmota/test_binary_sensor.py | 29 +++++ tests/components/tasmota/test_common.py | 120 ++++++++++++++++++ tests/components/tasmota/test_cover.py | 36 ++++++ tests/components/tasmota/test_fan.py | 27 ++++ tests/components/tasmota/test_light.py | 27 ++++ tests/components/tasmota/test_sensor.py | 38 ++++++ tests/components/tasmota/test_switch.py | 25 ++++ 11 files changed, 313 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 42fc849a2cf..2ce81772774 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.7.3"] + "requirements": ["HATasmota==0.8.0"] } diff --git a/homeassistant/components/tasmota/mixins.py b/homeassistant/components/tasmota/mixins.py index 21030b8c14b..48dbe51fd67 100644 --- a/homeassistant/components/tasmota/mixins.py +++ b/homeassistant/components/tasmota/mixins.py @@ -112,8 +112,11 @@ class TasmotaAvailability(TasmotaEntity): def __init__(self, **kwds: Any) -> None: """Initialize the availability mixin.""" - self._available = False super().__init__(**kwds) + if self._tasmota_entity.deep_sleep_enabled: + self._available = True + else: + self._available = False async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" @@ -122,6 +125,8 @@ class TasmotaAvailability(TasmotaEntity): async_subscribe_connection_status(self.hass, self.async_mqtt_connected) ) await super().async_added_to_hass() + if self._tasmota_entity.deep_sleep_enabled: + await self._tasmota_entity.poll_status() async def availability_updated(self, available: bool) -> None: """Handle updated availability.""" @@ -135,6 +140,8 @@ class TasmotaAvailability(TasmotaEntity): if not self.hass.is_stopping: if not mqtt_connected(self.hass): self._available = False + elif self._tasmota_entity.deep_sleep_enabled: + self._available = True self.async_write_ha_state() @property diff --git a/requirements_all.txt b/requirements_all.txt index 7fa1835fa7e..dc463541355 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -28,7 +28,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.7.3 +HATasmota==0.8.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4251ebc218..2def893d495 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -25,7 +25,7 @@ DoorBirdPy==2.1.0 HAP-python==4.9.1 # homeassistant.components.tasmota -HATasmota==0.7.3 +HATasmota==0.8.0 # homeassistant.components.doods # homeassistant.components.generic diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py index 2bfb4a9d5e2..d5f1e4d7101 100644 --- a/tests/components/tasmota/test_binary_sensor.py +++ b/tests/components/tasmota/test_binary_sensor.py @@ -31,6 +31,8 @@ from .test_common import ( help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -313,6 +315,21 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["swc"][0] = 1 + config["swn"][0] = "Test" + await help_test_deep_sleep_availability_when_connection_lost( + hass, mqtt_client_mock, mqtt_mock, Platform.BINARY_SENSOR, config + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -323,6 +340,18 @@ async def test_availability( await help_test_availability(hass, mqtt_mock, Platform.BINARY_SENSOR, config) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability when deep sleep is enabled.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["swc"][0] = 1 + config["swn"][0] = "Test" + await help_test_deep_sleep_availability( + hass, mqtt_mock, Platform.BINARY_SENSOR, config + ) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index a184f650fae..1f414cb4e5a 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -4,6 +4,7 @@ import json from unittest.mock import ANY from hatasmota.const import ( + CONF_DEEP_SLEEP, CONF_MAC, CONF_OFFLINE, CONF_ONLINE, @@ -188,6 +189,76 @@ async def help_test_availability_when_connection_lost( assert state.state != STATE_UNAVAILABLE +async def help_test_deep_sleep_availability_when_connection_lost( + hass, + mqtt_client_mock, + mqtt_mock, + domain, + config, + sensor_config=None, + object_id="tasmota_test", +): + """Test availability after MQTT disconnection when deep sleep is enabled. + + This is a test helper for the TasmotaAvailability mixin. + """ + config[CONF_DEEP_SLEEP] = 1 + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + if sensor_config: + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors", + json.dumps(sensor_config), + ) + await hass.async_block_till_done() + + # Device online + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + # Disconnected from MQTT server -> state changed to unavailable + mqtt_mock.connected = False + await hass.async_add_executor_job(mqtt_client_mock.on_disconnect, None, None, 0) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state == STATE_UNAVAILABLE + + # Reconnected to MQTT server -> state no longer unavailable + mqtt_mock.connected = True + await hass.async_add_executor_job(mqtt_client_mock.on_connect, None, None, None, 0) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + # Receive LWT again + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_online(config), + ) + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_offline(config), + ) + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + async def help_test_availability( hass, mqtt_mock, @@ -236,6 +307,55 @@ async def help_test_availability( assert state.state == STATE_UNAVAILABLE +async def help_test_deep_sleep_availability( + hass, + mqtt_mock, + domain, + config, + sensor_config=None, + object_id="tasmota_test", +): + """Test availability when deep sleep is enabled. + + This is a test helper for the TasmotaAvailability mixin. + """ + config[CONF_DEEP_SLEEP] = 1 + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + if sensor_config: + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/sensors", + json.dumps(sensor_config), + ) + await hass.async_block_till_done() + + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_online(config), + ) + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message( + hass, + get_topic_tele_will(config), + config_get_state_offline(config), + ) + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{object_id}") + assert state.state != STATE_UNAVAILABLE + + async def help_test_availability_discovery_update( hass, mqtt_mock, diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index e2bdc8b2ca7..cae65521e21 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -22,6 +22,8 @@ from .test_common import ( help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -663,6 +665,27 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["rl"][0] = 3 + config["rl"][1] = 3 + await help_test_deep_sleep_availability_when_connection_lost( + hass, + mqtt_client_mock, + mqtt_mock, + Platform.COVER, + config, + object_id="test_cover_1", + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -676,6 +699,19 @@ async def test_availability( ) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Test" + config["rl"][0] = 3 + config["rl"][1] = 3 + await help_test_deep_sleep_availability( + hass, mqtt_mock, Platform.COVER, config, object_id="test_cover_1" + ) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index 2a50e2d43b5..05e3151be2e 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -22,6 +22,8 @@ from .test_common import ( help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -232,6 +234,20 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["if"] = 1 + await help_test_deep_sleep_availability_when_connection_lost( + hass, mqtt_client_mock, mqtt_mock, Platform.FAN, config, object_id="tasmota" + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -243,6 +259,17 @@ async def test_availability( ) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["if"] = 1 + await help_test_deep_sleep_availability( + hass, mqtt_mock, Platform.FAN, config, object_id="tasmota" + ) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 27b7bd1a82a..50f11fb7757 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -22,6 +22,8 @@ from .test_common import ( help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -1669,6 +1671,21 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 1 # 1 channel light (Dimmer) + await help_test_deep_sleep_availability_when_connection_lost( + hass, mqtt_client_mock, mqtt_mock, Platform.LIGHT, config + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -1679,6 +1696,16 @@ async def test_availability( await help_test_availability(hass, mqtt_mock, Platform.LIGHT, config) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 1 # 1 channel light (Dimmer) + await help_test_deep_sleep_availability(hass, mqtt_mock, Platform.LIGHT, config) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 2f50a84ffdd..dc4820779a6 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -28,6 +28,8 @@ from .test_common import ( help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -1222,6 +1224,26 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG) + await help_test_deep_sleep_availability_when_connection_lost( + hass, + mqtt_client_mock, + mqtt_mock, + Platform.SENSOR, + config, + sensor_config, + "tasmota_dht11_temperature", + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -1238,6 +1260,22 @@ async def test_availability( ) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG) + await help_test_deep_sleep_availability( + hass, + mqtt_mock, + Platform.SENSOR, + config, + sensor_config, + "tasmota_dht11_temperature", + ) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: diff --git a/tests/components/tasmota/test_switch.py b/tests/components/tasmota/test_switch.py index 54d94b46fe8..1a16f372fc9 100644 --- a/tests/components/tasmota/test_switch.py +++ b/tests/components/tasmota/test_switch.py @@ -20,6 +20,8 @@ from .test_common import ( help_test_availability_discovery_update, help_test_availability_poll_state, help_test_availability_when_connection_lost, + help_test_deep_sleep_availability, + help_test_deep_sleep_availability_when_connection_lost, help_test_discovery_device_remove, help_test_discovery_removal, help_test_discovery_update_unchanged, @@ -158,6 +160,20 @@ async def test_availability_when_connection_lost( ) +async def test_deep_sleep_availability_when_connection_lost( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock: MqttMockHAClient, + setup_tasmota, +) -> None: + """Test availability after MQTT disconnection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 1 + await help_test_deep_sleep_availability_when_connection_lost( + hass, mqtt_client_mock, mqtt_mock, Platform.SWITCH, config + ) + + async def test_availability( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: @@ -167,6 +183,15 @@ async def test_availability( await help_test_availability(hass, mqtt_mock, Platform.SWITCH, config) +async def test_deep_sleep_availability( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test availability.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 1 + await help_test_deep_sleep_availability(hass, mqtt_mock, Platform.SWITCH, config) + + async def test_availability_discovery_update( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: From 0023d66c80b5ba4f2848ca0bde0360de0fed6846 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:29:50 +0100 Subject: [PATCH 11/29] Bump plugwise to v0.35.3 (#105442) --- homeassistant/components/plugwise/const.py | 19 ++++++++++++++++++- .../components/plugwise/manifest.json | 2 +- homeassistant/components/plugwise/number.py | 3 +-- homeassistant/components/plugwise/select.py | 3 +-- .../components/plugwise/strings.json | 5 ++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../all_data.json | 19 ++++++++++++------- .../anna_heatpump_heating/all_data.json | 2 +- .../fixtures/m_adam_cooling/all_data.json | 4 ++-- .../fixtures/m_adam_heating/all_data.json | 4 ++-- .../m_anna_heatpump_cooling/all_data.json | 2 +- .../m_anna_heatpump_idle/all_data.json | 2 +- .../plugwise/snapshots/test_diagnostics.ambr | 9 +++++++-- 14 files changed, 53 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index 34bb5c926ae..f5677c0b4a9 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Final +from typing import Final, Literal from homeassistant.const import Platform @@ -36,6 +36,23 @@ ZEROCONF_MAP: Final[dict[str, str]] = { "stretch": "Stretch", } +NumberType = Literal[ + "maximum_boiler_temperature", + "max_dhw_temperature", + "temperature_offset", +] + +SelectType = Literal[ + "select_dhw_mode", + "select_regulation_mode", + "select_schedule", +] +SelectOptionsType = Literal[ + "dhw_modes", + "regulation_modes", + "available_schedules", +] + # Default directives DEFAULT_MAX_TEMP: Final = 30 DEFAULT_MIN_TEMP: Final = 4 diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index bb2b428bf19..92923e98d2c 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["crcmod", "plugwise"], - "requirements": ["plugwise==0.34.5"], + "requirements": ["plugwise==0.35.3"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index 2c87edddf04..c21ecbd94c7 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -5,7 +5,6 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from plugwise import Smile -from plugwise.constants import NumberType from homeassistant.components.number import ( NumberDeviceClass, @@ -18,7 +17,7 @@ from homeassistant.const import EntityCategory, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DOMAIN, NumberType from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index c12ca671554..eef873703c1 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -5,7 +5,6 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from plugwise import Smile -from plugwise.constants import SelectOptionsType, SelectType from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry @@ -13,7 +12,7 @@ from homeassistant.const import STATE_ON, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DOMAIN, SelectOptionsType, SelectType from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 5348a1dc484..addd1ceadb1 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -108,7 +108,10 @@ } }, "select_schedule": { - "name": "Thermostat schedule" + "name": "Thermostat schedule", + "state": { + "off": "Off" + } } }, "sensor": { diff --git a/requirements_all.txt b/requirements_all.txt index dc463541355..e0f764c4817 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1476,7 +1476,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.34.5 +plugwise==0.35.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2def893d495..7f3e4910c1a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1134,7 +1134,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.34.5 +plugwise==0.35.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json index 279fe6b8a43..f97182782e6 100644 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json @@ -112,7 +112,8 @@ "Bios Schema met Film Avond", "GF7 Woonkamer", "Badkamer Schema", - "CV Jessie" + "CV Jessie", + "off" ], "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", @@ -251,7 +252,8 @@ "Bios Schema met Film Avond", "GF7 Woonkamer", "Badkamer Schema", - "CV Jessie" + "CV Jessie", + "off" ], "dev_class": "zone_thermostat", "firmware": "2016-08-02T02:00:00+02:00", @@ -334,7 +336,8 @@ "Bios Schema met Film Avond", "GF7 Woonkamer", "Badkamer Schema", - "CV Jessie" + "CV Jessie", + "off" ], "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", @@ -344,7 +347,7 @@ "model": "Lisa", "name": "Zone Lisa Bios", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "select_schedule": "None", + "select_schedule": "off", "sensors": { "battery": 67, "setpoint": 13.0, @@ -373,7 +376,8 @@ "Bios Schema met Film Avond", "GF7 Woonkamer", "Badkamer Schema", - "CV Jessie" + "CV Jessie", + "off" ], "dev_class": "thermostatic_radiator_valve", "firmware": "2019-03-27T01:00:00+01:00", @@ -383,7 +387,7 @@ "model": "Tom/Floor", "name": "CV Kraan Garage", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "select_schedule": "None", + "select_schedule": "off", "sensors": { "battery": 68, "setpoint": 5.5, @@ -414,7 +418,8 @@ "Bios Schema met Film Avond", "GF7 Woonkamer", "Badkamer Schema", - "CV Jessie" + "CV Jessie", + "off" ], "dev_class": "zone_thermostat", "firmware": "2016-10-27T02:00:00+02:00", diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json index 9ef93d63bdd..d655f95c79b 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json @@ -59,7 +59,7 @@ }, "3cb70739631c4d17a86b8b12e8a5161b": { "active_preset": "home", - "available_schedules": ["standaard"], + "available_schedules": ["standaard", "off"], "dev_class": "thermostat", "firmware": "2018-02-08T11:15:53+01:00", "hardware": "6539-1301-5002", diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json index 2e1063d14d3..7b570a6cf61 100644 --- a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json @@ -52,7 +52,7 @@ "ad4838d7d35c4d6ea796ee12ae5aedf8": { "active_preset": "asleep", "available": true, - "available_schedules": ["Weekschema", "Badkamer", "Test"], + "available_schedules": ["Weekschema", "Badkamer", "Test", "off"], "control_state": "cooling", "dev_class": "thermostat", "location": "f2bf9048bef64cc5b6d5110154e33c81", @@ -102,7 +102,7 @@ "e2f4322d57924fa090fbbc48b3a140dc": { "active_preset": "home", "available": true, - "available_schedules": ["Weekschema", "Badkamer", "Test"], + "available_schedules": ["Weekschema", "Badkamer", "Test", "off"], "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-10T02:00:00+02:00", diff --git a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json index 81d60bed9d4..57259047698 100644 --- a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json @@ -80,7 +80,7 @@ "ad4838d7d35c4d6ea796ee12ae5aedf8": { "active_preset": "asleep", "available": true, - "available_schedules": ["Weekschema", "Badkamer", "Test"], + "available_schedules": ["Weekschema", "Badkamer", "Test", "off"], "control_state": "preheating", "dev_class": "thermostat", "location": "f2bf9048bef64cc5b6d5110154e33c81", @@ -124,7 +124,7 @@ "e2f4322d57924fa090fbbc48b3a140dc": { "active_preset": "home", "available": true, - "available_schedules": ["Weekschema", "Badkamer", "Test"], + "available_schedules": ["Weekschema", "Badkamer", "Test", "off"], "control_state": "off", "dev_class": "zone_thermostat", "firmware": "2016-10-10T02:00:00+02:00", diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json index 844eae4c2f7..92c95f6c5a9 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json @@ -59,7 +59,7 @@ }, "3cb70739631c4d17a86b8b12e8a5161b": { "active_preset": "home", - "available_schedules": ["standaard"], + "available_schedules": ["standaard", "off"], "dev_class": "thermostat", "firmware": "2018-02-08T11:15:53+01:00", "hardware": "6539-1301-5002", diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json index f6be6f35188..be400b9bc98 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json @@ -59,7 +59,7 @@ }, "3cb70739631c4d17a86b8b12e8a5161b": { "active_preset": "home", - "available_schedules": ["standaard"], + "available_schedules": ["standaard", "off"], "dev_class": "thermostat", "firmware": "2018-02-08T11:15:53+01:00", "hardware": "6539-1301-5002", diff --git a/tests/components/plugwise/snapshots/test_diagnostics.ambr b/tests/components/plugwise/snapshots/test_diagnostics.ambr index 29f23a137fb..c2bbea9418a 100644 --- a/tests/components/plugwise/snapshots/test_diagnostics.ambr +++ b/tests/components/plugwise/snapshots/test_diagnostics.ambr @@ -115,6 +115,7 @@ 'GF7 Woonkamer', 'Badkamer Schema', 'CV Jessie', + 'off', ]), 'dev_class': 'zone_thermostat', 'firmware': '2016-10-27T02:00:00+02:00', @@ -260,6 +261,7 @@ 'GF7 Woonkamer', 'Badkamer Schema', 'CV Jessie', + 'off', ]), 'dev_class': 'zone_thermostat', 'firmware': '2016-08-02T02:00:00+02:00', @@ -349,6 +351,7 @@ 'GF7 Woonkamer', 'Badkamer Schema', 'CV Jessie', + 'off', ]), 'dev_class': 'zone_thermostat', 'firmware': '2016-10-27T02:00:00+02:00', @@ -364,7 +367,7 @@ 'vacation', 'no_frost', ]), - 'select_schedule': 'None', + 'select_schedule': 'off', 'sensors': dict({ 'battery': 67, 'setpoint': 13.0, @@ -394,6 +397,7 @@ 'GF7 Woonkamer', 'Badkamer Schema', 'CV Jessie', + 'off', ]), 'dev_class': 'thermostatic_radiator_valve', 'firmware': '2019-03-27T01:00:00+01:00', @@ -409,7 +413,7 @@ 'vacation', 'no_frost', ]), - 'select_schedule': 'None', + 'select_schedule': 'off', 'sensors': dict({ 'battery': 68, 'setpoint': 5.5, @@ -441,6 +445,7 @@ 'GF7 Woonkamer', 'Badkamer Schema', 'CV Jessie', + 'off', ]), 'dev_class': 'zone_thermostat', 'firmware': '2016-10-27T02:00:00+02:00', From 399f98a726ff1a945ed24b045a7caed77b6c9ecb Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 10 Dec 2023 17:27:01 +0100 Subject: [PATCH 12/29] Fix alexa calling not featured cover services (#105444) * Fix alexa calls not supported cover services * Follow up comment and additional tests --- homeassistant/components/alexa/handlers.py | 19 +- tests/components/alexa/test_smart_home.py | 443 ++++++++++++++++----- 2 files changed, 354 insertions(+), 108 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index f99b0231e4d..2796c10795b 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1304,13 +1304,14 @@ async def async_api_set_range( service = None data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} range_value = directive.payload["rangeValue"] + supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) # Cover Position if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": range_value = int(range_value) - if range_value == 0: + if supported & cover.CoverEntityFeature.CLOSE and range_value == 0: service = cover.SERVICE_CLOSE_COVER - elif range_value == 100: + elif supported & cover.CoverEntityFeature.OPEN and range_value == 100: service = cover.SERVICE_OPEN_COVER else: service = cover.SERVICE_SET_COVER_POSITION @@ -1319,9 +1320,9 @@ async def async_api_set_range( # Cover Tilt elif instance == f"{cover.DOMAIN}.tilt": range_value = int(range_value) - if range_value == 0: + if supported & cover.CoverEntityFeature.CLOSE_TILT and range_value == 0: service = cover.SERVICE_CLOSE_COVER_TILT - elif range_value == 100: + elif supported & cover.CoverEntityFeature.OPEN_TILT and range_value == 100: service = cover.SERVICE_OPEN_COVER_TILT else: service = cover.SERVICE_SET_COVER_TILT_POSITION @@ -1332,13 +1333,11 @@ async def async_api_set_range( range_value = int(range_value) if range_value == 0: service = fan.SERVICE_TURN_OFF + elif supported & fan.FanEntityFeature.SET_SPEED: + service = fan.SERVICE_SET_PERCENTAGE + data[fan.ATTR_PERCENTAGE] = range_value else: - supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if supported and fan.FanEntityFeature.SET_SPEED: - service = fan.SERVICE_SET_PERCENTAGE - data[fan.ATTR_PERCENTAGE] = range_value - else: - service = fan.SERVICE_TURN_ON + service = fan.SERVICE_TURN_ON # Humidifier target humidity elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}": diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 7a1abe96110..0a5b1f79f72 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -6,7 +6,7 @@ import pytest from homeassistant.components.alexa import smart_home, state_report import homeassistant.components.camera as camera -from homeassistant.components.cover import CoverDeviceClass +from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature from homeassistant.components.media_player import MediaPlayerEntityFeature from homeassistant.components.vacuum import VacuumEntityFeature from homeassistant.config import async_process_ha_core_config @@ -1884,8 +1884,199 @@ async def test_group(hass: HomeAssistant) -> None: ) -async def test_cover_position_range(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("position", "position_attr_in_service_call", "supported_features", "service_call"), + [ + ( + 30, + 30, + CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE, + "cover.set_cover_position", + ), + ( + 0, + None, + CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE, + "cover.close_cover", + ), + ( + 99, + 99, + CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE, + "cover.set_cover_position", + ), + ( + 100, + None, + CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE, + "cover.open_cover", + ), + ( + 0, + 0, + CoverEntityFeature.SET_POSITION, + "cover.set_cover_position", + ), + ( + 60, + 60, + CoverEntityFeature.SET_POSITION, + "cover.set_cover_position", + ), + ( + 100, + 100, + CoverEntityFeature.SET_POSITION, + "cover.set_cover_position", + ), + ( + 0, + 0, + CoverEntityFeature.SET_POSITION | CoverEntityFeature.OPEN, + "cover.set_cover_position", + ), + ( + 100, + 100, + CoverEntityFeature.SET_POSITION | CoverEntityFeature.CLOSE, + "cover.set_cover_position", + ), + ], + ids=[ + "position_30_open_close", + "position_0_open_close", + "position_99_open_close", + "position_100_open_close", + "position_0_no_open_close", + "position_60_no_open_close", + "position_100_no_open_close", + "position_0_no_close", + "position_100_no_open", + ], +) +async def test_cover_position( + hass: HomeAssistant, + position: int, + position_attr_in_service_call: int | None, + supported_features: CoverEntityFeature, + service_call: str, +) -> None: """Test cover discovery and position using rangeController.""" + device = ( + "cover.test_range", + "open", + { + "friendly_name": "Test cover range", + "device_class": "blind", + "supported_features": supported_features, + "position": position, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "cover#test_range" + assert appliance["displayCategories"][0] == "INTERIOR_BLIND" + assert appliance["friendlyName"] == "Test cover range" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PowerController", + "Alexa.RangeController", + "Alexa.EndpointHealth", + "Alexa", + ) + + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "cover.position" + + properties = range_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "rangeValue"} in properties["supported"] + + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "text", + "value": {"text": "Position", "locale": "en-US"}, + } in capability_resources["friendlyNames"] + + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.Opening"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + assert configuration["unitOfMeasure"] == "Alexa.Unit.Percent" + + supported_range = configuration["supportedRange"] + assert supported_range["minimumValue"] == 0 + assert supported_range["maximumValue"] == 100 + assert supported_range["precision"] == 1 + + # Assert for Position Semantics + position_semantics = range_capability["semantics"] + assert position_semantics is not None + + position_action_mappings = position_semantics["actionMappings"] + assert position_action_mappings is not None + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Lower", "Alexa.Actions.Close"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, + } in position_action_mappings + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Raise", "Alexa.Actions.Open"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, + } in position_action_mappings + + position_state_mappings = position_semantics["stateMappings"] + assert position_state_mappings is not None + assert { + "@type": "StatesToValue", + "states": ["Alexa.States.Closed"], + "value": 0, + } in position_state_mappings + assert { + "@type": "StatesToRange", + "states": ["Alexa.States.Open"], + "range": {"minimumValue": 1, "maximumValue": 100}, + } in position_state_mappings + + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "cover#test_range", + service_call, + hass, + payload={"rangeValue": position}, + instance="cover.position", + ) + assert call.data.get("position") == position_attr_in_service_call + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == position + + +async def test_cover_position_range( + hass: HomeAssistant, +) -> None: + """Test cover discovery and position range using rangeController. + + Also tests an invalid cover position being handled correctly. + """ + device = ( "cover.test_range", "open", @@ -1969,59 +2160,6 @@ async def test_cover_position_range(hass: HomeAssistant) -> None: "range": {"minimumValue": 1, "maximumValue": 100}, } in position_state_mappings - call, _ = await assert_request_calls_service( - "Alexa.RangeController", - "SetRangeValue", - "cover#test_range", - "cover.set_cover_position", - hass, - payload={"rangeValue": 50}, - instance="cover.position", - ) - assert call.data["position"] == 50 - - call, msg = await assert_request_calls_service( - "Alexa.RangeController", - "SetRangeValue", - "cover#test_range", - "cover.close_cover", - hass, - payload={"rangeValue": 0}, - instance="cover.position", - ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 0 - - call, msg = await assert_request_calls_service( - "Alexa.RangeController", - "SetRangeValue", - "cover#test_range", - "cover.open_cover", - hass, - payload={"rangeValue": 100}, - instance="cover.position", - ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 100 - - call, msg = await assert_request_calls_service( - "Alexa.RangeController", - "AdjustRangeValue", - "cover#test_range", - "cover.open_cover", - hass, - payload={"rangeValueDelta": 99, "rangeValueDeltaDefault": False}, - instance="cover.position", - ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 100 - call, msg = await assert_request_calls_service( "Alexa.RangeController", "AdjustRangeValue", @@ -3435,8 +3573,159 @@ async def test_presence_sensor(hass: HomeAssistant) -> None: assert {"name": "humanPresenceDetectionState"} in properties["supported"] -async def test_cover_tilt_position_range(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ( + "tilt_position", + "tilt_position_attr_in_service_call", + "supported_features", + "service_call", + ), + [ + ( + 30, + 30, + CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT, + "cover.set_cover_tilt_position", + ), + ( + 0, + None, + CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT, + "cover.close_cover_tilt", + ), + ( + 99, + 99, + CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT, + "cover.set_cover_tilt_position", + ), + ( + 100, + None, + CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT, + "cover.open_cover_tilt", + ), + ( + 0, + 0, + CoverEntityFeature.SET_TILT_POSITION, + "cover.set_cover_tilt_position", + ), + ( + 60, + 60, + CoverEntityFeature.SET_TILT_POSITION, + "cover.set_cover_tilt_position", + ), + ( + 100, + 100, + CoverEntityFeature.SET_TILT_POSITION, + "cover.set_cover_tilt_position", + ), + ( + 0, + 0, + CoverEntityFeature.SET_TILT_POSITION | CoverEntityFeature.OPEN_TILT, + "cover.set_cover_tilt_position", + ), + ( + 100, + 100, + CoverEntityFeature.SET_TILT_POSITION | CoverEntityFeature.CLOSE_TILT, + "cover.set_cover_tilt_position", + ), + ], + ids=[ + "tilt_position_30_open_close", + "tilt_position_0_open_close", + "tilt_position_99_open_close", + "tilt_position_100_open_close", + "tilt_position_0_no_open_close", + "tilt_position_60_no_open_close", + "tilt_position_100_no_open_close", + "tilt_position_0_no_close", + "tilt_position_100_no_open", + ], +) +async def test_cover_tilt_position( + hass: HomeAssistant, + tilt_position: int, + tilt_position_attr_in_service_call: int | None, + supported_features: CoverEntityFeature, + service_call: str, +) -> None: """Test cover discovery and tilt position using rangeController.""" + device = ( + "cover.test_tilt_range", + "open", + { + "friendly_name": "Test cover tilt range", + "device_class": "blind", + "supported_features": supported_features, + "tilt_position": tilt_position, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "cover#test_tilt_range" + assert appliance["displayCategories"][0] == "INTERIOR_BLIND" + assert appliance["friendlyName"] == "Test cover tilt range" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PowerController", + "Alexa.RangeController", + "Alexa.EndpointHealth", + "Alexa", + ) + + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "cover.tilt" + + semantics = range_capability["semantics"] + assert semantics is not None + + action_mappings = semantics["actionMappings"] + assert action_mappings is not None + + state_mappings = semantics["stateMappings"] + assert state_mappings is not None + + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "cover#test_tilt_range", + service_call, + hass, + payload={"rangeValue": tilt_position}, + instance="cover.tilt", + ) + assert call.data.get("tilt_position") == tilt_position_attr_in_service_call + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == tilt_position + + +async def test_cover_tilt_position_range(hass: HomeAssistant) -> None: + """Test cover discovery and tilt position range using rangeController. + + Also tests and invalid tilt position being handled correctly. + """ device = ( "cover.test_tilt_range", "open", @@ -3485,48 +3774,6 @@ async def test_cover_tilt_position_range(hass: HomeAssistant) -> None: ) assert call.data["tilt_position"] == 50 - call, msg = await assert_request_calls_service( - "Alexa.RangeController", - "SetRangeValue", - "cover#test_tilt_range", - "cover.close_cover_tilt", - hass, - payload={"rangeValue": 0}, - instance="cover.tilt", - ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 0 - - call, msg = await assert_request_calls_service( - "Alexa.RangeController", - "SetRangeValue", - "cover#test_tilt_range", - "cover.open_cover_tilt", - hass, - payload={"rangeValue": 100}, - instance="cover.tilt", - ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 100 - - call, msg = await assert_request_calls_service( - "Alexa.RangeController", - "AdjustRangeValue", - "cover#test_tilt_range", - "cover.open_cover_tilt", - hass, - payload={"rangeValueDelta": 99, "rangeValueDeltaDefault": False}, - instance="cover.tilt", - ) - properties = msg["context"]["properties"][0] - assert properties["name"] == "rangeValue" - assert properties["namespace"] == "Alexa.RangeController" - assert properties["value"] == 100 - call, msg = await assert_request_calls_service( "Alexa.RangeController", "AdjustRangeValue", From 0683b8be21d5ab95ec2262b7dc7ef21a78fde418 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 10 Dec 2023 14:09:58 -0800 Subject: [PATCH 13/29] Fix fitbit oauth reauth debug logging (#105450) --- homeassistant/components/fitbit/application_credentials.py | 5 ++++- tests/components/fitbit/test_init.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fitbit/application_credentials.py b/homeassistant/components/fitbit/application_credentials.py index caf0384eca2..caa47351f45 100644 --- a/homeassistant/components/fitbit/application_credentials.py +++ b/homeassistant/components/fitbit/application_credentials.py @@ -60,7 +60,10 @@ class FitbitOAuth2Implementation(AuthImplementation): resp.raise_for_status() except aiohttp.ClientResponseError as err: if _LOGGER.isEnabledFor(logging.DEBUG): - error_body = await resp.text() if not session.closed else "" + try: + error_body = await resp.text() + except aiohttp.ClientError: + error_body = "" _LOGGER.debug( "Client response error status=%s, body=%s", err.status, error_body ) diff --git a/tests/components/fitbit/test_init.py b/tests/components/fitbit/test_init.py index b6bf75c1c69..3ed3695ff3d 100644 --- a/tests/components/fitbit/test_init.py +++ b/tests/components/fitbit/test_init.py @@ -107,18 +107,21 @@ async def test_token_refresh_success( @pytest.mark.parametrize("token_expiration_time", [12345]) +@pytest.mark.parametrize("closing", [True, False]) async def test_token_requires_reauth( hass: HomeAssistant, integration_setup: Callable[[], Awaitable[bool]], config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, setup_credentials: None, + closing: bool, ) -> None: """Test where token is expired and the refresh attempt requires reauth.""" aioclient_mock.post( OAUTH2_TOKEN, status=HTTPStatus.UNAUTHORIZED, + closing=closing, ) assert not await integration_setup() From 3b7add533b5e75895491fb8030d5f19eb24e3b2c Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:22:10 +0100 Subject: [PATCH 14/29] Write Enphase Envoy data to log when in debug mode (#105456) --- homeassistant/components/enphase_envoy/coordinator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index 75f2ef39289..02a9d2f2491 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -144,7 +144,10 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): if not self._setup_complete: await self._async_setup_and_authenticate() self._async_mark_setup_complete() - return (await envoy.update()).raw + # dump all received data in debug mode to assist troubleshooting + envoy_data = await envoy.update() + _LOGGER.debug("Envoy data: %s", envoy_data) + return envoy_data.raw except INVALID_AUTH_ERRORS as err: if self._setup_complete and tries == 0: # token likely expired or firmware changed, try to re-authenticate From 557ce965cf928fda32dfaf0b953d805ae8985293 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 10 Dec 2023 15:46:27 -0800 Subject: [PATCH 15/29] Bump ical to 6.1.1 (#105462) --- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index d7b16ee3bef..f5a24e07b0c 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==6.1.0"] + "requirements": ["ical==6.1.1"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 4c3a8e10a62..335a89eab3c 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==6.1.0"] + "requirements": ["ical==6.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index e0f764c4817..89ad5de43bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1054,7 +1054,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==6.1.0 +ical==6.1.1 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f3e4910c1a..b35c8b19882 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -832,7 +832,7 @@ ibeacon-ble==1.0.1 # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==6.1.0 +ical==6.1.1 # homeassistant.components.ping icmplib==3.0 From ba84af93743417033e0b688a46bbbd696beb0a3e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Dec 2023 20:47:53 -1000 Subject: [PATCH 16/29] Bump zeroconf to 0.128.4 (#105465) * Bump zeroconf to 0.128.3 significant bug fixes changelog: https://github.com/python-zeroconf/python-zeroconf/compare/0.128.0...0.128.3 * .4 --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 8351212f0b8..6738431b304 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.128.0"] + "requirements": ["zeroconf==0.128.4"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7116cdae98a..7d5f53f8f56 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -57,7 +57,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 yarl==1.9.2 -zeroconf==0.128.0 +zeroconf==0.128.4 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 89ad5de43bb..e4355ec9161 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2810,7 +2810,7 @@ zamg==0.3.3 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.128.0 +zeroconf==0.128.4 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b35c8b19882..0962ef50a52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2105,7 +2105,7 @@ yt-dlp==2023.11.16 zamg==0.3.3 # homeassistant.components.zeroconf -zeroconf==0.128.0 +zeroconf==0.128.4 # homeassistant.components.zeversolar zeversolar==0.3.1 From 8a0b1637b1a482b81e090bf657b1e42cdf6a1d75 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 11 Dec 2023 12:09:43 +0100 Subject: [PATCH 17/29] Remove Aftership import issue when entry already exists (#105476) --- .../components/aftership/config_flow.py | 23 +++---------------- .../components/aftership/strings.json | 4 ---- .../components/aftership/test_config_flow.py | 10 +++++--- 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/aftership/config_flow.py b/homeassistant/components/aftership/config_flow.py index 3da6ac9e3d5..94578091501 100644 --- a/homeassistant/components/aftership/config_flow.py +++ b/homeassistant/components/aftership/config_flow.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -51,25 +51,6 @@ class AfterShipConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import(self, config: dict[str, Any]) -> FlowResult: """Import configuration from yaml.""" - try: - self._async_abort_entries_match({CONF_API_KEY: config[CONF_API_KEY]}) - except AbortFlow as err: - async_create_issue( - self.hass, - DOMAIN, - "deprecated_yaml_import_issue_already_configured", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_import_issue_already_configured", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "AfterShip", - }, - ) - raise err - async_create_issue( self.hass, HOMEASSISTANT_DOMAIN, @@ -84,6 +65,8 @@ class AfterShipConfigFlow(ConfigFlow, domain=DOMAIN): "integration_title": "AfterShip", }, ) + + self._async_abort_entries_match({CONF_API_KEY: config[CONF_API_KEY]}) return self.async_create_entry( title=config.get(CONF_NAME, "AfterShip"), data={CONF_API_KEY: config[CONF_API_KEY]}, diff --git a/homeassistant/components/aftership/strings.json b/homeassistant/components/aftership/strings.json index b49c19976a6..ace8eb6d2d3 100644 --- a/homeassistant/components/aftership/strings.json +++ b/homeassistant/components/aftership/strings.json @@ -49,10 +49,6 @@ } }, "issues": { - "deprecated_yaml_import_issue_already_configured": { - "title": "The {integration_title} YAML configuration import failed", - "description": "Configuring {integration_title} using YAML is being removed but the YAML configuration was already imported.\n\nRemove the YAML configuration and restart Home Assistant." - }, "deprecated_yaml_import_issue_cannot_connect": { "title": "The {integration_title} YAML configuration import failed", "description": "Configuring {integration_title} using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." diff --git a/tests/components/aftership/test_config_flow.py b/tests/components/aftership/test_config_flow.py index 2ac5919a555..4668e7a61e4 100644 --- a/tests/components/aftership/test_config_flow.py +++ b/tests/components/aftership/test_config_flow.py @@ -77,7 +77,9 @@ async def test_flow_cannot_connect(hass: HomeAssistant, mock_setup_entry) -> Non } -async def test_import_flow(hass: HomeAssistant, mock_setup_entry) -> None: +async def test_import_flow( + hass: HomeAssistant, issue_registry: ir.IssueRegistry, mock_setup_entry +) -> None: """Test importing yaml config.""" with patch( @@ -95,11 +97,12 @@ async def test_import_flow(hass: HomeAssistant, mock_setup_entry) -> None: assert result["data"] == { CONF_API_KEY: "yaml-api-key", } - issue_registry = ir.async_get(hass) assert len(issue_registry.issues) == 1 -async def test_import_flow_already_exists(hass: HomeAssistant) -> None: +async def test_import_flow_already_exists( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: """Test importing yaml config where entry already exists.""" entry = MockConfigEntry(domain=DOMAIN, data={CONF_API_KEY: "yaml-api-key"}) entry.add_to_hass(hass) @@ -108,3 +111,4 @@ async def test_import_flow_already_exists(hass: HomeAssistant) -> None: ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" + assert len(issue_registry.issues) == 1 From 48498844678126c1c1472758155170a5cf919ca6 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 11 Dec 2023 10:18:46 -0600 Subject: [PATCH 18/29] Disconnect before reconnecting to satellite (#105500) Disconnect before reconnecting --- homeassistant/components/wyoming/satellite.py | 26 ++++++++++++++++--- tests/components/wyoming/test_satellite.py | 23 ++++++++++++---- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/wyoming/satellite.py b/homeassistant/components/wyoming/satellite.py index 0e8e5d62f4b..45d882dd1e2 100644 --- a/homeassistant/components/wyoming/satellite.py +++ b/homeassistant/components/wyoming/satellite.py @@ -70,11 +70,11 @@ class WyomingSatellite: while self.is_running: try: # Check if satellite has been disabled - if not self.device.is_enabled: + while not self.device.is_enabled: await self.on_disabled() if not self.is_running: # Satellite was stopped while waiting to be enabled - break + return # Connect and run pipeline loop await self._run_once() @@ -86,7 +86,7 @@ class WyomingSatellite: # Ensure sensor is off self.device.set_is_active(False) - await self.on_stopped() + await self.on_stopped() def stop(self) -> None: """Signal satellite task to stop running.""" @@ -129,6 +129,7 @@ class WyomingSatellite: self._audio_queue.put_nowait(None) self._enabled_changed_event.set() + self._enabled_changed_event.clear() def _pipeline_changed(self) -> None: """Run when device pipeline changes.""" @@ -243,9 +244,17 @@ class WyomingSatellite: chunk = AudioChunk.from_event(client_event) chunk = self._chunk_converter.convert(chunk) self._audio_queue.put_nowait(chunk.audio) + elif AudioStop.is_type(client_event.type): + # Stop pipeline + _LOGGER.debug("Client requested pipeline to stop") + self._audio_queue.put_nowait(b"") + break else: _LOGGER.debug("Unexpected event from satellite: %s", client_event) + # Ensure task finishes + await _pipeline_task + _LOGGER.debug("Pipeline finished") def _event_callback(self, event: assist_pipeline.PipelineEvent) -> None: @@ -336,12 +345,23 @@ class WyomingSatellite: async def _connect(self) -> None: """Connect to satellite over TCP.""" + await self._disconnect() + _LOGGER.debug( "Connecting to satellite at %s:%s", self.service.host, self.service.port ) self._client = AsyncTcpClient(self.service.host, self.service.port) await self._client.connect() + async def _disconnect(self) -> None: + """Disconnect if satellite is currently connected.""" + if self._client is None: + return + + _LOGGER.debug("Disconnecting from satellite") + await self._client.disconnect() + self._client = None + async def _stream_tts(self, media_id: str) -> None: """Stream TTS WAV audio to satellite in chunks.""" assert self._client is not None diff --git a/tests/components/wyoming/test_satellite.py b/tests/components/wyoming/test_satellite.py index 50252007aa5..83e4d98d971 100644 --- a/tests/components/wyoming/test_satellite.py +++ b/tests/components/wyoming/test_satellite.py @@ -322,11 +322,12 @@ async def test_satellite_disabled(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry, service: WyomingService ): satellite = original_make_satellite(hass, config_entry, service) - satellite.device.is_enabled = False + satellite.device.set_is_enabled(False) return satellite async def on_disabled(self): + self.device.set_is_enabled(True) on_disabled_event.set() with patch( @@ -368,11 +369,19 @@ async def test_satellite_restart(hass: HomeAssistant) -> None: async def test_satellite_reconnect(hass: HomeAssistant) -> None: """Test satellite reconnect call after connection refused.""" - on_reconnect_event = asyncio.Event() + num_reconnects = 0 + reconnect_event = asyncio.Event() + stopped_event = asyncio.Event() async def on_reconnect(self): - self.stop() - on_reconnect_event.set() + nonlocal num_reconnects + num_reconnects += 1 + if num_reconnects >= 2: + reconnect_event.set() + self.stop() + + async def on_stopped(self): + stopped_event.set() with patch( "homeassistant.components.wyoming.data.load_wyoming_info", @@ -383,10 +392,14 @@ async def test_satellite_reconnect(hass: HomeAssistant) -> None: ), patch( "homeassistant.components.wyoming.satellite.WyomingSatellite.on_reconnect", on_reconnect, + ), patch( + "homeassistant.components.wyoming.satellite.WyomingSatellite.on_stopped", + on_stopped, ): await setup_config_entry(hass) async with asyncio.timeout(1): - await on_reconnect_event.wait() + await reconnect_event.wait() + await stopped_event.wait() async def test_satellite_disconnect_before_pipeline(hass: HomeAssistant) -> None: From 9d0fed85394c8628ae114bb39f06f3af2bf82bc6 Mon Sep 17 00:00:00 2001 From: "Julien \"_FrnchFrgg_\" Rivaud" Date: Tue, 12 Dec 2023 04:42:52 +0100 Subject: [PATCH 19/29] Bump caldav to 1.3.8 (#105508) * Bump caldav to 1.3.8 1.3.8 fixes a bug where duplicate STATUS properties would be emitted for a single VTODO depending on the case of the arguments used. That bug meant that even though that is the intended API usage, passing lowercase for the status argument name would be rejected by caldav servers checking conformance with the spec which forbids duplicate STATUS. This in turn prevented HomeAssistant to add new items to a caldav todo list. Bump the requirements to 1.3.8 to repair that feature * Update global requirements --- homeassistant/components/caldav/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index a7365515758..619523ae7a1 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/caldav", "iot_class": "cloud_polling", "loggers": ["caldav", "vobject"], - "requirements": ["caldav==1.3.6"] + "requirements": ["caldav==1.3.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index e4355ec9161..cb9e04ea638 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -604,7 +604,7 @@ btsmarthub-devicelist==0.2.3 buienradar==1.0.5 # homeassistant.components.caldav -caldav==1.3.6 +caldav==1.3.8 # homeassistant.components.circuit circuit-webhook==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0962ef50a52..f6817910405 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -503,7 +503,7 @@ bthome-ble==3.2.0 buienradar==1.0.5 # homeassistant.components.caldav -caldav==1.3.6 +caldav==1.3.8 # homeassistant.components.coinbase coinbase==2.1.0 From fbb315b0a89583899199618ed33994775fd282c2 Mon Sep 17 00:00:00 2001 From: Khole <29937485+KJonline@users.noreply.github.com> Date: Tue, 12 Dec 2023 07:38:12 +0000 Subject: [PATCH 20/29] Bump pyhiveapi to v0.5.16 (#105513) Co-authored-by: Khole Jones --- homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 67da3617b44..870223f8fe6 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -9,5 +9,5 @@ }, "iot_class": "cloud_polling", "loggers": ["apyhiveapi"], - "requirements": ["pyhiveapi==0.5.14"] + "requirements": ["pyhiveapi==0.5.16"] } diff --git a/requirements_all.txt b/requirements_all.txt index cb9e04ea638..3ec2df6ae86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1775,7 +1775,7 @@ pyhaversion==22.8.0 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.14 +pyhiveapi==0.5.16 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6817910405..1610539940d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1340,7 +1340,7 @@ pyhaversion==22.8.0 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.14 +pyhiveapi==0.5.16 # homeassistant.components.homematic pyhomematic==0.1.77 From 8b3ba452bc9e073143fdb1fd474d1fe28ee5f798 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 12 Dec 2023 20:43:09 +0100 Subject: [PATCH 21/29] Fix setup Fast.com (#105580) * Fix setup fastdotcom * Add if --- homeassistant/components/fastdotcom/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index 2fe5b3ccafc..56f9ba4fd5f 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -35,15 +35,16 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup_platform(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Fast.com component. (deprecated).""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config[DOMAIN], + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config[DOMAIN], + ) ) - ) return True From 8812ca13d9af64bcdc13bdede64d24c378cb9707 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 12 Dec 2023 21:54:15 -0600 Subject: [PATCH 22/29] Add name slot to HassClimateGetTemperature intent (#105585) --- homeassistant/components/climate/intent.py | 16 +++++++++++++++- tests/components/climate/test_intent.py | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/intent.py b/homeassistant/components/climate/intent.py index 23cc3d5bcd2..4152fb5ee2d 100644 --- a/homeassistant/components/climate/intent.py +++ b/homeassistant/components/climate/intent.py @@ -21,7 +21,7 @@ class GetTemperatureIntent(intent.IntentHandler): """Handle GetTemperature intents.""" intent_type = INTENT_GET_TEMPERATURE - slot_schema = {vol.Optional("area"): str} + slot_schema = {vol.Optional("area"): str, vol.Optional("name"): str} async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the intent.""" @@ -49,6 +49,20 @@ class GetTemperatureIntent(intent.IntentHandler): if climate_state is None: raise intent.IntentHandleError(f"No climate entity in area {area_name}") + climate_entity = component.get_entity(climate_state.entity_id) + elif "name" in slots: + # Filter by name + entity_name = slots["name"]["value"] + + for maybe_climate in intent.async_match_states( + hass, name=entity_name, domains=[DOMAIN] + ): + climate_state = maybe_climate + break + + if climate_state is None: + raise intent.IntentHandleError(f"No climate entity named {entity_name}") + climate_entity = component.get_entity(climate_state.entity_id) else: # First entity diff --git a/tests/components/climate/test_intent.py b/tests/components/climate/test_intent.py index eaf7029d303..6473eca1b88 100644 --- a/tests/components/climate/test_intent.py +++ b/tests/components/climate/test_intent.py @@ -153,7 +153,7 @@ async def test_get_temperature( state = response.matched_states[0] assert state.attributes["current_temperature"] == 10.0 - # Select by area instead (climate_2) + # Select by area (climate_2) response = await intent.async_handle( hass, "test", @@ -166,6 +166,19 @@ async def test_get_temperature( state = response.matched_states[0] assert state.attributes["current_temperature"] == 22.0 + # Select by name (climate_2) + response = await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"name": {"value": "Climate 2"}}, + ) + assert response.response_type == intent.IntentResponseType.QUERY_ANSWER + assert len(response.matched_states) == 1 + assert response.matched_states[0].entity_id == climate_2.entity_id + state = response.matched_states[0] + assert state.attributes["current_temperature"] == 22.0 + async def test_get_temperature_no_entities( hass: HomeAssistant, From 797af140120ba25ee76b96594cf39dd63f16833b Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 12 Dec 2023 23:21:16 -0600 Subject: [PATCH 23/29] Skip TTS events entirely with empty text (#105617) --- .../components/assist_pipeline/pipeline.py | 60 ++++++++++--------- .../snapshots/test_websocket.ambr | 28 +++++++-- .../assist_pipeline/test_websocket.py | 11 ++-- 3 files changed, 59 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index ed9029d1c2c..26d599da836 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -369,6 +369,7 @@ class PipelineStage(StrEnum): STT = "stt" INTENT = "intent" TTS = "tts" + END = "end" PIPELINE_STAGE_ORDER = [ @@ -1024,35 +1025,32 @@ class PipelineRun: ) ) - if tts_input := tts_input.strip(): - try: - # Synthesize audio and get URL - tts_media_id = tts_generate_media_source_id( - self.hass, - tts_input, - engine=self.tts_engine, - language=self.pipeline.tts_language, - options=self.tts_options, - ) - tts_media = await media_source.async_resolve_media( - self.hass, - tts_media_id, - None, - ) - except Exception as src_error: - _LOGGER.exception("Unexpected error during text-to-speech") - raise TextToSpeechError( - code="tts-failed", - message="Unexpected error during text-to-speech", - ) from src_error + try: + # Synthesize audio and get URL + tts_media_id = tts_generate_media_source_id( + self.hass, + tts_input, + engine=self.tts_engine, + language=self.pipeline.tts_language, + options=self.tts_options, + ) + tts_media = await media_source.async_resolve_media( + self.hass, + tts_media_id, + None, + ) + except Exception as src_error: + _LOGGER.exception("Unexpected error during text-to-speech") + raise TextToSpeechError( + code="tts-failed", + message="Unexpected error during text-to-speech", + ) from src_error - _LOGGER.debug("TTS result %s", tts_media) - tts_output = { - "media_id": tts_media_id, - **asdict(tts_media), - } - else: - tts_output = {} + _LOGGER.debug("TTS result %s", tts_media) + tts_output = { + "media_id": tts_media_id, + **asdict(tts_media), + } self.process_event( PipelineEvent(PipelineEventType.TTS_END, {"tts_output": tts_output}) @@ -1345,7 +1343,11 @@ class PipelineInput: self.conversation_id, self.device_id, ) - current_stage = PipelineStage.TTS + if tts_input.strip(): + current_stage = PipelineStage.TTS + else: + # Skip TTS + current_stage = PipelineStage.END if self.run.end_stage != PipelineStage.INTENT: # text-to-speech diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 072b1ff730a..c165675a6ff 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -662,15 +662,33 @@ # --- # name: test_pipeline_empty_tts_output.1 dict({ - 'engine': 'test', - 'language': 'en-US', - 'tts_input': '', - 'voice': 'james_earl_jones', + 'conversation_id': None, + 'device_id': None, + 'engine': 'homeassistant', + 'intent_input': 'never mind', + 'language': 'en', }) # --- # name: test_pipeline_empty_tts_output.2 dict({ - 'tts_output': dict({ + 'intent_output': dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + }), + }), }), }) # --- diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index 0e2a3ad538c..458320a9a90 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -2467,10 +2467,10 @@ async def test_pipeline_empty_tts_output( await client.send_json_auto_id( { "type": "assist_pipeline/run", - "start_stage": "tts", + "start_stage": "intent", "end_stage": "tts", "input": { - "text": "", + "text": "never mind", }, } ) @@ -2486,16 +2486,15 @@ async def test_pipeline_empty_tts_output( assert msg["event"]["data"] == snapshot events.append(msg["event"]) - # text-to-speech + # intent msg = await client.receive_json() - assert msg["event"]["type"] == "tts-start" + assert msg["event"]["type"] == "intent-start" assert msg["event"]["data"] == snapshot events.append(msg["event"]) msg = await client.receive_json() - assert msg["event"]["type"] == "tts-end" + assert msg["event"]["type"] == "intent-end" assert msg["event"]["data"] == snapshot - assert not msg["event"]["data"]["tts_output"] events.append(msg["event"]) # run end From 36c56eb32abc856c1e4a53ca2b0a48b8e120b0f5 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 13 Dec 2023 02:09:22 -0600 Subject: [PATCH 24/29] Rename "satellite enabled" to "mute" (#105619) --- homeassistant/components/wyoming/devices.py | 28 ++++++------ homeassistant/components/wyoming/satellite.py | 44 +++++++++---------- homeassistant/components/wyoming/strings.json | 4 +- homeassistant/components/wyoming/switch.py | 18 ++++---- tests/components/wyoming/test_devices.py | 20 ++++----- tests/components/wyoming/test_satellite.py | 26 +++++------ tests/components/wyoming/test_switch.py | 24 +++++----- 7 files changed, 81 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/wyoming/devices.py b/homeassistant/components/wyoming/devices.py index 90dad889707..47a5cdc7eb8 100644 --- a/homeassistant/components/wyoming/devices.py +++ b/homeassistant/components/wyoming/devices.py @@ -17,11 +17,11 @@ class SatelliteDevice: satellite_id: str device_id: str is_active: bool = False - is_enabled: bool = True + is_muted: bool = False pipeline_name: str | None = None _is_active_listener: Callable[[], None] | None = None - _is_enabled_listener: Callable[[], None] | None = None + _is_muted_listener: Callable[[], None] | None = None _pipeline_listener: Callable[[], None] | None = None @callback @@ -33,12 +33,12 @@ class SatelliteDevice: self._is_active_listener() @callback - def set_is_enabled(self, enabled: bool) -> None: - """Set enabled state.""" - if enabled != self.is_enabled: - self.is_enabled = enabled - if self._is_enabled_listener is not None: - self._is_enabled_listener() + def set_is_muted(self, muted: bool) -> None: + """Set muted state.""" + if muted != self.is_muted: + self.is_muted = muted + if self._is_muted_listener is not None: + self._is_muted_listener() @callback def set_pipeline_name(self, pipeline_name: str) -> None: @@ -54,9 +54,9 @@ class SatelliteDevice: self._is_active_listener = is_active_listener @callback - def set_is_enabled_listener(self, is_enabled_listener: Callable[[], None]) -> None: - """Listen for updates to is_enabled.""" - self._is_enabled_listener = is_enabled_listener + def set_is_muted_listener(self, is_muted_listener: Callable[[], None]) -> None: + """Listen for updates to muted status.""" + self._is_muted_listener = is_muted_listener @callback def set_pipeline_listener(self, pipeline_listener: Callable[[], None]) -> None: @@ -70,11 +70,11 @@ class SatelliteDevice: "binary_sensor", DOMAIN, f"{self.satellite_id}-assist_in_progress" ) - def get_satellite_enabled_entity_id(self, hass: HomeAssistant) -> str | None: - """Return entity id for satellite enabled switch.""" + def get_muted_entity_id(self, hass: HomeAssistant) -> str | None: + """Return entity id for satellite muted switch.""" ent_reg = er.async_get(hass) return ent_reg.async_get_entity_id( - "switch", DOMAIN, f"{self.satellite_id}-satellite_enabled" + "switch", DOMAIN, f"{self.satellite_id}-mute" ) def get_pipeline_entity_id(self, hass: HomeAssistant) -> str | None: diff --git a/homeassistant/components/wyoming/satellite.py b/homeassistant/components/wyoming/satellite.py index 45d882dd1e2..16240cb625b 100644 --- a/homeassistant/components/wyoming/satellite.py +++ b/homeassistant/components/wyoming/satellite.py @@ -49,7 +49,6 @@ class WyomingSatellite: self.hass = hass self.service = service self.device = device - self.is_enabled = True self.is_running = True self._client: AsyncTcpClient | None = None @@ -57,9 +56,9 @@ class WyomingSatellite: self._is_pipeline_running = False self._audio_queue: asyncio.Queue[bytes | None] = asyncio.Queue() self._pipeline_id: str | None = None - self._enabled_changed_event = asyncio.Event() + self._muted_changed_event = asyncio.Event() - self.device.set_is_enabled_listener(self._enabled_changed) + self.device.set_is_muted_listener(self._muted_changed) self.device.set_pipeline_listener(self._pipeline_changed) async def run(self) -> None: @@ -69,11 +68,11 @@ class WyomingSatellite: try: while self.is_running: try: - # Check if satellite has been disabled - while not self.device.is_enabled: - await self.on_disabled() + # Check if satellite has been muted + while self.device.is_muted: + await self.on_muted() if not self.is_running: - # Satellite was stopped while waiting to be enabled + # Satellite was stopped while waiting to be unmuted return # Connect and run pipeline loop @@ -92,8 +91,8 @@ class WyomingSatellite: """Signal satellite task to stop running.""" self.is_running = False - # Unblock waiting for enabled - self._enabled_changed_event.set() + # Unblock waiting for unmuted + self._muted_changed_event.set() async def on_restart(self) -> None: """Block until pipeline loop will be restarted.""" @@ -111,9 +110,9 @@ class WyomingSatellite: ) await asyncio.sleep(_RECONNECT_SECONDS) - async def on_disabled(self) -> None: - """Block until device may be enabled again.""" - await self._enabled_changed_event.wait() + async def on_muted(self) -> None: + """Block until device may be unmated again.""" + await self._muted_changed_event.wait() async def on_stopped(self) -> None: """Run when run() has fully stopped.""" @@ -121,15 +120,14 @@ class WyomingSatellite: # ------------------------------------------------------------------------- - def _enabled_changed(self) -> None: - """Run when device enabled status changes.""" - - if not self.device.is_enabled: + def _muted_changed(self) -> None: + """Run when device muted status changes.""" + if self.device.is_muted: # Cancel any running pipeline self._audio_queue.put_nowait(None) - self._enabled_changed_event.set() - self._enabled_changed_event.clear() + self._muted_changed_event.set() + self._muted_changed_event.clear() def _pipeline_changed(self) -> None: """Run when device pipeline changes.""" @@ -141,7 +139,7 @@ class WyomingSatellite: """Run pipelines until an error occurs.""" self.device.set_is_active(False) - while self.is_running and self.is_enabled: + while self.is_running and (not self.device.is_muted): try: await self._connect() break @@ -151,7 +149,7 @@ class WyomingSatellite: assert self._client is not None _LOGGER.debug("Connected to satellite") - if (not self.is_running) or (not self.is_enabled): + if (not self.is_running) or self.device.is_muted: # Run was cancelled or satellite was disabled during connection return @@ -160,7 +158,7 @@ class WyomingSatellite: # Wait until we get RunPipeline event run_pipeline: RunPipeline | None = None - while self.is_running and self.is_enabled: + while self.is_running and (not self.device.is_muted): run_event = await self._client.read_event() if run_event is None: raise ConnectionResetError("Satellite disconnected") @@ -174,7 +172,7 @@ class WyomingSatellite: assert run_pipeline is not None _LOGGER.debug("Received run information: %s", run_pipeline) - if (not self.is_running) or (not self.is_enabled): + if (not self.is_running) or self.device.is_muted: # Run was cancelled or satellite was disabled while waiting for # RunPipeline event. return @@ -189,7 +187,7 @@ class WyomingSatellite: raise ValueError(f"Invalid end stage: {end_stage}") # Each loop is a pipeline run - while self.is_running and self.is_enabled: + while self.is_running and (not self.device.is_muted): # Use select to get pipeline each time in case it's changed pipeline_id = pipeline_select.get_chosen_pipeline( self.hass, diff --git a/homeassistant/components/wyoming/strings.json b/homeassistant/components/wyoming/strings.json index 19b6a513d4b..c7ae63e7b95 100644 --- a/homeassistant/components/wyoming/strings.json +++ b/homeassistant/components/wyoming/strings.json @@ -42,8 +42,8 @@ } }, "switch": { - "satellite_enabled": { - "name": "Satellite enabled" + "mute": { + "name": "Mute" } } } diff --git a/homeassistant/components/wyoming/switch.py b/homeassistant/components/wyoming/switch.py index 2bc43122588..7366a52efab 100644 --- a/homeassistant/components/wyoming/switch.py +++ b/homeassistant/components/wyoming/switch.py @@ -29,17 +29,17 @@ async def async_setup_entry( # Setup is only forwarded for satellites assert item.satellite is not None - async_add_entities([WyomingSatelliteEnabledSwitch(item.satellite.device)]) + async_add_entities([WyomingSatelliteMuteSwitch(item.satellite.device)]) -class WyomingSatelliteEnabledSwitch( +class WyomingSatelliteMuteSwitch( WyomingSatelliteEntity, restore_state.RestoreEntity, SwitchEntity ): - """Entity to represent if satellite is enabled.""" + """Entity to represent if satellite is muted.""" entity_description = SwitchEntityDescription( - key="satellite_enabled", - translation_key="satellite_enabled", + key="mute", + translation_key="mute", entity_category=EntityCategory.CONFIG, ) @@ -49,17 +49,17 @@ class WyomingSatelliteEnabledSwitch( state = await self.async_get_last_state() - # Default to on - self._attr_is_on = (state is None) or (state.state == STATE_ON) + # Default to off + self._attr_is_on = (state is not None) and (state.state == STATE_ON) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on.""" self._attr_is_on = True self.async_write_ha_state() - self._device.set_is_enabled(True) + self._device.set_is_muted(True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off.""" self._attr_is_on = False self.async_write_ha_state() - self._device.set_is_enabled(False) + self._device.set_is_muted(False) diff --git a/tests/components/wyoming/test_devices.py b/tests/components/wyoming/test_devices.py index 549f76f20f1..0273a7da275 100644 --- a/tests/components/wyoming/test_devices.py +++ b/tests/components/wyoming/test_devices.py @@ -5,7 +5,7 @@ from homeassistant.components.assist_pipeline.select import OPTION_PREFERRED from homeassistant.components.wyoming import DOMAIN from homeassistant.components.wyoming.devices import SatelliteDevice from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -34,11 +34,11 @@ async def test_device_registry_info( assert assist_in_progress_state is not None assert assist_in_progress_state.state == STATE_OFF - satellite_enabled_id = satellite_device.get_satellite_enabled_entity_id(hass) - assert satellite_enabled_id - satellite_enabled_state = hass.states.get(satellite_enabled_id) - assert satellite_enabled_state is not None - assert satellite_enabled_state.state == STATE_ON + muted_id = satellite_device.get_muted_entity_id(hass) + assert muted_id + muted_state = hass.states.get(muted_id) + assert muted_state is not None + assert muted_state.state == STATE_OFF pipeline_entity_id = satellite_device.get_pipeline_entity_id(hass) assert pipeline_entity_id @@ -59,9 +59,9 @@ async def test_remove_device_registry_entry( assert assist_in_progress_id assert hass.states.get(assist_in_progress_id) is not None - satellite_enabled_id = satellite_device.get_satellite_enabled_entity_id(hass) - assert satellite_enabled_id - assert hass.states.get(satellite_enabled_id) is not None + muted_id = satellite_device.get_muted_entity_id(hass) + assert muted_id + assert hass.states.get(muted_id) is not None pipeline_entity_id = satellite_device.get_pipeline_entity_id(hass) assert pipeline_entity_id @@ -74,5 +74,5 @@ async def test_remove_device_registry_entry( # Everything should be gone assert hass.states.get(assist_in_progress_id) is None - assert hass.states.get(satellite_enabled_id) is None + assert hass.states.get(muted_id) is None assert hass.states.get(pipeline_entity_id) is None diff --git a/tests/components/wyoming/test_satellite.py b/tests/components/wyoming/test_satellite.py index 83e4d98d971..07a6aa8925e 100644 --- a/tests/components/wyoming/test_satellite.py +++ b/tests/components/wyoming/test_satellite.py @@ -196,7 +196,7 @@ async def test_satellite_pipeline(hass: HomeAssistant) -> None: await mock_client.detect_event.wait() assert not device.is_active - assert device.is_enabled + assert not device.is_muted # Wake word is detected event_callback( @@ -312,36 +312,36 @@ async def test_satellite_pipeline(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def test_satellite_disabled(hass: HomeAssistant) -> None: - """Test callback for a satellite that has been disabled.""" - on_disabled_event = asyncio.Event() +async def test_satellite_muted(hass: HomeAssistant) -> None: + """Test callback for a satellite that has been muted.""" + on_muted_event = asyncio.Event() original_make_satellite = wyoming._make_satellite - def make_disabled_satellite( + def make_muted_satellite( hass: HomeAssistant, config_entry: ConfigEntry, service: WyomingService ): satellite = original_make_satellite(hass, config_entry, service) - satellite.device.set_is_enabled(False) + satellite.device.set_is_muted(True) return satellite - async def on_disabled(self): - self.device.set_is_enabled(True) - on_disabled_event.set() + async def on_muted(self): + self.device.set_is_muted(False) + on_muted_event.set() with patch( "homeassistant.components.wyoming.data.load_wyoming_info", return_value=SATELLITE_INFO, ), patch( - "homeassistant.components.wyoming._make_satellite", make_disabled_satellite + "homeassistant.components.wyoming._make_satellite", make_muted_satellite ), patch( - "homeassistant.components.wyoming.satellite.WyomingSatellite.on_disabled", - on_disabled, + "homeassistant.components.wyoming.satellite.WyomingSatellite.on_muted", + on_muted, ): await setup_config_entry(hass) async with asyncio.timeout(1): - await on_disabled_event.wait() + await on_muted_event.wait() async def test_satellite_restart(hass: HomeAssistant) -> None: diff --git a/tests/components/wyoming/test_switch.py b/tests/components/wyoming/test_switch.py index 0b05724d761..fc5a8689ceb 100644 --- a/tests/components/wyoming/test_switch.py +++ b/tests/components/wyoming/test_switch.py @@ -5,28 +5,28 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -async def test_satellite_enabled( +async def test_muted( hass: HomeAssistant, satellite_config_entry: ConfigEntry, satellite_device: SatelliteDevice, ) -> None: - """Test satellite enabled.""" - satellite_enabled_id = satellite_device.get_satellite_enabled_entity_id(hass) - assert satellite_enabled_id + """Test satellite muted.""" + muted_id = satellite_device.get_muted_entity_id(hass) + assert muted_id - state = hass.states.get(satellite_enabled_id) + state = hass.states.get(muted_id) assert state is not None - assert state.state == STATE_ON - assert satellite_device.is_enabled + assert state.state == STATE_OFF + assert not satellite_device.is_muted await hass.services.async_call( "switch", - "turn_off", - {"entity_id": satellite_enabled_id}, + "turn_on", + {"entity_id": muted_id}, blocking=True, ) - state = hass.states.get(satellite_enabled_id) + state = hass.states.get(muted_id) assert state is not None - assert state.state == STATE_OFF - assert not satellite_device.is_enabled + assert state.state == STATE_ON + assert satellite_device.is_muted From 8744b054687cad23eb25db79d681f373d1da979b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 13 Dec 2023 14:53:10 +0100 Subject: [PATCH 25/29] Bump version to 2023.12.2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index df965001035..fcebd21eafd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 112a03f5e5b..7b06c7a6506 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.12.1" +version = "2023.12.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 896ca8ce83ff2c945b62e424028beb81e64b7c1d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 13 Dec 2023 16:48:46 +0100 Subject: [PATCH 26/29] Fix timing issue in Withings (#105203) --- homeassistant/components/withings/__init__.py | 85 +++++++++++-------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 701f7f444cf..f42fb7a57b9 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -192,52 +192,67 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = withings_data + register_lock = asyncio.Lock() + webhooks_registered = False + async def unregister_webhook( _: Any, ) -> None: - LOGGER.debug("Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID]) - webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) - await async_unsubscribe_webhooks(client) - for coordinator in withings_data.coordinators: - coordinator.webhook_subscription_listener(False) + nonlocal webhooks_registered + async with register_lock: + LOGGER.debug( + "Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID] + ) + webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) + await async_unsubscribe_webhooks(client) + for coordinator in withings_data.coordinators: + coordinator.webhook_subscription_listener(False) + webhooks_registered = False async def register_webhook( _: Any, ) -> None: - if cloud.async_active_subscription(hass): - webhook_url = await _async_cloudhook_generate_url(hass, entry) - else: - webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID]) - url = URL(webhook_url) - if url.scheme != "https" or url.port != 443: - LOGGER.warning( - "Webhook not registered - " - "https and port 443 is required to register the webhook" + nonlocal webhooks_registered + async with register_lock: + if webhooks_registered: + return + if cloud.async_active_subscription(hass): + webhook_url = await _async_cloudhook_generate_url(hass, entry) + else: + webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID]) + url = URL(webhook_url) + if url.scheme != "https" or url.port != 443: + LOGGER.warning( + "Webhook not registered - " + "https and port 443 is required to register the webhook" + ) + return + + webhook_name = "Withings" + if entry.title != DEFAULT_TITLE: + webhook_name = f"{DEFAULT_TITLE} {entry.title}" + + webhook_register( + hass, + DOMAIN, + webhook_name, + entry.data[CONF_WEBHOOK_ID], + get_webhook_handler(withings_data), + allowed_methods=[METH_POST], ) - return + LOGGER.debug("Registered Withings webhook at hass: %s", webhook_url) - webhook_name = "Withings" - if entry.title != DEFAULT_TITLE: - webhook_name = f"{DEFAULT_TITLE} {entry.title}" - - webhook_register( - hass, - DOMAIN, - webhook_name, - entry.data[CONF_WEBHOOK_ID], - get_webhook_handler(withings_data), - allowed_methods=[METH_POST], - ) - - await async_subscribe_webhooks(client, webhook_url) - for coordinator in withings_data.coordinators: - coordinator.webhook_subscription_listener(True) - LOGGER.debug("Register Withings webhook: %s", webhook_url) - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) - ) + await async_subscribe_webhooks(client, webhook_url) + for coordinator in withings_data.coordinators: + coordinator.webhook_subscription_listener(True) + LOGGER.debug("Registered Withings webhook at Withings: %s", webhook_url) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) + ) + webhooks_registered = True async def manage_cloudhook(state: cloud.CloudConnectionState) -> None: + LOGGER.debug("Cloudconnection state changed to %s", state) if state is cloud.CloudConnectionState.CLOUD_CONNECTED: await register_webhook(None) From 75bdd84c80cf7c4dca2e083fcaf0b1afe81b4341 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 11 Dec 2023 17:04:07 +0100 Subject: [PATCH 27/29] Update pylint to 3.0.3 (#105491) --- homeassistant/components/improv_ble/config_flow.py | 2 +- homeassistant/components/mqtt/__init__.py | 1 - homeassistant/components/zha/__init__.py | 2 +- requirements_test.txt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/improv_ble/config_flow.py b/homeassistant/components/improv_ble/config_flow.py index bfc86ac0162..762f37ef5d4 100644 --- a/homeassistant/components/improv_ble/config_flow.py +++ b/homeassistant/components/improv_ble/config_flow.py @@ -405,7 +405,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): raise AbortFlow("characteristic_missing") from err except improv_ble_errors.CommandFailed: raise - except Exception as err: # pylint: disable=broad-except + except Exception as err: _LOGGER.exception("Unexpected exception") raise AbortFlow("unknown") from err diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 16f584db011..593d5bbd202 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -247,7 +247,6 @@ async def async_check_config_schema( schema(config) except vol.Invalid as exc: integration = await async_get_integration(hass, DOMAIN) - # pylint: disable-next=protected-access message = conf_util.format_schema_error( hass, exc, domain, config, integration.documentation ) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 2046070d6a5..340e0db40a6 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -182,7 +182,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) from exc except TransientConnectionError as exc: raise ConfigEntryNotReady from exc - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: _LOGGER.debug( "Couldn't start coordinator (attempt %s of %s)", attempt + 1, diff --git a/requirements_test.txt b/requirements_test.txt index d880fecaca5..ee45a757669 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,7 +14,7 @@ mock-open==1.4.0 mypy==1.7.1 pre-commit==3.5.0 pydantic==1.10.12 -pylint==3.0.2 +pylint==3.0.3 pylint-per-file-ignores==1.2.1 pipdeptree==2.11.0 pytest-asyncio==0.21.0 From db52b9b0e1d16fbf7087c869f589415735849017 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:24:26 -0500 Subject: [PATCH 28/29] Reload ZHA integration on any error, not just recoverable ones (#105659) --- homeassistant/components/zha/__init__.py | 75 +++++++++------------- homeassistant/components/zha/core/const.py | 3 - tests/components/zha/conftest.py | 2 +- tests/components/zha/test_repairs.py | 4 +- 4 files changed, 34 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 340e0db40a6..1eb3369c1be 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -37,8 +37,6 @@ from .core.const import ( DOMAIN, PLATFORMS, SIGNAL_ADD_ENTITIES, - STARTUP_FAILURE_DELAY_S, - STARTUP_RETRIES, RadioType, ) from .core.device import get_device_automation_triggers @@ -161,49 +159,40 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b _LOGGER.debug("Trigger cache: %s", zha_data.device_trigger_cache) - # Retry setup a few times before giving up to deal with missing serial ports in VMs - for attempt in range(STARTUP_RETRIES): - try: - zha_gateway = await ZHAGateway.async_from_config( - hass=hass, - config=zha_data.yaml_config, - config_entry=config_entry, - ) - break - except NetworkSettingsInconsistent as exc: - await warn_on_inconsistent_network_settings( - hass, - config_entry=config_entry, - old_state=exc.old_state, - new_state=exc.new_state, - ) - raise ConfigEntryError( - "Network settings do not match most recent backup" - ) from exc - except TransientConnectionError as exc: - raise ConfigEntryNotReady from exc - except Exception as exc: - _LOGGER.debug( - "Couldn't start coordinator (attempt %s of %s)", - attempt + 1, - STARTUP_RETRIES, - exc_info=exc, - ) + try: + zha_gateway = await ZHAGateway.async_from_config( + hass=hass, + config=zha_data.yaml_config, + config_entry=config_entry, + ) + except NetworkSettingsInconsistent as exc: + await warn_on_inconsistent_network_settings( + hass, + config_entry=config_entry, + old_state=exc.old_state, + new_state=exc.new_state, + ) + raise ConfigEntryError( + "Network settings do not match most recent backup" + ) from exc + except TransientConnectionError as exc: + raise ConfigEntryNotReady from exc + except Exception as exc: + _LOGGER.debug("Failed to set up ZHA", exc_info=exc) + device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] - if attempt < STARTUP_RETRIES - 1: - await asyncio.sleep(STARTUP_FAILURE_DELAY_S) - continue + if ( + not device_path.startswith("socket://") + and RadioType[config_entry.data[CONF_RADIO_TYPE]] == RadioType.ezsp + ): + try: + # Ignore all exceptions during probing, they shouldn't halt setup + if await warn_on_wrong_silabs_firmware(hass, device_path): + raise ConfigEntryError("Incorrect firmware installed") from exc + except AlreadyRunningEZSP as ezsp_exc: + raise ConfigEntryNotReady from ezsp_exc - if RadioType[config_entry.data[CONF_RADIO_TYPE]] == RadioType.ezsp: - try: - # Ignore all exceptions during probing, they shouldn't halt setup - await warn_on_wrong_silabs_firmware( - hass, config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] - ) - except AlreadyRunningEZSP as ezsp_exc: - raise ConfigEntryNotReady from ezsp_exc - - raise + raise ConfigEntryNotReady from exc repairs.async_delete_blocking_issues(hass) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index f89ed8d9a52..ecbd347a621 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -409,9 +409,6 @@ class Strobe(t.enum8): Strobe = 0x01 -STARTUP_FAILURE_DELAY_S = 3 -STARTUP_RETRIES = 3 - EZSP_OVERWRITE_EUI64 = ( "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" ) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 1b3a536007a..55405d0a51c 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -46,7 +46,7 @@ def disable_request_retry_delay(): with patch( "homeassistant.components.zha.core.cluster_handlers.RETRYABLE_REQUEST_DECORATOR", zigpy.util.retryable_request(tries=3, delay=0), - ), patch("homeassistant.components.zha.STARTUP_FAILURE_DELAY_S", 0.01): + ): yield diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index d168e2e57b1..0efff5ecb52 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -95,7 +95,6 @@ def test_detect_radio_hardware_failure(hass: HomeAssistant) -> None: assert _detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.OTHER -@patch("homeassistant.components.zha.STARTUP_RETRIES", new=1) @pytest.mark.parametrize( ("detected_hardware", "expected_learn_more_url"), [ @@ -176,7 +175,7 @@ async def test_multipan_firmware_no_repair_on_probe_failure( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state == ConfigEntryState.SETUP_RETRY await hass.config_entries.async_unload(config_entry.entry_id) @@ -189,7 +188,6 @@ async def test_multipan_firmware_no_repair_on_probe_failure( assert issue is None -@patch("homeassistant.components.zha.STARTUP_RETRIES", new=1) async def test_multipan_firmware_retry_on_probe_ezsp( hass: HomeAssistant, config_entry: MockConfigEntry, From ebb54d7a6cd796218e1457e566840795ac94dcc6 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 13 Dec 2023 11:06:46 -0500 Subject: [PATCH 29/29] Bump ZHA dependencies (#105661) --- homeassistant/components/zha/core/const.py | 1 - homeassistant/components/zha/core/gateway.py | 10 ----- homeassistant/components/zha/manifest.json | 8 ++-- homeassistant/components/zha/radio_manager.py | 2 - requirements_all.txt | 8 ++-- requirements_test_all.txt | 8 ++-- tests/components/zha/test_gateway.py | 45 +------------------ 7 files changed, 13 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index ecbd347a621..7e591a596e5 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -139,7 +139,6 @@ CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" CONF_ENABLE_QUIRKS = "enable_quirks" CONF_RADIO_TYPE = "radio_type" CONF_USB_PATH = "usb_path" -CONF_USE_THREAD = "use_thread" CONF_ZIGPY = "zigpy_config" CONF_CONSIDER_UNAVAILABLE_MAINS = "consider_unavailable_mains" diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 5c038a2d7f8..6c461ac45c3 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -46,7 +46,6 @@ from .const import ( ATTR_SIGNATURE, ATTR_TYPE, CONF_RADIO_TYPE, - CONF_USE_THREAD, CONF_ZIGPY, DEBUG_COMP_BELLOWS, DEBUG_COMP_ZHA, @@ -158,15 +157,6 @@ class ZHAGateway: if CONF_NWK_VALIDATE_SETTINGS not in app_config: app_config[CONF_NWK_VALIDATE_SETTINGS] = True - # The bellows UART thread sometimes propagates a cancellation into the main Core - # event loop, when a connection to a TCP coordinator fails in a specific way - if ( - CONF_USE_THREAD not in app_config - and radio_type is RadioType.ezsp - and app_config[CONF_DEVICE][CONF_DEVICE_PATH].startswith("socket://") - ): - app_config[CONF_USE_THREAD] = False - # Local import to avoid circular dependencies # pylint: disable-next=import-outside-toplevel from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 4c8a58a12cf..fe58ff044cd 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,13 +21,13 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.37.1", + "bellows==0.37.3", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.107", - "zigpy-deconz==0.22.0", - "zigpy==0.60.0", - "zigpy-xbee==0.20.0", + "zigpy-deconz==0.22.2", + "zigpy==0.60.1", + "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.0", "universal-silabs-flasher==0.0.15", diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index d3ca03de8d8..92a90e0e13a 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -10,7 +10,6 @@ import logging import os from typing import Any, Self -from bellows.config import CONF_USE_THREAD import voluptuous as vol from zigpy.application import ControllerApplication import zigpy.backups @@ -175,7 +174,6 @@ class ZhaRadioManager: app_config[CONF_DATABASE] = database_path app_config[CONF_DEVICE] = self.device_settings app_config[CONF_NWK_BACKUP_ENABLED] = False - app_config[CONF_USE_THREAD] = False app_config = self.radio_type.controller.SCHEMA(app_config) app = await self.radio_type.controller.new( diff --git a/requirements_all.txt b/requirements_all.txt index 3ec2df6ae86..27a14e0b137 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -523,7 +523,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.37.1 +bellows==0.37.3 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -2825,10 +2825,10 @@ zhong-hong-hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.22.0 +zigpy-deconz==0.22.2 # homeassistant.components.zha -zigpy-xbee==0.20.0 +zigpy-xbee==0.20.1 # homeassistant.components.zha zigpy-zigate==0.12.0 @@ -2837,7 +2837,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.0 # homeassistant.components.zha -zigpy==0.60.0 +zigpy==0.60.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1610539940d..cae0417ff0a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -445,7 +445,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.37.1 +bellows==0.37.3 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -2114,10 +2114,10 @@ zeversolar==0.3.1 zha-quirks==0.0.107 # homeassistant.components.zha -zigpy-deconz==0.22.0 +zigpy-deconz==0.22.2 # homeassistant.components.zha -zigpy-xbee==0.20.0 +zigpy-xbee==0.20.1 # homeassistant.components.zha zigpy-zigate==0.12.0 @@ -2126,7 +2126,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.0 # homeassistant.components.zha -zigpy==0.60.0 +zigpy==0.60.1 # homeassistant.components.zwave_js zwave-js-server-python==0.54.0 diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 4f520920704..1d9042daa4a 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,9 +1,8 @@ """Test ZHA Gateway.""" import asyncio -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest -from zigpy.application import ControllerApplication import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting @@ -223,48 +222,6 @@ async def test_gateway_create_group_with_id( assert zha_group.group_id == 0x1234 -@patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_devices", - MagicMock(), -) -@patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_groups", - MagicMock(), -) -@pytest.mark.parametrize( - ("device_path", "thread_state", "config_override"), - [ - ("/dev/ttyUSB0", True, {}), - ("socket://192.168.1.123:9999", False, {}), - ("socket://192.168.1.123:9999", True, {"use_thread": True}), - ], -) -async def test_gateway_initialize_bellows_thread( - device_path: str, - thread_state: bool, - config_override: dict, - hass: HomeAssistant, - zigpy_app_controller: ControllerApplication, - config_entry: MockConfigEntry, -) -> None: - """Test ZHA disabling the UART thread when connecting to a TCP coordinator.""" - config_entry.data = dict(config_entry.data) - config_entry.data["device"]["path"] = device_path - config_entry.add_to_hass(hass) - - zha_gateway = ZHAGateway(hass, {"zigpy_config": config_override}, config_entry) - - with patch( - "bellows.zigbee.application.ControllerApplication.new", - return_value=zigpy_app_controller, - ) as mock_new: - await zha_gateway.async_initialize() - - mock_new.mock_calls[-1].kwargs["config"]["use_thread"] is thread_state - - await zha_gateway.shutdown() - - @pytest.mark.parametrize( ("device_path", "config_override", "expected_channel"), [