From 18afe07c169e05fe818e8eb5f7af2ec3303c3aed Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 31 Jul 2024 22:38:50 +0200 Subject: [PATCH 01/95] Bump version to 2024.8.0b0 --- 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 7d58bdb1e94..b315d8c2618 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index f71e9bd6013..e705101e4ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0.dev0" +version = "2024.8.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2520fcd284e94f0eebee1165ef393966d0e9ede1 Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Fri, 2 Aug 2024 08:13:56 -0500 Subject: [PATCH 02/95] Lyric: Properly tie room accessories to the data coordinator (#115902) * properly tie lyric accessories to the data coordinator so sensors recieve updates * only check for accessories for LCC devices * revert: meant to give it its own branch and PR --- homeassistant/components/lyric/__init__.py | 22 ++++++++++++++++++---- homeassistant/components/lyric/sensor.py | 3 +-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 7c002229741..e1eaed6602c 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -192,8 +192,8 @@ class LyricAccessoryEntity(LyricDeviceEntity): ) -> None: """Initialize the Honeywell Lyric accessory entity.""" super().__init__(coordinator, location, device, key) - self._room = room - self._accessory = accessory + self._room_id = room.id + self._accessory_id = accessory.id @property def device_info(self) -> DeviceInfo: @@ -202,11 +202,25 @@ class LyricAccessoryEntity(LyricDeviceEntity): identifiers={ ( f"{dr.CONNECTION_NETWORK_MAC}_room_accessory", - f"{self._mac_id}_room{self._room.id}_accessory{self._accessory.id}", + f"{self._mac_id}_room{self._room_id}_accessory{self._accessory_id}", ) }, manufacturer="Honeywell", model="RCHTSENSOR", - name=f"{self._room.roomName} Sensor", + name=f"{self.room.roomName} Sensor", via_device=(dr.CONNECTION_NETWORK_MAC, self._mac_id), ) + + @property + def room(self) -> LyricRoom: + """Get the Lyric Device.""" + return self.coordinator.data.rooms_dict[self._mac_id][self._room_id] + + @property + def accessory(self) -> LyricAccessories: + """Get the Lyric Device.""" + return next( + accessory + for accessory in self.room.accessories + if accessory.id == self._accessory_id + ) diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 64f60fa6611..9f05354c399 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -244,7 +244,6 @@ class LyricAccessorySensor(LyricAccessoryEntity, SensorEntity): accessory, f"{parentDevice.macID}_room{room.id}_acc{accessory.id}_{description.key}", ) - self.room = room self.entity_description = description if description.device_class == SensorDeviceClass.TEMPERATURE: if parentDevice.units == "Fahrenheit": @@ -255,4 +254,4 @@ class LyricAccessorySensor(LyricAccessoryEntity, SensorEntity): @property def native_value(self) -> StateType | datetime: """Return the state.""" - return self.entity_description.value_fn(self._room, self._accessory) + return self.entity_description.value_fn(self.room, self.accessory) From 1b1d86409cc4605485b129c930b07d6675a3e00c Mon Sep 17 00:00:00 2001 From: DeerMaximum <43999966+DeerMaximum@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:32:37 +0200 Subject: [PATCH 03/95] Velux use node id as fallback for unique id (#117508) Co-authored-by: Robert Resch --- homeassistant/components/velux/__init__.py | 8 ++++++-- homeassistant/components/velux/cover.py | 6 +++--- homeassistant/components/velux/light.py | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 4b89fc66a84..1b7cbd1ff93 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -108,10 +108,14 @@ class VeluxEntity(Entity): _attr_should_poll = False - def __init__(self, node: Node) -> None: + def __init__(self, node: Node, config_entry_id: str) -> None: """Initialize the Velux device.""" self.node = node - self._attr_unique_id = node.serial_number + self._attr_unique_id = ( + node.serial_number + if node.serial_number + else f"{config_entry_id}_{node.node_id}" + ) self._attr_name = node.name if node.name else f"#{node.node_id}" @callback diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index c8688e4d186..cd7564eee81 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -29,7 +29,7 @@ async def async_setup_entry( """Set up cover(s) for Velux platform.""" module = hass.data[DOMAIN][config.entry_id] async_add_entities( - VeluxCover(node) + VeluxCover(node, config.entry_id) for node in module.pyvlx.nodes if isinstance(node, OpeningDevice) ) @@ -41,9 +41,9 @@ class VeluxCover(VeluxEntity, CoverEntity): _is_blind = False node: OpeningDevice - def __init__(self, node: OpeningDevice) -> None: + def __init__(self, node: OpeningDevice, config_entry_id: str) -> None: """Initialize VeluxCover.""" - super().__init__(node) + super().__init__(node, config_entry_id) self._attr_device_class = CoverDeviceClass.WINDOW if isinstance(node, Awning): self._attr_device_class = CoverDeviceClass.AWNING diff --git a/homeassistant/components/velux/light.py b/homeassistant/components/velux/light.py index bbe9822648e..e98632701f3 100644 --- a/homeassistant/components/velux/light.py +++ b/homeassistant/components/velux/light.py @@ -23,7 +23,7 @@ async def async_setup_entry( module = hass.data[DOMAIN][config.entry_id] async_add_entities( - VeluxLight(node) + VeluxLight(node, config.entry_id) for node in module.pyvlx.nodes if isinstance(node, LighteningDevice) ) From 804d7aa4c040b3d4ca0f97811d380128a90daafa Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:44:19 +0200 Subject: [PATCH 04/95] Fix translation key for power exchange sensor in ViCare (#122339) --- homeassistant/components/vicare/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index 7c0088d065f..0452a560cb8 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -319,8 +319,8 @@ "ess_discharge_total": { "name": "Battery discharge total" }, - "pcc_current_power_exchange": { - "name": "Grid power exchange" + "pcc_transfer_power_exchange": { + "name": "Power exchange with grid" }, "pcc_energy_consumption": { "name": "Energy import from grid" From 1a7085b068a61f44802d129c2fc6573c0f2cacd6 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Fri, 2 Aug 2024 09:05:06 +0300 Subject: [PATCH 05/95] Add aliases to script llm tool description (#122380) * Add aliases to script llm tool description * Also add name --- homeassistant/helpers/llm.py | 13 +++++++++++++ tests/helpers/test_llm.py | 36 +++++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 4ddb00166b6..e37aa0c532d 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -677,6 +677,19 @@ class ScriptTool(Tool): self.parameters = vol.Schema(schema) + aliases: list[str] = [] + if entity_entry.name: + aliases.append(entity_entry.name) + if entity_entry.aliases: + aliases.extend(entity_entry.aliases) + if aliases: + if self.description: + self.description = ( + self.description + ". Aliases: " + str(list(aliases)) + ) + else: + self.description = "Aliases: " + str(list(aliases)) + parameters_cache[entity_entry.unique_id] = ( self.description, self.parameters, diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index ea6e628d1d4..4d14abb9819 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -411,7 +411,9 @@ async def test_assist_api_prompt( ) hass.states.async_set(entry2.entity_id, "on", {"friendly_name": "Living Room"}) - def create_entity(device: dr.DeviceEntry, write_state=True) -> None: + def create_entity( + device: dr.DeviceEntry, write_state=True, aliases: set[str] | None = None + ) -> None: """Create an entity for a device and track entity_id.""" entity = entity_registry.async_get_or_create( "light", @@ -421,6 +423,8 @@ async def test_assist_api_prompt( original_name=str(device.name or "Unnamed Device"), suggested_object_id=str(device.name or "unnamed_device"), ) + if aliases: + entity_registry.async_update_entity(entity.entity_id, aliases=aliases) if write_state: entity.write_unavailable_state(hass) @@ -432,7 +436,8 @@ async def test_assist_api_prompt( manufacturer="Test Manufacturer", model="Test Model", suggested_area="Test Area", - ) + ), + aliases={"my test light"}, ) for i in range(3): create_entity( @@ -516,7 +521,7 @@ async def test_assist_api_prompt( domain: light state: 'on' areas: Test Area, Alternative name -- names: Test Device +- names: Test Device, my test light domain: light state: unavailable areas: Test Area, Alternative name @@ -616,6 +621,7 @@ async def test_assist_api_prompt( async def test_script_tool( hass: HomeAssistant, + entity_registry: er.EntityRegistry, area_registry: ar.AreaRegistry, floor_registry: fr.FloorRegistry, ) -> None: @@ -659,6 +665,10 @@ async def test_script_tool( ) async_expose_entity(hass, "conversation", "script.test_script", True) + entity_registry.async_update_entity( + "script.test_script", name="script name", aliases={"script alias"} + ) + area = area_registry.async_create("Living room") floor = floor_registry.async_create("2") @@ -671,7 +681,10 @@ async def test_script_tool( tool = tools[0] assert tool.name == "test_script" - assert tool.description == "This is a test script" + assert ( + tool.description + == "This is a test script. Aliases: ['script name', 'script alias']" + ) schema = { vol.Required("beer", description="Number of beers"): cv.string, vol.Optional("wine"): selector.NumberSelector({"min": 0, "max": 3}), @@ -684,7 +697,10 @@ async def test_script_tool( assert tool.parameters.schema == schema assert hass.data[llm.SCRIPT_PARAMETERS_CACHE] == { - "test_script": ("This is a test script", vol.Schema(schema)) + "test_script": ( + "This is a test script. Aliases: ['script name', 'script alias']", + vol.Schema(schema), + ) } tool_input = llm.ToolInput( @@ -754,12 +770,18 @@ async def test_script_tool( tool = tools[0] assert tool.name == "test_script" - assert tool.description == "This is a new test script" + assert ( + tool.description + == "This is a new test script. Aliases: ['script name', 'script alias']" + ) schema = {vol.Required("beer", description="Number of beers"): cv.string} assert tool.parameters.schema == schema assert hass.data[llm.SCRIPT_PARAMETERS_CACHE] == { - "test_script": ("This is a new test script", vol.Schema(schema)) + "test_script": ( + "This is a new test script. Aliases: ['script name', 'script alias']", + vol.Schema(schema), + ) } From 6a6814af61216df481410f57ecf9d971037eccff Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 2 Aug 2024 06:10:04 -0700 Subject: [PATCH 06/95] Use text/multiple selector for input_select.set_options (#122539) --- homeassistant/components/input_select/services.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml index 92279e58a54..04a09e5366a 100644 --- a/homeassistant/components/input_select/services.yaml +++ b/homeassistant/components/input_select/services.yaml @@ -48,6 +48,7 @@ set_options: required: true example: '["Item A", "Item B", "Item C"]' selector: - object: + text: + multiple: true reload: From dfb4e9c159e57f9731d7fd3745676ece4a657de0 Mon Sep 17 00:00:00 2001 From: Matrix Date: Fri, 2 Aug 2024 21:57:15 +0800 Subject: [PATCH 07/95] Yolink device model adaptation (#122824) --- homeassistant/components/yolink/const.py | 4 ++++ homeassistant/components/yolink/sensor.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 894c85d3f1b..686160d9248 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -17,5 +17,9 @@ YOLINK_OFFLINE_TIME = 32400 DEV_MODEL_WATER_METER_YS5007 = "YS5007" DEV_MODEL_MULTI_OUTLET_YS6801 = "YS6801" +DEV_MODEL_TH_SENSOR_YS8004_UC = "YS8004-UC" +DEV_MODEL_TH_SENSOR_YS8004_EC = "YS8004-EC" +DEV_MODEL_TH_SENSOR_YS8014_UC = "YS8014-UC" +DEV_MODEL_TH_SENSOR_YS8014_EC = "YS8014-EC" DEV_MODEL_TH_SENSOR_YS8017_UC = "YS8017-UC" DEV_MODEL_TH_SENSOR_YS8017_EC = "YS8017-EC" diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 4426602f133..77bbccb2f6a 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -48,7 +48,15 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import percentage -from .const import DEV_MODEL_TH_SENSOR_YS8017_EC, DEV_MODEL_TH_SENSOR_YS8017_UC, DOMAIN +from .const import ( + DEV_MODEL_TH_SENSOR_YS8004_EC, + DEV_MODEL_TH_SENSOR_YS8004_UC, + DEV_MODEL_TH_SENSOR_YS8014_EC, + DEV_MODEL_TH_SENSOR_YS8014_UC, + DEV_MODEL_TH_SENSOR_YS8017_EC, + DEV_MODEL_TH_SENSOR_YS8017_UC, + DOMAIN, +) from .coordinator import YoLinkCoordinator from .entity import YoLinkEntity @@ -109,6 +117,10 @@ MCU_DEV_TEMPERATURE_SENSOR = [ ] NONE_HUMIDITY_SENSOR_MODELS = [ + DEV_MODEL_TH_SENSOR_YS8004_EC, + DEV_MODEL_TH_SENSOR_YS8004_UC, + DEV_MODEL_TH_SENSOR_YS8014_EC, + DEV_MODEL_TH_SENSOR_YS8014_UC, DEV_MODEL_TH_SENSOR_YS8017_UC, DEV_MODEL_TH_SENSOR_YS8017_EC, ] From 3b462906d9982999d971c99b7841b19ce88a2964 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Thu, 1 Aug 2024 03:59:19 -0700 Subject: [PATCH 08/95] Restrict nws.get_forecasts_extra selector to nws weather entities (#122986) --- homeassistant/components/nws/services.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nws/services.yaml b/homeassistant/components/nws/services.yaml index 0d439a9d278..a3d241c775d 100644 --- a/homeassistant/components/nws/services.yaml +++ b/homeassistant/components/nws/services.yaml @@ -2,6 +2,7 @@ get_forecasts_extra: target: entity: domain: weather + integration: nws fields: type: required: true From cb37ae660843142bc8b951c3b91ccd0c5739863f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Aug 2024 01:31:22 -0500 Subject: [PATCH 09/95] Update doorbird error notification to be a repair flow (#122987) --- homeassistant/components/doorbird/__init__.py | 37 ++++++----- .../components/doorbird/manifest.json | 2 +- homeassistant/components/doorbird/repairs.py | 55 +++++++++++++++++ .../components/doorbird/strings.json | 13 ++++ tests/components/doorbird/test_repairs.py | 61 +++++++++++++++++++ 5 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/doorbird/repairs.py create mode 100644 tests/components/doorbird/test_repairs.py diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 8989e0ec0be..113b8031d9b 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -3,11 +3,11 @@ from __future__ import annotations from http import HTTPStatus +import logging from aiohttp import ClientResponseError from doorbirdpy import DoorBird -from homeassistant.components import persistent_notification from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -17,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -30,6 +31,8 @@ CONF_CUSTOM_URL = "hass_url_override" CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the DoorBird component.""" @@ -68,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) -> door_bird_data = DoorBirdData(door_station, info, event_entity_ids) door_station.update_events(events) # Subscribe to doorbell or motion events - if not await _async_register_events(hass, door_station): + if not await _async_register_events(hass, door_station, entry): raise ConfigEntryNotReady entry.async_on_unload(entry.add_update_listener(_update_listener)) @@ -84,24 +87,30 @@ async def async_unload_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) -> async def _async_register_events( - hass: HomeAssistant, door_station: ConfiguredDoorBird + hass: HomeAssistant, door_station: ConfiguredDoorBird, entry: DoorBirdConfigEntry ) -> bool: """Register events on device.""" + issue_id = f"doorbird_schedule_error_{entry.entry_id}" try: await door_station.async_register_events() - except ClientResponseError: - persistent_notification.async_create( + except ClientResponseError as ex: + ir.async_create_issue( hass, - ( - "Doorbird configuration failed. Please verify that API " - "Operator permission is enabled for the Doorbird user. " - "A restart will be required once permissions have been " - "verified." - ), - title="Doorbird Configuration Failure", - notification_id="doorbird_schedule_error", + DOMAIN, + issue_id, + severity=ir.IssueSeverity.ERROR, + translation_key="error_registering_events", + data={"entry_id": entry.entry_id}, + is_fixable=True, + translation_placeholders={ + "error": str(ex), + "name": door_station.name or entry.data[CONF_NAME], + }, ) + _LOGGER.debug("Error registering DoorBird events", exc_info=True) return False + else: + ir.async_delete_issue(hass, DOMAIN, issue_id) return True @@ -111,4 +120,4 @@ async def _update_listener(hass: HomeAssistant, entry: DoorBirdConfigEntry) -> N door_station = entry.runtime_data.door_station door_station.update_events(entry.options[CONF_EVENTS]) # Subscribe to doorbell or motion events - await _async_register_events(hass, door_station) + await _async_register_events(hass, door_station, entry) diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index e77f9aaf0a4..0e9f03c8ef8 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -3,7 +3,7 @@ "name": "DoorBird", "codeowners": ["@oblogic7", "@bdraco", "@flacjacket"], "config_flow": true, - "dependencies": ["http"], + "dependencies": ["http", "repairs"], "documentation": "https://www.home-assistant.io/integrations/doorbird", "iot_class": "local_push", "loggers": ["doorbirdpy"], diff --git a/homeassistant/components/doorbird/repairs.py b/homeassistant/components/doorbird/repairs.py new file mode 100644 index 00000000000..c8f9b73ecbd --- /dev/null +++ b/homeassistant/components/doorbird/repairs.py @@ -0,0 +1,55 @@ +"""Repairs for DoorBird.""" + +from __future__ import annotations + +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import RepairsFlow +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + + +class DoorBirdReloadConfirmRepairFlow(RepairsFlow): + """Handler to show doorbird error and reload.""" + + def __init__(self, entry_id: str) -> None: + """Initialize the flow.""" + self.entry_id = entry_id + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + self.hass.config_entries.async_schedule_reload(self.entry_id) + return self.async_create_entry(data={}) + + issue_registry = ir.async_get(self.hass) + description_placeholders = None + if issue := issue_registry.async_get_issue(self.handler, self.issue_id): + description_placeholders = issue.translation_placeholders + + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema({}), + description_placeholders=description_placeholders, + ) + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: + """Create flow.""" + assert data is not None + entry_id = data["entry_id"] + assert isinstance(entry_id, str) + return DoorBirdReloadConfirmRepairFlow(entry_id=entry_id) diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index 29c85ec7311..090ba4f161f 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -11,6 +11,19 @@ } } }, + "issues": { + "error_registering_events": { + "title": "DoorBird {name} configuration failure", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::doorbird::issues::error_registering_events::title%]", + "description": "Configuring DoorBird {name} failed with error: `{error}`. Please enable the API Operator permission for the DoorBird user and continue to reload the integration." + } + } + } + } + }, "config": { "step": { "user": { diff --git a/tests/components/doorbird/test_repairs.py b/tests/components/doorbird/test_repairs.py new file mode 100644 index 00000000000..7449250b718 --- /dev/null +++ b/tests/components/doorbird/test_repairs.py @@ -0,0 +1,61 @@ +"""Test repairs for doorbird.""" + +from __future__ import annotations + +from http import HTTPStatus + +from homeassistant.components.doorbird.const import DOMAIN +from homeassistant.components.repairs.issue_handler import ( + async_process_repairs_platforms, +) +from homeassistant.components.repairs.websocket_api import ( + RepairsFlowIndexView, + RepairsFlowResourceView, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + +from . import mock_not_found_exception +from .conftest import DoorbirdMockerType + +from tests.typing import ClientSessionGenerator + + +async def test_change_schedule_fails( + hass: HomeAssistant, + doorbird_mocker: DoorbirdMockerType, + hass_client: ClientSessionGenerator, +) -> None: + """Test a doorbird when change_schedule fails.""" + assert await async_setup_component(hass, "repairs", {}) + doorbird_entry = await doorbird_mocker( + favorites_side_effect=mock_not_found_exception() + ) + assert doorbird_entry.entry.state is ConfigEntryState.SETUP_RETRY + issue_reg = ir.async_get(hass) + assert len(issue_reg.issues) == 1 + issue = list(issue_reg.issues.values())[0] + issue_id = issue.issue_id + assert issue.domain == DOMAIN + + await async_process_repairs_platforms(hass) + client = await hass_client() + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": issue_id}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + placeholders = data["description_placeholders"] + assert "404" in placeholders["error"] + assert data["step_id"] == "confirm" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" From 0216455137c86a28f8a91b57a81262cac125fad2 Mon Sep 17 00:00:00 2001 From: Matrix Date: Thu, 1 Aug 2024 14:32:16 +0800 Subject: [PATCH 10/95] Fix yolink protocol changed (#122989) --- homeassistant/components/yolink/valve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/yolink/valve.py b/homeassistant/components/yolink/valve.py index a24ad7d385d..d8c199697c3 100644 --- a/homeassistant/components/yolink/valve.py +++ b/homeassistant/components/yolink/valve.py @@ -37,7 +37,7 @@ DEVICE_TYPES: tuple[YoLinkValveEntityDescription, ...] = ( key="valve_state", translation_key="meter_valve_state", device_class=ValveDeviceClass.WATER, - value=lambda value: value == "closed" if value is not None else None, + value=lambda value: value != "open" if value is not None else None, exists_fn=lambda device: device.device_type == ATTR_DEVICE_WATER_METER_CONTROLLER and not device.device_model_name.startswith(DEV_MODEL_WATER_METER_YS5007), From acf523b5fb75cae738ef77c35c2331381bf99abe Mon Sep 17 00:00:00 2001 From: amccook <30292381+amccook@users.noreply.github.com> Date: Thu, 1 Aug 2024 07:24:09 -0700 Subject: [PATCH 11/95] Fix handling of directory type playlists in Plex (#122990) Ignore type directory --- homeassistant/components/plex/media_browser.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index e47e6145761..87e9f47af66 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -132,7 +132,11 @@ def browse_media( # noqa: C901 "children": [], } for playlist in plex_server.playlists(): - if playlist.playlistType != "audio" and platform == "sonos": + if ( + playlist.type != "directory" + and playlist.playlistType != "audio" + and platform == "sonos" + ): continue try: playlists_info["children"].append(item_payload(playlist)) From 55abe68a5fcb3e74726a0321f80b188a602d1125 Mon Sep 17 00:00:00 2001 From: Ivan Belokobylskiy Date: Thu, 1 Aug 2024 12:51:41 +0400 Subject: [PATCH 12/95] Bump aioymaps to 1.2.5 (#123005) Bump aiomaps, fix sessionId parsing --- homeassistant/components/yandex_transport/manifest.json | 2 +- homeassistant/components/yandex_transport/sensor.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json index c29b4d3dc98..1d1219d5a95 100644 --- a/homeassistant/components/yandex_transport/manifest.json +++ b/homeassistant/components/yandex_transport/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@rishatik92", "@devbis"], "documentation": "https://www.home-assistant.io/integrations/yandex_transport", "iot_class": "cloud_polling", - "requirements": ["aioymaps==1.2.4"] + "requirements": ["aioymaps==1.2.5"] } diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 30227e3261e..95c4785a341 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import timedelta import logging -from aioymaps import CaptchaError, YandexMapsRequester +from aioymaps import CaptchaError, NoSessionError, YandexMapsRequester import voluptuous as vol from homeassistant.components.sensor import ( @@ -88,7 +88,7 @@ class DiscoverYandexTransport(SensorEntity): closer_time = None try: yandex_reply = await self.requester.get_stop_info(self._stop_id) - except CaptchaError as ex: + except (CaptchaError, NoSessionError) as ex: _LOGGER.error( "%s. You may need to disable the integration for some time", ex, diff --git a/requirements_all.txt b/requirements_all.txt index ad08342230d..3bd6cfa8b2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -407,7 +407,7 @@ aiowebostv==0.4.2 aiowithings==3.0.2 # homeassistant.components.yandex_transport -aioymaps==1.2.4 +aioymaps==1.2.5 # homeassistant.components.airgradient airgradient==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5bdf58ff217..21c8fd5f677 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -389,7 +389,7 @@ aiowebostv==0.4.2 aiowithings==3.0.2 # homeassistant.components.yandex_transport -aioymaps==1.2.4 +aioymaps==1.2.5 # homeassistant.components.airgradient airgradient==0.7.1 From e9bfe82582f94f92c3db5597eb869cf7c0fcf678 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Aug 2024 11:51:45 +0200 Subject: [PATCH 13/95] Make the Android timer notification high priority (#123006) --- homeassistant/components/mobile_app/timers.py | 2 ++ tests/components/mobile_app/test_timers.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/mobile_app/timers.py b/homeassistant/components/mobile_app/timers.py index 93b4ac53be5..e092298c5d7 100644 --- a/homeassistant/components/mobile_app/timers.py +++ b/homeassistant/components/mobile_app/timers.py @@ -39,6 +39,8 @@ def async_handle_timer_event( # Android "channel": "Timers", "importance": "high", + "ttl": 0, + "priority": "high", # iOS "push": { "interruption-level": "time-sensitive", diff --git a/tests/components/mobile_app/test_timers.py b/tests/components/mobile_app/test_timers.py index 0eba88f7328..9f7d4cebc58 100644 --- a/tests/components/mobile_app/test_timers.py +++ b/tests/components/mobile_app/test_timers.py @@ -61,6 +61,8 @@ async def test_timer_events( "channel": "Timers", "group": "timers", "importance": "high", + "ttl": 0, + "priority": "high", "push": { "interruption-level": "time-sensitive", }, From ecbff61332364059bf089b69d00248a63f4b4a2a Mon Sep 17 00:00:00 2001 From: Matrix Date: Thu, 1 Aug 2024 17:49:58 +0800 Subject: [PATCH 14/95] Bump yolink api to 0.4.6 (#123012) --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index 5353d5d5b8c..ceb4e4ceff3 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.4.4"] + "requirements": ["yolink-api==0.4.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3bd6cfa8b2a..0bf4b77e9d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2962,7 +2962,7 @@ yeelight==0.7.14 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.4.4 +yolink-api==0.4.6 # homeassistant.components.youless youless-api==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21c8fd5f677..f9670987b70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2339,7 +2339,7 @@ yalexs==6.4.3 yeelight==0.7.14 # homeassistant.components.yolink -yolink-api==0.4.4 +yolink-api==0.4.6 # homeassistant.components.youless youless-api==2.1.2 From a42615add0692b9752f2b1bfa395f8eeedd4e4cf Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 2 Aug 2024 15:58:41 +0200 Subject: [PATCH 15/95] Fix and improve tedee lock states (#123022) Improve tedee lock states --- homeassistant/components/tedee/lock.py | 13 +++++++++++-- tests/components/tedee/test_lock.py | 22 ++++++++++++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tedee/lock.py b/homeassistant/components/tedee/lock.py index d11c873a94a..8d5fa028e12 100644 --- a/homeassistant/components/tedee/lock.py +++ b/homeassistant/components/tedee/lock.py @@ -55,8 +55,13 @@ class TedeeLockEntity(TedeeEntity, LockEntity): super().__init__(lock, coordinator, "lock") @property - def is_locked(self) -> bool: + def is_locked(self) -> bool | None: """Return true if lock is locked.""" + if self._lock.state in ( + TedeeLockState.HALF_OPEN, + TedeeLockState.UNKNOWN, + ): + return None return self._lock.state == TedeeLockState.LOCKED @property @@ -87,7 +92,11 @@ class TedeeLockEntity(TedeeEntity, LockEntity): @property def available(self) -> bool: """Return True if entity is available.""" - return super().available and self._lock.is_connected + return ( + super().available + and self._lock.is_connected + and self._lock.state != TedeeLockState.UNCALIBRATED + ) async def async_unlock(self, **kwargs: Any) -> None: """Unlock the door.""" diff --git a/tests/components/tedee/test_lock.py b/tests/components/tedee/test_lock.py index ffc4a8c30d6..741bc3156cb 100644 --- a/tests/components/tedee/test_lock.py +++ b/tests/components/tedee/test_lock.py @@ -25,7 +25,7 @@ from homeassistant.components.lock import ( STATE_UNLOCKING, ) from homeassistant.components.webhook import async_generate_url -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -276,10 +276,21 @@ async def test_new_lock( assert state +@pytest.mark.parametrize( + ("lib_state", "expected_state"), + [ + (TedeeLockState.LOCKED, STATE_LOCKED), + (TedeeLockState.HALF_OPEN, STATE_UNKNOWN), + (TedeeLockState.UNKNOWN, STATE_UNKNOWN), + (TedeeLockState.UNCALIBRATED, STATE_UNAVAILABLE), + ], +) async def test_webhook_update( hass: HomeAssistant, mock_tedee: MagicMock, hass_client_no_auth: ClientSessionGenerator, + lib_state: TedeeLockState, + expected_state: str, ) -> None: """Test updated data set through webhook.""" @@ -287,10 +298,9 @@ async def test_webhook_update( assert state assert state.state == STATE_UNLOCKED - webhook_data = {"dummystate": 6} - mock_tedee.locks_dict[ - 12345 - ].state = TedeeLockState.LOCKED # is updated in the lib, so mock and assert in L296 + webhook_data = {"dummystate": lib_state.value} + # is updated in the lib, so mock and assert below + mock_tedee.locks_dict[12345].state = lib_state client = await hass_client_no_auth() webhook_url = async_generate_url(hass, WEBHOOK_ID) @@ -302,4 +312,4 @@ async def test_webhook_update( state = hass.states.get("lock.lock_1a2b") assert state - assert state.state == STATE_LOCKED + assert state.state == expected_state From 5ce8a2d974a7cc80ac8e9a28640c5bb568fad9fc Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 1 Aug 2024 15:39:17 -0500 Subject: [PATCH 16/95] Standardize assist pipelines on 10ms chunk size (#123024) * Make chunk size always 10ms * Fix voip --- .../components/assist_pipeline/__init__.py | 8 ++ .../assist_pipeline/audio_enhancer.py | 18 +--- .../components/assist_pipeline/const.py | 4 +- .../components/assist_pipeline/pipeline.py | 75 +++---------- homeassistant/components/voip/voip.py | 22 ++-- tests/components/assist_pipeline/conftest.py | 13 +++ .../snapshots/test_websocket.ambr | 2 +- tests/components/assist_pipeline/test_init.py | 71 +++++++------ .../assist_pipeline/test_websocket.py | 100 +++++++++++------- 9 files changed, 154 insertions(+), 159 deletions(-) diff --git a/homeassistant/components/assist_pipeline/__init__.py b/homeassistant/components/assist_pipeline/__init__.py index f481411e551..8ee053162b0 100644 --- a/homeassistant/components/assist_pipeline/__init__.py +++ b/homeassistant/components/assist_pipeline/__init__.py @@ -16,6 +16,10 @@ from .const import ( DATA_LAST_WAKE_UP, DOMAIN, EVENT_RECORDING, + SAMPLE_CHANNELS, + SAMPLE_RATE, + SAMPLE_WIDTH, + SAMPLES_PER_CHUNK, ) from .error import PipelineNotFound from .pipeline import ( @@ -53,6 +57,10 @@ __all__ = ( "PipelineNotFound", "WakeWordSettings", "EVENT_RECORDING", + "SAMPLES_PER_CHUNK", + "SAMPLE_RATE", + "SAMPLE_WIDTH", + "SAMPLE_CHANNELS", ) CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/assist_pipeline/audio_enhancer.py b/homeassistant/components/assist_pipeline/audio_enhancer.py index e7a149bd00e..c9c60f421b1 100644 --- a/homeassistant/components/assist_pipeline/audio_enhancer.py +++ b/homeassistant/components/assist_pipeline/audio_enhancer.py @@ -6,6 +6,8 @@ import logging from pymicro_vad import MicroVad +from .const import BYTES_PER_CHUNK + _LOGGER = logging.getLogger(__name__) @@ -38,11 +40,6 @@ class AudioEnhancer(ABC): def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk: """Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples.""" - @property - @abstractmethod - def samples_per_chunk(self) -> int | None: - """Return number of samples per chunk or None if chunking isn't required.""" - class MicroVadEnhancer(AudioEnhancer): """Audio enhancer that just runs microVAD.""" @@ -61,22 +58,15 @@ class MicroVadEnhancer(AudioEnhancer): _LOGGER.debug("Initialized microVAD with threshold=%s", self.threshold) def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk: - """Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples.""" + """Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples.""" is_speech: bool | None = None if self.vad is not None: # Run VAD + assert len(audio) == BYTES_PER_CHUNK speech_prob = self.vad.Process10ms(audio) is_speech = speech_prob > self.threshold return EnhancedAudioChunk( audio=audio, timestamp_ms=timestamp_ms, is_speech=is_speech ) - - @property - def samples_per_chunk(self) -> int | None: - """Return number of samples per chunk or None if chunking isn't required.""" - if self.is_vad_enabled: - return 160 # 10ms - - return None diff --git a/homeassistant/components/assist_pipeline/const.py b/homeassistant/components/assist_pipeline/const.py index 14b93a90372..f7306b89a54 100644 --- a/homeassistant/components/assist_pipeline/const.py +++ b/homeassistant/components/assist_pipeline/const.py @@ -19,4 +19,6 @@ EVENT_RECORDING = f"{DOMAIN}_recording" SAMPLE_RATE = 16000 # hertz SAMPLE_WIDTH = 2 # bytes SAMPLE_CHANNELS = 1 # mono -SAMPLES_PER_CHUNK = 240 # 20 ms @ 16Khz +MS_PER_CHUNK = 10 +SAMPLES_PER_CHUNK = SAMPLE_RATE // (1000 // MS_PER_CHUNK) # 10 ms @ 16Khz +BYTES_PER_CHUNK = SAMPLES_PER_CHUNK * SAMPLE_WIDTH * SAMPLE_CHANNELS # 16-bit diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index af29888eb07..9fada934ca1 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -51,11 +51,13 @@ from homeassistant.util.limited_size_dict import LimitedSizeDict from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, MicroVadEnhancer from .const import ( + BYTES_PER_CHUNK, CONF_DEBUG_RECORDING_DIR, DATA_CONFIG, DATA_LAST_WAKE_UP, DATA_MIGRATIONS, DOMAIN, + MS_PER_CHUNK, SAMPLE_CHANNELS, SAMPLE_RATE, SAMPLE_WIDTH, @@ -502,9 +504,6 @@ class AudioSettings: is_vad_enabled: bool = True """True if VAD is used to determine the end of the voice command.""" - samples_per_chunk: int | None = None - """Number of samples that will be in each audio chunk (None for no chunking).""" - silence_seconds: float = 0.5 """Seconds of silence after voice command has ended.""" @@ -525,11 +524,6 @@ class AudioSettings: or (self.auto_gain_dbfs > 0) ) - @property - def is_chunking_enabled(self) -> bool: - """True if chunk size is set.""" - return self.samples_per_chunk is not None - @dataclass class PipelineRun: @@ -566,7 +560,9 @@ class PipelineRun: audio_enhancer: AudioEnhancer | None = None """VAD/noise suppression/auto gain""" - audio_chunking_buffer: AudioBuffer | None = None + audio_chunking_buffer: AudioBuffer = field( + default_factory=lambda: AudioBuffer(BYTES_PER_CHUNK) + ) """Buffer used when splitting audio into chunks for audio processing""" _device_id: str | None = None @@ -599,8 +595,6 @@ class PipelineRun: self.audio_settings.is_vad_enabled, ) - self.audio_chunking_buffer = AudioBuffer(self.samples_per_chunk * SAMPLE_WIDTH) - def __eq__(self, other: object) -> bool: """Compare pipeline runs by id.""" if isinstance(other, PipelineRun): @@ -608,14 +602,6 @@ class PipelineRun: return False - @property - def samples_per_chunk(self) -> int: - """Return number of samples expected in each audio chunk.""" - if self.audio_enhancer is not None: - return self.audio_enhancer.samples_per_chunk or SAMPLES_PER_CHUNK - - return self.audio_settings.samples_per_chunk or SAMPLES_PER_CHUNK - @callback def process_event(self, event: PipelineEvent) -> None: """Log an event and call listener.""" @@ -728,7 +714,7 @@ class PipelineRun: # after wake-word-detection. num_audio_chunks_to_buffer = int( (wake_word_settings.audio_seconds_to_buffer * SAMPLE_RATE) - / self.samples_per_chunk + / SAMPLES_PER_CHUNK ) stt_audio_buffer: deque[EnhancedAudioChunk] | None = None @@ -1216,60 +1202,31 @@ class PipelineRun: self.debug_recording_thread = None async def process_volume_only( - self, - audio_stream: AsyncIterable[bytes], - sample_rate: int = SAMPLE_RATE, - sample_width: int = SAMPLE_WIDTH, + self, audio_stream: AsyncIterable[bytes] ) -> AsyncGenerator[EnhancedAudioChunk]: """Apply volume transformation only (no VAD/audio enhancements) with optional chunking.""" - assert self.audio_chunking_buffer is not None - - bytes_per_chunk = self.samples_per_chunk * sample_width - ms_per_sample = sample_rate // 1000 - ms_per_chunk = self.samples_per_chunk // ms_per_sample timestamp_ms = 0 - async for chunk in audio_stream: if self.audio_settings.volume_multiplier != 1.0: chunk = _multiply_volume(chunk, self.audio_settings.volume_multiplier) - if self.audio_settings.is_chunking_enabled: - for sub_chunk in chunk_samples( - chunk, bytes_per_chunk, self.audio_chunking_buffer - ): - yield EnhancedAudioChunk( - audio=sub_chunk, - timestamp_ms=timestamp_ms, - is_speech=None, # no VAD - ) - timestamp_ms += ms_per_chunk - else: - # No chunking + for sub_chunk in chunk_samples( + chunk, BYTES_PER_CHUNK, self.audio_chunking_buffer + ): yield EnhancedAudioChunk( - audio=chunk, + audio=sub_chunk, timestamp_ms=timestamp_ms, is_speech=None, # no VAD ) - timestamp_ms += (len(chunk) // sample_width) // ms_per_sample + timestamp_ms += MS_PER_CHUNK async def process_enhance_audio( - self, - audio_stream: AsyncIterable[bytes], - sample_rate: int = SAMPLE_RATE, - sample_width: int = SAMPLE_WIDTH, + self, audio_stream: AsyncIterable[bytes] ) -> AsyncGenerator[EnhancedAudioChunk]: - """Split audio into 10 ms chunks and apply VAD/noise suppression/auto gain/volume transformation.""" + """Split audio into chunks and apply VAD/noise suppression/auto gain/volume transformation.""" assert self.audio_enhancer is not None - assert self.audio_enhancer.samples_per_chunk is not None - assert self.audio_chunking_buffer is not None - bytes_per_chunk = self.audio_enhancer.samples_per_chunk * sample_width - ms_per_sample = sample_rate // 1000 - ms_per_chunk = ( - self.audio_enhancer.samples_per_chunk // sample_width - ) // ms_per_sample timestamp_ms = 0 - async for dirty_samples in audio_stream: if self.audio_settings.volume_multiplier != 1.0: # Static gain @@ -1279,10 +1236,10 @@ class PipelineRun: # Split into chunks for audio enhancements/VAD for dirty_chunk in chunk_samples( - dirty_samples, bytes_per_chunk, self.audio_chunking_buffer + dirty_samples, BYTES_PER_CHUNK, self.audio_chunking_buffer ): yield self.audio_enhancer.enhance_chunk(dirty_chunk, timestamp_ms) - timestamp_ms += ms_per_chunk + timestamp_ms += MS_PER_CHUNK def _multiply_volume(chunk: bytes, volume_multiplier: float) -> bytes: diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index 243909629cf..161e938a3b6 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -21,7 +21,7 @@ from voip_utils import ( VoipDatagramProtocol, ) -from homeassistant.components import stt, tts +from homeassistant.components import assist_pipeline, stt, tts from homeassistant.components.assist_pipeline import ( Pipeline, PipelineEvent, @@ -331,15 +331,14 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): async with asyncio.timeout(self.audio_timeout): chunk = await self._audio_queue.get() - assert audio_enhancer.samples_per_chunk is not None - vad_buffer = AudioBuffer(audio_enhancer.samples_per_chunk * WIDTH) + vad_buffer = AudioBuffer(assist_pipeline.SAMPLES_PER_CHUNK * WIDTH) while chunk: chunk_buffer.append(chunk) segmenter.process_with_vad( chunk, - audio_enhancer.samples_per_chunk, + assist_pipeline.SAMPLES_PER_CHUNK, lambda x: audio_enhancer.enhance_chunk(x, 0).is_speech is True, vad_buffer, ) @@ -371,13 +370,12 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): async with asyncio.timeout(self.audio_timeout): chunk = await self._audio_queue.get() - assert audio_enhancer.samples_per_chunk is not None - vad_buffer = AudioBuffer(audio_enhancer.samples_per_chunk * WIDTH) + vad_buffer = AudioBuffer(assist_pipeline.SAMPLES_PER_CHUNK * WIDTH) while chunk: if not segmenter.process_with_vad( chunk, - audio_enhancer.samples_per_chunk, + assist_pipeline.SAMPLES_PER_CHUNK, lambda x: audio_enhancer.enhance_chunk(x, 0).is_speech is True, vad_buffer, ): @@ -437,13 +435,13 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): sample_channels = wav_file.getnchannels() if ( - (sample_rate != 16000) - or (sample_width != 2) - or (sample_channels != 1) + (sample_rate != RATE) + or (sample_width != WIDTH) + or (sample_channels != CHANNELS) ): raise ValueError( - "Expected rate/width/channels as 16000/2/1," - " got {sample_rate}/{sample_width}/{sample_channels}}" + f"Expected rate/width/channels as {RATE}/{WIDTH}/{CHANNELS}," + f" got {sample_rate}/{sample_width}/{sample_channels}" ) audio_bytes = wav_file.readframes(wav_file.getnframes()) diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index c041a54d8fa..b2eca1e7ce1 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -11,6 +11,12 @@ import pytest from homeassistant.components import stt, tts, wake_word from homeassistant.components.assist_pipeline import DOMAIN, select as assist_select +from homeassistant.components.assist_pipeline.const import ( + BYTES_PER_CHUNK, + SAMPLE_CHANNELS, + SAMPLE_RATE, + SAMPLE_WIDTH, +) from homeassistant.components.assist_pipeline.pipeline import ( PipelineData, PipelineStorageCollection, @@ -33,6 +39,8 @@ from tests.common import ( _TRANSCRIPT = "test transcript" +BYTES_ONE_SECOND = SAMPLE_RATE * SAMPLE_WIDTH * SAMPLE_CHANNELS + @pytest.fixture(autouse=True) def mock_tts_cache_dir_autouse(mock_tts_cache_dir: Path) -> Path: @@ -462,3 +470,8 @@ def pipeline_data(hass: HomeAssistant, init_components) -> PipelineData: def pipeline_storage(pipeline_data) -> PipelineStorageCollection: """Return pipeline storage collection.""" return pipeline_data.pipeline_store + + +def make_10ms_chunk(header: bytes) -> bytes: + """Return 10ms of zeros with the given header.""" + return header + bytes(BYTES_PER_CHUNK - len(header)) diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 0b04b67bb22..e5ae18d28f2 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -440,7 +440,7 @@ # --- # name: test_device_capture_override.2 dict({ - 'audio': 'Y2h1bmsx', + 'audio': 'Y2h1bmsxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', 'channels': 1, 'rate': 16000, 'type': 'audio', diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index 8fb7ce5b5a5..4206a288331 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -13,6 +13,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components import assist_pipeline, media_source, stt, tts from homeassistant.components.assist_pipeline.const import ( + BYTES_PER_CHUNK, CONF_DEBUG_RECORDING_DIR, DOMAIN, ) @@ -20,16 +21,16 @@ from homeassistant.core import Context, HomeAssistant from homeassistant.setup import async_setup_component from .conftest import ( + BYTES_ONE_SECOND, MockSttProvider, MockSttProviderEntity, MockTTSProvider, MockWakeWordEntity, + make_10ms_chunk, ) from tests.typing import ClientSessionGenerator, WebSocketGenerator -BYTES_ONE_SECOND = 16000 * 2 - def process_events(events: list[assist_pipeline.PipelineEvent]) -> list[dict]: """Process events to remove dynamic values.""" @@ -58,8 +59,8 @@ async def test_pipeline_from_audio_stream_auto( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" await assist_pipeline.async_pipeline_from_audio_stream( @@ -79,7 +80,9 @@ async def test_pipeline_from_audio_stream_auto( ) assert process_events(events) == snapshot - assert mock_stt_provider.received == [b"part1", b"part2"] + assert len(mock_stt_provider.received) == 2 + assert mock_stt_provider.received[0].startswith(b"part1") + assert mock_stt_provider.received[1].startswith(b"part2") async def test_pipeline_from_audio_stream_legacy( @@ -98,8 +101,8 @@ async def test_pipeline_from_audio_stream_legacy( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" # Create a pipeline using an stt entity @@ -142,7 +145,9 @@ async def test_pipeline_from_audio_stream_legacy( ) assert process_events(events) == snapshot - assert mock_stt_provider.received == [b"part1", b"part2"] + assert len(mock_stt_provider.received) == 2 + assert mock_stt_provider.received[0].startswith(b"part1") + assert mock_stt_provider.received[1].startswith(b"part2") async def test_pipeline_from_audio_stream_entity( @@ -161,8 +166,8 @@ async def test_pipeline_from_audio_stream_entity( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" # Create a pipeline using an stt entity @@ -205,7 +210,9 @@ async def test_pipeline_from_audio_stream_entity( ) assert process_events(events) == snapshot - assert mock_stt_provider_entity.received == [b"part1", b"part2"] + assert len(mock_stt_provider_entity.received) == 2 + assert mock_stt_provider_entity.received[0].startswith(b"part1") + assert mock_stt_provider_entity.received[1].startswith(b"part2") async def test_pipeline_from_audio_stream_no_stt( @@ -224,8 +231,8 @@ async def test_pipeline_from_audio_stream_no_stt( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" # Create a pipeline without stt support @@ -285,8 +292,8 @@ async def test_pipeline_from_audio_stream_unknown_pipeline( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" # Try to use the created pipeline @@ -327,7 +334,7 @@ async def test_pipeline_from_audio_stream_wake_word( # [0, 2, ...] wake_chunk_2 = bytes(it.islice(it.cycle(range(0, 256, 2)), BYTES_ONE_SECOND)) - samples_per_chunk = 160 + samples_per_chunk = 160 # 10ms @ 16Khz bytes_per_chunk = samples_per_chunk * 2 # 16-bit async def audio_data(): @@ -343,8 +350,8 @@ async def test_pipeline_from_audio_stream_wake_word( yield wake_chunk_2[i : i + bytes_per_chunk] i += bytes_per_chunk - for chunk in (b"wake word!", b"part1", b"part2"): - yield chunk + bytes(bytes_per_chunk - len(chunk)) + for header in (b"wake word!", b"part1", b"part2"): + yield make_10ms_chunk(header) yield b"" @@ -365,9 +372,7 @@ async def test_pipeline_from_audio_stream_wake_word( wake_word_settings=assist_pipeline.WakeWordSettings( audio_seconds_to_buffer=1.5 ), - audio_settings=assist_pipeline.AudioSettings( - is_vad_enabled=False, samples_per_chunk=samples_per_chunk - ), + audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), ) assert process_events(events) == snapshot @@ -408,13 +413,11 @@ async def test_pipeline_save_audio( pipeline = assist_pipeline.async_get_pipeline(hass) events: list[assist_pipeline.PipelineEvent] = [] - # Pad out to an even number of bytes since these "samples" will be saved - # as 16-bit values. async def audio_data(): - yield b"wake word_" + yield make_10ms_chunk(b"wake word") # queued audio - yield b"part1_" - yield b"part2_" + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" await assist_pipeline.async_pipeline_from_audio_stream( @@ -457,12 +460,16 @@ async def test_pipeline_save_audio( # Verify wake file with wave.open(str(wake_file), "rb") as wake_wav: wake_data = wake_wav.readframes(wake_wav.getnframes()) - assert wake_data == b"wake word_" + assert wake_data.startswith(b"wake word") # Verify stt file with wave.open(str(stt_file), "rb") as stt_wav: stt_data = stt_wav.readframes(stt_wav.getnframes()) - assert stt_data == b"queued audiopart1_part2_" + assert stt_data.startswith(b"queued audio") + stt_data = stt_data[len(b"queued audio") :] + assert stt_data.startswith(b"part1") + stt_data = stt_data[BYTES_PER_CHUNK:] + assert stt_data.startswith(b"part2") async def test_pipeline_saved_audio_with_device_id( @@ -645,10 +652,10 @@ async def test_wake_word_detection_aborted( events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): - yield b"silence!" - yield b"wake word!" - yield b"part1" - yield b"part2" + yield make_10ms_chunk(b"silence!") + yield make_10ms_chunk(b"wake word!") + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") yield b"" pipeline_store = pipeline_data.pipeline_store diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index 7d4a9b18c12..2da914f4252 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -8,7 +8,12 @@ from unittest.mock import ANY, patch import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.assist_pipeline.const import DOMAIN +from homeassistant.components.assist_pipeline.const import ( + DOMAIN, + SAMPLE_CHANNELS, + SAMPLE_RATE, + SAMPLE_WIDTH, +) from homeassistant.components.assist_pipeline.pipeline import ( DeviceAudioQueue, Pipeline, @@ -18,7 +23,13 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr -from .conftest import MockWakeWordEntity, MockWakeWordEntity2 +from .conftest import ( + BYTES_ONE_SECOND, + BYTES_PER_CHUNK, + MockWakeWordEntity, + MockWakeWordEntity2, + make_10ms_chunk, +) from tests.common import MockConfigEntry from tests.typing import WebSocketGenerator @@ -205,7 +216,7 @@ async def test_audio_pipeline_with_wake_word_timeout( "start_stage": "wake_word", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, "timeout": 1, }, } @@ -229,7 +240,7 @@ async def test_audio_pipeline_with_wake_word_timeout( events.append(msg["event"]) # 2 seconds of silence - await client.send_bytes(bytes([1]) + bytes(16000 * 2 * 2)) + await client.send_bytes(bytes([1]) + bytes(2 * BYTES_ONE_SECOND)) # Time out error msg = await client.receive_json() @@ -259,7 +270,7 @@ async def test_audio_pipeline_with_wake_word_no_timeout( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "timeout": 0, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "timeout": 0, "no_vad": True}, } ) @@ -282,9 +293,10 @@ async def test_audio_pipeline_with_wake_word_no_timeout( events.append(msg["event"]) # "audio" - await client.send_bytes(bytes([handler_id]) + b"wake word") + await client.send_bytes(bytes([handler_id]) + make_10ms_chunk(b"wake word")) - msg = await client.receive_json() + async with asyncio.timeout(1): + msg = await client.receive_json() assert msg["event"]["type"] == "wake_word-end" assert msg["event"]["data"] == snapshot events.append(msg["event"]) @@ -365,7 +377,7 @@ async def test_audio_pipeline_no_wake_word_engine( "start_stage": "wake_word", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, }, } ) @@ -402,7 +414,7 @@ async def test_audio_pipeline_no_wake_word_entity( "start_stage": "wake_word", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, }, } ) @@ -1771,7 +1783,7 @@ async def test_audio_pipeline_with_enhancements( "start_stage": "stt", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, # Enhancements "noise_suppression_level": 2, "auto_gain_dbfs": 15, @@ -1801,7 +1813,7 @@ async def test_audio_pipeline_with_enhancements( # One second of silence. # This will pass through the audio enhancement pipeline, but we don't test # the actual output. - await client.send_bytes(bytes([handler_id]) + bytes(16000 * 2)) + await client.send_bytes(bytes([handler_id]) + bytes(BYTES_ONE_SECOND)) # End of audio stream (handler id + empty payload) await client.send_bytes(bytes([handler_id])) @@ -1871,7 +1883,7 @@ async def test_wake_word_cooldown_same_id( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -1880,7 +1892,7 @@ async def test_wake_word_cooldown_same_id( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -1914,8 +1926,8 @@ async def test_wake_word_cooldown_same_id( assert msg["event"]["data"] == snapshot # Wake both up at the same time - await client_1.send_bytes(bytes([handler_id_1]) + b"wake word") - await client_2.send_bytes(bytes([handler_id_2]) + b"wake word") + await client_1.send_bytes(bytes([handler_id_1]) + make_10ms_chunk(b"wake word")) + await client_2.send_bytes(bytes([handler_id_2]) + make_10ms_chunk(b"wake word")) # Get response events error_data: dict[str, Any] | None = None @@ -1954,7 +1966,7 @@ async def test_wake_word_cooldown_different_ids( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -1963,7 +1975,7 @@ async def test_wake_word_cooldown_different_ids( "type": "assist_pipeline/run", "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -1997,8 +2009,8 @@ async def test_wake_word_cooldown_different_ids( assert msg["event"]["data"] == snapshot # Wake both up at the same time, but they will have different wake word ids - await client_1.send_bytes(bytes([handler_id_1]) + b"wake word") - await client_2.send_bytes(bytes([handler_id_2]) + b"wake word") + await client_1.send_bytes(bytes([handler_id_1]) + make_10ms_chunk(b"wake word")) + await client_2.send_bytes(bytes([handler_id_2]) + make_10ms_chunk(b"wake word")) # Get response events msg = await client_1.receive_json() @@ -2073,7 +2085,7 @@ async def test_wake_word_cooldown_different_entities( "pipeline": pipeline_id_1, "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -2084,7 +2096,7 @@ async def test_wake_word_cooldown_different_entities( "pipeline": pipeline_id_2, "start_stage": "wake_word", "end_stage": "tts", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, } ) @@ -2119,8 +2131,8 @@ async def test_wake_word_cooldown_different_entities( # Wake both up at the same time. # They will have the same wake word id, but different entities. - await client_1.send_bytes(bytes([handler_id_1]) + b"wake word") - await client_2.send_bytes(bytes([handler_id_2]) + b"wake word") + await client_1.send_bytes(bytes([handler_id_1]) + make_10ms_chunk(b"wake word")) + await client_2.send_bytes(bytes([handler_id_2]) + make_10ms_chunk(b"wake word")) # Get response events error_data: dict[str, Any] | None = None @@ -2158,7 +2170,11 @@ async def test_device_capture( identifiers={("demo", "satellite-1234")}, ) - audio_chunks = [b"chunk1", b"chunk2", b"chunk3"] + audio_chunks = [ + make_10ms_chunk(b"chunk1"), + make_10ms_chunk(b"chunk2"), + make_10ms_chunk(b"chunk3"), + ] # Start capture client_capture = await hass_ws_client(hass) @@ -2181,7 +2197,7 @@ async def test_device_capture( "type": "assist_pipeline/run", "start_stage": "stt", "end_stage": "stt", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, "device_id": satellite_device.id, } ) @@ -2232,9 +2248,9 @@ async def test_device_capture( # Verify audio chunks for i, audio_chunk in enumerate(audio_chunks): assert events[i]["type"] == "audio" - assert events[i]["rate"] == 16000 - assert events[i]["width"] == 2 - assert events[i]["channels"] == 1 + assert events[i]["rate"] == SAMPLE_RATE + assert events[i]["width"] == SAMPLE_WIDTH + assert events[i]["channels"] == SAMPLE_CHANNELS # Audio is base64 encoded assert events[i]["audio"] == base64.b64encode(audio_chunk).decode("ascii") @@ -2259,7 +2275,11 @@ async def test_device_capture_override( identifiers={("demo", "satellite-1234")}, ) - audio_chunks = [b"chunk1", b"chunk2", b"chunk3"] + audio_chunks = [ + make_10ms_chunk(b"chunk1"), + make_10ms_chunk(b"chunk2"), + make_10ms_chunk(b"chunk3"), + ] # Start first capture client_capture_1 = await hass_ws_client(hass) @@ -2282,7 +2302,7 @@ async def test_device_capture_override( "type": "assist_pipeline/run", "start_stage": "stt", "end_stage": "stt", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, "device_id": satellite_device.id, } ) @@ -2365,9 +2385,9 @@ async def test_device_capture_override( # Verify all but first audio chunk for i, audio_chunk in enumerate(audio_chunks[1:]): assert events[i]["type"] == "audio" - assert events[i]["rate"] == 16000 - assert events[i]["width"] == 2 - assert events[i]["channels"] == 1 + assert events[i]["rate"] == SAMPLE_RATE + assert events[i]["width"] == SAMPLE_WIDTH + assert events[i]["channels"] == SAMPLE_CHANNELS # Audio is base64 encoded assert events[i]["audio"] == base64.b64encode(audio_chunk).decode("ascii") @@ -2427,7 +2447,7 @@ async def test_device_capture_queue_full( "type": "assist_pipeline/run", "start_stage": "stt", "end_stage": "stt", - "input": {"sample_rate": 16000, "no_vad": True}, + "input": {"sample_rate": SAMPLE_RATE, "no_vad": True}, "device_id": satellite_device.id, } ) @@ -2448,8 +2468,8 @@ async def test_device_capture_queue_full( assert msg["event"]["type"] == "stt-start" assert msg["event"]["data"] == snapshot - # Single sample will "overflow" the queue - await client_pipeline.send_bytes(bytes([handler_id, 0, 0])) + # Single chunk will "overflow" the queue + await client_pipeline.send_bytes(bytes([handler_id]) + bytes(BYTES_PER_CHUNK)) # End of audio stream await client_pipeline.send_bytes(bytes([handler_id])) @@ -2557,7 +2577,7 @@ async def test_stt_cooldown_same_id( "start_stage": "stt", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, "wake_word_phrase": "ok_nabu", }, } @@ -2569,7 +2589,7 @@ async def test_stt_cooldown_same_id( "start_stage": "stt", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, "wake_word_phrase": "ok_nabu", }, } @@ -2628,7 +2648,7 @@ async def test_stt_cooldown_different_ids( "start_stage": "stt", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, "wake_word_phrase": "ok_nabu", }, } @@ -2640,7 +2660,7 @@ async def test_stt_cooldown_different_ids( "start_stage": "stt", "end_stage": "tts", "input": { - "sample_rate": 16000, + "sample_rate": SAMPLE_RATE, "wake_word_phrase": "hey_jarvis", }, } From d87366b1e7154637811091de4c2523ad730aff3a Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 2 Aug 2024 06:22:36 -0400 Subject: [PATCH 17/95] Make ZHA load quirks earlier (#123027) --- homeassistant/components/zha/__init__.py | 4 +++- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zha/test_repairs.py | 10 +++++----- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 216261e3011..fc573b19ab1 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -117,6 +117,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ha_zha_data.config_entry = config_entry zha_lib_data: ZHAData = create_zha_config(hass, ha_zha_data) + zha_gateway = await Gateway.async_from_config(zha_lib_data) + # Load and cache device trigger information early device_registry = dr.async_get(hass) radio_mgr = ZhaRadioManager.from_config_entry(hass, config_entry) @@ -140,7 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b _LOGGER.debug("Trigger cache: %s", zha_lib_data.device_trigger_cache) try: - zha_gateway = await Gateway.async_from_config(zha_lib_data) + await zha_gateway.async_initialize() except NetworkSettingsInconsistent as exc: await warn_on_inconsistent_network_settings( hass, diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index d2d328cc84b..6e35339c53f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.24"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.25"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index 0bf4b77e9d2..99a237efe02 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2986,7 +2986,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.24 +zha==0.0.25 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9670987b70..4ee6914700f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2360,7 +2360,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.24 +zha==0.0.25 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index 7f9b2b4a016..c2925161748 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -148,7 +148,7 @@ async def test_multipan_firmware_repair( autospec=True, ), patch( - "homeassistant.components.zha.Gateway.async_from_config", + "homeassistant.components.zha.Gateway.async_initialize", side_effect=RuntimeError(), ), patch( @@ -199,7 +199,7 @@ async def test_multipan_firmware_no_repair_on_probe_failure( autospec=True, ), patch( - "homeassistant.components.zha.Gateway.async_from_config", + "homeassistant.components.zha.Gateway.async_initialize", side_effect=RuntimeError(), ), ): @@ -236,7 +236,7 @@ async def test_multipan_firmware_retry_on_probe_ezsp( autospec=True, ), patch( - "homeassistant.components.zha.Gateway.async_from_config", + "homeassistant.components.zha.Gateway.async_initialize", side_effect=RuntimeError(), ), ): @@ -311,7 +311,7 @@ async def test_inconsistent_settings_keep_new( old_state = network_backup with patch( - "homeassistant.components.zha.Gateway.async_from_config", + "homeassistant.components.zha.Gateway.async_initialize", side_effect=NetworkSettingsInconsistent( message="Network settings are inconsistent", new_state=new_state, @@ -390,7 +390,7 @@ async def test_inconsistent_settings_restore_old( old_state = network_backup with patch( - "homeassistant.components.zha.Gateway.async_from_config", + "homeassistant.components.zha.Gateway.async_initialize", side_effect=NetworkSettingsInconsistent( message="Network settings are inconsistent", new_state=new_state, From a624ada8d66f15942a6995c4f1f7a9f9699f0f66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Aug 2024 15:37:26 -0500 Subject: [PATCH 18/95] Fix doorbird models are missing the schedule API (#123033) * Fix doorbird models are missing the schedule API fixes #122997 * cover --- homeassistant/components/doorbird/device.py | 14 +++++++--- tests/components/doorbird/__init__.py | 29 ++++++++++----------- tests/components/doorbird/conftest.py | 2 ++ tests/components/doorbird/test_init.py | 10 +++++++ 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/doorbird/device.py b/homeassistant/components/doorbird/device.py index 866251f3d28..7cd45487464 100644 --- a/homeassistant/components/doorbird/device.py +++ b/homeassistant/components/doorbird/device.py @@ -5,9 +5,11 @@ from __future__ import annotations from collections import defaultdict from dataclasses import dataclass from functools import cached_property +from http import HTTPStatus import logging from typing import Any +from aiohttp import ClientResponseError from doorbirdpy import ( DoorBird, DoorBirdScheduleEntry, @@ -170,15 +172,21 @@ class ConfiguredDoorBird: ) -> DoorbirdEventConfig: """Get events and unconfigured favorites from http favorites.""" device = self.device - schedule = await device.schedule() + events: list[DoorbirdEvent] = [] + unconfigured_favorites: defaultdict[str, list[str]] = defaultdict(list) + try: + schedule = await device.schedule() + except ClientResponseError as ex: + if ex.status == HTTPStatus.NOT_FOUND: + # D301 models do not support schedules + return DoorbirdEventConfig(events, [], unconfigured_favorites) + raise favorite_input_type = { output.param: entry.input for entry in schedule for output in entry.output if output.event == HTTP_EVENT_TYPE } - events: list[DoorbirdEvent] = [] - unconfigured_favorites: defaultdict[str, list[str]] = defaultdict(list) default_event_types = { self._get_event_name(event): event_type for event, event_type in DEFAULT_EVENT_TYPES diff --git a/tests/components/doorbird/__init__.py b/tests/components/doorbird/__init__.py index 41def92f121..2d517dfcefe 100644 --- a/tests/components/doorbird/__init__.py +++ b/tests/components/doorbird/__init__.py @@ -47,31 +47,30 @@ def get_mock_doorbird_api( info: dict[str, Any] | None = None, info_side_effect: Exception | None = None, schedule: list[DoorBirdScheduleEntry] | None = None, + schedule_side_effect: Exception | None = None, favorites: dict[str, dict[str, Any]] | None = None, favorites_side_effect: Exception | None = None, change_schedule: tuple[bool, int] | None = None, ) -> DoorBird: """Return a mock DoorBirdAPI object with return values.""" doorbirdapi_mock = MagicMock(spec_set=DoorBird) - type(doorbirdapi_mock).info = AsyncMock( - side_effect=info_side_effect, return_value=info + api_mock_type = type(doorbirdapi_mock) + api_mock_type.info = AsyncMock(side_effect=info_side_effect, return_value=info) + api_mock_type.favorites = AsyncMock( + side_effect=favorites_side_effect, return_value=favorites ) - type(doorbirdapi_mock).favorites = AsyncMock( - side_effect=favorites_side_effect, - return_value=favorites, - ) - type(doorbirdapi_mock).change_favorite = AsyncMock(return_value=True) - type(doorbirdapi_mock).change_schedule = AsyncMock( + api_mock_type.change_favorite = AsyncMock(return_value=True) + api_mock_type.change_schedule = AsyncMock( return_value=change_schedule or (True, 200) ) - type(doorbirdapi_mock).schedule = AsyncMock(return_value=schedule) - type(doorbirdapi_mock).energize_relay = AsyncMock(return_value=True) - type(doorbirdapi_mock).turn_light_on = AsyncMock(return_value=True) - type(doorbirdapi_mock).delete_favorite = AsyncMock(return_value=True) - type(doorbirdapi_mock).get_image = AsyncMock(return_value=b"image") - type(doorbirdapi_mock).doorbell_state = AsyncMock( - side_effect=mock_unauthorized_exception() + api_mock_type.schedule = AsyncMock( + return_value=schedule, side_effect=schedule_side_effect ) + api_mock_type.energize_relay = AsyncMock(return_value=True) + api_mock_type.turn_light_on = AsyncMock(return_value=True) + api_mock_type.delete_favorite = AsyncMock(return_value=True) + api_mock_type.get_image = AsyncMock(return_value=b"image") + api_mock_type.doorbell_state = AsyncMock(side_effect=mock_unauthorized_exception()) return doorbirdapi_mock diff --git a/tests/components/doorbird/conftest.py b/tests/components/doorbird/conftest.py index 59ead250293..2e367e4e1d8 100644 --- a/tests/components/doorbird/conftest.py +++ b/tests/components/doorbird/conftest.py @@ -102,6 +102,7 @@ async def doorbird_mocker( info: dict[str, Any] | None = None, info_side_effect: Exception | None = None, schedule: list[DoorBirdScheduleEntry] | None = None, + schedule_side_effect: Exception | None = None, favorites: dict[str, dict[str, Any]] | None = None, favorites_side_effect: Exception | None = None, options: dict[str, Any] | None = None, @@ -118,6 +119,7 @@ async def doorbird_mocker( info=info or doorbird_info, info_side_effect=info_side_effect, schedule=schedule or doorbird_schedule, + schedule_side_effect=schedule_side_effect, favorites=favorites or doorbird_favorites, favorites_side_effect=favorites_side_effect, change_schedule=change_schedule, diff --git a/tests/components/doorbird/test_init.py b/tests/components/doorbird/test_init.py index fb8bad2fb46..31266c4acf0 100644 --- a/tests/components/doorbird/test_init.py +++ b/tests/components/doorbird/test_init.py @@ -56,6 +56,16 @@ async def test_http_favorites_request_fails( assert doorbird_entry.entry.state is ConfigEntryState.SETUP_RETRY +async def test_http_schedule_api_missing( + doorbird_mocker: DoorbirdMockerType, +) -> None: + """Test missing the schedule API is non-fatal as not all models support it.""" + doorbird_entry = await doorbird_mocker( + schedule_side_effect=mock_not_found_exception() + ) + assert doorbird_entry.entry.state is ConfigEntryState.LOADED + + async def test_events_changed( hass: HomeAssistant, doorbird_mocker: DoorbirdMockerType, From b06a5af069cbf13e96e452db23c0966b3b49b193 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 2 Aug 2024 12:53:39 +0200 Subject: [PATCH 19/95] Address post-merge reviews for KNX integration (#123038) --- homeassistant/components/knx/__init__.py | 2 +- homeassistant/components/knx/binary_sensor.py | 13 +++---- homeassistant/components/knx/button.py | 14 ++++---- homeassistant/components/knx/climate.py | 14 +++++--- homeassistant/components/knx/cover.py | 13 +++---- homeassistant/components/knx/date.py | 14 +++++--- homeassistant/components/knx/datetime.py | 12 ++++--- homeassistant/components/knx/fan.py | 13 +++---- homeassistant/components/knx/knx_entity.py | 36 ++++++++++++++++--- homeassistant/components/knx/light.py | 30 ++++++++-------- homeassistant/components/knx/notify.py | 14 +++++--- homeassistant/components/knx/number.py | 12 ++++--- homeassistant/components/knx/project.py | 6 ++-- homeassistant/components/knx/scene.py | 13 +++---- homeassistant/components/knx/select.py | 12 ++++--- homeassistant/components/knx/sensor.py | 17 +++++---- .../components/knx/storage/config_store.py | 28 ++++++++------- homeassistant/components/knx/switch.py | 30 ++++++++-------- homeassistant/components/knx/text.py | 12 ++++--- homeassistant/components/knx/time.py | 17 +++++---- homeassistant/components/knx/weather.py | 14 +++++--- 21 files changed, 211 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 709a82b31fd..fd46cad8489 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -302,7 +302,7 @@ class KNXModule: self.entry = entry self.project = KNXProject(hass=hass, entry=entry) - self.config_store = KNXConfigStore(hass=hass, entry=entry) + self.config_store = KNXConfigStore(hass=hass, config_entry=entry) self.xknx = XKNX( connection_config=self.connection_config(), diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 0423c1d7b32..ff15f725fae 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any -from xknx import XKNX from xknx.devices import BinarySensor as XknxBinarySensor from homeassistant import config_entries @@ -23,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ATTR_COUNTER, ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import BinarySensorSchema @@ -34,11 +34,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the KNX binary sensor platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: ConfigType = hass.data[DATA_KNX_CONFIG] async_add_entities( - KNXBinarySensor(xknx, entity_config) + KNXBinarySensor(knx_module, entity_config) for entity_config in config[Platform.BINARY_SENSOR] ) @@ -48,11 +48,12 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity): _device: XknxBinarySensor - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX binary sensor.""" super().__init__( + knx_module=knx_module, device=XknxBinarySensor( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address_state=config[BinarySensorSchema.CONF_STATE_ADDRESS], invert=config[BinarySensorSchema.CONF_INVERT], @@ -62,7 +63,7 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity): ], context_timeout=config.get(BinarySensorSchema.CONF_CONTEXT_TIMEOUT), reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER), - ) + ), ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_device_class = config.get(CONF_DEVICE_CLASS) diff --git a/homeassistant/components/knx/button.py b/homeassistant/components/knx/button.py index a38d8ad1b6c..2eb68eebe43 100644 --- a/homeassistant/components/knx/button.py +++ b/homeassistant/components/knx/button.py @@ -2,7 +2,6 @@ from __future__ import annotations -from xknx import XKNX from xknx.devices import RawValue as XknxRawValue from homeassistant import config_entries @@ -12,6 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import CONF_PAYLOAD_LENGTH, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity @@ -22,11 +22,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the KNX binary sensor platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: ConfigType = hass.data[DATA_KNX_CONFIG] async_add_entities( - KNXButton(xknx, entity_config) for entity_config in config[Platform.BUTTON] + KNXButton(knx_module, entity_config) + for entity_config in config[Platform.BUTTON] ) @@ -35,15 +36,16 @@ class KNXButton(KnxEntity, ButtonEntity): _device: XknxRawValue - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX button.""" super().__init__( + knx_module=knx_module, device=XknxRawValue( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], payload_length=config[CONF_PAYLOAD_LENGTH], group_address=config[KNX_ADDRESS], - ) + ), ) self._payload = config[CONF_PAYLOAD] self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 26be6a03a79..7470d60ef4b 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -27,6 +27,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONTROLLER_MODES, CURRENT_HVAC_ACTIONS, @@ -48,10 +49,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up climate(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.CLIMATE] - async_add_entities(KNXClimate(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXClimate(knx_module, entity_config) for entity_config in config + ) def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate: @@ -137,9 +140,12 @@ class KNXClimate(KnxEntity, ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _enable_turn_on_off_backwards_compatibility = False - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of a KNX climate device.""" - super().__init__(_create_climate(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_climate(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE if self._device.supports_on_off: diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 9d86d6ac272..1962db0ad3f 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import Callable from typing import Any -from xknx import XKNX from xknx.devices import Cover as XknxCover from homeassistant import config_entries @@ -26,6 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import CoverSchema @@ -37,10 +37,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up cover(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.COVER] - async_add_entities(KNXCover(xknx, entity_config) for entity_config in config) + async_add_entities(KNXCover(knx_module, entity_config) for entity_config in config) class KNXCover(KnxEntity, CoverEntity): @@ -48,11 +48,12 @@ class KNXCover(KnxEntity, CoverEntity): _device: XknxCover - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize the cover.""" super().__init__( + knx_module=knx_module, device=XknxCover( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS), group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS), @@ -70,7 +71,7 @@ class KNXCover(KnxEntity, CoverEntity): invert_updown=config[CoverSchema.CONF_INVERT_UPDOWN], invert_position=config[CoverSchema.CONF_INVERT_POSITION], invert_angle=config[CoverSchema.CONF_INVERT_ANGLE], - ) + ), ) self._unsubscribe_auto_updater: Callable[[], None] | None = None diff --git a/homeassistant/components/knx/date.py b/homeassistant/components/knx/date.py index 98cd22e0751..80fea63d0a6 100644 --- a/homeassistant/components/knx/date.py +++ b/homeassistant/components/knx/date.py @@ -22,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -39,10 +40,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATE] - async_add_entities(KNXDateEntity(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXDateEntity(knx_module, entity_config) for entity_config in config + ) def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateDevice: @@ -63,9 +66,12 @@ class KNXDateEntity(KnxEntity, DateEntity, RestoreEntity): _device: XknxDateDevice - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX time.""" - super().__init__(_create_xknx_device(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_xknx_device(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/datetime.py b/homeassistant/components/knx/datetime.py index d4a25b522eb..16ccb7474a7 100644 --- a/homeassistant/components/knx/datetime.py +++ b/homeassistant/components/knx/datetime.py @@ -23,6 +23,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -40,11 +41,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATETIME] async_add_entities( - KNXDateTimeEntity(xknx, entity_config) for entity_config in config + KNXDateTimeEntity(knx_module, entity_config) for entity_config in config ) @@ -66,9 +67,12 @@ class KNXDateTimeEntity(KnxEntity, DateTimeEntity, RestoreEntity): _device: XknxDateTimeDevice - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX time.""" - super().__init__(_create_xknx_device(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_xknx_device(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 426a750f766..940e241ccda 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -5,7 +5,6 @@ from __future__ import annotations import math from typing import Any, Final -from xknx import XKNX from xknx.devices import Fan as XknxFan from homeassistant import config_entries @@ -20,6 +19,7 @@ from homeassistant.util.percentage import ( ) from homeassistant.util.scaling import int_states_in_range +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import FanSchema @@ -33,10 +33,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up fan(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.FAN] - async_add_entities(KNXFan(xknx, entity_config) for entity_config in config) + async_add_entities(KNXFan(knx_module, entity_config) for entity_config in config) class KNXFan(KnxEntity, FanEntity): @@ -45,12 +45,13 @@ class KNXFan(KnxEntity, FanEntity): _device: XknxFan _enable_turn_on_off_backwards_compatibility = False - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX fan.""" max_step = config.get(FanSchema.CONF_MAX_STEP) super().__init__( + knx_module=knx_module, device=XknxFan( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address_speed=config.get(KNX_ADDRESS), group_address_speed_state=config.get(FanSchema.CONF_STATE_ADDRESS), @@ -61,7 +62,7 @@ class KNXFan(KnxEntity, FanEntity): FanSchema.CONF_OSCILLATION_STATE_ADDRESS ), max_step=max_step, - ) + ), ) # FanSpeedMode.STEP if max_step is set self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index eebddbb0623..2b8d2e71186 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -2,23 +2,29 @@ from __future__ import annotations -from typing import cast +from typing import TYPE_CHECKING from xknx.devices import Device as XknxDevice +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import KNXModule from .const import DOMAIN +if TYPE_CHECKING: + from . import KNXModule + +SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal.{{}}" + class KnxEntity(Entity): """Representation of a KNX entity.""" _attr_should_poll = False - def __init__(self, device: XknxDevice) -> None: + def __init__(self, knx_module: KNXModule, device: XknxDevice) -> None: """Set up device.""" + self._knx_module = knx_module self._device = device @property @@ -29,8 +35,7 @@ class KnxEntity(Entity): @property def available(self) -> bool: """Return True if entity is available.""" - knx_module = cast(KNXModule, self.hass.data[DOMAIN]) - return knx_module.connected + return self._knx_module.connected async def async_update(self) -> None: """Request a state update from KNX bus.""" @@ -44,8 +49,29 @@ class KnxEntity(Entity): """Store register state change callback and start device object.""" self._device.register_device_updated_cb(self.after_update_callback) self._device.xknx.devices.async_add(self._device) + # super call needed to have methods of mulit-inherited classes called + # eg. for restoring state (like _KNXSwitch) + await super().async_added_to_hass() async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" self._device.unregister_device_updated_cb(self.after_update_callback) self._device.xknx.devices.async_remove(self._device) + + +class KnxUIEntity(KnxEntity): + """Representation of a KNX UI entity.""" + + _attr_unique_id: str + + async def async_added_to_hass(self) -> None: + """Register callbacks when entity added to hass.""" + await super().async_added_to_hass() + self._knx_module.config_store.entities.add(self._attr_unique_id) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_ENTITY_REMOVE.format(self._attr_unique_id), + self.async_remove, + ) + ) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 8ec42f3ee56..1197f09354b 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -27,7 +27,7 @@ import homeassistant.util.color as color_util from . import KNXModule from .const import CONF_SYNC_STATE, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, ColorTempModes -from .knx_entity import KnxEntity +from .knx_entity import KnxEntity, KnxUIEntity from .schema import LightSchema from .storage.const import ( CONF_COLOR_TEMP_MAX, @@ -65,10 +65,10 @@ async def async_setup_entry( knx_module: KNXModule = hass.data[DOMAIN] entities: list[KnxEntity] = [] - if yaml_config := hass.data[DATA_KNX_CONFIG].get(Platform.LIGHT): + if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.LIGHT): entities.extend( - KnxYamlLight(knx_module.xknx, entity_config) - for entity_config in yaml_config + KnxYamlLight(knx_module, entity_config) + for entity_config in yaml_platform_config ) if ui_config := knx_module.config_store.data["entities"].get(Platform.LIGHT): entities.extend( @@ -294,7 +294,7 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight ) -class _KnxLight(KnxEntity, LightEntity): +class _KnxLight(LightEntity): """Representation of a KNX light.""" _attr_max_color_temp_kelvin: int @@ -519,14 +519,17 @@ class _KnxLight(KnxEntity, LightEntity): await self._device.set_off() -class KnxYamlLight(_KnxLight): +class KnxYamlLight(_KnxLight, KnxEntity): """Representation of a KNX light.""" _device: XknxLight - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX light.""" - super().__init__(_create_yaml_light(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_yaml_light(knx_module.xknx, config), + ) self._attr_max_color_temp_kelvin: int = config[LightSchema.CONF_MAX_KELVIN] self._attr_min_color_temp_kelvin: int = config[LightSchema.CONF_MIN_KELVIN] self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) @@ -543,20 +546,21 @@ class KnxYamlLight(_KnxLight): ) -class KnxUiLight(_KnxLight): +class KnxUiLight(_KnxLight, KnxUIEntity): """Representation of a KNX light.""" - _device: XknxLight _attr_has_entity_name = True + _device: XknxLight def __init__( self, knx_module: KNXModule, unique_id: str, config: ConfigType ) -> None: """Initialize of KNX light.""" super().__init__( - _create_ui_light( + knx_module=knx_module, + device=_create_ui_light( knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME] - ) + ), ) self._attr_max_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MAX] self._attr_min_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MIN] @@ -565,5 +569,3 @@ class KnxUiLight(_KnxLight): self._attr_unique_id = unique_id if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO): self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)}) - - knx_module.config_store.entities[unique_id] = self diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 997bdb81057..b349681990c 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity @@ -44,7 +45,7 @@ async def async_get_service( class KNXNotificationService(BaseNotificationService): - """Implement demo notification service.""" + """Implement notification service.""" def __init__(self, devices: list[XknxNotification]) -> None: """Initialize the service.""" @@ -86,10 +87,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up notify(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.NOTIFY] - async_add_entities(KNXNotify(xknx, entity_config) for entity_config in config) + async_add_entities(KNXNotify(knx_module, entity_config) for entity_config in config) def _create_notification_instance(xknx: XKNX, config: ConfigType) -> XknxNotification: @@ -107,9 +108,12 @@ class KNXNotify(KnxEntity, NotifyEntity): _device: XknxNotification - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX notification.""" - super().__init__(_create_notification_instance(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_notification_instance(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index 8a9f1dea87c..3d4af503dff 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -39,10 +40,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up number(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.NUMBER] - async_add_entities(KNXNumber(xknx, entity_config) for entity_config in config) + async_add_entities(KNXNumber(knx_module, entity_config) for entity_config in config) def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue: @@ -62,9 +63,12 @@ class KNXNumber(KnxEntity, RestoreNumber): _device: NumericValue - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX number.""" - super().__init__(_create_numeric_value(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_numeric_value(knx_module.xknx, config), + ) self._attr_native_max_value = config.get( NumberSchema.CONF_MAX, self._device.sensor_value.dpt_class.value_max, diff --git a/homeassistant/components/knx/project.py b/homeassistant/components/knx/project.py index 3b3309dfc7d..b5bafe00724 100644 --- a/homeassistant/components/knx/project.py +++ b/homeassistant/components/knx/project.py @@ -8,9 +8,11 @@ from typing import Final from xknx import XKNX from xknx.dpt import DPTBase +from xknx.telegram.address import DeviceAddressableType from xknxproject import XKNXProj from xknxproject.models import ( Device, + DPTType, GroupAddress as GroupAddressModel, KNXProject as KNXProjectModel, ProjectInfo, @@ -89,7 +91,7 @@ class KNXProject: self.devices = project["devices"] self.info = project["info"] xknx.group_address_dpt.clear() - xknx_ga_dict = {} + xknx_ga_dict: dict[DeviceAddressableType, DPTType] = {} for ga_model in project["group_addresses"].values(): ga_info = _create_group_address_info(ga_model) @@ -97,7 +99,7 @@ class KNXProject: if (dpt_model := ga_model.get("dpt")) is not None: xknx_ga_dict[ga_model["address"]] = dpt_model - xknx.group_address_dpt.set(xknx_ga_dict) # type: ignore[arg-type] + xknx.group_address_dpt.set(xknx_ga_dict) _LOGGER.debug( "Loaded KNX project data with %s group addresses from storage", diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index 342d0f9eb83..fc37f36dd01 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any -from xknx import XKNX from xknx.devices import Scene as XknxScene from homeassistant import config_entries @@ -14,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import SceneSchema @@ -25,10 +25,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up scene(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.SCENE] - async_add_entities(KNXScene(xknx, entity_config) for entity_config in config) + async_add_entities(KNXScene(knx_module, entity_config) for entity_config in config) class KNXScene(KnxEntity, Scene): @@ -36,15 +36,16 @@ class KNXScene(KnxEntity, Scene): _device: XknxScene - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Init KNX scene.""" super().__init__( + knx_module=knx_module, device=XknxScene( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address=config[KNX_ADDRESS], scene_number=config[SceneSchema.CONF_SCENE_NUMBER], - ) + ), ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = ( diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index f338bf9feaf..1b862010c2a 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -20,6 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_PAYLOAD_LENGTH, CONF_RESPOND_TO_READ, @@ -39,10 +40,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up select(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.SELECT] - async_add_entities(KNXSelect(xknx, entity_config) for entity_config in config) + async_add_entities(KNXSelect(knx_module, entity_config) for entity_config in config) def _create_raw_value(xknx: XKNX, config: ConfigType) -> RawValue: @@ -63,9 +64,12 @@ class KNXSelect(KnxEntity, SelectEntity, RestoreEntity): _device: RawValue - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX select.""" - super().__init__(_create_raw_value(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_raw_value(knx_module.xknx, config), + ) self._option_payloads: dict[str, int] = { option[SelectSchema.CONF_OPTION]: option[CONF_PAYLOAD] for option in config[SelectSchema.CONF_OPTIONS] diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 5a09a921901..ab363e2a35f 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -116,17 +116,17 @@ async def async_setup_entry( ) -> None: """Set up sensor(s) for KNX platform.""" knx_module: KNXModule = hass.data[DOMAIN] - - async_add_entities( + entities: list[SensorEntity] = [] + entities.extend( KNXSystemSensor(knx_module, description) for description in SYSTEM_ENTITY_DESCRIPTIONS ) - config: list[ConfigType] = hass.data[DATA_KNX_CONFIG].get(Platform.SENSOR) if config: - async_add_entities( - KNXSensor(knx_module.xknx, entity_config) for entity_config in config + entities.extend( + KNXSensor(knx_module, entity_config) for entity_config in config ) + async_add_entities(entities) def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor: @@ -146,9 +146,12 @@ class KNXSensor(KnxEntity, SensorEntity): _device: XknxSensor - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of a KNX sensor.""" - super().__init__(_create_sensor(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_sensor(knx_module.xknx, config), + ) if device_class := config.get(CONF_DEVICE_CLASS): self._attr_device_class = device_class else: diff --git a/homeassistant/components/knx/storage/config_store.py b/homeassistant/components/knx/storage/config_store.py index 7ea61e1dd3e..876fe19a4b9 100644 --- a/homeassistant/components/knx/storage/config_store.py +++ b/homeassistant/components/knx/storage/config_store.py @@ -2,21 +2,20 @@ from collections.abc import Callable import logging -from typing import TYPE_CHECKING, Any, Final, TypedDict +from typing import Any, Final, TypedDict from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PLATFORM, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store from homeassistant.util.ulid import ulid_now from ..const import DOMAIN +from ..knx_entity import SIGNAL_ENTITY_REMOVE from .const import CONF_DATA -if TYPE_CHECKING: - from ..knx_entity import KnxEntity - _LOGGER = logging.getLogger(__name__) STORAGE_VERSION: Final = 1 @@ -40,15 +39,16 @@ class KNXConfigStore: def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, + config_entry: ConfigEntry, ) -> None: """Initialize config store.""" self.hass = hass + self.config_entry = config_entry self._store = Store[KNXConfigStoreModel](hass, STORAGE_VERSION, STORAGE_KEY) self.data = KNXConfigStoreModel(entities={}) - # entities and async_add_entity are filled by platform setups - self.entities: dict[str, KnxEntity] = {} # unique_id as key + # entities and async_add_entity are filled by platform / entity setups + self.entities: set[str] = set() # unique_id as values self.async_add_entity: dict[ Platform, Callable[[str, dict[str, Any]], None] ] = {} @@ -108,7 +108,7 @@ class KNXConfigStore: raise ConfigStoreException( f"Entity not found in storage: {entity_id} - {unique_id}" ) - await self.entities.pop(unique_id).async_remove() + async_dispatcher_send(self.hass, SIGNAL_ENTITY_REMOVE.format(unique_id)) self.async_add_entity[platform](unique_id, data) # store data after entity is added to make sure config doesn't raise exceptions self.data["entities"][platform][unique_id] = data @@ -126,7 +126,7 @@ class KNXConfigStore: f"Entity not found in {entry.domain}: {entry.unique_id}" ) from err try: - del self.entities[entry.unique_id] + self.entities.remove(entry.unique_id) except KeyError: _LOGGER.warning("Entity not initialized when deleted: %s", entity_id) entity_registry.async_remove(entity_id) @@ -134,10 +134,14 @@ class KNXConfigStore: def get_entity_entries(self) -> list[er.RegistryEntry]: """Get entity_ids of all configured entities by platform.""" + entity_registry = er.async_get(self.hass) + return [ - entity.registry_entry - for entity in self.entities.values() - if entity.registry_entry is not None + registry_entry + for registry_entry in er.async_entries_for_config_entry( + entity_registry, self.config_entry.entry_id + ) + if registry_entry.unique_id in self.entities ] diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 0a8a1dff964..a5f430e6157 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any -from xknx import XKNX from xknx.devices import Switch as XknxSwitch from homeassistant import config_entries @@ -33,7 +32,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxEntity, KnxUIEntity from .schema import SwitchSchema from .storage.const import ( CONF_DEVICE_INFO, @@ -54,10 +53,10 @@ async def async_setup_entry( knx_module: KNXModule = hass.data[DOMAIN] entities: list[KnxEntity] = [] - if yaml_config := hass.data[DATA_KNX_CONFIG].get(Platform.SWITCH): + if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.SWITCH): entities.extend( - KnxYamlSwitch(knx_module.xknx, entity_config) - for entity_config in yaml_config + KnxYamlSwitch(knx_module, entity_config) + for entity_config in yaml_platform_config ) if ui_config := knx_module.config_store.data["entities"].get(Platform.SWITCH): entities.extend( @@ -75,7 +74,7 @@ async def async_setup_entry( knx_module.config_store.async_add_entity[Platform.SWITCH] = add_new_ui_switch -class _KnxSwitch(KnxEntity, SwitchEntity, RestoreEntity): +class _KnxSwitch(SwitchEntity, RestoreEntity): """Base class for a KNX switch.""" _device: XknxSwitch @@ -103,36 +102,41 @@ class _KnxSwitch(KnxEntity, SwitchEntity, RestoreEntity): await self._device.set_off() -class KnxYamlSwitch(_KnxSwitch): +class KnxYamlSwitch(_KnxSwitch, KnxEntity): """Representation of a KNX switch configured from YAML.""" - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + _device: XknxSwitch + + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of KNX switch.""" super().__init__( + knx_module=knx_module, device=XknxSwitch( - xknx, + xknx=knx_module.xknx, name=config[CONF_NAME], group_address=config[KNX_ADDRESS], group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS), respond_to_read=config[CONF_RESPOND_TO_READ], invert=config[SwitchSchema.CONF_INVERT], - ) + ), ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._attr_unique_id = str(self._device.switch.group_address) -class KnxUiSwitch(_KnxSwitch): +class KnxUiSwitch(_KnxSwitch, KnxUIEntity): """Representation of a KNX switch configured from UI.""" _attr_has_entity_name = True + _device: XknxSwitch def __init__( self, knx_module: KNXModule, unique_id: str, config: dict[str, Any] ) -> None: """Initialize of KNX switch.""" super().__init__( + knx_module=knx_module, device=XknxSwitch( knx_module.xknx, name=config[CONF_ENTITY][CONF_NAME], @@ -144,11 +148,9 @@ class KnxUiSwitch(_KnxSwitch): respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ], sync_state=config[DOMAIN][CONF_SYNC_STATE], invert=config[DOMAIN][CONF_INVERT], - ) + ), ) self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY] self._attr_unique_id = unique_id if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO): self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)}) - - knx_module.config_store.entities[unique_id] = self diff --git a/homeassistant/components/knx/text.py b/homeassistant/components/knx/text.py index 22d008cd5ce..9bca37434ac 100644 --- a/homeassistant/components/knx/text.py +++ b/homeassistant/components/knx/text.py @@ -22,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -38,10 +39,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensor(s) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.TEXT] - async_add_entities(KNXText(xknx, entity_config) for entity_config in config) + async_add_entities(KNXText(knx_module, entity_config) for entity_config in config) def _create_notification(xknx: XKNX, config: ConfigType) -> XknxNotification: @@ -62,9 +63,12 @@ class KNXText(KnxEntity, TextEntity, RestoreEntity): _device: XknxNotification _attr_native_max = 14 - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX text.""" - super().__init__(_create_notification(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_notification(knx_module.xknx, config), + ) self._attr_mode = config[CONF_MODE] self._attr_pattern = ( r"[\u0000-\u00ff]*" # Latin-1 diff --git a/homeassistant/components/knx/time.py b/homeassistant/components/knx/time.py index 28e1419233c..5d9225a1e41 100644 --- a/homeassistant/components/knx/time.py +++ b/homeassistant/components/knx/time.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import time as dt_time -from typing import Final from xknx import XKNX from xknx.devices import TimeDevice as XknxTimeDevice @@ -23,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, @@ -33,8 +33,6 @@ from .const import ( ) from .knx_entity import KnxEntity -_TIME_TRANSLATION_FORMAT: Final = "%H:%M:%S" - async def async_setup_entry( hass: HomeAssistant, @@ -42,10 +40,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.TIME] - async_add_entities(KNXTimeEntity(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXTimeEntity(knx_module, entity_config) for entity_config in config + ) def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxTimeDevice: @@ -66,9 +66,12 @@ class KNXTimeEntity(KnxEntity, TimeEntity, RestoreEntity): _device: XknxTimeDevice - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize a KNX time.""" - super().__init__(_create_xknx_device(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_xknx_device(knx_module.xknx, config), + ) self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 584c9fd3323..11dae452e2f 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -19,6 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN from .knx_entity import KnxEntity from .schema import WeatherSchema @@ -30,10 +31,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up switch(es) for KNX platform.""" - xknx: XKNX = hass.data[DOMAIN].xknx + knx_module: KNXModule = hass.data[DOMAIN] config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.WEATHER] - async_add_entities(KNXWeather(xknx, entity_config) for entity_config in config) + async_add_entities( + KNXWeather(knx_module, entity_config) for entity_config in config + ) def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather: @@ -80,9 +83,12 @@ class KNXWeather(KnxEntity, WeatherEntity): _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND - def __init__(self, xknx: XKNX, config: ConfigType) -> None: + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: """Initialize of a KNX sensor.""" - super().__init__(_create_weather(xknx, config)) + super().__init__( + knx_module=knx_module, + device=_create_weather(knx_module.xknx, config), + ) self._attr_unique_id = str(self._device._temperature.group_address_state) # noqa: SLF001 self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) From dcae2f35ce917516d0abb8c8d45bb6931a700c37 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 2 Aug 2024 08:50:19 +0200 Subject: [PATCH 20/95] Mitigate breaking change for KNX climate schema (#123043) --- homeassistant/components/knx/schema.py | 7 +++-- homeassistant/components/knx/validation.py | 34 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 43037ad8188..c31b3d30ad0 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -56,6 +56,7 @@ from .const import ( ColorTempModes, ) from .validation import ( + backwards_compatible_xknx_climate_enum_member, dpt_base_type_validator, ga_list_validator, ga_validator, @@ -409,10 +410,12 @@ class ClimateSchema(KNXPlatformSchema): CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT ): cv.boolean, vol.Optional(CONF_OPERATION_MODES): vol.All( - cv.ensure_list, [vol.All(vol.Upper, cv.enum(HVACOperationMode))] + cv.ensure_list, + [backwards_compatible_xknx_climate_enum_member(HVACOperationMode)], ), vol.Optional(CONF_CONTROLLER_MODES): vol.All( - cv.ensure_list, [vol.All(vol.Upper, cv.enum(HVACControllerMode))] + cv.ensure_list, + [backwards_compatible_xknx_climate_enum_member(HVACControllerMode)], ), vol.Optional( CONF_DEFAULT_CONTROLLER_MODE, default=HVACMode.HEAT diff --git a/homeassistant/components/knx/validation.py b/homeassistant/components/knx/validation.py index 422b8474fd9..0283b65f899 100644 --- a/homeassistant/components/knx/validation.py +++ b/homeassistant/components/knx/validation.py @@ -1,6 +1,7 @@ """Validation helpers for KNX config schemas.""" from collections.abc import Callable +from enum import Enum import ipaddress from typing import Any @@ -104,3 +105,36 @@ sync_state_validator = vol.Any( cv.boolean, cv.matches_regex(r"^(init|expire|every)( \d*)?$"), ) + + +def backwards_compatible_xknx_climate_enum_member(enumClass: type[Enum]) -> vol.All: + """Transform a string to an enum member. + + Backwards compatible with member names of xknx 2.x climate DPT Enums + due to unintentional breaking change in HA 2024.8. + """ + + def _string_transform(value: Any) -> str: + """Upper and slugify string and substitute old member names. + + Previously this was checked against Enum values instead of names. These + looked like `FAN_ONLY = "Fan only"`, therefore the upper & replace part. + """ + if not isinstance(value, str): + raise vol.Invalid("value should be a string") + name = value.upper().replace(" ", "_") + match name: + case "NIGHT": + return "ECONOMY" + case "FROST_PROTECTION": + return "BUILDING_PROTECTION" + case "DRY": + return "DEHUMIDIFICATION" + case _: + return name + + return vol.All( + _string_transform, + vol.In(enumClass.__members__), + enumClass.__getitem__, + ) From bb597a908d4b4962175fbf4b5051e677e1da2031 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 2 Aug 2024 08:48:41 +0200 Subject: [PATCH 21/95] Use freezer in KNX tests (#123044) use freezer in tests --- tests/components/knx/test_binary_sensor.py | 27 ++++++++++++------- tests/components/knx/test_button.py | 9 ++++--- tests/components/knx/test_expose.py | 13 ++++----- tests/components/knx/test_interface_device.py | 10 ++++--- tests/components/knx/test_light.py | 11 ++++---- 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py index 1b304293a86..dbb8d2ee832 100644 --- a/tests/components/knx/test_binary_sensor.py +++ b/tests/components/knx/test_binary_sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta +from freezegun.api import FrozenDateTimeFactory + from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE from homeassistant.components.knx.schema import BinarySensorSchema from homeassistant.const import ( @@ -13,7 +15,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util from .conftest import KNXTestKit @@ -141,9 +142,12 @@ async def test_binary_sensor_ignore_internal_state( assert len(events) == 6 -async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_binary_sensor_counter( + hass: HomeAssistant, + knx: KNXTestKit, + freezer: FrozenDateTimeFactory, +) -> None: """Test KNX binary_sensor with context timeout.""" - async_fire_time_changed(hass, dt_util.utcnow()) context_timeout = 1 await knx.setup_integration( @@ -167,7 +171,8 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> No state = hass.states.get("binary_sensor.test") assert state.state is STATE_OFF assert state.attributes.get("counter") == 0 - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=context_timeout)) + freezer.tick(timedelta(seconds=context_timeout)) + async_fire_time_changed(hass) await knx.xknx.task_registry.block_till_done() # state changed twice after context timeout - once to ON with counter 1 and once to counter 0 state = hass.states.get("binary_sensor.test") @@ -188,7 +193,8 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> No state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON assert state.attributes.get("counter") == 0 - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=context_timeout)) + freezer.tick(timedelta(seconds=context_timeout)) + async_fire_time_changed(hass) await knx.xknx.task_registry.block_till_done() state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON @@ -202,10 +208,12 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit) -> No assert event.get("old_state").attributes.get("counter") == 2 -async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_binary_sensor_reset( + hass: HomeAssistant, + knx: KNXTestKit, + freezer: FrozenDateTimeFactory, +) -> None: """Test KNX binary_sensor with reset_after function.""" - async_fire_time_changed(hass, dt_util.utcnow()) - await knx.setup_integration( { BinarySensorSchema.PLATFORM: [ @@ -223,7 +231,8 @@ async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit) -> None await knx.receive_write("2/2/2", True) state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) await hass.async_block_till_done() # state reset after after timeout state = hass.states.get("binary_sensor.test") diff --git a/tests/components/knx/test_button.py b/tests/components/knx/test_button.py index 613208d5595..a05752eced1 100644 --- a/tests/components/knx/test_button.py +++ b/tests/components/knx/test_button.py @@ -3,20 +3,22 @@ from datetime import timedelta import logging +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.knx.const import CONF_PAYLOAD_LENGTH, DOMAIN, KNX_ADDRESS from homeassistant.components.knx.schema import ButtonSchema from homeassistant.const import CONF_NAME, CONF_PAYLOAD, CONF_TYPE from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util from .conftest import KNXTestKit from tests.common import async_capture_events, async_fire_time_changed -async def test_button_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_button_simple( + hass: HomeAssistant, knx: KNXTestKit, freezer: FrozenDateTimeFactory +) -> None: """Test KNX button with default payload.""" await knx.setup_integration( { @@ -38,7 +40,8 @@ async def test_button_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: # received telegrams on button GA are ignored by the entity old_state = hass.states.get("button.test") - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3)) + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) await knx.receive_write("1/2/3", False) await knx.receive_write("1/2/3", True) new_state = hass.states.get("button.test") diff --git a/tests/components/knx/test_expose.py b/tests/components/knx/test_expose.py index 96b00241ab6..c4d0acf0ce2 100644 --- a/tests/components/knx/test_expose.py +++ b/tests/components/knx/test_expose.py @@ -3,6 +3,7 @@ from datetime import timedelta from freezegun import freeze_time +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.knx import CONF_KNX_EXPOSE, DOMAIN, KNX_ADDRESS @@ -14,11 +15,10 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util from .conftest import KNXTestKit -from tests.common import async_fire_time_changed_exact +from tests.common import async_fire_time_changed async def test_binary_expose(hass: HomeAssistant, knx: KNXTestKit) -> None: @@ -206,7 +206,9 @@ async def test_expose_string(hass: HomeAssistant, knx: KNXTestKit) -> None: ) -async def test_expose_cooldown(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_expose_cooldown( + hass: HomeAssistant, knx: KNXTestKit, freezer: FrozenDateTimeFactory +) -> None: """Test an expose with cooldown.""" cooldown_time = 2 entity_id = "fake.entity" @@ -234,9 +236,8 @@ async def test_expose_cooldown(hass: HomeAssistant, knx: KNXTestKit) -> None: await hass.async_block_till_done() await knx.assert_no_telegram() # Wait for cooldown to pass - async_fire_time_changed_exact( - hass, dt_util.utcnow() + timedelta(seconds=cooldown_time) - ) + freezer.tick(timedelta(seconds=cooldown_time)) + async_fire_time_changed(hass) await hass.async_block_till_done() await knx.assert_write("1/1/8", (3,)) diff --git a/tests/components/knx/test_interface_device.py b/tests/components/knx/test_interface_device.py index 8010496ef0d..79114d4ffd5 100644 --- a/tests/components/knx/test_interface_device.py +++ b/tests/components/knx/test_interface_device.py @@ -2,6 +2,7 @@ from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory from xknx.core import XknxConnectionState, XknxConnectionType from xknx.telegram import IndividualAddress @@ -10,7 +11,6 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from homeassistant.util import dt as dt_util from .conftest import KNXTestKit @@ -19,7 +19,10 @@ from tests.typing import WebSocketGenerator async def test_diagnostic_entities( - hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry + hass: HomeAssistant, + knx: KNXTestKit, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test diagnostic entities.""" await knx.setup_integration({}) @@ -50,7 +53,8 @@ async def test_diagnostic_entities( knx.xknx.connection_manager.cemi_count_outgoing_error = 2 events = async_capture_events(hass, "state_changed") - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(events) == 3 # 5 polled sensors - 2 disabled diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index 8c966a77a0b..04f849bb555 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta +from freezegun.api import FrozenDateTimeFactory from xknx.core import XknxConnectionState from xknx.devices.light import Light as XknxLight @@ -19,7 +20,6 @@ from homeassistant.components.light import ( ) from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util from . import KnxEntityGenerator from .conftest import KNXTestKit @@ -643,7 +643,9 @@ async def test_light_rgb_individual(hass: HomeAssistant, knx: KNXTestKit) -> Non await knx.assert_write(test_blue, (45,)) -async def test_light_rgbw_individual(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_light_rgbw_individual( + hass: HomeAssistant, knx: KNXTestKit, freezer: FrozenDateTimeFactory +) -> None: """Test KNX light with rgbw color in individual GAs.""" test_red = "1/1/3" test_red_state = "1/1/4" @@ -763,9 +765,8 @@ async def test_light_rgbw_individual(hass: HomeAssistant, knx: KNXTestKit) -> No await knx.receive_write(test_green, (0,)) # # individual color debounce takes 0.2 seconds if not all 4 addresses received knx.assert_state("light.test", STATE_ON) - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=XknxLight.DEBOUNCE_TIMEOUT) - ) + freezer.tick(timedelta(seconds=XknxLight.DEBOUNCE_TIMEOUT)) + async_fire_time_changed(hass) await knx.xknx.task_registry.block_till_done() knx.assert_state("light.test", STATE_OFF) # turn ON from KNX From abeba39842d51bb5f66fafc1d11b9b008907ca0c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Aug 2024 12:08:44 +0200 Subject: [PATCH 22/95] OpenAI make supported features reflect the config entry options (#123047) --- .../openai_conversation/conversation.py | 15 +++++++++++++++ .../openai_conversation/test_conversation.py | 2 ++ 2 files changed, 17 insertions(+) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 483b37945d6..b482126e27c 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -23,6 +23,7 @@ from voluptuous_openapi import convert from homeassistant.components import assist_pipeline, conversation from homeassistant.components.conversation import trace +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, TemplateError @@ -109,6 +110,9 @@ class OpenAIConversationEntity( self.hass, "conversation", self.entry.entry_id, self.entity_id ) conversation.async_set_agent(self.hass, self.entry, self) + self.entry.async_on_unload( + self.entry.add_update_listener(self._async_entry_update_listener) + ) async def async_will_remove_from_hass(self) -> None: """When entity will be removed from Home Assistant.""" @@ -319,3 +323,14 @@ class OpenAIConversationEntity( return conversation.ConversationResult( response=intent_response, conversation_id=conversation_id ) + + async def _async_entry_update_listener( + self, hass: HomeAssistant, entry: ConfigEntry + ) -> None: + """Handle options update.""" + if entry.options.get(CONF_LLM_HASS_API): + self._attr_supported_features = ( + conversation.ConversationEntityFeature.CONTROL + ) + else: + self._attr_supported_features = conversation.ConversationEntityFeature(0) diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index 3364d822245..e0665bc449f 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -521,6 +521,8 @@ async def test_unknown_hass_api( }, ) + await hass.async_block_till_done() + result = await conversation.async_converse( hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id ) From d1411220082fcd046423be8241bad7ad6fb7ad55 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Aug 2024 12:31:31 +0200 Subject: [PATCH 23/95] Ollama implement CONTROL supported feature (#123049) --- .../components/ollama/conversation.py | 18 +++++++++++++ tests/components/ollama/test_conversation.py | 25 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ollama/conversation.py b/homeassistant/components/ollama/conversation.py index f59e268394b..9f66083f506 100644 --- a/homeassistant/components/ollama/conversation.py +++ b/homeassistant/components/ollama/conversation.py @@ -106,6 +106,10 @@ class OllamaConversationEntity( self._history: dict[str, MessageHistory] = {} self._attr_name = entry.title self._attr_unique_id = entry.entry_id + if self.entry.options.get(CONF_LLM_HASS_API): + self._attr_supported_features = ( + conversation.ConversationEntityFeature.CONTROL + ) async def async_added_to_hass(self) -> None: """When entity is added to Home Assistant.""" @@ -114,6 +118,9 @@ class OllamaConversationEntity( self.hass, "conversation", self.entry.entry_id, self.entity_id ) conversation.async_set_agent(self.hass, self.entry, self) + self.entry.async_on_unload( + self.entry.add_update_listener(self._async_entry_update_listener) + ) async def async_will_remove_from_hass(self) -> None: """When entity will be removed from Home Assistant.""" @@ -334,3 +341,14 @@ class OllamaConversationEntity( message_history.messages = [ message_history.messages[0] ] + message_history.messages[drop_index:] + + async def _async_entry_update_listener( + self, hass: HomeAssistant, entry: ConfigEntry + ) -> None: + """Handle options update.""" + if entry.options.get(CONF_LLM_HASS_API): + self._attr_supported_features = ( + conversation.ConversationEntityFeature.CONTROL + ) + else: + self._attr_supported_features = conversation.ConversationEntityFeature(0) diff --git a/tests/components/ollama/test_conversation.py b/tests/components/ollama/test_conversation.py index b5a94cc6f57..c83dce3b565 100644 --- a/tests/components/ollama/test_conversation.py +++ b/tests/components/ollama/test_conversation.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import conversation, ollama from homeassistant.components.conversation import trace -from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL +from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_LLM_HASS_API, MATCH_ALL from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import intent, llm @@ -554,3 +554,26 @@ async def test_conversation_agent( mock_config_entry.entry_id ) assert agent.supported_languages == MATCH_ALL + + state = hass.states.get("conversation.mock_title") + assert state + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + +async def test_conversation_agent_with_assist( + hass: HomeAssistant, + mock_config_entry_with_assist: MockConfigEntry, + mock_init_component, +) -> None: + """Test OllamaConversationEntity.""" + agent = conversation.get_agent_manager(hass).async_get_agent( + mock_config_entry_with_assist.entry_id + ) + assert agent.supported_languages == MATCH_ALL + + state = hass.states.get("conversation.mock_title") + assert state + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == conversation.ConversationEntityFeature.CONTROL + ) From c1043ada22a51246ba6f6644b7cc74cdf32820a9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Aug 2024 11:58:07 +0200 Subject: [PATCH 24/95] Correct type annotation for `EntityPlatform.async_register_entity_service` (#123054) Correct type annotation for EntityPlatform.async_register_entity_service Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/helpers/entity_platform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index d868e582f8f..6774780f00f 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -985,7 +985,7 @@ class EntityPlatform: def async_register_entity_service( self, name: str, - schema: VolDictType | VolSchemaType | None, + schema: VolDictType | VolSchemaType, func: str | Callable[..., Any], required_features: Iterable[int] | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, From 15ad6db1a72455a94958e13eef0ccd7484565855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=2E=20=C3=81rkosi=20R=C3=B3bert?= Date: Fri, 2 Aug 2024 14:25:43 +0200 Subject: [PATCH 25/95] Add LinkPlay models (#123056) * Add some LinkPlay models * Update utils.py * Update utils.py * Update utils.py * Update homeassistant/components/linkplay/utils.py * Update homeassistant/components/linkplay/utils.py * Update utils.py --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/linkplay/utils.py | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/homeassistant/components/linkplay/utils.py b/homeassistant/components/linkplay/utils.py index 9ca76b3933d..7532c9b354a 100644 --- a/homeassistant/components/linkplay/utils.py +++ b/homeassistant/components/linkplay/utils.py @@ -3,9 +3,19 @@ from typing import Final MANUFACTURER_ARTSOUND: Final[str] = "ArtSound" +MANUFACTURER_ARYLIC: Final[str] = "Arylic" +MANUFACTURER_IEAST: Final[str] = "iEAST" MANUFACTURER_GENERIC: Final[str] = "Generic" MODELS_ARTSOUND_SMART_ZONE4: Final[str] = "Smart Zone 4 AMP" MODELS_ARTSOUND_SMART_HYDE: Final[str] = "Smart Hyde" +MODELS_ARYLIC_S50: Final[str] = "S50+" +MODELS_ARYLIC_S50_PRO: Final[str] = "S50 Pro" +MODELS_ARYLIC_A30: Final[str] = "A30" +MODELS_ARYLIC_A50S: Final[str] = "A50+" +MODELS_ARYLIC_UP2STREAM_AMP_V3: Final[str] = "Up2Stream Amp v3" +MODELS_ARYLIC_UP2STREAM_AMP_V4: Final[str] = "Up2Stream Amp v4" +MODELS_ARYLIC_UP2STREAM_PRO_V3: Final[str] = "Up2Stream Pro v3" +MODELS_IEAST_AUDIOCAST_M5: Final[str] = "AudioCast M5" MODELS_GENERIC: Final[str] = "Generic" @@ -16,5 +26,21 @@ def get_info_from_project(project: str) -> tuple[str, str]: return MANUFACTURER_ARTSOUND, MODELS_ARTSOUND_SMART_ZONE4 case "SMART_HYDE": return MANUFACTURER_ARTSOUND, MODELS_ARTSOUND_SMART_HYDE + case "ARYLIC_S50": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_S50 + case "RP0016_S50PRO_S": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_S50_PRO + case "RP0011_WB60_S": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_A30 + case "ARYLIC_A50S": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_A50S + case "UP2STREAM_AMP_V3": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_UP2STREAM_AMP_V3 + case "UP2STREAM_AMP_V4": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_UP2STREAM_AMP_V4 + case "UP2STREAM_PRO_V3": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_UP2STREAM_PRO_V3 + case "iEAST-02": + return MANUFACTURER_IEAST, MODELS_IEAST_AUDIOCAST_M5 case _: return MANUFACTURER_GENERIC, MODELS_GENERIC From f9276e28b03bed2e88d3653cec98cf2a26686792 Mon Sep 17 00:00:00 2001 From: Fabian <115155196+Fabiann2205@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:19:55 +0200 Subject: [PATCH 26/95] Add device class (#123059) --- homeassistant/components/google_travel_time/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 6c45033eeb7..618dda50bd4 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -8,7 +8,11 @@ import logging from googlemaps import Client from googlemaps.distance_matrix import distance_matrix -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, @@ -72,6 +76,8 @@ class GoogleTravelTimeSensor(SensorEntity): _attr_attribution = ATTRIBUTION _attr_native_unit_of_measurement = UnitOfTime.MINUTES + _attr_device_class = SensorDeviceClass.DURATION + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, config_entry, name, api_key, origin, destination, client): """Initialize the sensor.""" From d7cc2a7e9a3df922e64d78ef5487e41f74b9e508 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Aug 2024 11:49:47 +0200 Subject: [PATCH 27/95] Correct squeezebox service (#123060) --- homeassistant/components/squeezebox/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index aaf64c34ddf..c0a6dad7a47 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -185,7 +185,7 @@ async def async_setup_entry( {vol.Required(ATTR_OTHER_PLAYER): cv.string}, "async_sync", ) - platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync") + platform.async_register_entity_service(SERVICE_UNSYNC, {}, "async_unsync") # Start server discovery task if not already running entry.async_on_unload(async_at_start(hass, start_server_discovery)) From 9c7134a86513e2f17b46767e01dbeff596e40125 Mon Sep 17 00:00:00 2001 From: Philip Vanloo <26272906+dukeofphilberg@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:23:45 +0200 Subject: [PATCH 28/95] LinkPlay: Bump python-linkplay to 0.0.6 (#123062) Bump python-linkplay to 0.0.6 --- homeassistant/components/linkplay/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index 0345d4ad727..9ac2a9e66e6 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/linkplay", "integration_type": "hub", "iot_class": "local_polling", - "requirements": ["python-linkplay==0.0.5"], + "requirements": ["python-linkplay==0.0.6"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 99a237efe02..747f6509604 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2298,7 +2298,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.1 # homeassistant.components.linkplay -python-linkplay==0.0.5 +python-linkplay==0.0.6 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ee6914700f..e85a748d13e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1816,7 +1816,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.1 # homeassistant.components.linkplay -python-linkplay==0.0.5 +python-linkplay==0.0.6 # homeassistant.components.matter python-matter-server==6.3.0 From 13c9d69440813adce42ad1e5a0ac791e7e5c9f7c Mon Sep 17 00:00:00 2001 From: Philip Vanloo <26272906+dukeofphilberg@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:38:05 +0200 Subject: [PATCH 29/95] Add additional items to REPEAT_MAP in LinkPlay (#123063) * Upgrade python-linkplay, add items to REPEAT_MAP * Undo dependency bump --- homeassistant/components/linkplay/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/linkplay/media_player.py b/homeassistant/components/linkplay/media_player.py index 103b09f46da..398add235bd 100644 --- a/homeassistant/components/linkplay/media_player.py +++ b/homeassistant/components/linkplay/media_player.py @@ -58,6 +58,8 @@ REPEAT_MAP: dict[LoopMode, RepeatMode] = { LoopMode.CONTINUOUS_PLAYBACK: RepeatMode.ALL, LoopMode.RANDOM_PLAYBACK: RepeatMode.ALL, LoopMode.LIST_CYCLE: RepeatMode.ALL, + LoopMode.SHUFF_DISABLED_REPEAT_DISABLED: RepeatMode.OFF, + LoopMode.SHUFF_ENABLED_REPEAT_ENABLED_LOOP_ONCE: RepeatMode.ALL, } REPEAT_MAP_INV: dict[RepeatMode, LoopMode] = {v: k for k, v in REPEAT_MAP.items()} From b36059fc64c6fd27b33069df234fdea468467573 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 2 Aug 2024 13:38:56 +0200 Subject: [PATCH 30/95] Do not raise repair issue about missing integration in safe mode (#123066) --- homeassistant/setup.py | 27 ++++++++++++++------------- tests/test_setup.py | 11 ++++++++++- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 12dd17b289c..102c48e1d07 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -281,19 +281,20 @@ async def _async_setup_component( integration = await loader.async_get_integration(hass, domain) except loader.IntegrationNotFound: _log_error_setup_error(hass, domain, None, "Integration not found.") - ir.async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"integration_not_found.{domain}", - is_fixable=True, - issue_domain=HOMEASSISTANT_DOMAIN, - severity=IssueSeverity.ERROR, - translation_key="integration_not_found", - translation_placeholders={ - "domain": domain, - }, - data={"domain": domain}, - ) + if not hass.config.safe_mode: + ir.async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"integration_not_found.{domain}", + is_fixable=True, + issue_domain=HOMEASSISTANT_DOMAIN, + severity=IssueSeverity.ERROR, + translation_key="integration_not_found", + translation_placeholders={ + "domain": domain, + }, + data={"domain": domain}, + ) return False log_error = partial(_log_error_setup_error, hass, domain, integration) diff --git a/tests/test_setup.py b/tests/test_setup.py index 3430c17960c..4e7c23865da 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -245,7 +245,7 @@ async def test_validate_platform_config_4(hass: HomeAssistant) -> None: async def test_component_not_found( hass: HomeAssistant, issue_registry: IssueRegistry ) -> None: - """setup_component should not crash if component doesn't exist.""" + """setup_component should raise a repair issue if component doesn't exist.""" assert await setup.async_setup_component(hass, "non_existing", {}) is False assert len(issue_registry.issues) == 1 issue = issue_registry.async_get_issue( @@ -255,6 +255,15 @@ async def test_component_not_found( assert issue.translation_key == "integration_not_found" +async def test_component_missing_not_raising_in_safe_mode( + hass: HomeAssistant, issue_registry: IssueRegistry +) -> None: + """setup_component should not raise an issue if component doesn't exist in safe.""" + hass.config.safe_mode = True + assert await setup.async_setup_component(hass, "non_existing", {}) is False + assert len(issue_registry.issues) == 0 + + async def test_component_not_double_initialized(hass: HomeAssistant) -> None: """Test we do not set up a component twice.""" mock_setup = Mock(return_value=True) From 433c1a57e76b9206e4f201c4b9e42fbc9eccba25 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 Aug 2024 16:48:37 +0200 Subject: [PATCH 31/95] Update frontend to 20240802.0 (#123072) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 60cfa0a26ff..95afe1221ec 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240731.0"] + "requirements": ["home-assistant-frontend==20240802.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 58f39907269..1cc6a0fa85d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240731.0 +home-assistant-frontend==20240802.0 home-assistant-intents==2024.7.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 747f6509604..479e22a3bfc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240731.0 +home-assistant-frontend==20240802.0 # homeassistant.components.conversation home-assistant-intents==2024.7.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e85a748d13e..9d92bde7aa8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -916,7 +916,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240731.0 +home-assistant-frontend==20240802.0 # homeassistant.components.conversation home-assistant-intents==2024.7.29 From fe82e7f24d1362a4a657c9561fd98880d0236ace Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Aug 2024 17:46:01 +0200 Subject: [PATCH 32/95] Bump version to 2024.8.0b1 --- 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 b315d8c2618..08ee0bb77f9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index e705101e4ae..c60a01663da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b0" +version = "2024.8.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 7b1bf82e3c0179637cb43d4fcc5cd3a704122632 Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Mon, 5 Aug 2024 05:18:34 -0400 Subject: [PATCH 33/95] Update greeclimate to 2.0.0 (#121030) Co-authored-by: Joostlek --- homeassistant/components/gree/const.py | 2 + homeassistant/components/gree/coordinator.py | 65 ++++++- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gree/test_bridge.py | 35 +++- tests/components/gree/test_climate.py | 187 ++++++++++--------- 7 files changed, 190 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/gree/const.py b/homeassistant/components/gree/const.py index 46479210921..f926eb1c53e 100644 --- a/homeassistant/components/gree/const.py +++ b/homeassistant/components/gree/const.py @@ -18,3 +18,5 @@ FAN_MEDIUM_HIGH = "medium high" MAX_ERRORS = 2 TARGET_TEMPERATURE_STEP = 1 + +UPDATE_INTERVAL = 60 diff --git a/homeassistant/components/gree/coordinator.py b/homeassistant/components/gree/coordinator.py index 1bccf3bbc48..ae8b22706ef 100644 --- a/homeassistant/components/gree/coordinator.py +++ b/homeassistant/components/gree/coordinator.py @@ -2,16 +2,20 @@ from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging +from typing import Any from greeclimate.device import Device, DeviceInfo from greeclimate.discovery import Discovery, Listener from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError +from greeclimate.network import Response from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.json import json_dumps from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.dt import utcnow from .const import ( COORDINATORS, @@ -19,12 +23,13 @@ from .const import ( DISPATCH_DEVICE_DISCOVERED, DOMAIN, MAX_ERRORS, + UPDATE_INTERVAL, ) _LOGGER = logging.getLogger(__name__) -class DeviceDataUpdateCoordinator(DataUpdateCoordinator): +class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Manages polling for state changes from the device.""" def __init__(self, hass: HomeAssistant, device: Device) -> None: @@ -34,28 +39,68 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator): hass, _LOGGER, name=f"{DOMAIN}-{device.device_info.name}", - update_interval=timedelta(seconds=60), + update_interval=timedelta(seconds=UPDATE_INTERVAL), + always_update=False, ) self.device = device - self._error_count = 0 + self.device.add_handler(Response.DATA, self.device_state_updated) + self.device.add_handler(Response.RESULT, self.device_state_updated) - async def _async_update_data(self): + self._error_count: int = 0 + self._last_response_time: datetime = utcnow() + self._last_error_time: datetime | None = None + + def device_state_updated(self, *args: Any) -> None: + """Handle device state updates.""" + _LOGGER.debug("Device state updated: %s", json_dumps(args)) + self._error_count = 0 + self._last_response_time = utcnow() + self.async_set_updated_data(self.device.raw_properties) + + async def _async_update_data(self) -> dict[str, Any]: """Update the state of the device.""" + _LOGGER.debug( + "Updating device state: %s, error count: %d", self.name, self._error_count + ) try: await self.device.update_state() except DeviceNotBoundError as error: - raise UpdateFailed(f"Device {self.name} is unavailable") from error + raise UpdateFailed( + f"Device {self.name} is unavailable, device is not bound." + ) from error except DeviceTimeoutError as error: self._error_count += 1 # Under normal conditions GREE units timeout every once in a while if self.last_update_success and self._error_count >= MAX_ERRORS: _LOGGER.warning( - "Device is unavailable: %s (%s)", - self.name, - self.device.device_info, + "Device %s is unavailable: %s", self.name, self.device.device_info ) - raise UpdateFailed(f"Device {self.name} is unavailable") from error + raise UpdateFailed( + f"Device {self.name} is unavailable, could not send update request" + ) from error + else: + # raise update failed if time for more than MAX_ERRORS has passed since last update + now = utcnow() + elapsed_success = now - self._last_response_time + if self.update_interval and elapsed_success >= self.update_interval: + if not self._last_error_time or ( + (now - self.update_interval) >= self._last_error_time + ): + self._last_error_time = now + self._error_count += 1 + + _LOGGER.warning( + "Device %s is unresponsive for %s seconds", + self.name, + elapsed_success, + ) + if self.last_update_success and self._error_count >= MAX_ERRORS: + raise UpdateFailed( + f"Device {self.name} is unresponsive for too long and now unavailable" + ) + + return self.device.raw_properties async def push_state_update(self): """Send state updates to the physical device.""" diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index a7c884c4042..ca1c4b5b754 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/gree", "iot_class": "local_polling", "loggers": ["greeclimate"], - "requirements": ["greeclimate==1.4.6"] + "requirements": ["greeclimate==2.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 479e22a3bfc..b73248c5e16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1007,7 +1007,7 @@ gpiozero==1.6.2 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.6 +greeclimate==2.0.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d92bde7aa8..a0b634d0e2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -848,7 +848,7 @@ govee-local-api==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.6 +greeclimate==2.0.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/tests/components/gree/test_bridge.py b/tests/components/gree/test_bridge.py index 37b0b0dc15e..32372bebf37 100644 --- a/tests/components/gree/test_bridge.py +++ b/tests/components/gree/test_bridge.py @@ -5,8 +5,12 @@ from datetime import timedelta from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components.climate import DOMAIN -from homeassistant.components.gree.const import COORDINATORS, DOMAIN as GREE +from homeassistant.components.climate import DOMAIN, HVACMode +from homeassistant.components.gree.const import ( + COORDINATORS, + DOMAIN as GREE, + UPDATE_INTERVAL, +) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -69,3 +73,30 @@ async def test_discovery_after_setup( device_infos = [x.device.device_info for x in hass.data[GREE][COORDINATORS]] assert device_infos[0].ip == "1.1.1.2" assert device_infos[1].ip == "2.2.2.1" + + +async def test_coordinator_updates( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device +) -> None: + """Test gree devices update their state.""" + await async_setup_gree(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all(DOMAIN)) == 1 + + callback = device().add_handler.call_args_list[0][0][1] + + async def fake_update_state(*args) -> None: + """Fake update state.""" + device().power = True + callback() + + device().update_state.side_effect = fake_update_state + + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID_1) + assert state is not None + assert state.state != HVACMode.OFF diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index e6f24ade1aa..1bf49bbca26 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -48,7 +48,12 @@ from homeassistant.components.gree.climate import ( HVAC_MODES_REVERSE, GreeClimateEntity, ) -from homeassistant.components.gree.const import FAN_MEDIUM_HIGH, FAN_MEDIUM_LOW +from homeassistant.components.gree.const import ( + DISCOVERY_SCAN_INTERVAL, + FAN_MEDIUM_HIGH, + FAN_MEDIUM_LOW, + UPDATE_INTERVAL, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, @@ -61,7 +66,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er -import homeassistant.util.dt as dt_util from .common import async_setup_gree, build_device_mock @@ -70,12 +74,6 @@ from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_device_1" -@pytest.fixture -def mock_now(): - """Fixture for dtutil.now.""" - return dt_util.utcnow() - - async def test_discovery_called_once(hass: HomeAssistant, discovery, device) -> None: """Test discovery is only ever called once.""" await async_setup_gree(hass) @@ -104,7 +102,7 @@ async def test_discovery_setup(hass: HomeAssistant, discovery, device) -> None: async def test_discovery_setup_connection_error( - hass: HomeAssistant, discovery, device, mock_now + hass: HomeAssistant, discovery, device ) -> None: """Test gree integration is setup.""" MockDevice1 = build_device_mock( @@ -126,7 +124,7 @@ async def test_discovery_setup_connection_error( async def test_discovery_after_setup( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test gree devices don't change after multiple discoveries.""" MockDevice1 = build_device_mock( @@ -142,8 +140,7 @@ async def test_discovery_after_setup( discovery.return_value.mock_devices = [MockDevice1, MockDevice2] device.side_effect = [MockDevice1, MockDevice2] - await async_setup_gree(hass) - await hass.async_block_till_done() + await async_setup_gree(hass) # Update 1 assert discovery.return_value.scan_count == 1 assert len(hass.states.async_all(DOMAIN)) == 2 @@ -152,9 +149,8 @@ async def test_discovery_after_setup( discovery.return_value.mock_devices = [MockDevice1, MockDevice2] device.side_effect = [MockDevice1, MockDevice2] - next_update = mock_now + timedelta(minutes=6) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert discovery.return_value.scan_count == 2 @@ -162,7 +158,7 @@ async def test_discovery_after_setup( async def test_discovery_add_device_after_setup( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test gree devices can be added after initial setup.""" MockDevice1 = build_device_mock( @@ -178,6 +174,8 @@ async def test_discovery_add_device_after_setup( discovery.return_value.mock_devices = [MockDevice1] device.side_effect = [MockDevice1] + await async_setup_gree(hass) # Update 1 + await async_setup_gree(hass) await hass.async_block_till_done() @@ -188,9 +186,8 @@ async def test_discovery_add_device_after_setup( discovery.return_value.mock_devices = [MockDevice2] device.side_effect = [MockDevice2] - next_update = mock_now + timedelta(minutes=6) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert discovery.return_value.scan_count == 2 @@ -198,7 +195,7 @@ async def test_discovery_add_device_after_setup( async def test_discovery_device_bind_after_setup( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test gree devices can be added after a late device bind.""" MockDevice1 = build_device_mock( @@ -210,8 +207,7 @@ async def test_discovery_device_bind_after_setup( discovery.return_value.mock_devices = [MockDevice1] device.return_value = MockDevice1 - await async_setup_gree(hass) - await hass.async_block_till_done() + await async_setup_gree(hass) # Update 1 assert len(hass.states.async_all(DOMAIN)) == 1 state = hass.states.get(ENTITY_ID) @@ -222,9 +218,8 @@ async def test_discovery_device_bind_after_setup( MockDevice1.bind.side_effect = None MockDevice1.update_state.side_effect = None - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -232,7 +227,7 @@ async def test_discovery_device_bind_after_setup( async def test_update_connection_failure( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Testing update hvac connection failure exception.""" device().update_state.side_effect = [ @@ -241,36 +236,32 @@ async def test_update_connection_failure( DeviceTimeoutError, ] - await async_setup_gree(hass) + await async_setup_gree(hass) # Update 1 + + async def run_update(): + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - # First update to make the device available + # Update 2 + await run_update() state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE - next_update = mock_now + timedelta(minutes=10) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + # Update 3 + await run_update() - next_update = mock_now + timedelta(minutes=15) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - - # Then two more update failures to make the device unavailable + # Update 4 + await run_update() state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state == STATE_UNAVAILABLE -async def test_update_connection_failure_recovery( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now +async def test_update_connection_send_failure_recovery( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Testing update hvac connection failure recovery.""" device().update_state.side_effect = [ @@ -279,31 +270,27 @@ async def test_update_connection_failure_recovery( DEFAULT_MOCK, ] - await async_setup_gree(hass) + await async_setup_gree(hass) # Update 1 + + async def run_update(): + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) - # First update becomes unavailable - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + await run_update() # Update 2 state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state == STATE_UNAVAILABLE - # Second update restores the connection - next_update = mock_now + timedelta(minutes=10) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - + await run_update() # Update 3 state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE async def test_update_unhandled_exception( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Testing update hvac connection unhandled response exception.""" device().update_state.side_effect = [DEFAULT_MOCK, Exception] @@ -314,9 +301,8 @@ async def test_update_unhandled_exception( assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE - next_update = mock_now + timedelta(minutes=10) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -325,15 +311,13 @@ async def test_update_unhandled_exception( async def test_send_command_device_timeout( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test for sending power on command to the device with a device timeout.""" await async_setup_gree(hass) - # First update to make the device available - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -355,7 +339,40 @@ async def test_send_command_device_timeout( assert state.state != STATE_UNAVAILABLE -async def test_send_power_on(hass: HomeAssistant, discovery, device, mock_now) -> None: +async def test_unresponsive_device( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device +) -> None: + """Test for unresponsive device.""" + await async_setup_gree(hass) + + async def run_update(): + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Update 2 + await run_update() + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state != STATE_UNAVAILABLE + + # Update 3, 4, 5 + await run_update() + await run_update() + await run_update() + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state == STATE_UNAVAILABLE + + # Receiving update from device will reset the state to available again + device().device_state_updated("test") + await run_update() + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state != STATE_UNAVAILABLE + + +async def test_send_power_on(hass: HomeAssistant, discovery, device) -> None: """Test for sending power on command to the device.""" await async_setup_gree(hass) @@ -372,7 +389,7 @@ async def test_send_power_on(hass: HomeAssistant, discovery, device, mock_now) - async def test_send_power_off_device_timeout( - hass: HomeAssistant, discovery, device, mock_now + hass: HomeAssistant, discovery, device ) -> None: """Test for sending power off command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -543,9 +560,7 @@ async def test_update_target_temperature( @pytest.mark.parametrize( "preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE] ) -async def test_send_preset_mode( - hass: HomeAssistant, discovery, device, mock_now, preset -) -> None: +async def test_send_preset_mode(hass: HomeAssistant, discovery, device, preset) -> None: """Test for sending preset mode command to the device.""" await async_setup_gree(hass) @@ -561,9 +576,7 @@ async def test_send_preset_mode( assert state.attributes.get(ATTR_PRESET_MODE) == preset -async def test_send_invalid_preset_mode( - hass: HomeAssistant, discovery, device, mock_now -) -> None: +async def test_send_invalid_preset_mode(hass: HomeAssistant, discovery, device) -> None: """Test for sending preset mode command to the device.""" await async_setup_gree(hass) @@ -584,7 +597,7 @@ async def test_send_invalid_preset_mode( "preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE] ) async def test_send_preset_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, preset + hass: HomeAssistant, discovery, device, preset ) -> None: """Test for sending preset mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -607,7 +620,7 @@ async def test_send_preset_mode_device_timeout( "preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE] ) async def test_update_preset_mode( - hass: HomeAssistant, discovery, device, mock_now, preset + hass: HomeAssistant, discovery, device, preset ) -> None: """Test for updating preset mode from the device.""" device().steady_heat = preset == PRESET_AWAY @@ -634,7 +647,7 @@ async def test_update_preset_mode( ], ) async def test_send_hvac_mode( - hass: HomeAssistant, discovery, device, mock_now, hvac_mode + hass: HomeAssistant, discovery, device, hvac_mode ) -> None: """Test for sending hvac mode command to the device.""" await async_setup_gree(hass) @@ -656,7 +669,7 @@ async def test_send_hvac_mode( [HVACMode.AUTO, HVACMode.COOL, HVACMode.DRY, HVACMode.FAN_ONLY, HVACMode.HEAT], ) async def test_send_hvac_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, hvac_mode + hass: HomeAssistant, discovery, device, hvac_mode ) -> None: """Test for sending hvac mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -687,7 +700,7 @@ async def test_send_hvac_mode_device_timeout( ], ) async def test_update_hvac_mode( - hass: HomeAssistant, discovery, device, mock_now, hvac_mode + hass: HomeAssistant, discovery, device, hvac_mode ) -> None: """Test for updating hvac mode from the device.""" device().power = hvac_mode != HVACMode.OFF @@ -704,9 +717,7 @@ async def test_update_hvac_mode( "fan_mode", [FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH], ) -async def test_send_fan_mode( - hass: HomeAssistant, discovery, device, mock_now, fan_mode -) -> None: +async def test_send_fan_mode(hass: HomeAssistant, discovery, device, fan_mode) -> None: """Test for sending fan mode command to the device.""" await async_setup_gree(hass) @@ -722,9 +733,7 @@ async def test_send_fan_mode( assert state.attributes.get(ATTR_FAN_MODE) == fan_mode -async def test_send_invalid_fan_mode( - hass: HomeAssistant, discovery, device, mock_now -) -> None: +async def test_send_invalid_fan_mode(hass: HomeAssistant, discovery, device) -> None: """Test for sending fan mode command to the device.""" await async_setup_gree(hass) @@ -746,7 +755,7 @@ async def test_send_invalid_fan_mode( [FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH], ) async def test_send_fan_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, fan_mode + hass: HomeAssistant, discovery, device, fan_mode ) -> None: """Test for sending fan mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -770,7 +779,7 @@ async def test_send_fan_mode_device_timeout( [FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH], ) async def test_update_fan_mode( - hass: HomeAssistant, discovery, device, mock_now, fan_mode + hass: HomeAssistant, discovery, device, fan_mode ) -> None: """Test for updating fan mode from the device.""" device().fan_speed = FAN_MODES_REVERSE.get(fan_mode) @@ -786,7 +795,7 @@ async def test_update_fan_mode( "swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL] ) async def test_send_swing_mode( - hass: HomeAssistant, discovery, device, mock_now, swing_mode + hass: HomeAssistant, discovery, device, swing_mode ) -> None: """Test for sending swing mode command to the device.""" await async_setup_gree(hass) @@ -803,9 +812,7 @@ async def test_send_swing_mode( assert state.attributes.get(ATTR_SWING_MODE) == swing_mode -async def test_send_invalid_swing_mode( - hass: HomeAssistant, discovery, device, mock_now -) -> None: +async def test_send_invalid_swing_mode(hass: HomeAssistant, discovery, device) -> None: """Test for sending swing mode command to the device.""" await async_setup_gree(hass) @@ -826,7 +833,7 @@ async def test_send_invalid_swing_mode( "swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL] ) async def test_send_swing_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, swing_mode + hass: HomeAssistant, discovery, device, swing_mode ) -> None: """Test for sending swing mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -849,7 +856,7 @@ async def test_send_swing_mode_device_timeout( "swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL] ) async def test_update_swing_mode( - hass: HomeAssistant, discovery, device, mock_now, swing_mode + hass: HomeAssistant, discovery, device, swing_mode ) -> None: """Test for updating swing mode from the device.""" device().horizontal_swing = ( From 50b7eb44d1706ad69ebc016e0fe6423932cbde93 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 3 Aug 2024 08:16:30 +0200 Subject: [PATCH 34/95] Add CONTROL supported feature to Google conversation when API access (#123046) * Add CONTROL supported feature to Google conversation when API access * Better function name * Handle entry update inline * Reload instead of update --- .../conversation.py | 14 ++++++++++++++ .../snapshots/test_conversation.ambr | 8 ++++---- .../test_conversation.py | 15 +++++++++++---- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index a5c911bb757..1d8b46cde2f 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -164,6 +164,10 @@ class GoogleGenerativeAIConversationEntity( model="Generative AI", entry_type=dr.DeviceEntryType.SERVICE, ) + if self.entry.options.get(CONF_LLM_HASS_API): + self._attr_supported_features = ( + conversation.ConversationEntityFeature.CONTROL + ) @property def supported_languages(self) -> list[str] | Literal["*"]: @@ -177,6 +181,9 @@ class GoogleGenerativeAIConversationEntity( self.hass, "conversation", self.entry.entry_id, self.entity_id ) conversation.async_set_agent(self.hass, self.entry, self) + self.entry.async_on_unload( + self.entry.add_update_listener(self._async_entry_update_listener) + ) async def async_will_remove_from_hass(self) -> None: """When entity will be removed from Home Assistant.""" @@ -397,3 +404,10 @@ class GoogleGenerativeAIConversationEntity( parts.append(llm_api.api_prompt) return "\n".join(parts) + + async def _async_entry_update_listener( + self, hass: HomeAssistant, entry: ConfigEntry + ) -> None: + """Handle options update.""" + # Reload as we update device info + entity name + supported features + await hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr index abd3658e869..65238c5212a 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr @@ -215,7 +215,7 @@ ), ]) # --- -# name: test_default_prompt[config_entry_options0-None] +# name: test_default_prompt[config_entry_options0-0-None] list([ tuple( '', @@ -263,7 +263,7 @@ ), ]) # --- -# name: test_default_prompt[config_entry_options0-conversation.google_generative_ai_conversation] +# name: test_default_prompt[config_entry_options0-0-conversation.google_generative_ai_conversation] list([ tuple( '', @@ -311,7 +311,7 @@ ), ]) # --- -# name: test_default_prompt[config_entry_options1-None] +# name: test_default_prompt[config_entry_options1-1-None] list([ tuple( '', @@ -360,7 +360,7 @@ ), ]) # --- -# name: test_default_prompt[config_entry_options1-conversation.google_generative_ai_conversation] +# name: test_default_prompt[config_entry_options1-1-conversation.google_generative_ai_conversation] list([ tuple( '', diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 41f96c7b0ac..98f469643af 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -18,7 +18,7 @@ from homeassistant.components.google_generative_ai_conversation.const import ( from homeassistant.components.google_generative_ai_conversation.conversation import ( _escape_decode, ) -from homeassistant.const import CONF_LLM_HASS_API +from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_LLM_HASS_API from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import intent, llm @@ -38,10 +38,13 @@ def freeze_the_time(): "agent_id", [None, "conversation.google_generative_ai_conversation"] ) @pytest.mark.parametrize( - "config_entry_options", + ("config_entry_options", "expected_features"), [ - {}, - {CONF_LLM_HASS_API: llm.LLM_API_ASSIST}, + ({}, 0), + ( + {CONF_LLM_HASS_API: llm.LLM_API_ASSIST}, + conversation.ConversationEntityFeature.CONTROL, + ), ], ) @pytest.mark.usefixtures("mock_init_component") @@ -51,6 +54,7 @@ async def test_default_prompt( snapshot: SnapshotAssertion, agent_id: str | None, config_entry_options: {}, + expected_features: conversation.ConversationEntityFeature, hass_ws_client: WebSocketGenerator, ) -> None: """Test that the default prompt works.""" @@ -97,6 +101,9 @@ async def test_default_prompt( assert [tuple(mock_call) for mock_call in mock_model.mock_calls] == snapshot assert mock_get_tools.called == (CONF_LLM_HASS_API in config_entry_options) + state = hass.states.get("conversation.google_generative_ai_conversation") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == expected_features + @pytest.mark.parametrize( ("model_name", "supports_system_instruction"), From bee77041e83227d6a1097d1a4f860a9c12514e47 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Sat, 3 Aug 2024 09:14:24 +0300 Subject: [PATCH 35/95] Change enum type to string for Google Generative AI Conversation (#123069) --- .../conversation.py | 14 ++++- .../test_conversation.py | 59 +++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 1d8b46cde2f..0d24ddbf39f 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -89,9 +89,9 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: key = "type_" val = val.upper() elif key == "format": - if (schema.get("type") == "string" and val != "enum") or ( - schema.get("type") not in ("number", "integer", "string") - ): + if schema.get("type") == "string" and val != "enum": + continue + if schema.get("type") not in ("number", "integer", "string"): continue key = "format_" elif key == "items": @@ -100,11 +100,19 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]: val = {k: _format_schema(v) for k, v in val.items()} result[key] = val + if result.get("enum") and result.get("type_") != "STRING": + # enum is only allowed for STRING type. This is safe as long as the schema + # contains vol.Coerce for the respective type, for example: + # vol.All(vol.Coerce(int), vol.In([1, 2, 3])) + result["type_"] = "STRING" + result["enum"] = [str(item) for item in result["enum"]] + if result.get("type_") == "OBJECT" and not result.get("properties"): # An object with undefined properties is not supported by Gemini API. # Fallback to JSON string. This will probably fail for most tools that want it, # but we don't have a better fallback strategy so far. result["properties"] = {"json": {"type_": "STRING"}} + result["required"] = [] return result diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 98f469643af..a8eae34e08b 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -17,6 +17,7 @@ from homeassistant.components.google_generative_ai_conversation.const import ( ) from homeassistant.components.google_generative_ai_conversation.conversation import ( _escape_decode, + _format_schema, ) from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_LLM_HASS_API from homeassistant.core import Context, HomeAssistant @@ -629,3 +630,61 @@ async def test_escape_decode() -> None: "param2": "param2's value", "param3": {"param31": "Cheminée", "param32": "Cheminée"}, } + + +@pytest.mark.parametrize( + ("openapi", "protobuf"), + [ + ( + {"type": "string", "enum": ["a", "b", "c"]}, + {"type_": "STRING", "enum": ["a", "b", "c"]}, + ), + ( + {"type": "integer", "enum": [1, 2, 3]}, + {"type_": "STRING", "enum": ["1", "2", "3"]}, + ), + ({"anyOf": [{"type": "integer"}, {"type": "number"}]}, {"type_": "INTEGER"}), + ( + { + "anyOf": [ + {"anyOf": [{"type": "integer"}, {"type": "number"}]}, + {"anyOf": [{"type": "integer"}, {"type": "number"}]}, + ] + }, + {"type_": "INTEGER"}, + ), + ({"type": "string", "format": "lower"}, {"type_": "STRING"}), + ({"type": "boolean", "format": "bool"}, {"type_": "BOOLEAN"}), + ( + {"type": "number", "format": "percent"}, + {"type_": "NUMBER", "format_": "percent"}, + ), + ( + { + "type": "object", + "properties": {"var": {"type": "string"}}, + "required": [], + }, + { + "type_": "OBJECT", + "properties": {"var": {"type_": "STRING"}}, + "required": [], + }, + ), + ( + {"type": "object", "additionalProperties": True}, + { + "type_": "OBJECT", + "properties": {"json": {"type_": "STRING"}}, + "required": [], + }, + ), + ( + {"type": "array", "items": {"type": "string"}}, + {"type_": "ARRAY", "items": {"type_": "STRING"}}, + ), + ], +) +async def test_format_schema(openapi, protobuf) -> None: + """Test _format_schema.""" + assert _format_schema(openapi) == protobuf From fa241dcd0440336622413dd87216823a1238247c Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Mon, 5 Aug 2024 08:43:29 +0200 Subject: [PATCH 36/95] Catch exception in coordinator setup of IronOS integration (#123079) --- .../components/iron_os/coordinator.py | 6 ++++- tests/components/iron_os/test_init.py | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/components/iron_os/test_init.py diff --git a/homeassistant/components/iron_os/coordinator.py b/homeassistant/components/iron_os/coordinator.py index e8424478d86..aefb14b689b 100644 --- a/homeassistant/components/iron_os/coordinator.py +++ b/homeassistant/components/iron_os/coordinator.py @@ -46,4 +46,8 @@ class IronOSCoordinator(DataUpdateCoordinator[LiveDataResponse]): async def _async_setup(self) -> None: """Set up the coordinator.""" - self.device_info = await self.device.get_device_info() + try: + self.device_info = await self.device.get_device_info() + + except CommunicationError as e: + raise UpdateFailed("Cannot connect to device") from e diff --git a/tests/components/iron_os/test_init.py b/tests/components/iron_os/test_init.py new file mode 100644 index 00000000000..fb0a782ea36 --- /dev/null +++ b/tests/components/iron_os/test_init.py @@ -0,0 +1,26 @@ +"""Test init of IronOS integration.""" + +from unittest.mock import AsyncMock + +from pynecil import CommunicationError +import pytest + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("ble_device") +async def test_setup_config_entry_not_ready( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, +) -> None: + """Test config entry not ready.""" + mock_pynecil.get_device_info.side_effect = CommunicationError + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY From 7623ee49e49867c388e5a74debf97dc7f36b550a Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 2 Aug 2024 21:56:49 +0300 Subject: [PATCH 37/95] Ignore Shelly IPv6 address in zeroconf (#123081) --- .../components/shelly/config_flow.py | 2 ++ homeassistant/components/shelly/strings.json | 3 ++- tests/components/shelly/test_config_flow.py | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index cb3bca6aa47..c80d1e84d6f 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -279,6 +279,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): self, discovery_info: ZeroconfServiceInfo ) -> ConfigFlowResult: """Handle zeroconf discovery.""" + if discovery_info.ip_address.version == 6: + return self.async_abort(reason="ipv6_not_supported") host = discovery_info.host # First try to get the mac address from the name # so we can avoid making another connection to the diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 8ae4ff1f3e4..f76319eb08c 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -52,7 +52,8 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again.", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", - "another_device": "Re-configuration was unsuccessful, the IP address/hostname of another Shelly device was used." + "another_device": "Re-configuration was unsuccessful, the IP address/hostname of another Shelly device was used.", + "ipv6_not_supported": "IPv6 is not supported." } }, "device_automation": { diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index a3040fc2eb8..0c574a33e0c 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1305,3 +1305,22 @@ async def test_reconfigure_with_exception( ) assert result["errors"] == {"base": base_error} + + +async def test_zeroconf_rejects_ipv6(hass: HomeAssistant) -> None: + """Test zeroconf discovery rejects ipv6.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("fd00::b27c:63bb:cc85:4ea0"), + ip_addresses=[ip_address("fd00::b27c:63bb:cc85:4ea0")], + hostname="mock_hostname", + name="shelly1pm-12345", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "shelly1pm-12345"}, + type="mock_type", + ), + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "ipv6_not_supported" From fdb1baadbeed6e7f135c0323f229e3a85f32de4e Mon Sep 17 00:00:00 2001 From: Louis Christ Date: Sat, 3 Aug 2024 22:32:47 +0200 Subject: [PATCH 38/95] Fix wrong DeviceInfo in bluesound integration (#123101) Fix bluesound device info --- homeassistant/components/bluesound/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 809ba293f89..dc09feaed63 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -255,7 +255,7 @@ class BluesoundPlayer(MediaPlayerEntity): self._attr_unique_id = format_unique_id(sync_status.mac, port) # there should always be one player with the default port per mac - if port is DEFAULT_PORT: + if port == DEFAULT_PORT: self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, format_mac(sync_status.mac))}, connections={(CONNECTION_NETWORK_MAC, format_mac(sync_status.mac))}, From eccce7017f21269245bb84be8b9ca2cf7f92464b Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Sat, 3 Aug 2024 17:21:12 +0200 Subject: [PATCH 39/95] Bump pyenphase to 1.22.0 (#123103) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 09c55fb23ac..aa06a1ff79f 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.20.6"], + "requirements": ["pyenphase==1.22.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index b73248c5e16..b48fdd28553 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1840,7 +1840,7 @@ pyeiscp==0.0.7 pyemoncms==0.0.7 # homeassistant.components.enphase_envoy -pyenphase==1.20.6 +pyenphase==1.22.0 # homeassistant.components.envisalink pyenvisalink==4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0b634d0e2f..c9f3878dbdf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1469,7 +1469,7 @@ pyegps==0.2.5 pyemoncms==0.0.7 # homeassistant.components.enphase_envoy -pyenphase==1.20.6 +pyenphase==1.22.0 # homeassistant.components.everlights pyeverlights==0.1.0 From 832bac8c63ccb063ed74e17c6ffca83c07cb57bb Mon Sep 17 00:00:00 2001 From: Kim de Vos Date: Sat, 3 Aug 2024 15:08:01 +0200 Subject: [PATCH 40/95] Use slugify to create id for UniFi WAN latency (#123108) Use slugify to create id for latency --- homeassistant/components/unifi/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index d86b72d1b2f..08bd0ddb869 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -44,6 +44,7 @@ from homeassistant.core import Event as core_Event, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.util import slugify import homeassistant.util.dt as dt_util from . import UnifiConfigEntry @@ -247,8 +248,9 @@ def make_wan_latency_sensors() -> tuple[UnifiSensorEntityDescription, ...]: def make_wan_latency_entity_description( wan: Literal["WAN", "WAN2"], name: str, monitor_target: str ) -> UnifiSensorEntityDescription: + name_wan = f"{name} {wan}" return UnifiSensorEntityDescription[Devices, Device]( - key=f"{name} {wan} latency", + key=f"{name_wan} latency", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfTime.MILLISECONDS, state_class=SensorStateClass.MEASUREMENT, @@ -257,13 +259,12 @@ def make_wan_latency_sensors() -> tuple[UnifiSensorEntityDescription, ...]: api_handler_fn=lambda api: api.devices, available_fn=async_device_available_fn, device_info_fn=async_device_device_info_fn, - name_fn=lambda _: f"{name} {wan} latency", + name_fn=lambda device: f"{name_wan} latency", object_fn=lambda api, obj_id: api.devices[obj_id], supported_fn=partial( async_device_wan_latency_supported_fn, wan, monitor_target ), - unique_id_fn=lambda hub, - obj_id: f"{name.lower}_{wan.lower}_latency-{obj_id}", + unique_id_fn=lambda hub, obj_id: f"{slugify(name_wan)}_latency-{obj_id}", value_fn=partial(async_device_wan_latency_value_fn, wan, monitor_target), ) From c8a0e5228da591e521502890e8b7e43c514e8f0a Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 5 Aug 2024 05:19:57 -0400 Subject: [PATCH 41/95] Bump ZHA lib to 0.0.27 (#123125) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 6e35339c53f..d7dc53b5167 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.25"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.27"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index b48fdd28553..fb881f5a8c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2986,7 +2986,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.25 +zha==0.0.27 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9f3878dbdf..ac3d594a541 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2360,7 +2360,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.25 +zha==0.0.27 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 From 0b4d92176275865c2a627d09c65337862489d3e6 Mon Sep 17 00:00:00 2001 From: Steve Repsher Date: Sun, 4 Aug 2024 08:28:45 -0400 Subject: [PATCH 42/95] Restore old service worker URL (#123131) --- homeassistant/components/frontend/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 5b462842e4a..c5df84cf549 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -398,6 +398,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: static_paths_configs: list[StaticPathConfig] = [] for path, should_cache in ( + ("service_worker.js", False), ("sw-modern.js", False), ("sw-modern.js.map", False), ("sw-legacy.js", False), From f6c4b6b0456cf8dd4b2bd4dce808e5c8b06caec2 Mon Sep 17 00:00:00 2001 From: dupondje Date: Mon, 5 Aug 2024 10:32:58 +0200 Subject: [PATCH 43/95] dsmr: migrate hourly_gas_meter_reading to mbus device (#123149) --- homeassistant/components/dsmr/sensor.py | 4 +- tests/components/dsmr/test_mbus_migration.py | 100 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index f794d1d05e9..b298ed5bfc0 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -439,7 +439,9 @@ def rename_old_gas_to_mbus( entries = er.async_entries_for_device(ent_reg, device_id) for entity in entries: - if entity.unique_id.endswith("belgium_5min_gas_meter_reading"): + if entity.unique_id.endswith( + "belgium_5min_gas_meter_reading" + ) or entity.unique_id.endswith("hourly_gas_meter_reading"): try: ent_reg.async_update_entity( entity.entity_id, diff --git a/tests/components/dsmr/test_mbus_migration.py b/tests/components/dsmr/test_mbus_migration.py index a28bc2c3a33..20b3d253f39 100644 --- a/tests/components/dsmr/test_mbus_migration.py +++ b/tests/components/dsmr/test_mbus_migration.py @@ -119,6 +119,106 @@ async def test_migrate_gas_to_mbus( ) +async def test_migrate_hourly_gas_to_mbus( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], +) -> None: + """Test migration of unique_id.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="/dev/ttyUSB0", + data={ + "port": "/dev/ttyUSB0", + "dsmr_version": "5", + "serial_id": "1234", + "serial_id_gas": "4730303738353635363037343639323231", + }, + options={ + "time_between_update": 0, + }, + ) + + mock_entry.add_to_hass(hass) + + old_unique_id = "4730303738353635363037343639323231_hourly_gas_meter_reading" + + device = device_registry.async_get_or_create( + config_entry_id=mock_entry.entry_id, + identifiers={(DOMAIN, mock_entry.entry_id)}, + name="Gas Meter", + ) + await hass.async_block_till_done() + + entity: er.RegistryEntry = entity_registry.async_get_or_create( + suggested_object_id="gas_meter_reading", + disabled_by=None, + domain=SENSOR_DOMAIN, + platform=DOMAIN, + device_id=device.id, + unique_id=old_unique_id, + config_entry=mock_entry, + ) + assert entity.unique_id == old_unique_id + await hass.async_block_till_done() + + telegram = Telegram() + telegram.add( + MBUS_DEVICE_TYPE, + CosemObject((0, 1), [{"value": "003", "unit": ""}]), + "MBUS_DEVICE_TYPE", + ) + telegram.add( + MBUS_EQUIPMENT_IDENTIFIER, + CosemObject( + (0, 1), + [{"value": "4730303738353635363037343639323231", "unit": ""}], + ), + "MBUS_EQUIPMENT_IDENTIFIER", + ) + telegram.add( + MBUS_METER_READING, + MBusObject( + (0, 1), + [ + {"value": datetime.datetime.fromtimestamp(1722749707)}, + {"value": Decimal(778.963), "unit": "m3"}, + ], + ), + "MBUS_METER_READING", + ) + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + telegram_callback = connection_factory.call_args_list[0][0][2] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to be created + await hass.async_block_till_done() + + dev_entities = er.async_entries_for_device( + entity_registry, device.id, include_disabled_entities=True + ) + assert not dev_entities + + assert ( + entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, old_unique_id) + is None + ) + assert ( + entity_registry.async_get_entity_id( + SENSOR_DOMAIN, DOMAIN, "4730303738353635363037343639323231" + ) + == "sensor.gas_meter_reading" + ) + + async def test_migrate_gas_to_mbus_exists( hass: HomeAssistant, entity_registry: er.EntityRegistry, From 73a2ad7304cd9d4b7d9b3b191dae64601e203bdc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Aug 2024 01:39:04 -0500 Subject: [PATCH 44/95] Bump aiohttp to 3.10.1 (#123159) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1cc6a0fa85d..3b251e91179 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-zlib==0.1.1 -aiohttp==3.10.0 +aiohttp==3.10.1 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index c60a01663da..b3da2cdb631 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.10.0", + "aiohttp==3.10.1", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index c851927f9c6..1beefe73914 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.2.0 -aiohttp==3.10.0 +aiohttp==3.10.1 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 From 85700fd80fe4229845ae7102ee22e95293d5dafe Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 5 Aug 2024 17:59:33 +1000 Subject: [PATCH 45/95] Fix class attribute condition in Tesla Fleet (#123162) --- homeassistant/components/tesla_fleet/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 2c5ee1b5c75..8257bf75cd0 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -86,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - vehicles: list[TeslaFleetVehicleData] = [] energysites: list[TeslaFleetEnergyData] = [] for product in products: - if "vin" in product and tesla.vehicle: + if "vin" in product and hasattr(tesla, "vehicle"): # Remove the protobuff 'cached_data' that we do not use to save memory product.pop("cached_data", None) vin = product["vin"] @@ -111,7 +111,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - device=device, ) ) - elif "energy_site_id" in product and tesla.energy: + elif "energy_site_id" in product and hasattr(tesla, "energy"): site_id = product["energy_site_id"] if not ( product["components"]["battery"] From cdb378066c14e7cd554b42e07e3801cbddacfb90 Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Mon, 5 Aug 2024 04:02:15 -0400 Subject: [PATCH 46/95] Add Govee H612B to the Matter transition blocklist (#123163) --- homeassistant/components/matter/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 65c3a535216..d05a7c85f9d 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -54,6 +54,7 @@ TRANSITION_BLOCKLIST = ( (4488, 514, "1.0", "1.0.0"), (4488, 260, "1.0", "1.0.0"), (5010, 769, "3.0", "1.0.0"), + (4999, 24875, "1.0", "27.0"), (4999, 25057, "1.0", "27.0"), (4448, 36866, "V1", "V1.0.0.5"), (5009, 514, "1.0", "1.0.0"), From 35a3d2306c8b77d0cf13442cc6c2c8e5cf928297 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Aug 2024 12:22:03 +0200 Subject: [PATCH 47/95] Bump version to 2024.8.0b2 --- 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 08ee0bb77f9..7f27548a68c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index b3da2cdb631..f23c571feb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b1" +version = "2024.8.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 4898ba932d5bfa4e6088b36f69e79c6eb4b4a7e6 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 5 Aug 2024 12:34:48 +0200 Subject: [PATCH 48/95] Use KNX UI entity platform controller class (#123128) --- homeassistant/components/knx/binary_sensor.py | 4 +- homeassistant/components/knx/button.py | 4 +- homeassistant/components/knx/climate.py | 4 +- homeassistant/components/knx/cover.py | 4 +- homeassistant/components/knx/date.py | 4 +- homeassistant/components/knx/datetime.py | 4 +- homeassistant/components/knx/fan.py | 4 +- homeassistant/components/knx/knx_entity.py | 76 +++++++++++++------ homeassistant/components/knx/light.py | 39 +++++----- homeassistant/components/knx/notify.py | 4 +- homeassistant/components/knx/number.py | 4 +- homeassistant/components/knx/scene.py | 4 +- homeassistant/components/knx/select.py | 4 +- homeassistant/components/knx/sensor.py | 4 +- .../components/knx/storage/config_store.py | 54 +++++++------ homeassistant/components/knx/switch.py | 59 +++++++------- homeassistant/components/knx/text.py | 4 +- homeassistant/components/knx/time.py | 4 +- homeassistant/components/knx/weather.py | 4 +- 19 files changed, 165 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index ff15f725fae..7d80ca55bf6 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -24,7 +24,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNXModule from .const import ATTR_COUNTER, ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import BinarySensorSchema @@ -43,7 +43,7 @@ async def async_setup_entry( ) -class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity): +class KNXBinarySensor(KnxYamlEntity, BinarySensorEntity, RestoreEntity): """Representation of a KNX binary sensor.""" _device: XknxBinarySensor diff --git a/homeassistant/components/knx/button.py b/homeassistant/components/knx/button.py index 2eb68eebe43..f6627fc527b 100644 --- a/homeassistant/components/knx/button.py +++ b/homeassistant/components/knx/button.py @@ -13,7 +13,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNXModule from .const import CONF_PAYLOAD_LENGTH, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_setup_entry( @@ -31,7 +31,7 @@ async def async_setup_entry( ) -class KNXButton(KnxEntity, ButtonEntity): +class KNXButton(KnxYamlEntity, ButtonEntity): """Representation of a KNX button.""" _device: XknxRawValue diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 7470d60ef4b..9abc9023617 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -35,7 +35,7 @@ from .const import ( DOMAIN, PRESET_MODES, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import ClimateSchema ATTR_COMMAND_VALUE = "command_value" @@ -133,7 +133,7 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate: ) -class KNXClimate(KnxEntity, ClimateEntity): +class KNXClimate(KnxYamlEntity, ClimateEntity): """Representation of a KNX climate device.""" _device: XknxClimate diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 1962db0ad3f..408f746e094 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -27,7 +27,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import CoverSchema @@ -43,7 +43,7 @@ async def async_setup_entry( async_add_entities(KNXCover(knx_module, entity_config) for entity_config in config) -class KNXCover(KnxEntity, CoverEntity): +class KNXCover(KnxYamlEntity, CoverEntity): """Representation of a KNX cover.""" _device: XknxCover diff --git a/homeassistant/components/knx/date.py b/homeassistant/components/knx/date.py index 80fea63d0a6..9f04a4acd7e 100644 --- a/homeassistant/components/knx/date.py +++ b/homeassistant/components/knx/date.py @@ -31,7 +31,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_setup_entry( @@ -61,7 +61,7 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateDevice: ) -class KNXDateEntity(KnxEntity, DateEntity, RestoreEntity): +class KNXDateEntity(KnxYamlEntity, DateEntity, RestoreEntity): """Representation of a KNX date.""" _device: XknxDateDevice diff --git a/homeassistant/components/knx/datetime.py b/homeassistant/components/knx/datetime.py index 16ccb7474a7..8f1a25e6e3c 100644 --- a/homeassistant/components/knx/datetime.py +++ b/homeassistant/components/knx/datetime.py @@ -32,7 +32,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_setup_entry( @@ -62,7 +62,7 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTimeDevice: ) -class KNXDateTimeEntity(KnxEntity, DateTimeEntity, RestoreEntity): +class KNXDateTimeEntity(KnxYamlEntity, DateTimeEntity, RestoreEntity): """Representation of a KNX datetime.""" _device: XknxDateTimeDevice diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 940e241ccda..6fd87be97d1 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -21,7 +21,7 @@ from homeassistant.util.scaling import int_states_in_range from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import FanSchema DEFAULT_PERCENTAGE: Final = 50 @@ -39,7 +39,7 @@ async def async_setup_entry( async_add_entities(KNXFan(knx_module, entity_config) for entity_config in config) -class KNXFan(KnxEntity, FanEntity): +class KNXFan(KnxYamlEntity, FanEntity): """Representation of a KNX fan.""" _device: XknxFan diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index 2b8d2e71186..c81a6ee06db 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -2,30 +2,55 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any from xknx.devices import Device as XknxDevice -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity - -from .const import DOMAIN +from homeassistant.helpers.entity_platform import EntityPlatform +from homeassistant.helpers.entity_registry import RegistryEntry if TYPE_CHECKING: from . import KNXModule -SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal.{{}}" +from .storage.config_store import PlatformControllerBase -class KnxEntity(Entity): +class KnxUiEntityPlatformController(PlatformControllerBase): + """Class to manage dynamic adding and reloading of UI entities.""" + + def __init__( + self, + knx_module: KNXModule, + entity_platform: EntityPlatform, + entity_class: type[KnxUiEntity], + ) -> None: + """Initialize the UI platform.""" + self._knx_module = knx_module + self._entity_platform = entity_platform + self._entity_class = entity_class + + async def create_entity(self, unique_id: str, config: dict[str, Any]) -> None: + """Add a new UI entity.""" + await self._entity_platform.async_add_entities( + [self._entity_class(self._knx_module, unique_id, config)] + ) + + async def update_entity( + self, entity_entry: RegistryEntry, config: dict[str, Any] + ) -> None: + """Update an existing UI entities configuration.""" + await self._entity_platform.async_remove_entity(entity_entry.entity_id) + await self.create_entity(unique_id=entity_entry.unique_id, config=config) + + +class _KnxEntityBase(Entity): """Representation of a KNX entity.""" _attr_should_poll = False - - def __init__(self, knx_module: KNXModule, device: XknxDevice) -> None: - """Set up device.""" - self._knx_module = knx_module - self._device = device + _knx_module: KNXModule + _device: XknxDevice @property def name(self) -> str: @@ -49,7 +74,7 @@ class KnxEntity(Entity): """Store register state change callback and start device object.""" self._device.register_device_updated_cb(self.after_update_callback) self._device.xknx.devices.async_add(self._device) - # super call needed to have methods of mulit-inherited classes called + # super call needed to have methods of multi-inherited classes called # eg. for restoring state (like _KNXSwitch) await super().async_added_to_hass() @@ -59,19 +84,22 @@ class KnxEntity(Entity): self._device.xknx.devices.async_remove(self._device) -class KnxUIEntity(KnxEntity): +class KnxYamlEntity(_KnxEntityBase): + """Representation of a KNX entity configured from YAML.""" + + def __init__(self, knx_module: KNXModule, device: XknxDevice) -> None: + """Initialize the YAML entity.""" + self._knx_module = knx_module + self._device = device + + +class KnxUiEntity(_KnxEntityBase, ABC): """Representation of a KNX UI entity.""" _attr_unique_id: str - async def async_added_to_hass(self) -> None: - """Register callbacks when entity added to hass.""" - await super().async_added_to_hass() - self._knx_module.config_store.entities.add(self._attr_unique_id) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - SIGNAL_ENTITY_REMOVE.format(self._attr_unique_id), - self.async_remove, - ) - ) + @abstractmethod + def __init__( + self, knx_module: KNXModule, unique_id: str, config: dict[str, Any] + ) -> None: + """Initialize the UI entity.""" diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 1197f09354b..a2ce8f8d2cb 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -19,15 +19,18 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util from . import KNXModule from .const import CONF_SYNC_STATE, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, ColorTempModes -from .knx_entity import KnxEntity, KnxUIEntity +from .knx_entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity from .schema import LightSchema from .storage.const import ( CONF_COLOR_TEMP_MAX, @@ -63,8 +66,17 @@ async def async_setup_entry( ) -> None: """Set up light(s) for KNX platform.""" knx_module: KNXModule = hass.data[DOMAIN] + platform = async_get_current_platform() + knx_module.config_store.add_platform( + platform=Platform.LIGHT, + controller=KnxUiEntityPlatformController( + knx_module=knx_module, + entity_platform=platform, + entity_class=KnxUiLight, + ), + ) - entities: list[KnxEntity] = [] + entities: list[KnxYamlEntity | KnxUiEntity] = [] if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.LIGHT): entities.extend( KnxYamlLight(knx_module, entity_config) @@ -78,13 +90,6 @@ async def async_setup_entry( if entities: async_add_entities(entities) - @callback - def add_new_ui_light(unique_id: str, config: dict[str, Any]) -> None: - """Add KNX entity at runtime.""" - async_add_entities([KnxUiLight(knx_module, unique_id, config)]) - - knx_module.config_store.async_add_entity[Platform.LIGHT] = add_new_ui_light - def _create_yaml_light(xknx: XKNX, config: ConfigType) -> XknxLight: """Return a KNX Light device to be used within XKNX.""" @@ -519,7 +524,7 @@ class _KnxLight(LightEntity): await self._device.set_off() -class KnxYamlLight(_KnxLight, KnxEntity): +class KnxYamlLight(_KnxLight, KnxYamlEntity): """Representation of a KNX light.""" _device: XknxLight @@ -546,7 +551,7 @@ class KnxYamlLight(_KnxLight, KnxEntity): ) -class KnxUiLight(_KnxLight, KnxUIEntity): +class KnxUiLight(_KnxLight, KnxUiEntity): """Representation of a KNX light.""" _attr_has_entity_name = True @@ -556,11 +561,9 @@ class KnxUiLight(_KnxLight, KnxUIEntity): self, knx_module: KNXModule, unique_id: str, config: ConfigType ) -> None: """Initialize of KNX light.""" - super().__init__( - knx_module=knx_module, - device=_create_ui_light( - knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME] - ), + self._knx_module = knx_module + self._device = _create_ui_light( + knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME] ) self._attr_max_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MAX] self._attr_min_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MIN] diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index b349681990c..173ab3119a0 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_get_service( @@ -103,7 +103,7 @@ def _create_notification_instance(xknx: XKNX, config: ConfigType) -> XknxNotific ) -class KNXNotify(KnxEntity, NotifyEntity): +class KNXNotify(KnxYamlEntity, NotifyEntity): """Representation of a KNX notification entity.""" _device: XknxNotification diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index 3d4af503dff..cbbe91aba54 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -30,7 +30,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import NumberSchema @@ -58,7 +58,7 @@ def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue: ) -class KNXNumber(KnxEntity, RestoreNumber): +class KNXNumber(KnxYamlEntity, RestoreNumber): """Representation of a KNX number.""" _device: NumericValue diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index fc37f36dd01..2de832ae54a 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -15,7 +15,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import SceneSchema @@ -31,7 +31,7 @@ async def async_setup_entry( async_add_entities(KNXScene(knx_module, entity_config) for entity_config in config) -class KNXScene(KnxEntity, Scene): +class KNXScene(KnxYamlEntity, Scene): """Representation of a KNX scene.""" _device: XknxScene diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index 1b862010c2a..6c73bf8d573 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -30,7 +30,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import SelectSchema @@ -59,7 +59,7 @@ def _create_raw_value(xknx: XKNX, config: ConfigType) -> RawValue: ) -class KNXSelect(KnxEntity, SelectEntity, RestoreEntity): +class KNXSelect(KnxYamlEntity, SelectEntity, RestoreEntity): """Representation of a KNX select.""" _device: RawValue diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index ab363e2a35f..a28c1a339e6 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -35,7 +35,7 @@ from homeassistant.util.enum import try_parse_enum from . import KNXModule from .const import ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import SensorSchema SCAN_INTERVAL = timedelta(seconds=10) @@ -141,7 +141,7 @@ def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor: ) -class KNXSensor(KnxEntity, SensorEntity): +class KNXSensor(KnxYamlEntity, SensorEntity): """Representation of a KNX sensor.""" _device: XknxSensor diff --git a/homeassistant/components/knx/storage/config_store.py b/homeassistant/components/knx/storage/config_store.py index 876fe19a4b9..ce7a705e629 100644 --- a/homeassistant/components/knx/storage/config_store.py +++ b/homeassistant/components/knx/storage/config_store.py @@ -1,6 +1,6 @@ """KNX entity configuration store.""" -from collections.abc import Callable +from abc import ABC, abstractmethod import logging from typing import Any, Final, TypedDict @@ -8,12 +8,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PLATFORM, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store from homeassistant.util.ulid import ulid_now from ..const import DOMAIN -from ..knx_entity import SIGNAL_ENTITY_REMOVE from .const import CONF_DATA _LOGGER = logging.getLogger(__name__) @@ -33,6 +31,20 @@ class KNXConfigStoreModel(TypedDict): entities: KNXEntityStoreModel +class PlatformControllerBase(ABC): + """Entity platform controller base class.""" + + @abstractmethod + async def create_entity(self, unique_id: str, config: dict[str, Any]) -> None: + """Create a new entity.""" + + @abstractmethod + async def update_entity( + self, entity_entry: er.RegistryEntry, config: dict[str, Any] + ) -> None: + """Update an existing entities configuration.""" + + class KNXConfigStore: """Manage KNX config store data.""" @@ -46,12 +58,7 @@ class KNXConfigStore: self.config_entry = config_entry self._store = Store[KNXConfigStoreModel](hass, STORAGE_VERSION, STORAGE_KEY) self.data = KNXConfigStoreModel(entities={}) - - # entities and async_add_entity are filled by platform / entity setups - self.entities: set[str] = set() # unique_id as values - self.async_add_entity: dict[ - Platform, Callable[[str, dict[str, Any]], None] - ] = {} + self._platform_controllers: dict[Platform, PlatformControllerBase] = {} async def load_data(self) -> None: """Load config store data from storage.""" @@ -62,14 +69,19 @@ class KNXConfigStore: len(self.data["entities"]), ) + def add_platform( + self, platform: Platform, controller: PlatformControllerBase + ) -> None: + """Add platform controller.""" + self._platform_controllers[platform] = controller + async def create_entity( self, platform: Platform, data: dict[str, Any] ) -> str | None: """Create a new entity.""" - if platform not in self.async_add_entity: - raise ConfigStoreException(f"Entity platform not ready: {platform}") + platform_controller = self._platform_controllers[platform] unique_id = f"knx_es_{ulid_now()}" - self.async_add_entity[platform](unique_id, data) + await platform_controller.create_entity(unique_id, data) # store data after entity was added to be sure config didn't raise exceptions self.data["entities"].setdefault(platform, {})[unique_id] = data await self._store.async_save(self.data) @@ -95,8 +107,7 @@ class KNXConfigStore: self, platform: Platform, entity_id: str, data: dict[str, Any] ) -> None: """Update an existing entity.""" - if platform not in self.async_add_entity: - raise ConfigStoreException(f"Entity platform not ready: {platform}") + platform_controller = self._platform_controllers[platform] entity_registry = er.async_get(self.hass) if (entry := entity_registry.async_get(entity_id)) is None: raise ConfigStoreException(f"Entity not found: {entity_id}") @@ -108,8 +119,7 @@ class KNXConfigStore: raise ConfigStoreException( f"Entity not found in storage: {entity_id} - {unique_id}" ) - async_dispatcher_send(self.hass, SIGNAL_ENTITY_REMOVE.format(unique_id)) - self.async_add_entity[platform](unique_id, data) + await platform_controller.update_entity(entry, data) # store data after entity is added to make sure config doesn't raise exceptions self.data["entities"][platform][unique_id] = data await self._store.async_save(self.data) @@ -125,23 +135,21 @@ class KNXConfigStore: raise ConfigStoreException( f"Entity not found in {entry.domain}: {entry.unique_id}" ) from err - try: - self.entities.remove(entry.unique_id) - except KeyError: - _LOGGER.warning("Entity not initialized when deleted: %s", entity_id) entity_registry.async_remove(entity_id) await self._store.async_save(self.data) def get_entity_entries(self) -> list[er.RegistryEntry]: - """Get entity_ids of all configured entities by platform.""" + """Get entity_ids of all UI configured entities.""" entity_registry = er.async_get(self.hass) - + unique_ids = { + uid for platform in self.data["entities"].values() for uid in platform + } return [ registry_entry for registry_entry in er.async_entries_for_config_entry( entity_registry, self.config_entry.entry_id ) - if registry_entry.unique_id in self.entities + if registry_entry.unique_id in unique_ids ] diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index a5f430e6157..ebe930957d6 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -17,9 +17,12 @@ from homeassistant.const import ( STATE_UNKNOWN, Platform, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType @@ -32,7 +35,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity, KnxUIEntity +from .knx_entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity from .schema import SwitchSchema from .storage.const import ( CONF_DEVICE_INFO, @@ -51,8 +54,17 @@ async def async_setup_entry( ) -> None: """Set up switch(es) for KNX platform.""" knx_module: KNXModule = hass.data[DOMAIN] + platform = async_get_current_platform() + knx_module.config_store.add_platform( + platform=Platform.SWITCH, + controller=KnxUiEntityPlatformController( + knx_module=knx_module, + entity_platform=platform, + entity_class=KnxUiSwitch, + ), + ) - entities: list[KnxEntity] = [] + entities: list[KnxYamlEntity | KnxUiEntity] = [] if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.SWITCH): entities.extend( KnxYamlSwitch(knx_module, entity_config) @@ -66,13 +78,6 @@ async def async_setup_entry( if entities: async_add_entities(entities) - @callback - def add_new_ui_switch(unique_id: str, config: dict[str, Any]) -> None: - """Add KNX entity at runtime.""" - async_add_entities([KnxUiSwitch(knx_module, unique_id, config)]) - - knx_module.config_store.async_add_entity[Platform.SWITCH] = add_new_ui_switch - class _KnxSwitch(SwitchEntity, RestoreEntity): """Base class for a KNX switch.""" @@ -102,7 +107,7 @@ class _KnxSwitch(SwitchEntity, RestoreEntity): await self._device.set_off() -class KnxYamlSwitch(_KnxSwitch, KnxEntity): +class KnxYamlSwitch(_KnxSwitch, KnxYamlEntity): """Representation of a KNX switch configured from YAML.""" _device: XknxSwitch @@ -125,7 +130,7 @@ class KnxYamlSwitch(_KnxSwitch, KnxEntity): self._attr_unique_id = str(self._device.switch.group_address) -class KnxUiSwitch(_KnxSwitch, KnxUIEntity): +class KnxUiSwitch(_KnxSwitch, KnxUiEntity): """Representation of a KNX switch configured from UI.""" _attr_has_entity_name = True @@ -134,21 +139,19 @@ class KnxUiSwitch(_KnxSwitch, KnxUIEntity): def __init__( self, knx_module: KNXModule, unique_id: str, config: dict[str, Any] ) -> None: - """Initialize of KNX switch.""" - super().__init__( - knx_module=knx_module, - device=XknxSwitch( - knx_module.xknx, - name=config[CONF_ENTITY][CONF_NAME], - group_address=config[DOMAIN][CONF_GA_SWITCH][CONF_GA_WRITE], - group_address_state=[ - config[DOMAIN][CONF_GA_SWITCH][CONF_GA_STATE], - *config[DOMAIN][CONF_GA_SWITCH][CONF_GA_PASSIVE], - ], - respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ], - sync_state=config[DOMAIN][CONF_SYNC_STATE], - invert=config[DOMAIN][CONF_INVERT], - ), + """Initialize KNX switch.""" + self._knx_module = knx_module + self._device = XknxSwitch( + knx_module.xknx, + name=config[CONF_ENTITY][CONF_NAME], + group_address=config[DOMAIN][CONF_GA_SWITCH][CONF_GA_WRITE], + group_address_state=[ + config[DOMAIN][CONF_GA_SWITCH][CONF_GA_STATE], + *config[DOMAIN][CONF_GA_SWITCH][CONF_GA_PASSIVE], + ], + respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ], + sync_state=config[DOMAIN][CONF_SYNC_STATE], + invert=config[DOMAIN][CONF_INVERT], ) self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY] self._attr_unique_id = unique_id diff --git a/homeassistant/components/knx/text.py b/homeassistant/components/knx/text.py index 9bca37434ac..381cb95ad32 100644 --- a/homeassistant/components/knx/text.py +++ b/homeassistant/components/knx/text.py @@ -30,7 +30,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_setup_entry( @@ -57,7 +57,7 @@ def _create_notification(xknx: XKNX, config: ConfigType) -> XknxNotification: ) -class KNXText(KnxEntity, TextEntity, RestoreEntity): +class KNXText(KnxYamlEntity, TextEntity, RestoreEntity): """Representation of a KNX text.""" _device: XknxNotification diff --git a/homeassistant/components/knx/time.py b/homeassistant/components/knx/time.py index 5d9225a1e41..b4e562a8869 100644 --- a/homeassistant/components/knx/time.py +++ b/homeassistant/components/knx/time.py @@ -31,7 +31,7 @@ from .const import ( DOMAIN, KNX_ADDRESS, ) -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity async def async_setup_entry( @@ -61,7 +61,7 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxTimeDevice: ) -class KNXTimeEntity(KnxEntity, TimeEntity, RestoreEntity): +class KNXTimeEntity(KnxYamlEntity, TimeEntity, RestoreEntity): """Representation of a KNX time.""" _device: XknxTimeDevice diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 11dae452e2f..99f4be962fe 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNXModule from .const import DATA_KNX_CONFIG, DOMAIN -from .knx_entity import KnxEntity +from .knx_entity import KnxYamlEntity from .schema import WeatherSchema @@ -75,7 +75,7 @@ def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather: ) -class KNXWeather(KnxEntity, WeatherEntity): +class KNXWeather(KnxYamlEntity, WeatherEntity): """Representation of a KNX weather device.""" _device: XknxWeather From 0427aeccb0cdeb6757116441ae5db2ebe949ab72 Mon Sep 17 00:00:00 2001 From: musapinar Date: Mon, 5 Aug 2024 14:21:01 +0200 Subject: [PATCH 49/95] Add Matter Leedarson RGBTW Bulb to the transition blocklist (#123182) --- homeassistant/components/matter/light.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index d05a7c85f9d..6e9019c46fa 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -51,18 +51,19 @@ DEFAULT_TRANSITION = 0.2 # hw version (attributeKey 0/40/8) # sw version (attributeKey 0/40/10) TRANSITION_BLOCKLIST = ( - (4488, 514, "1.0", "1.0.0"), - (4488, 260, "1.0", "1.0.0"), - (5010, 769, "3.0", "1.0.0"), - (4999, 24875, "1.0", "27.0"), - (4999, 25057, "1.0", "27.0"), - (4448, 36866, "V1", "V1.0.0.5"), - (5009, 514, "1.0", "1.0.0"), (4107, 8475, "v1.0", "v1.0"), (4107, 8550, "v1.0", "v1.0"), (4107, 8551, "v1.0", "v1.0"), - (4107, 8656, "v1.0", "v1.0"), (4107, 8571, "v1.0", "v1.0"), + (4107, 8656, "v1.0", "v1.0"), + (4448, 36866, "V1", "V1.0.0.5"), + (4456, 1011, "1.0.0", "2.00.00"), + (4488, 260, "1.0", "1.0.0"), + (4488, 514, "1.0", "1.0.0"), + (4999, 24875, "1.0", "27.0"), + (4999, 25057, "1.0", "27.0"), + (5009, 514, "1.0", "1.0.0"), + (5010, 769, "3.0", "1.0.0"), ) From ea20c4b375c1ada418b42b13e172027124ed5c98 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 5 Aug 2024 15:07:01 +0200 Subject: [PATCH 50/95] Fix MPD issue creation (#123187) --- homeassistant/components/mpd/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 3538b1c7973..92f0f5cfcc4 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -86,7 +86,7 @@ async def async_setup_platform( ) if ( result["type"] is FlowResultType.CREATE_ENTRY - or result["reason"] == "single_instance_allowed" + or result["reason"] == "already_configured" ): async_create_issue( hass, From 6b10dbb38c61c6a50dd3074aaa8f182094910a0e Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:17:46 +0200 Subject: [PATCH 51/95] Fix state icon for closed valve entities (#123190) --- homeassistant/components/valve/icons.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/valve/icons.json b/homeassistant/components/valve/icons.json index 1261d1cc398..2c887ebf273 100644 --- a/homeassistant/components/valve/icons.json +++ b/homeassistant/components/valve/icons.json @@ -3,7 +3,7 @@ "_": { "default": "mdi:valve-open", "state": { - "off": "mdi:valve-closed" + "closed": "mdi:valve-closed" } }, "gas": { @@ -12,7 +12,7 @@ "water": { "default": "mdi:valve-open", "state": { - "off": "mdi:valve-closed" + "closed": "mdi:valve-closed" } } }, From b16bf2981907c68e1be67bf98d170c76922f9006 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 5 Aug 2024 18:23:44 +0200 Subject: [PATCH 52/95] Update frontend to 20240805.1 (#123196) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 95afe1221ec..82dc9cdb83f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240802.0"] + "requirements": ["home-assistant-frontend==20240805.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3b251e91179..6fc0d6f535d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240802.0 +home-assistant-frontend==20240805.1 home-assistant-intents==2024.7.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index fb881f5a8c6..2fd2c93a687 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240802.0 +home-assistant-frontend==20240805.1 # homeassistant.components.conversation home-assistant-intents==2024.7.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac3d594a541..3a71321bc5b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -916,7 +916,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240802.0 +home-assistant-frontend==20240805.1 # homeassistant.components.conversation home-assistant-intents==2024.7.29 From 859874487eed3c6593e3b3a0d965d4a3ad3794de Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Aug 2024 20:40:44 +0200 Subject: [PATCH 53/95] Mark tag to be an entity component (#123200) --- homeassistant/components/tag/__init__.py | 1 - homeassistant/components/tag/icons.json | 8 +++----- homeassistant/components/tag/manifest.json | 1 + homeassistant/components/tag/strings.json | 18 ++++++++---------- homeassistant/generated/integrations.json | 5 ----- 5 files changed, 12 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 97307112f22..0462c5bec34 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -364,7 +364,6 @@ class TagEntity(Entity): """Representation of a Tag entity.""" _unrecorded_attributes = frozenset({TAG_ID}) - _attr_translation_key = DOMAIN _attr_should_poll = False def __init__( diff --git a/homeassistant/components/tag/icons.json b/homeassistant/components/tag/icons.json index d9532aadf73..c931ae8614c 100644 --- a/homeassistant/components/tag/icons.json +++ b/homeassistant/components/tag/icons.json @@ -1,9 +1,7 @@ { - "entity": { - "tag": { - "tag": { - "default": "mdi:tag-outline" - } + "entity_component": { + "_": { + "default": "mdi:tag-outline" } } } diff --git a/homeassistant/components/tag/manifest.json b/homeassistant/components/tag/manifest.json index 14701763573..738e7f7e744 100644 --- a/homeassistant/components/tag/manifest.json +++ b/homeassistant/components/tag/manifest.json @@ -3,5 +3,6 @@ "name": "Tags", "codeowners": ["@balloob", "@dmulcahey"], "documentation": "https://www.home-assistant.io/integrations/tag", + "integration_type": "entity", "quality_scale": "internal" } diff --git a/homeassistant/components/tag/strings.json b/homeassistant/components/tag/strings.json index 75cec1f9ef4..4adbf1d48fc 100644 --- a/homeassistant/components/tag/strings.json +++ b/homeassistant/components/tag/strings.json @@ -1,15 +1,13 @@ { "title": "Tag", - "entity": { - "tag": { - "tag": { - "state_attributes": { - "tag_id": { - "name": "Tag ID" - }, - "last_scanned_by_device_id": { - "name": "Last scanned by device ID" - } + "entity_component": { + "_": { + "state_attributes": { + "tag_id": { + "name": "Tag ID" + }, + "last_scanned_by_device_id": { + "name": "Last scanned by device ID" } } } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 3cc3ea71df9..816241035e6 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6013,10 +6013,6 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "tag": { - "integration_type": "hub", - "config_flow": false - }, "tailscale": { "name": "Tailscale", "integration_type": "hub", @@ -7365,7 +7361,6 @@ "shopping_list", "sun", "switch_as_x", - "tag", "threshold", "time_date", "tod", From 62d38e786d21e3e80246ac5ff765a34c8061c49e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Aug 2024 21:07:11 +0200 Subject: [PATCH 54/95] Mark assist_pipeline as a system integration type (#123202) --- homeassistant/components/assist_pipeline/manifest.json | 1 + homeassistant/generated/integrations.json | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/assist_pipeline/manifest.json b/homeassistant/components/assist_pipeline/manifest.json index dd3ec77f165..00950b138fd 100644 --- a/homeassistant/components/assist_pipeline/manifest.json +++ b/homeassistant/components/assist_pipeline/manifest.json @@ -4,6 +4,7 @@ "codeowners": ["@balloob", "@synesthesiam"], "dependencies": ["conversation", "stt", "tts", "wake_word"], "documentation": "https://www.home-assistant.io/integrations/assist_pipeline", + "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", "requirements": ["pymicro-vad==1.0.1"] diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 816241035e6..850c7d78bc0 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -490,12 +490,6 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "assist_pipeline": { - "name": "Assist pipeline", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_push" - }, "asterisk": { "name": "Asterisk", "integrations": { From 4f722e864c13f465dce8b46aadafc1add0646069 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Aug 2024 21:06:58 +0200 Subject: [PATCH 55/95] Mark webhook as a system integration type (#123204) --- homeassistant/components/webhook/manifest.json | 1 + homeassistant/generated/integrations.json | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/webhook/manifest.json b/homeassistant/components/webhook/manifest.json index c2795e8ac17..43f5321d9f6 100644 --- a/homeassistant/components/webhook/manifest.json +++ b/homeassistant/components/webhook/manifest.json @@ -4,5 +4,6 @@ "codeowners": ["@home-assistant/core"], "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/webhook", + "integration_type": "system", "quality_scale": "internal" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 850c7d78bc0..5107587ab89 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6810,11 +6810,6 @@ } } }, - "webhook": { - "name": "Webhook", - "integration_type": "hub", - "config_flow": false - }, "webmin": { "name": "Webmin", "integration_type": "device", From d530137bec3e23a194841e56d32246d8a6e741fa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Aug 2024 21:12:09 +0200 Subject: [PATCH 56/95] Bump version to 2024.8.0b3 --- 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 7f27548a68c..981fc42fd36 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index f23c571feb5..c29252b1ced 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b2" +version = "2024.8.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From fd5533d719ee82177579df92b225214b14ff7d9a Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Tue, 6 Aug 2024 06:35:47 -0400 Subject: [PATCH 57/95] Fix yamaha legacy receivers (#122985) --- .../components/yamaha/media_player.py | 15 +++++--- tests/components/yamaha/test_media_player.py | 37 ++++++++++++++++++- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 507f485fcc7..a8200ea3373 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations +import contextlib import logging from typing import Any @@ -129,11 +130,15 @@ def _discovery(config_info): else: _LOGGER.debug("Config Zones") zones = None - for recv in rxv.find(): - if recv.ctrl_url == config_info.ctrl_url: - _LOGGER.debug("Config Zones Matched %s", config_info.ctrl_url) - zones = recv.zone_controllers() - break + + # Fix for upstream issues in rxv.find() with some hardware. + with contextlib.suppress(AttributeError): + for recv in rxv.find(): + if recv.ctrl_url == config_info.ctrl_url: + _LOGGER.debug("Config Zones Matched %s", config_info.ctrl_url) + zones = recv.zone_controllers() + break + if not zones: _LOGGER.debug("Config Zones Fallback") zones = rxv.RXV(config_info.ctrl_url, config_info.name).zone_controllers() diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 66d0a42f256..804b800aaef 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -53,7 +53,20 @@ def device_fixture(main_zone): yield device -async def test_setup_host(hass: HomeAssistant, device, main_zone) -> None: +@pytest.fixture(name="device2") +def device2_fixture(main_zone): + """Mock the yamaha device.""" + device = FakeYamahaDevice( + "http://127.0.0.1:80/YamahaRemoteControl/ctrl", "Receiver 2", zones=[main_zone] + ) + with ( + patch("rxv.RXV", return_value=device), + patch("rxv.find", return_value=[device]), + ): + yield device + + +async def test_setup_host(hass: HomeAssistant, device, device2, main_zone) -> None: """Test set up integration with host.""" assert await async_setup_component(hass, MP_DOMAIN, CONFIG) await hass.async_block_till_done() @@ -63,6 +76,28 @@ async def test_setup_host(hass: HomeAssistant, device, main_zone) -> None: assert state is not None assert state.state == "off" + with patch("rxv.find", return_value=[device2]): + assert await async_setup_component(hass, MP_DOMAIN, CONFIG) + await hass.async_block_till_done() + + state = hass.states.get("media_player.yamaha_receiver_main_zone") + + assert state is not None + assert state.state == "off" + + +async def test_setup_attribute_error(hass: HomeAssistant, device, main_zone) -> None: + """Test set up integration encountering an Attribute Error.""" + + with patch("rxv.find", side_effect=AttributeError): + assert await async_setup_component(hass, MP_DOMAIN, CONFIG) + await hass.async_block_till_done() + + state = hass.states.get("media_player.yamaha_receiver_main_zone") + + assert state is not None + assert state.state == "off" + async def test_setup_no_host(hass: HomeAssistant, device, main_zone) -> None: """Test set up integration without host.""" From 6d47a4d7e4d927f1d61448b32f3c6d7a6dc81ecf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 6 Aug 2024 20:12:31 +1200 Subject: [PATCH 58/95] Add support for ESPHome update entities to be checked on demand (#123161) --- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/esphome/update.py | 11 +++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/esphome/test_update.py | 14 ++++++++++++++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index ff7569bbc5f..97724a12203 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==24.6.2", + "aioesphomeapi==25.0.0", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.0.0" ], diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index e86c88ddf5b..b7905fb4fdb 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -8,6 +8,7 @@ from typing import Any from aioesphomeapi import ( DeviceInfo as ESPHomeDeviceInfo, EntityInfo, + UpdateCommand, UpdateInfo, UpdateState, ) @@ -259,9 +260,15 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity): """Return the title of the update.""" return self._state.title + @convert_api_error_ha_error + async def async_update(self) -> None: + """Command device to check for update.""" + if self.available: + self._client.update_command(key=self._key, command=UpdateCommand.CHECK) + @convert_api_error_ha_error async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: - """Update the current value.""" - self._client.update_command(key=self._key, install=True) + """Command device to install update.""" + self._client.update_command(key=self._key, command=UpdateCommand.INSTALL) diff --git a/requirements_all.txt b/requirements_all.txt index 2fd2c93a687..f54ae205864 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -237,7 +237,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==24.6.2 +aioesphomeapi==25.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a71321bc5b..2974d065380 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -225,7 +225,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==24.6.2 +aioesphomeapi==25.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index c9826c3f347..83e89b1de00 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -8,6 +8,7 @@ from aioesphomeapi import ( APIClient, EntityInfo, EntityState, + UpdateCommand, UpdateInfo, UpdateState, UserService, @@ -15,6 +16,10 @@ from aioesphomeapi import ( import pytest from homeassistant.components.esphome.dashboard import async_get_dashboard +from homeassistant.components.homeassistant import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, +) from homeassistant.components.update import ( DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL, @@ -527,3 +532,12 @@ async def test_generic_device_update_entity_has_update( assert state is not None assert state.state == STATE_ON assert state.attributes["in_progress"] == 50 + + await hass.services.async_call( + HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: "update.test_myupdate"}, + blocking=True, + ) + + mock_client.update_command.assert_called_with(key=1, command=UpdateCommand.CHECK) From 6af1e25d7e2a83463b2ac33f9e61442dd7aeb6eb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 6 Aug 2024 20:11:08 +1200 Subject: [PATCH 59/95] Show project version as `sw_version` in ESPHome (#123183) --- homeassistant/components/esphome/manager.py | 6 +++--- tests/components/esphome/test_manager.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index e8d002fba9d..ef2a6862f10 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -654,12 +654,13 @@ def _async_setup_device_registry( if device_info.manufacturer: manufacturer = device_info.manufacturer model = device_info.model - hw_version = None if device_info.project_name: project_name = device_info.project_name.split(".") manufacturer = project_name[0] model = project_name[1] - hw_version = device_info.project_version + sw_version = ( + f"{device_info.project_version} (ESPHome {device_info.esphome_version})" + ) suggested_area = None if device_info.suggested_area: @@ -674,7 +675,6 @@ def _async_setup_device_registry( manufacturer=manufacturer, model=model, sw_version=sw_version, - hw_version=hw_version, suggested_area=suggested_area, ) return device_entry.id diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 01f267581f4..651c52cd083 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -1024,7 +1024,7 @@ async def test_esphome_device_with_project( ) assert dev.manufacturer == "mfr" assert dev.model == "model" - assert dev.hw_version == "2.2.2" + assert dev.sw_version == "2.2.2 (ESPHome 1.0.0)" async def test_esphome_device_with_manufacturer( From 495fd946bc044d251b07500336490c58e84f093d Mon Sep 17 00:00:00 2001 From: flopp999 <21694965+flopp999@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:21:48 +0200 Subject: [PATCH 60/95] Fix growatt server tlx battery api key (#123191) --- .../components/growatt_server/sensor_types/tlx.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/growatt_server/sensor_types/tlx.py b/homeassistant/components/growatt_server/sensor_types/tlx.py index d8f158f2421..bf8746e08ac 100644 --- a/homeassistant/components/growatt_server/sensor_types/tlx.py +++ b/homeassistant/components/growatt_server/sensor_types/tlx.py @@ -327,14 +327,14 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( GrowattSensorEntityDescription( key="tlx_battery_2_discharge_w", translation_key="tlx_battery_2_discharge_w", - api_key="bdc1DischargePower", + api_key="bdc2DischargePower", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="tlx_battery_2_discharge_total", translation_key="tlx_battery_2_discharge_total", - api_key="bdc1DischargeTotal", + api_key="bdc2DischargeTotal", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -376,14 +376,14 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( GrowattSensorEntityDescription( key="tlx_battery_2_charge_w", translation_key="tlx_battery_2_charge_w", - api_key="bdc1ChargePower", + api_key="bdc2ChargePower", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="tlx_battery_2_charge_total", translation_key="tlx_battery_2_charge_total", - api_key="bdc1ChargeTotal", + api_key="bdc2ChargeTotal", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, From f796950493eeedd9be486684b4f1fdd4661229ab Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Tue, 6 Aug 2024 04:06:35 -0400 Subject: [PATCH 61/95] Update greeclimate to 2.1.0 (#123210) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index ca1c4b5b754..dba8cd6077c 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/gree", "iot_class": "local_polling", "loggers": ["greeclimate"], - "requirements": ["greeclimate==2.0.0"] + "requirements": ["greeclimate==2.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index f54ae205864..04daff1f122 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1007,7 +1007,7 @@ gpiozero==1.6.2 gps3==0.33.3 # homeassistant.components.gree -greeclimate==2.0.0 +greeclimate==2.1.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2974d065380..617a239e388 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -848,7 +848,7 @@ govee-local-api==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==2.0.0 +greeclimate==2.1.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 From 01b54fe1a97faa0100c76282feb3d843c43ff632 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 6 Aug 2024 11:51:04 +0200 Subject: [PATCH 62/95] Update knx-frontend to 2024.8.6.85349 (#123226) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 0f96970f3ae..37206df4c83 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==3.0.0", "xknxproject==3.7.1", - "knx-frontend==2024.7.25.204106" + "knx-frontend==2024.8.6.85349" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 04daff1f122..f62e8ba616f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1216,7 +1216,7 @@ kiwiki-client==0.1.1 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.7.25.204106 +knx-frontend==2024.8.6.85349 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 617a239e388..3475a4f9f9a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1012,7 +1012,7 @@ kegtron-ble==0.4.0 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.7.25.204106 +knx-frontend==2024.8.6.85349 # homeassistant.components.konnected konnected==1.2.0 From 97587fae08ffb0c0a9463daa345b2b3507466c29 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 6 Aug 2024 12:22:14 +0200 Subject: [PATCH 63/95] Bump yt-dlp to 2023.08.06 (#123229) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index cd312413db3..2285d7bce7d 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp==2024.07.16"], + "requirements": ["yt-dlp==2024.08.06"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index f62e8ba616f..e275d37d0cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2971,7 +2971,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.07.16 +yt-dlp==2024.08.06 # homeassistant.components.zamg zamg==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3475a4f9f9a..39fff2f33c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2348,7 +2348,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.07.16 +yt-dlp==2024.08.06 # homeassistant.components.zamg zamg==0.3.6 From 77bcbbcf538abcd89aca33c142e20ad2cf778cfe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 12:48:15 +0200 Subject: [PATCH 64/95] Update frontend to 20240806.0 (#123230) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 82dc9cdb83f..a91feb82461 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240805.1"] + "requirements": ["home-assistant-frontend==20240806.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6fc0d6f535d..96cf6d7624c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240805.1 +home-assistant-frontend==20240806.0 home-assistant-intents==2024.7.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index e275d37d0cd..4614d760a84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240805.1 +home-assistant-frontend==20240806.0 # homeassistant.components.conversation home-assistant-intents==2024.7.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39fff2f33c9..e9f7b37122c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -916,7 +916,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240805.1 +home-assistant-frontend==20240806.0 # homeassistant.components.conversation home-assistant-intents==2024.7.29 From c1953e938d530f7dd1056ede09cc0ee32942551d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 13:18:22 +0200 Subject: [PATCH 65/95] Mark Alexa integration as system type (#123232) --- homeassistant/components/alexa/manifest.json | 1 + homeassistant/generated/integrations.json | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index 84a4e152c1d..de59d28925f 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@home-assistant/cloud", "@ochlocracy", "@jbouwh"], "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/alexa", + "integration_type": "system", "iot_class": "cloud_push" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 5107587ab89..73c60c1373e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -200,12 +200,6 @@ "amazon": { "name": "Amazon", "integrations": { - "alexa": { - "integration_type": "hub", - "config_flow": false, - "iot_class": "cloud_push", - "name": "Amazon Alexa" - }, "amazon_polly": { "integration_type": "hub", "config_flow": false, From 5b2e188b528d2a3ae1bc3b8cb3a03badd028b4d3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 13:20:32 +0200 Subject: [PATCH 66/95] Mark Google Assistant integration as system type (#123233) --- homeassistant/components/google_assistant/manifest.json | 1 + homeassistant/generated/integrations.json | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant/manifest.json b/homeassistant/components/google_assistant/manifest.json index e36f6a1ca87..a38ea4f7cfb 100644 --- a/homeassistant/components/google_assistant/manifest.json +++ b/homeassistant/components/google_assistant/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@home-assistant/cloud"], "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/google_assistant", + "integration_type": "system", "iot_class": "cloud_push" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 73c60c1373e..85369b238db 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2236,12 +2236,6 @@ "google": { "name": "Google", "integrations": { - "google_assistant": { - "integration_type": "hub", - "config_flow": false, - "iot_class": "cloud_push", - "name": "Google Assistant" - }, "google_assistant_sdk": { "integration_type": "service", "config_flow": true, From e9fe98f7f9bf2fd04d29c2145bd4719d06144db1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 13:22:46 +0200 Subject: [PATCH 67/95] Bump version to 2024.8.0b4 --- 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 981fc42fd36..ed0a44bb7cc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index c29252b1ced..86fa1c8bbc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b3" +version = "2024.8.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From a09d0117b1a225d20f08a8d7d45f1c6443103baa Mon Sep 17 00:00:00 2001 From: Yehazkel Date: Tue, 6 Aug 2024 17:21:34 +0300 Subject: [PATCH 68/95] Fix Tami4 device name is None (#123156) Co-authored-by: Robert Resch --- homeassistant/components/tami4/config_flow.py | 5 ++- tests/components/tami4/conftest.py | 25 ++++++++++++++ tests/components/tami4/test_config_flow.py | 33 +++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tami4/config_flow.py b/homeassistant/components/tami4/config_flow.py index 8c1edbfb60f..0fa05bbebe4 100644 --- a/homeassistant/components/tami4/config_flow.py +++ b/homeassistant/components/tami4/config_flow.py @@ -82,8 +82,11 @@ class Tami4ConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: + device_name = api.device_metadata.name + if device_name is None: + device_name = "Tami4" return self.async_create_entry( - title=api.device_metadata.name, + title=device_name, data={CONF_REFRESH_TOKEN: refresh_token}, ) diff --git a/tests/components/tami4/conftest.py b/tests/components/tami4/conftest.py index 2f4201d9a9e..2b4acac0b3f 100644 --- a/tests/components/tami4/conftest.py +++ b/tests/components/tami4/conftest.py @@ -60,6 +60,31 @@ def mock__get_devices_metadata(request: pytest.FixtureRequest) -> Generator[None yield +@pytest.fixture +def mock__get_devices_metadata_no_name( + request: pytest.FixtureRequest, +) -> Generator[None]: + """Fixture to mock _get_devices which makes a call to the API.""" + + side_effect = getattr(request, "param", None) + + device_metadata = DeviceMetadata( + id=1, + name=None, + connected=True, + psn="psn", + type="type", + device_firmware="v1.1", + ) + + with patch( + "Tami4EdgeAPI.Tami4EdgeAPI.Tami4EdgeAPI._get_devices_metadata", + return_value=[device_metadata], + side_effect=side_effect, + ): + yield + + @pytest.fixture def mock_get_device( request: pytest.FixtureRequest, diff --git a/tests/components/tami4/test_config_flow.py b/tests/components/tami4/test_config_flow.py index 4210c391d70..4dfc27bba94 100644 --- a/tests/components/tami4/test_config_flow.py +++ b/tests/components/tami4/test_config_flow.py @@ -120,6 +120,39 @@ async def test_step_otp_valid( assert "refresh_token" in result["data"] +@pytest.mark.usefixtures( + "mock_setup_entry", + "mock_request_otp", + "mock_submit_otp", + "mock__get_devices_metadata_no_name", +) +async def test_step_otp_valid_device_no_name(hass: HomeAssistant) -> None: + """Test user step with valid phone number.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PHONE: "+972555555555"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "otp" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"otp": "123456"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Tami4" + assert "refresh_token" in result["data"] + + @pytest.mark.parametrize( ("mock_submit_otp", "expected_error"), [ From 35a6679ae9350351da3497862b95f08d5c3bfec6 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 6 Aug 2024 14:55:14 +0200 Subject: [PATCH 69/95] Delete mobile_app cloudhook if not logged into the cloud (#123234) --- .../components/mobile_app/__init__.py | 18 ++++++--- tests/components/mobile_app/test_init.py | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index a8577cc596d..80893e0cbfa 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -124,12 +124,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ): await async_create_cloud_hook(hass, webhook_id, entry) - if ( - CONF_CLOUDHOOK_URL not in entry.data - and cloud.async_active_subscription(hass) - and cloud.async_is_connected(hass) - ): - await async_create_cloud_hook(hass, webhook_id, entry) + if cloud.async_is_logged_in(hass): + if ( + CONF_CLOUDHOOK_URL not in entry.data + and cloud.async_active_subscription(hass) + and cloud.async_is_connected(hass) + ): + await async_create_cloud_hook(hass, webhook_id, entry) + elif CONF_CLOUDHOOK_URL in entry.data: + # If we have a cloudhook but no longer logged in to the cloud, remove it from the entry + data = dict(entry.data) + data.pop(CONF_CLOUDHOOK_URL) + hass.config_entries.async_update_entry(entry, data=data) entry.async_on_unload(cloud.async_listen_connection_change(hass, manage_cloudhook)) diff --git a/tests/components/mobile_app/test_init.py b/tests/components/mobile_app/test_init.py index 15380a0d8d7..e1c7ed27cf9 100644 --- a/tests/components/mobile_app/test_init.py +++ b/tests/components/mobile_app/test_init.py @@ -89,6 +89,7 @@ async def _test_create_cloud_hook( "homeassistant.components.cloud.async_active_subscription", return_value=async_active_subscription_return_value, ), + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), patch("homeassistant.components.cloud.async_is_connected", return_value=True), patch( "homeassistant.components.cloud.async_get_or_create_cloudhook", @@ -187,3 +188,41 @@ async def test_create_cloud_hook_after_connection( ) await _test_create_cloud_hook(hass, hass_admin_user, {}, False, additional_steps) + + +@pytest.mark.parametrize( + ("cloud_logged_in", "should_cloudhook_exist"), + [(True, True), (False, False)], +) +async def test_delete_cloud_hook( + hass: HomeAssistant, + hass_admin_user: MockUser, + cloud_logged_in: bool, + should_cloudhook_exist: bool, +) -> None: + """Test deleting the cloud hook only when logged out of the cloud.""" + + config_entry = MockConfigEntry( + data={ + **REGISTER_CLEARTEXT, + CONF_WEBHOOK_ID: "test-webhook-id", + ATTR_DEVICE_NAME: "Test", + ATTR_DEVICE_ID: "Test", + CONF_USER_ID: hass_admin_user.id, + CONF_CLOUDHOOK_URL: "https://hook-url-already-exists", + }, + domain=DOMAIN, + title="Test", + ) + config_entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.cloud.async_is_logged_in", + return_value=cloud_logged_in, + ), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + assert (CONF_CLOUDHOOK_URL in config_entry.data) == should_cloudhook_exist From 870bb7efd4e52953ecd8d56184a152341ebd9cae Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 16:04:29 +0200 Subject: [PATCH 70/95] Mark FFmpeg integration as system type (#123241) --- homeassistant/components/ffmpeg/manifest.json | 1 + homeassistant/generated/integrations.json | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/ffmpeg/manifest.json b/homeassistant/components/ffmpeg/manifest.json index 8cd7b1f504d..ab9f3ed65c1 100644 --- a/homeassistant/components/ffmpeg/manifest.json +++ b/homeassistant/components/ffmpeg/manifest.json @@ -3,5 +3,6 @@ "name": "FFmpeg", "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/ffmpeg", + "integration_type": "system", "requirements": ["ha-ffmpeg==3.2.0"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 85369b238db..13009fb58be 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1798,11 +1798,6 @@ "ffmpeg": { "name": "FFmpeg", "integrations": { - "ffmpeg": { - "integration_type": "hub", - "config_flow": false, - "name": "FFmpeg" - }, "ffmpeg_motion": { "integration_type": "hub", "config_flow": false, From 7aae9d9ad3e405cf233fef401fc473ca94844bc8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Aug 2024 08:32:58 -0500 Subject: [PATCH 71/95] Fix sense doing blocking I/O in the event loop (#123247) --- homeassistant/components/sense/__init__.py | 12 ++++++++++-- homeassistant/components/sense/config_flow.py | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 28408c0cb7d..58e993ad6e0 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from datetime import timedelta +from functools import partial import logging from typing import Any @@ -80,8 +81,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: SenseConfigEntry) -> boo client_session = async_get_clientsession(hass) - gateway = ASyncSenseable( - api_timeout=timeout, wss_timeout=timeout, client_session=client_session + # Creating the AsyncSenseable object loads + # ssl certificates which does blocking IO + gateway = await hass.async_add_executor_job( + partial( + ASyncSenseable, + api_timeout=timeout, + wss_timeout=timeout, + client_session=client_session, + ) ) gateway.rate_limit = ACTIVE_UPDATE_RATE diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py index 25c6898aec8..dab80b99e1a 100644 --- a/homeassistant/components/sense/config_flow.py +++ b/homeassistant/components/sense/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Sense integration.""" from collections.abc import Mapping +from functools import partial import logging from typing import Any @@ -48,8 +49,15 @@ class SenseConfigFlow(ConfigFlow, domain=DOMAIN): timeout = self._auth_data[CONF_TIMEOUT] client_session = async_get_clientsession(self.hass) - self._gateway = ASyncSenseable( - api_timeout=timeout, wss_timeout=timeout, client_session=client_session + # Creating the AsyncSenseable object loads + # ssl certificates which does blocking IO + self._gateway = await self.hass.async_add_executor_job( + partial( + ASyncSenseable, + api_timeout=timeout, + wss_timeout=timeout, + client_session=client_session, + ) ) self._gateway.rate_limit = ACTIVE_UPDATE_RATE await self._gateway.authenticate( From 3d0a0cf376e880528aba6532503c02b853b95738 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 6 Aug 2024 16:16:22 +0200 Subject: [PATCH 72/95] Bump deebot-client to 8.3.0 (#123249) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 8838eb4f50a..560ee4d599c 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==8.2.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==8.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4614d760a84..64a93d91eff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -706,7 +706,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==8.2.0 +deebot-client==8.3.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9f7b37122c..d6f0efa56ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -599,7 +599,7 @@ dbus-fast==2.22.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==8.2.0 +deebot-client==8.3.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 3cf3780587be1e8a287507375de07b421dff31df Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 6 Aug 2024 16:28:37 +0200 Subject: [PATCH 73/95] Bump mficlient to 0.5.0 (#123250) --- homeassistant/components/mfi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/licenses.py | 3 --- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mfi/manifest.json b/homeassistant/components/mfi/manifest.json index db9cb547b28..b569009d400 100644 --- a/homeassistant/components/mfi/manifest.json +++ b/homeassistant/components/mfi/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/mfi", "iot_class": "local_polling", "loggers": ["mficlient"], - "requirements": ["mficlient==0.3.0"] + "requirements": ["mficlient==0.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 64a93d91eff..a8670fad05e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1327,7 +1327,7 @@ meteoalertapi==0.3.0 meteofrance-api==1.3.0 # homeassistant.components.mfi -mficlient==0.3.0 +mficlient==0.5.0 # homeassistant.components.xiaomi_miio micloud==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d6f0efa56ff..310e2cc9431 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1093,7 +1093,7 @@ melnor-bluetooth==0.0.25 meteofrance-api==1.3.0 # homeassistant.components.mfi -mficlient==0.3.0 +mficlient==0.5.0 # homeassistant.components.xiaomi_miio micloud==0.5 diff --git a/script/licenses.py b/script/licenses.py index 3b9ec389b08..dc89cdad9a9 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -182,9 +182,6 @@ TODO = { "asterisk_mbox": AwesomeVersion( "0.5.0" ), # No license, integration is deprecated and scheduled for removal in 2024.9.0 - "mficlient": AwesomeVersion( - "0.3.0" - ), # No license https://github.com/kk7ds/mficlient/issues/4 "pyflic": AwesomeVersion("2.0.3"), # No OSI approved license CC0-1.0 Universal) "uvcclient": AwesomeVersion( "0.11.0" From a243ed5b238deff82f84d918f591066e5f13f3d1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 17:54:40 +0200 Subject: [PATCH 74/95] Update frontend to 20240806.1 (#123252) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index a91feb82461..de423ee9ac6 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240806.0"] + "requirements": ["home-assistant-frontend==20240806.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 96cf6d7624c..13fbcabf0d1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240806.0 +home-assistant-frontend==20240806.1 home-assistant-intents==2024.7.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index a8670fad05e..d5658b2903e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1093,7 +1093,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240806.0 +home-assistant-frontend==20240806.1 # homeassistant.components.conversation home-assistant-intents==2024.7.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 310e2cc9431..b37af69bfc5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -916,7 +916,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240806.0 +home-assistant-frontend==20240806.1 # homeassistant.components.conversation home-assistant-intents==2024.7.29 From b636096ac308d8739f8549af7d87dbd63a479959 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Aug 2024 18:08:19 +0200 Subject: [PATCH 75/95] Bump version to 2024.8.0b5 --- 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 ed0a44bb7cc..dd181d74655 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 86fa1c8bbc1..1831a46a0af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b4" +version = "2024.8.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 0270026f7cb6c76841deba32afda55798ee9dc3a Mon Sep 17 00:00:00 2001 From: Steve Repsher Date: Tue, 6 Aug 2024 10:17:54 -0400 Subject: [PATCH 76/95] Adapt static resource handler to aiohttp 3.10 (#123166) --- homeassistant/components/http/static.py | 79 +++++++------------------ tests/components/http/test_static.py | 35 +++-------- 2 files changed, 30 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index a7280fb9b2f..29c5840a4bf 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -3,81 +3,46 @@ from __future__ import annotations from collections.abc import Mapping -import mimetypes from pathlib import Path from typing import Final -from aiohttp import hdrs +from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE from aiohttp.web import FileResponse, Request, StreamResponse -from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound +from aiohttp.web_fileresponse import CONTENT_TYPES, FALLBACK_CONTENT_TYPE from aiohttp.web_urldispatcher import StaticResource from lru import LRU -from .const import KEY_HASS - CACHE_TIME: Final = 31 * 86400 # = 1 month CACHE_HEADER = f"public, max-age={CACHE_TIME}" -CACHE_HEADERS: Mapping[str, str] = {hdrs.CACHE_CONTROL: CACHE_HEADER} -PATH_CACHE: LRU[tuple[str, Path], tuple[Path | None, str | None]] = LRU(512) - - -def _get_file_path(rel_url: str, directory: Path) -> Path | None: - """Return the path to file on disk or None.""" - filename = Path(rel_url) - if filename.anchor: - # rel_url is an absolute name like - # /static/\\machine_name\c$ or /static/D:\path - # where the static dir is totally different - raise HTTPForbidden - filepath: Path = directory.joinpath(filename).resolve() - filepath.relative_to(directory) - # on opening a dir, load its contents if allowed - if filepath.is_dir(): - return None - if filepath.is_file(): - return filepath - raise FileNotFoundError +CACHE_HEADERS: Mapping[str, str] = {CACHE_CONTROL: CACHE_HEADER} +RESPONSE_CACHE: LRU[tuple[str, Path], tuple[Path, str]] = LRU(512) class CachingStaticResource(StaticResource): """Static Resource handler that will add cache headers.""" async def _handle(self, request: Request) -> StreamResponse: - """Return requested file from disk as a FileResponse.""" + """Wrap base handler to cache file path resolution and content type guess.""" rel_url = request.match_info["filename"] key = (rel_url, self._directory) - if (filepath_content_type := PATH_CACHE.get(key)) is None: - hass = request.app[KEY_HASS] - try: - filepath = await hass.async_add_executor_job(_get_file_path, *key) - except (ValueError, FileNotFoundError) as error: - # relatively safe - raise HTTPNotFound from error - except HTTPForbidden: - # forbidden - raise - except Exception as error: - # perm error or other kind! - request.app.logger.exception("Unexpected exception") - raise HTTPNotFound from error + response: StreamResponse - content_type: str | None = None - if filepath is not None: - content_type = (mimetypes.guess_type(rel_url))[ - 0 - ] or "application/octet-stream" - PATH_CACHE[key] = (filepath, content_type) + if key in RESPONSE_CACHE: + file_path, content_type = RESPONSE_CACHE[key] + response = FileResponse(file_path, chunk_size=self._chunk_size) + response.headers[CONTENT_TYPE] = content_type else: - filepath, content_type = filepath_content_type - - if filepath and content_type: - return FileResponse( - filepath, - chunk_size=self._chunk_size, - headers={ - hdrs.CACHE_CONTROL: CACHE_HEADER, - hdrs.CONTENT_TYPE: content_type, - }, + response = await super()._handle(request) + if not isinstance(response, FileResponse): + # Must be directory index; ignore caching + return response + file_path = response._path # noqa: SLF001 + response.content_type = ( + CONTENT_TYPES.guess_type(file_path)[0] or FALLBACK_CONTENT_TYPE ) + # Cache actual header after setter construction. + content_type = response.headers[CONTENT_TYPE] + RESPONSE_CACHE[key] = (file_path, content_type) - raise HTTPForbidden if filepath is None else HTTPNotFound + response.headers[CACHE_CONTROL] = CACHE_HEADER + return response diff --git a/tests/components/http/test_static.py b/tests/components/http/test_static.py index 52a5db5daa7..2ac7c6ded93 100644 --- a/tests/components/http/test_static.py +++ b/tests/components/http/test_static.py @@ -4,11 +4,10 @@ from http import HTTPStatus from pathlib import Path from aiohttp.test_utils import TestClient -from aiohttp.web_exceptions import HTTPForbidden import pytest from homeassistant.components.http import StaticPathConfig -from homeassistant.components.http.static import CachingStaticResource, _get_file_path +from homeassistant.components.http.static import CachingStaticResource from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant from homeassistant.helpers.http import KEY_ALLOW_CONFIGURED_CORS @@ -31,37 +30,19 @@ async def mock_http_client(hass: HomeAssistant, aiohttp_client: ClientSessionGen return await aiohttp_client(hass.http.app, server_kwargs={"skip_url_asserts": True}) -@pytest.mark.parametrize( - ("url", "canonical_url"), - [ - ("//a", "//a"), - ("///a", "///a"), - ("/c:\\a\\b", "/c:%5Ca%5Cb"), - ], -) -async def test_static_path_blocks_anchors( - hass: HomeAssistant, - mock_http_client: TestClient, - tmp_path: Path, - url: str, - canonical_url: str, +async def test_static_resource_show_index( + hass: HomeAssistant, mock_http_client: TestClient, tmp_path: Path ) -> None: - """Test static paths block anchors.""" + """Test static resource will return a directory index.""" app = hass.http.app - resource = CachingStaticResource(url, str(tmp_path)) - assert resource.canonical == canonical_url + resource = CachingStaticResource("/", tmp_path, show_index=True) app.router.register_resource(resource) app[KEY_ALLOW_CONFIGURED_CORS](resource) - resp = await mock_http_client.get(canonical_url, allow_redirects=False) - assert resp.status == 403 - - # Tested directly since aiohttp will block it before - # it gets here but we want to make sure if aiohttp ever - # changes we still block it. - with pytest.raises(HTTPForbidden): - _get_file_path(canonical_url, tmp_path) + resp = await mock_http_client.get("/") + assert resp.status == 200 + assert resp.content_type == "text/html" async def test_async_register_static_paths( From 940327dccf8ad55a5bf368c5cd7ff09ab8b8a4f5 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:46:25 -0400 Subject: [PATCH 77/95] Bump ZHA to 0.0.28 (#123259) * Bump ZHA to 0.0.28 * Drop redundant radio schema conversion --- homeassistant/components/zha/manifest.json | 2 +- homeassistant/components/zha/radio_manager.py | 1 - requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index d7dc53b5167..4a597b0233c 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.27"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.28"], "usb": [ { "vid": "10C4", diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 2b7a65f4997..82c30b7678a 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -178,7 +178,6 @@ class ZhaRadioManager: 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( app_config, auto_form=False, start_radio=False diff --git a/requirements_all.txt b/requirements_all.txt index d5658b2903e..42850c46083 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2986,7 +2986,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.27 +zha==0.0.28 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b37af69bfc5..bad3a851c88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2360,7 +2360,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.27 +zha==0.0.28 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 From 9e75b639251359a3125bbd53d9cf31a4524fd1c0 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 7 Aug 2024 09:12:20 +0200 Subject: [PATCH 78/95] Update knx-frontend to 2024.8.6.211307 (#123261) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 37206df4c83..62364f641f4 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==3.0.0", "xknxproject==3.7.1", - "knx-frontend==2024.8.6.85349" + "knx-frontend==2024.8.6.211307" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 42850c46083..21270678ba6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1216,7 +1216,7 @@ kiwiki-client==0.1.1 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.8.6.85349 +knx-frontend==2024.8.6.211307 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bad3a851c88..283c84d3b01 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1012,7 +1012,7 @@ kegtron-ble==0.4.0 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.8.6.85349 +knx-frontend==2024.8.6.211307 # homeassistant.components.konnected konnected==1.2.0 From 1143efedc53ace8e28cef3875e31de1d93d39a38 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 7 Aug 2024 00:16:57 +0200 Subject: [PATCH 79/95] Bump reolink-aio to 0.9.7 (#123263) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/reolink/test_select.py | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 7289dac682c..9671a4b4fc1 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.9.6"] + "requirements": ["reolink-aio==0.9.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 21270678ba6..008253960d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2483,7 +2483,7 @@ renault-api==0.2.5 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.6 +reolink-aio==0.9.7 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 283c84d3b01..737d30f7b25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1965,7 +1965,7 @@ renault-api==0.2.5 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.9.6 +reolink-aio==0.9.7 # homeassistant.components.rflink rflink==0.0.66 diff --git a/tests/components/reolink/test_select.py b/tests/components/reolink/test_select.py index 908c06dc16f..53c1e494b3d 100644 --- a/tests/components/reolink/test_select.py +++ b/tests/components/reolink/test_select.py @@ -110,15 +110,15 @@ async def test_chime_select( host=reolink_connect, dev_id=12345678, channel=0, - name="Test chime", - event_info={ - "md": {"switch": 0, "musicId": 0}, - "people": {"switch": 0, "musicId": 1}, - "visitor": {"switch": 1, "musicId": 2}, - }, ) + TEST_CHIME.name = "Test chime" TEST_CHIME.volume = 3 TEST_CHIME.led_state = True + TEST_CHIME.event_info = { + "md": {"switch": 0, "musicId": 0}, + "people": {"switch": 0, "musicId": 1}, + "visitor": {"switch": 1, "musicId": 2}, + } reolink_connect.chime_list = [TEST_CHIME] From b0269faae4c40b69f40b33074019628068cad97b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Aug 2024 02:13:23 -0500 Subject: [PATCH 80/95] Allow non-admins to subscribe to newer registry update events (#123267) --- homeassistant/auth/permissions/events.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/auth/permissions/events.py b/homeassistant/auth/permissions/events.py index 9f2fb45f9f0..cb0506769bf 100644 --- a/homeassistant/auth/permissions/events.py +++ b/homeassistant/auth/permissions/events.py @@ -18,9 +18,12 @@ from homeassistant.const import ( EVENT_THEMES_UPDATED, ) from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED +from homeassistant.helpers.category_registry import EVENT_CATEGORY_REGISTRY_UPDATED from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED +from homeassistant.helpers.floor_registry import EVENT_FLOOR_REGISTRY_UPDATED from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED +from homeassistant.helpers.label_registry import EVENT_LABEL_REGISTRY_UPDATED from homeassistant.util.event_type import EventType # These are events that do not contain any sensitive data @@ -41,4 +44,7 @@ SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = { EVENT_SHOPPING_LIST_UPDATED, EVENT_STATE_CHANGED, EVENT_THEMES_UPDATED, + EVENT_LABEL_REGISTRY_UPDATED, + EVENT_CATEGORY_REGISTRY_UPDATED, + EVENT_FLOOR_REGISTRY_UPDATED, } From ad674a1c2bdb52360606c55f55a5ad44d5189e3c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 7 Aug 2024 20:17:01 +1200 Subject: [PATCH 81/95] Update ESPHome voice assistant pipeline log warning (#123269) --- homeassistant/components/esphome/manager.py | 2 +- tests/components/esphome/test_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index ef2a6862f10..4b4537d147f 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -346,7 +346,7 @@ class ESPHomeManager: ) -> int | None: """Start a voice assistant pipeline.""" if self.voice_assistant_pipeline is not None: - _LOGGER.warning("Voice assistant UDP server was not stopped") + _LOGGER.warning("Previous Voice assistant pipeline was not stopped") self.voice_assistant_pipeline.stop() self.voice_assistant_pipeline = None diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 651c52cd083..9d2a906466e 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -1229,7 +1229,7 @@ async def test_manager_voice_assistant_handlers_api( "", 0, None, None ) - assert "Voice assistant UDP server was not stopped" in caplog.text + assert "Previous Voice assistant pipeline was not stopped" in caplog.text await device.mock_voice_assistant_handle_audio(bytes(_ONE_SECOND)) From cc5699bf0894f107a94af74c8928db47c1ebcbd5 Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 7 Aug 2024 01:16:07 -0700 Subject: [PATCH 82/95] Fix Google Cloud TTS not respecting config values (#123275) --- .../components/google_cloud/helpers.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google_cloud/helpers.py b/homeassistant/components/google_cloud/helpers.py index 66dfbcf01eb..8ae6a456a4f 100644 --- a/homeassistant/components/google_cloud/helpers.py +++ b/homeassistant/components/google_cloud/helpers.py @@ -59,7 +59,10 @@ def tts_options_schema( vol.Optional( CONF_GENDER, description={"suggested_value": config_options.get(CONF_GENDER)}, - default=texttospeech.SsmlVoiceGender.NEUTRAL.name, # type: ignore[attr-defined] + default=config_options.get( + CONF_GENDER, + texttospeech.SsmlVoiceGender.NEUTRAL.name, # type: ignore[attr-defined] + ), ): vol.All( vol.Upper, SelectSelector( @@ -72,7 +75,7 @@ def tts_options_schema( vol.Optional( CONF_VOICE, description={"suggested_value": config_options.get(CONF_VOICE)}, - default=DEFAULT_VOICE, + default=config_options.get(CONF_VOICE, DEFAULT_VOICE), ): SelectSelector( SelectSelectorConfig( mode=SelectSelectorMode.DROPDOWN, @@ -82,7 +85,10 @@ def tts_options_schema( vol.Optional( CONF_ENCODING, description={"suggested_value": config_options.get(CONF_ENCODING)}, - default=texttospeech.AudioEncoding.MP3.name, # type: ignore[attr-defined] + default=config_options.get( + CONF_ENCODING, + texttospeech.AudioEncoding.MP3.name, # type: ignore[attr-defined] + ), ): vol.All( vol.Upper, SelectSelector( @@ -95,22 +101,22 @@ def tts_options_schema( vol.Optional( CONF_SPEED, description={"suggested_value": config_options.get(CONF_SPEED)}, - default=1.0, + default=config_options.get(CONF_SPEED, 1.0), ): NumberSelector(NumberSelectorConfig(min=0.25, max=4.0, step=0.01)), vol.Optional( CONF_PITCH, description={"suggested_value": config_options.get(CONF_PITCH)}, - default=0, + default=config_options.get(CONF_PITCH, 0), ): NumberSelector(NumberSelectorConfig(min=-20.0, max=20.0, step=0.1)), vol.Optional( CONF_GAIN, description={"suggested_value": config_options.get(CONF_GAIN)}, - default=0, + default=config_options.get(CONF_GAIN, 0), ): NumberSelector(NumberSelectorConfig(min=-96.0, max=16.0, step=0.1)), vol.Optional( CONF_PROFILES, description={"suggested_value": config_options.get(CONF_PROFILES)}, - default=[], + default=config_options.get(CONF_PROFILES, []), ): SelectSelector( SelectSelectorConfig( mode=SelectSelectorMode.DROPDOWN, @@ -132,7 +138,7 @@ def tts_options_schema( vol.Optional( CONF_TEXT_TYPE, description={"suggested_value": config_options.get(CONF_TEXT_TYPE)}, - default="text", + default=config_options.get(CONF_TEXT_TYPE, "text"), ): vol.All( vol.Lower, SelectSelector( From a10fed9d728392aeba80a5312dc6e5f4ae3521d8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2024 10:22:39 +0200 Subject: [PATCH 83/95] Bump version to 2024.8.0b6 --- 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 dd181d74655..02dcc77d36a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 1831a46a0af..58922676286 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b5" +version = "2024.8.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 270990fe3907229d3aad0dd327e83d0f97a7ea2a Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Wed, 7 Aug 2024 11:18:09 +0200 Subject: [PATCH 84/95] Tado change repair issue (#123256) --- homeassistant/components/tado/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index ab903dafb5b..39453cb5fe1 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -152,7 +152,7 @@ "issues": { "water_heater_fallback": { "title": "Tado Water Heater entities now support fallback options", - "description": "Due to added support for water heaters entities, these entities may use different overlay. Please configure integration entity and tado app water heater zone overlay options." + "description": "Due to added support for water heaters entities, these entities may use different overlay. Please configure integration entity and tado app water heater zone overlay options. Otherwise, please configure the integration entity and Tado app water heater zone overlay options (under Settings -> Rooms & Devices -> Hot Water)." } } } From db32460f3baceb3bf914979715cabfe2999c28d3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 7 Aug 2024 11:18:48 +0200 Subject: [PATCH 85/95] Reload conversation entries on update (#123279) --- homeassistant/components/ollama/conversation.py | 8 ++------ .../components/openai_conversation/conversation.py | 8 ++------ tests/components/ollama/conftest.py | 1 + tests/components/ollama/test_conversation.py | 1 + 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ollama/conversation.py b/homeassistant/components/ollama/conversation.py index 9f66083f506..c0423b258f0 100644 --- a/homeassistant/components/ollama/conversation.py +++ b/homeassistant/components/ollama/conversation.py @@ -346,9 +346,5 @@ class OllamaConversationEntity( self, hass: HomeAssistant, entry: ConfigEntry ) -> None: """Handle options update.""" - if entry.options.get(CONF_LLM_HASS_API): - self._attr_supported_features = ( - conversation.ConversationEntityFeature.CONTROL - ) - else: - self._attr_supported_features = conversation.ConversationEntityFeature(0) + # Reload as we update device info + entity name + supported features + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index b482126e27c..a7109a6d6ec 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -328,9 +328,5 @@ class OpenAIConversationEntity( self, hass: HomeAssistant, entry: ConfigEntry ) -> None: """Handle options update.""" - if entry.options.get(CONF_LLM_HASS_API): - self._attr_supported_features = ( - conversation.ConversationEntityFeature.CONTROL - ) - else: - self._attr_supported_features = conversation.ConversationEntityFeature(0) + # Reload as we update device info + entity name + supported features + await hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/ollama/conftest.py b/tests/components/ollama/conftest.py index 0355a13eba7..b28b8850cd5 100644 --- a/tests/components/ollama/conftest.py +++ b/tests/components/ollama/conftest.py @@ -48,6 +48,7 @@ async def mock_init_component(hass: HomeAssistant, mock_config_entry: MockConfig ): assert await async_setup_component(hass, ollama.DOMAIN, {}) await hass.async_block_till_done() + yield @pytest.fixture(autouse=True) diff --git a/tests/components/ollama/test_conversation.py b/tests/components/ollama/test_conversation.py index c83dce3b565..cb56b398342 100644 --- a/tests/components/ollama/test_conversation.py +++ b/tests/components/ollama/test_conversation.py @@ -312,6 +312,7 @@ async def test_unknown_hass_api( CONF_LLM_HASS_API: "non-existing", }, ) + await hass.async_block_till_done() result = await conversation.async_converse( hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id From af6f78a784399e491209a8c03c09d005421bf81a Mon Sep 17 00:00:00 2001 From: lunmay <28674102+lunmay@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:20:36 +0200 Subject: [PATCH 86/95] Fix typo on one of islamic_prayer_times calculation_method option (#123281) --- homeassistant/components/islamic_prayer_times/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/islamic_prayer_times/strings.json b/homeassistant/components/islamic_prayer_times/strings.json index 359d4626bd4..a90031c088d 100644 --- a/homeassistant/components/islamic_prayer_times/strings.json +++ b/homeassistant/components/islamic_prayer_times/strings.json @@ -45,7 +45,7 @@ "jakim": "Jabatan Kemajuan Islam Malaysia (JAKIM)", "tunisia": "Tunisia", "algeria": "Algeria", - "kemenag": "ementerian Agama Republik Indonesia", + "kemenag": "Kementerian Agama Republik Indonesia", "morocco": "Morocco", "portugal": "Comunidade Islamica de Lisboa", "jordan": "Ministry of Awqaf, Islamic Affairs and Holy Places, Jordan", From 782ff12e6eadbfc10342869351565a1e9f0e09be Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2024 11:26:03 +0200 Subject: [PATCH 87/95] Bump version to 2024.8.0b7 --- 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 02dcc77d36a..8e49ccc1b0b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 58922676286..4e2c2ebb72a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b6" +version = "2024.8.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 6bb55ce79e2bf34edfc80ed1580e6decdb39e30c Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 7 Aug 2024 21:11:03 +1000 Subject: [PATCH 88/95] Add missing application credential to Tesla Fleet (#123271) Co-authored-by: Franck Nijhof --- .../components/tesla_fleet/__init__.py | 10 ++++++- .../tesla_fleet/application_credentials.py | 27 ++++++++++--------- .../components/tesla_fleet/config_flow.py | 17 +++++++++++- homeassistant/components/tesla_fleet/const.py | 5 ++++ tests/components/tesla_fleet/__init__.py | 14 ++++++++++ tests/components/tesla_fleet/conftest.py | 19 ------------- .../tesla_fleet/test_config_flow.py | 5 ++-- 7 files changed, 61 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 8257bf75cd0..4eac1168674 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -13,6 +13,7 @@ from tesla_fleet_api.exceptions import ( TeslaFleetError, ) +from homeassistant.components.application_credentials import ClientCredential from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant @@ -26,7 +27,9 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceInfo -from .const import DOMAIN, LOGGER, MODELS +from .application_credentials import TeslaOAuth2Implementation +from .config_flow import OAuth2FlowHandler +from .const import CLIENT_ID, DOMAIN, LOGGER, MODELS, NAME from .coordinator import ( TeslaFleetEnergySiteInfoCoordinator, TeslaFleetEnergySiteLiveCoordinator, @@ -51,6 +54,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - scopes = token["scp"] region = token["ou_code"].lower() + OAuth2FlowHandler.async_register_implementation( + hass, + TeslaOAuth2Implementation(hass, DOMAIN, ClientCredential(CLIENT_ID, "", NAME)), + ) + implementation = await async_get_config_entry_implementation(hass, entry) oauth_session = OAuth2Session(hass, entry, implementation) refresh_lock = asyncio.Lock() diff --git a/homeassistant/components/tesla_fleet/application_credentials.py b/homeassistant/components/tesla_fleet/application_credentials.py index fda9fce8cec..32e16cc9244 100644 --- a/homeassistant/components/tesla_fleet/application_credentials.py +++ b/homeassistant/components/tesla_fleet/application_credentials.py @@ -5,15 +5,17 @@ import hashlib import secrets from typing import Any -from homeassistant.components.application_credentials import ClientCredential +from homeassistant.components.application_credentials import ( + AuthImplementation, + AuthorizationServer, + ClientCredential, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow -from .const import DOMAIN, SCOPES +from .const import AUTHORIZE_URL, DOMAIN, SCOPES, TOKEN_URL -CLIENT_ID = "71b813eb-4a2e-483a-b831-4dec5cb9bf0d" -AUTHORIZE_URL = "https://auth.tesla.com/oauth2/v3/authorize" -TOKEN_URL = "https://auth.tesla.com/oauth2/v3/token" +AUTH_SERVER = AuthorizationServer(AUTHORIZE_URL, TOKEN_URL) async def async_get_auth_implementation( @@ -23,15 +25,16 @@ async def async_get_auth_implementation( return TeslaOAuth2Implementation( hass, DOMAIN, + credential, ) -class TeslaOAuth2Implementation(config_entry_oauth2_flow.LocalOAuth2Implementation): +class TeslaOAuth2Implementation(AuthImplementation): """Tesla Fleet API Open Source Oauth2 implementation.""" - _name = "Tesla Fleet API" - - def __init__(self, hass: HomeAssistant, domain: str) -> None: + def __init__( + self, hass: HomeAssistant, domain: str, credential: ClientCredential + ) -> None: """Initialize local auth implementation.""" self.hass = hass self._domain = domain @@ -45,10 +48,8 @@ class TeslaOAuth2Implementation(config_entry_oauth2_flow.LocalOAuth2Implementati super().__init__( hass, domain, - CLIENT_ID, - "", # Implementation has no client secret - AUTHORIZE_URL, - TOKEN_URL, + credential, + AUTH_SERVER, ) @property diff --git a/homeassistant/components/tesla_fleet/config_flow.py b/homeassistant/components/tesla_fleet/config_flow.py index ad6ba8817c9..c09ea78177f 100644 --- a/homeassistant/components/tesla_fleet/config_flow.py +++ b/homeassistant/components/tesla_fleet/config_flow.py @@ -8,10 +8,12 @@ from typing import Any import jwt +from homeassistant.components.application_credentials import ClientCredential from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.helpers import config_entry_oauth2_flow -from .const import DOMAIN, LOGGER +from .application_credentials import TeslaOAuth2Implementation +from .const import CLIENT_ID, DOMAIN, LOGGER, NAME class OAuth2FlowHandler( @@ -27,6 +29,19 @@ class OAuth2FlowHandler( """Return logger.""" return LOGGER + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow start.""" + self.async_register_implementation( + self.hass, + TeslaOAuth2Implementation( + self.hass, DOMAIN, ClientCredential(CLIENT_ID, "", NAME) + ), + ) + + return await super().async_step_user() + async def async_oauth_create_entry( self, data: dict[str, Any], diff --git a/homeassistant/components/tesla_fleet/const.py b/homeassistant/components/tesla_fleet/const.py index ae622d2266c..9d78716a13e 100644 --- a/homeassistant/components/tesla_fleet/const.py +++ b/homeassistant/components/tesla_fleet/const.py @@ -13,6 +13,11 @@ CONF_REFRESH_TOKEN = "refresh_token" LOGGER = logging.getLogger(__package__) +NAME = "Home Assistant" +CLIENT_ID = "71b813eb-4a2e-483a-b831-4dec5cb9bf0d" +AUTHORIZE_URL = "https://auth.tesla.com/oauth2/v3/authorize" +TOKEN_URL = "https://auth.tesla.com/oauth2/v3/token" + SCOPES = [ Scope.OPENID, Scope.OFFLINE_ACCESS, diff --git a/tests/components/tesla_fleet/__init__.py b/tests/components/tesla_fleet/__init__.py index d5df0d0a2ed..78159402bff 100644 --- a/tests/components/tesla_fleet/__init__.py +++ b/tests/components/tesla_fleet/__init__.py @@ -4,9 +4,15 @@ from unittest.mock import patch from syrupy import SnapshotAssertion +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.tesla_fleet.const import CLIENT_ID, DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -18,6 +24,14 @@ async def setup_platform( ) -> None: """Set up the Tesla Fleet platform.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential(CLIENT_ID, "", "Home Assistant"), + DOMAIN, + ) + config_entry.add_to_hass(hass) if platforms is None: diff --git a/tests/components/tesla_fleet/conftest.py b/tests/components/tesla_fleet/conftest.py index ade2f6eb0a9..7d60ae5e174 100644 --- a/tests/components/tesla_fleet/conftest.py +++ b/tests/components/tesla_fleet/conftest.py @@ -10,14 +10,7 @@ from unittest.mock import AsyncMock, patch import jwt import pytest -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) -from homeassistant.components.tesla_fleet.application_credentials import CLIENT_ID from homeassistant.components.tesla_fleet.const import DOMAIN, SCOPES -from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .const import LIVE_STATUS, PRODUCTS, SITE_INFO, VEHICLE_DATA, VEHICLE_ONLINE @@ -71,18 +64,6 @@ def normal_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry: ) -@pytest.fixture(autouse=True) -async def setup_credentials(hass: HomeAssistant) -> None: - """Fixture to setup credentials.""" - assert await async_setup_component(hass, "application_credentials", {}) - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential(CLIENT_ID, ""), - DOMAIN, - ) - - @pytest.fixture(autouse=True) def mock_products() -> Generator[AsyncMock]: """Mock Tesla Fleet Api products method.""" diff --git a/tests/components/tesla_fleet/test_config_flow.py b/tests/components/tesla_fleet/test_config_flow.py index 334d8902fc7..bd1c7d7c2b8 100644 --- a/tests/components/tesla_fleet/test_config_flow.py +++ b/tests/components/tesla_fleet/test_config_flow.py @@ -5,12 +5,13 @@ from urllib.parse import parse_qs, urlparse import pytest -from homeassistant.components.tesla_fleet.application_credentials import ( +from homeassistant.components.tesla_fleet.const import ( AUTHORIZE_URL, CLIENT_ID, + DOMAIN, + SCOPES, TOKEN_URL, ) -from homeassistant.components.tesla_fleet.const import DOMAIN, SCOPES from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType From 4a212791a26a8f0d5a38aff399a58fb7cad53fef Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2024 13:12:29 +0200 Subject: [PATCH 89/95] Update wled to 0.20.1 (#123283) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index b19e5f16ccb..efeb414438d 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "platinum", - "requirements": ["wled==0.20.0"], + "requirements": ["wled==0.20.1"], "zeroconf": ["_wled._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 008253960d5..b12da4becd3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2915,7 +2915,7 @@ wiffi==1.1.2 wirelesstagpy==0.8.1 # homeassistant.components.wled -wled==0.20.0 +wled==0.20.1 # homeassistant.components.wolflink wolf-comm==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 737d30f7b25..7ec92ee20e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2298,7 +2298,7 @@ whois==0.9.27 wiffi==1.1.2 # homeassistant.components.wled -wled==0.20.0 +wled==0.20.1 # homeassistant.components.wolflink wolf-comm==0.0.9 From 082290b092985dcb4680655090f071692b911751 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2024 13:15:23 +0200 Subject: [PATCH 90/95] Bump version to 2024.8.0b8 --- 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 8e49ccc1b0b..987ee6572b6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 4e2c2ebb72a..29c58954c5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b7" +version = "2024.8.0b8" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ef564c537d2e3a23ee433ec8a96315038baf9a9f Mon Sep 17 00:00:00 2001 From: ashalita Date: Wed, 7 Aug 2024 15:19:05 +0300 Subject: [PATCH 91/95] Revert "Upgrade pycoolmasternet-async to 0.2.0" (#123286) --- homeassistant/components/coolmaster/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index b405a82ad62..9488e068d44 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/coolmaster", "iot_class": "local_polling", "loggers": ["pycoolmasternet_async"], - "requirements": ["pycoolmasternet-async==0.2.0"] + "requirements": ["pycoolmasternet-async==0.1.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index b12da4becd3..cf96a4363e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1777,7 +1777,7 @@ pycmus==0.1.1 pycomfoconnect==0.5.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.2.0 +pycoolmasternet-async==0.1.5 # homeassistant.components.microsoft pycsspeechtts==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ec92ee20e7..8c215eedcea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1427,7 +1427,7 @@ pycfdns==3.0.0 pycomfoconnect==0.5.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.2.0 +pycoolmasternet-async==0.1.5 # homeassistant.components.microsoft pycsspeechtts==1.0.8 From 7a51d4ff62eaf3408662e76fe62de52d3e966ad9 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 7 Aug 2024 16:45:46 +0200 Subject: [PATCH 92/95] Drop Matter Microwave Oven Mode select entity (#123294) --- homeassistant/components/matter/select.py | 13 --------- tests/components/matter/test_select.py | 32 ----------------------- 2 files changed, 45 deletions(-) diff --git a/homeassistant/components/matter/select.py b/homeassistant/components/matter/select.py index 350712061ba..4a9ef3780d1 100644 --- a/homeassistant/components/matter/select.py +++ b/homeassistant/components/matter/select.py @@ -27,7 +27,6 @@ type SelectCluster = ( | clusters.RvcRunMode | clusters.RvcCleanMode | clusters.DishwasherMode - | clusters.MicrowaveOvenMode | clusters.EnergyEvseMode | clusters.DeviceEnergyManagementMode ) @@ -199,18 +198,6 @@ DISCOVERY_SCHEMAS = [ clusters.DishwasherMode.Attributes.SupportedModes, ), ), - MatterDiscoverySchema( - platform=Platform.SELECT, - entity_description=MatterSelectEntityDescription( - key="MatterMicrowaveOvenMode", - translation_key="mode", - ), - entity_class=MatterModeSelectEntity, - required_attributes=( - clusters.MicrowaveOvenMode.Attributes.CurrentMode, - clusters.MicrowaveOvenMode.Attributes.SupportedModes, - ), - ), MatterDiscoverySchema( platform=Platform.SELECT, entity_description=MatterSelectEntityDescription( diff --git a/tests/components/matter/test_select.py b/tests/components/matter/test_select.py index 9b774f0430b..f84e5870392 100644 --- a/tests/components/matter/test_select.py +++ b/tests/components/matter/test_select.py @@ -25,16 +25,6 @@ async def dimmable_light_node_fixture( ) -@pytest.fixture(name="microwave_oven_node") -async def microwave_oven_node_fixture( - hass: HomeAssistant, matter_client: MagicMock -) -> MatterNode: - """Fixture for a microwave oven node.""" - return await setup_integration_with_node_fixture( - hass, "microwave-oven", matter_client - ) - - # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) async def test_mode_select_entities( @@ -87,28 +77,6 @@ async def test_mode_select_entities( # This tests needs to be adjusted to remove lingering tasks -@pytest.mark.parametrize("expected_lingering_tasks", [True]) -async def test_microwave_select_entities( - hass: HomeAssistant, - matter_client: MagicMock, - microwave_oven_node: MatterNode, -) -> None: - """Test select entities are created for the MicrowaveOvenMode cluster attributes.""" - state = hass.states.get("select.microwave_oven_mode") - assert state - assert state.state == "Normal" - assert state.attributes["options"] == [ - "Normal", - "Defrost", - ] - # name should just be Mode (from the translation key) - assert state.attributes["friendly_name"] == "Microwave Oven Mode" - set_node_attribute(microwave_oven_node, 1, 94, 1, 1) - await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("select.microwave_oven_mode") - assert state.state == "Defrost" - - @pytest.mark.parametrize("expected_lingering_tasks", [True]) async def test_attribute_select_entities( hass: HomeAssistant, From 5367886732a5af1d5b07d4975480394612bc3f12 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 7 Aug 2024 10:42:59 -0500 Subject: [PATCH 93/95] Bump intents to 2024.8.7 (#123295) --- homeassistant/components/conversation/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/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 65c79cef187..d7a308b8b2b 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.7.29"] + "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.8.7"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 13fbcabf0d1..472134fea37 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240806.1 -home-assistant-intents==2024.7.29 +home-assistant-intents==2024.8.7 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index cf96a4363e5..b8f50d328f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1096,7 +1096,7 @@ holidays==0.53 home-assistant-frontend==20240806.1 # homeassistant.components.conversation -home-assistant-intents==2024.7.29 +home-assistant-intents==2024.8.7 # homeassistant.components.home_connect homeconnect==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c215eedcea..f6602bf082b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -919,7 +919,7 @@ holidays==0.53 home-assistant-frontend==20240806.1 # homeassistant.components.conversation -home-assistant-intents==2024.7.29 +home-assistant-intents==2024.8.7 # homeassistant.components.home_connect homeconnect==0.8.0 From ac6abb363c98ce5aef70c7b7eed38e4bcf787432 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2024 18:24:15 +0200 Subject: [PATCH 94/95] Bump version to 2024.8.0b9 --- 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 987ee6572b6..6c63a980c5f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b8" +PATCH_VERSION: Final = "0b9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 29c58954c5b..a4e95216896 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b8" +version = "2024.8.0b9" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From be4810731a76c17a2c8206eb2e1bc90ec13b05cc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Aug 2024 19:04:33 +0200 Subject: [PATCH 95/95] Bump version to 2024.8.0 --- 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 6c63a980c5f..402f57a4f8b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b9" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index a4e95216896..dc943b0832a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0b9" +version = "2024.8.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"