From 0bb892588ef4019d1b412734fd15042cb5131834 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Dec 2023 21:23:25 +0100 Subject: [PATCH 001/112] Bump version to 2024.1.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 f6d479aeb42..572aa5d743d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Any, Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 3371ec81146..33b01c3c288 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.0.dev0" +version = "2024.1.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 8e4fade725cda6b3de697a1b17e969829993bf98 Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 28 Dec 2023 13:56:40 -0500 Subject: [PATCH 002/112] Move services to entity services in blink (#105413) * Use device name to lookup camera * Fix device registry serial * Move to entity based services * Update tests * Use config_entry Move refresh service out of camera * Use config entry for services * Fix service schema * Add depreciation note * Depreciation note * key error changes deprecated (not depreciated) repair issue * tweak message * deprication v2 * back out update field change * backout update schema changes * Finish rollback on update service * update doc strings * move to 2024.7.0 More verbosity to deprecation message --- homeassistant/components/blink/camera.py | 61 +++- homeassistant/components/blink/const.py | 1 + homeassistant/components/blink/services.py | 124 ++------ homeassistant/components/blink/services.yaml | 42 +-- homeassistant/components/blink/strings.json | 50 ++- tests/components/blink/test_services.py | 318 +++++-------------- 6 files changed, 209 insertions(+), 387 deletions(-) diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index f507364f17f..4d05aea88a5 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -8,17 +8,26 @@ import logging from typing import Any from requests.exceptions import ChunkedEncodingError +import voluptuous as vol from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_platform +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DEFAULT_BRAND, DOMAIN, SERVICE_TRIGGER +from .const import ( + DEFAULT_BRAND, + DOMAIN, + SERVICE_SAVE_RECENT_CLIPS, + SERVICE_SAVE_VIDEO, + SERVICE_TRIGGER, +) from .coordinator import BlinkUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -43,6 +52,16 @@ async def async_setup_entry( platform = entity_platform.async_get_current_platform() platform.async_register_entity_service(SERVICE_TRIGGER, {}, "trigger_camera") + platform.async_register_entity_service( + SERVICE_SAVE_RECENT_CLIPS, + {vol.Required(CONF_FILE_PATH): cv.string}, + "save_recent_clips", + ) + platform.async_register_entity_service( + SERVICE_SAVE_VIDEO, + {vol.Required(CONF_FILENAME): cv.string}, + "save_video", + ) class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera): @@ -64,7 +83,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera): manufacturer=DEFAULT_BRAND, model=camera.camera_type, ) - _LOGGER.debug("Initialized blink camera %s", self.name) + _LOGGER.debug("Initialized blink camera %s", self._camera.name) @property def extra_state_attributes(self) -> Mapping[str, Any] | None: @@ -121,3 +140,39 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera): except TypeError: _LOGGER.debug("No cached image for %s", self._camera.name) return None + + async def save_recent_clips(self, file_path) -> None: + """Save multiple recent clips to output directory.""" + if not self.hass.config.is_allowed_path(file_path): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="no_path", + translation_placeholders={"target": file_path}, + ) + + try: + await self._camera.save_recent_clips(output_dir=file_path) + except OSError as err: + raise ServiceValidationError( + str(err), + translation_domain=DOMAIN, + translation_key="cant_write", + ) from err + + async def save_video(self, filename) -> None: + """Handle save video service calls.""" + if not self.hass.config.is_allowed_path(filename): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="no_path", + translation_placeholders={"target": filename}, + ) + + try: + await self._camera.video_to_file(filename) + except OSError as err: + raise ServiceValidationError( + str(err), + translation_domain=DOMAIN, + translation_key="cant_write", + ) from err diff --git a/homeassistant/components/blink/const.py b/homeassistant/components/blink/const.py index d394b5c0008..7aa3d0d388e 100644 --- a/homeassistant/components/blink/const.py +++ b/homeassistant/components/blink/const.py @@ -24,6 +24,7 @@ SERVICE_TRIGGER = "trigger_camera" SERVICE_SAVE_VIDEO = "save_video" SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips" SERVICE_SEND_PIN = "send_pin" +ATTR_CONFIG_ENTRY_ID = "config_entry_id" PLATFORMS = [ Platform.ALARM_CONTROL_PANEL, diff --git a/homeassistant/components/blink/services.py b/homeassistant/components/blink/services.py index dae2f0ad951..5c034cdb7c5 100644 --- a/homeassistant/components/blink/services.py +++ b/homeassistant/components/blink/services.py @@ -4,25 +4,16 @@ from __future__ import annotations import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import ( - ATTR_DEVICE_ID, - CONF_FILE_PATH, - CONF_FILENAME, - CONF_NAME, - CONF_PIN, -) +from homeassistant.const import ATTR_DEVICE_ID, CONF_PIN from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError, ServiceValidationError -import homeassistant.helpers.config_validation as cv -import homeassistant.helpers.device_registry as dr - -from .const import ( - DOMAIN, - SERVICE_REFRESH, - SERVICE_SAVE_RECENT_CLIPS, - SERVICE_SAVE_VIDEO, - SERVICE_SEND_PIN, +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + issue_registry as ir, ) + +from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_REFRESH, SERVICE_SEND_PIN from .coordinator import BlinkUpdateCoordinator SERVICE_UPDATE_SCHEMA = vol.Schema( @@ -30,26 +21,12 @@ SERVICE_UPDATE_SCHEMA = vol.Schema( vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), } ) -SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FILENAME): cv.string, - } -) SERVICE_SEND_PIN_SCHEMA = vol.Schema( { - vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Required(ATTR_CONFIG_ENTRY_ID): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_PIN): cv.string, } ) -SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FILE_PATH): cv.string, - } -) def setup_services(hass: HomeAssistant) -> None: @@ -94,57 +71,22 @@ def setup_services(hass: HomeAssistant) -> None: coordinators.append(hass.data[DOMAIN][config_entry.entry_id]) return coordinators - async def async_handle_save_video_service(call: ServiceCall) -> None: - """Handle save video service calls.""" - camera_name = call.data[CONF_NAME] - video_path = call.data[CONF_FILENAME] - if not hass.config.is_allowed_path(video_path): - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="no_path", - translation_placeholders={"target": video_path}, - ) - - for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]): - all_cameras = coordinator.api.cameras - if camera_name in all_cameras: - try: - await all_cameras[camera_name].video_to_file(video_path) - except OSError as err: - raise ServiceValidationError( - str(err), - translation_domain=DOMAIN, - translation_key="cant_write", - ) from err - - async def async_handle_save_recent_clips_service(call: ServiceCall) -> None: - """Save multiple recent clips to output directory.""" - camera_name = call.data[CONF_NAME] - clips_dir = call.data[CONF_FILE_PATH] - if not hass.config.is_allowed_path(clips_dir): - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="no_path", - translation_placeholders={"target": clips_dir}, - ) - - for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]): - all_cameras = coordinator.api.cameras - if camera_name in all_cameras: - try: - await all_cameras[camera_name].save_recent_clips( - output_dir=clips_dir - ) - except OSError as err: - raise ServiceValidationError( - str(err), - translation_domain=DOMAIN, - translation_key="cant_write", - ) from err - async def send_pin(call: ServiceCall): """Call blink to send new pin.""" - for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]): + for entry_id in call.data[ATTR_CONFIG_ENTRY_ID]: + if not (config_entry := hass.config_entries.async_get_entry(entry_id)): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="integration_not_found", + translation_placeholders={"target": DOMAIN}, + ) + if config_entry.state != ConfigEntryState.LOADED: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="not_loaded", + translation_placeholders={"target": config_entry.title}, + ) + coordinator = hass.data[DOMAIN][entry_id] await coordinator.api.auth.send_auth_key( coordinator.api, call.data[CONF_PIN], @@ -152,22 +94,24 @@ def setup_services(hass: HomeAssistant) -> None: async def blink_refresh(call: ServiceCall): """Call blink to refresh info.""" + ir.async_create_issue( + hass, + DOMAIN, + "service_deprecation", + breaks_in_ha_version="2024.7.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_deprecation", + ) + for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]): await coordinator.api.refresh(force_cache=True) # Register all the above services + # Refresh service is deprecated and will be removed in 7/2024 service_mapping = [ (blink_refresh, SERVICE_REFRESH, SERVICE_UPDATE_SCHEMA), - ( - async_handle_save_video_service, - SERVICE_SAVE_VIDEO, - SERVICE_SAVE_VIDEO_SCHEMA, - ), - ( - async_handle_save_recent_clips_service, - SERVICE_SAVE_RECENT_CLIPS, - SERVICE_SAVE_RECENT_CLIPS_SCHEMA, - ), (send_pin, SERVICE_SEND_PIN, SERVICE_SEND_PIN_SCHEMA), ] diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml index aaecde64353..87083a990ef 100644 --- a/homeassistant/components/blink/services.yaml +++ b/homeassistant/components/blink/services.yaml @@ -9,25 +9,17 @@ blink_update: integration: blink trigger_camera: - fields: - device_id: - required: true - selector: - device: - integration: blink + target: + entity: + integration: blink + domain: camera save_video: + target: + entity: + integration: blink + domain: camera fields: - device_id: - required: true - selector: - device: - integration: blink - name: - required: true - example: "Living Room" - selector: - text: filename: required: true example: "/tmp/video.mp4" @@ -35,17 +27,11 @@ save_video: text: save_recent_clips: + target: + entity: + integration: blink + domain: camera fields: - device_id: - required: true - selector: - device: - integration: blink - name: - required: true - example: "Living Room" - selector: - text: file_path: required: true example: "/tmp" @@ -54,10 +40,10 @@ save_recent_clips: send_pin: fields: - device_id: + config_entry_id: required: true selector: - device: + config_entry: integration: blink pin: example: "abc123" diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index fc0450dc8ea..87e2fc68c20 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -67,29 +67,15 @@ }, "trigger_camera": { "name": "Trigger camera", - "description": "Requests camera to take new image.", - "fields": { - "device_id": { - "name": "Device ID", - "description": "The Blink device id." - } - } + "description": "Requests camera to take new image." }, "save_video": { "name": "Save video", "description": "Saves last recorded video clip to local file.", "fields": { - "name": { - "name": "[%key:common::config_flow::data::name%]", - "description": "Name of camera to grab video from." - }, "filename": { "name": "File name", "description": "Filename to writable path (directory may need to be included in allowlist_external_dirs in config)." - }, - "device_id": { - "name": "Device ID", - "description": "The Blink device id." } } }, @@ -97,17 +83,9 @@ "name": "Save recent clips", "description": "Saves all recent video clips to local directory with file pattern \"%Y%m%d_%H%M%S_{name}.mp4\".", "fields": { - "name": { - "name": "[%key:common::config_flow::data::name%]", - "description": "Name of camera to grab recent clips from." - }, "file_path": { "name": "Output directory", "description": "Directory name of writable path (directory may need to be included in allowlist_external_dirs in config)." - }, - "device_id": { - "name": "Device ID", - "description": "The Blink device id." } } }, @@ -119,19 +97,16 @@ "name": "Pin", "description": "PIN received from blink. Leave empty if you only received a verification email." }, - "device_id": { - "name": "Device ID", - "description": "The Blink device id." + "config_entry_id": { + "name": "Integration ID", + "description": "The Blink Integration id." } } } }, "exceptions": { - "invalid_device": { - "message": "Device '{target}' is not a {domain} device" - }, - "device_not_found": { - "message": "Device '{target}' not found in device registry" + "integration_not_found": { + "message": "Integraion '{target}' not found in registry" }, "no_path": { "message": "Can't write to directory {target}, no access to path!" @@ -142,5 +117,18 @@ "not_loaded": { "message": "{target} is not loaded" } + }, + "issues": { + "service_deprecation": { + "title": "Blink update service is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::blink::issues::service_deprecation::title%]", + "description": "Blink update service is deprecated and will be removed.\nPlease update your automations and scripts to use `Home Assistant Core Integration: Update entity`." + } + } + } + } } } diff --git a/tests/components/blink/test_services.py b/tests/components/blink/test_services.py index ccc326dac1f..1c2faa32d04 100644 --- a/tests/components/blink/test_services.py +++ b/tests/components/blink/test_services.py @@ -4,22 +4,15 @@ from unittest.mock import AsyncMock, MagicMock, Mock import pytest from homeassistant.components.blink.const import ( + ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_REFRESH, - SERVICE_SAVE_RECENT_CLIPS, - SERVICE_SAVE_VIDEO, SERVICE_SEND_PIN, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ( - ATTR_DEVICE_ID, - CONF_FILE_PATH, - CONF_FILENAME, - CONF_NAME, - CONF_PIN, -) +from homeassistant.const import ATTR_DEVICE_ID, CONF_PIN from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from tests.common import MockConfigEntry @@ -43,7 +36,6 @@ async def test_refresh_service_calls( await hass.async_block_till_done() device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")}) - assert device_entry assert mock_config_entry.state is ConfigEntryState.LOADED @@ -67,163 +59,8 @@ async def test_refresh_service_calls( ) -async def test_video_service_calls( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mock_blink_api: MagicMock, - mock_blink_auth_api: MagicMock, - mock_config_entry: MockConfigEntry, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test video service calls.""" - - mock_config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")}) - - assert device_entry - - assert mock_config_entry.state is ConfigEntryState.LOADED - assert mock_blink_api.refresh.call_count == 1 - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - DOMAIN, - SERVICE_SAVE_VIDEO, - { - ATTR_DEVICE_ID: [device_entry.id], - CONF_NAME: CAMERA_NAME, - CONF_FILENAME: FILENAME, - }, - blocking=True, - ) - - hass.config.is_allowed_path = Mock(return_value=True) - caplog.clear() - mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()} - await hass.services.async_call( - DOMAIN, - SERVICE_SAVE_VIDEO, - { - ATTR_DEVICE_ID: [device_entry.id], - CONF_NAME: CAMERA_NAME, - CONF_FILENAME: FILENAME, - }, - blocking=True, - ) - mock_blink_api.cameras[CAMERA_NAME].video_to_file.assert_awaited_once() - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - DOMAIN, - SERVICE_SAVE_VIDEO, - { - ATTR_DEVICE_ID: ["bad-device_id"], - CONF_NAME: CAMERA_NAME, - CONF_FILENAME: FILENAME, - }, - blocking=True, - ) - - mock_blink_api.cameras[CAMERA_NAME].video_to_file = AsyncMock(side_effect=OSError) - - with pytest.raises(ServiceValidationError): - await hass.services.async_call( - DOMAIN, - SERVICE_SAVE_VIDEO, - { - ATTR_DEVICE_ID: [device_entry.id], - CONF_NAME: CAMERA_NAME, - CONF_FILENAME: FILENAME, - }, - blocking=True, - ) - - hass.config.is_allowed_path = Mock(return_value=False) - - -async def test_picture_service_calls( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mock_blink_api: MagicMock, - mock_blink_auth_api: MagicMock, - mock_config_entry: MockConfigEntry, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test picture servcie calls.""" - - mock_config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")}) - - assert device_entry - - assert mock_config_entry.state is ConfigEntryState.LOADED - assert mock_blink_api.refresh.call_count == 1 - - with pytest.raises(ServiceValidationError): - await hass.services.async_call( - DOMAIN, - SERVICE_SAVE_RECENT_CLIPS, - { - ATTR_DEVICE_ID: [device_entry.id], - CONF_NAME: CAMERA_NAME, - CONF_FILE_PATH: FILENAME, - }, - blocking=True, - ) - - hass.config.is_allowed_path = Mock(return_value=True) - mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()} - - await hass.services.async_call( - DOMAIN, - SERVICE_SAVE_RECENT_CLIPS, - { - ATTR_DEVICE_ID: [device_entry.id], - CONF_NAME: CAMERA_NAME, - CONF_FILE_PATH: FILENAME, - }, - blocking=True, - ) - mock_blink_api.cameras[CAMERA_NAME].save_recent_clips.assert_awaited_once() - - mock_blink_api.cameras[CAMERA_NAME].save_recent_clips = AsyncMock( - side_effect=OSError - ) - - with pytest.raises(ServiceValidationError): - await hass.services.async_call( - DOMAIN, - SERVICE_SAVE_RECENT_CLIPS, - { - ATTR_DEVICE_ID: [device_entry.id], - CONF_NAME: CAMERA_NAME, - CONF_FILE_PATH: FILENAME, - }, - blocking=True, - ) - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - DOMAIN, - SERVICE_SAVE_RECENT_CLIPS, - { - ATTR_DEVICE_ID: ["bad-device_id"], - CONF_NAME: CAMERA_NAME, - CONF_FILE_PATH: FILENAME, - }, - blocking=True, - ) - - async def test_pin_service_calls( hass: HomeAssistant, - device_registry: dr.DeviceRegistry, mock_blink_api: MagicMock, mock_blink_auth_api: MagicMock, mock_config_entry: MockConfigEntry, @@ -234,17 +71,13 @@ async def test_pin_service_calls( assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")}) - - assert device_entry - assert mock_config_entry.state is ConfigEntryState.LOADED assert mock_blink_api.refresh.call_count == 1 await hass.services.async_call( DOMAIN, SERVICE_SEND_PIN, - {ATTR_DEVICE_ID: [device_entry.id], CONF_PIN: PIN}, + {ATTR_CONFIG_ENTRY_ID: [mock_config_entry.entry_id], CONF_PIN: PIN}, blocking=True, ) assert mock_blink_api.auth.send_auth_key.assert_awaited_once @@ -253,41 +86,18 @@ async def test_pin_service_calls( await hass.services.async_call( DOMAIN, SERVICE_SEND_PIN, - {ATTR_DEVICE_ID: ["bad-device_id"], CONF_PIN: PIN}, + {ATTR_CONFIG_ENTRY_ID: ["bad-config_id"], CONF_PIN: PIN}, blocking=True, ) -@pytest.mark.parametrize( - ("service", "params"), - [ - (SERVICE_SEND_PIN, {CONF_PIN: PIN}), - ( - SERVICE_SAVE_RECENT_CLIPS, - { - CONF_NAME: CAMERA_NAME, - CONF_FILE_PATH: FILENAME, - }, - ), - ( - SERVICE_SAVE_VIDEO, - { - CONF_NAME: CAMERA_NAME, - CONF_FILENAME: FILENAME, - }, - ), - ], -) -async def test_service_called_with_non_blink_device( +async def test_service_pin_called_with_non_blink_device( hass: HomeAssistant, - device_registry: dr.DeviceRegistry, mock_blink_api: MagicMock, mock_blink_auth_api: MagicMock, mock_config_entry: MockConfigEntry, - service, - params, ) -> None: - """Test service calls with non blink device.""" + """Test pin service calls with non blink device.""" mock_config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -295,11 +105,48 @@ async def test_service_called_with_non_blink_device( other_domain = "NotBlink" other_config_id = "555" - await hass.config_entries.async_add( - MockConfigEntry( - title="Not Blink", domain=other_domain, entry_id=other_config_id - ) + other_mock_config_entry = MockConfigEntry( + title="Not Blink", domain=other_domain, entry_id=other_config_id ) + await hass.config_entries.async_add(other_mock_config_entry) + + hass.config.is_allowed_path = Mock(return_value=True) + mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()} + + parameters = { + ATTR_CONFIG_ENTRY_ID: [other_mock_config_entry.entry_id], + CONF_PIN: PIN, + } + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_SEND_PIN, + parameters, + blocking=True, + ) + + +async def test_service_update_called_with_non_blink_device( + hass: HomeAssistant, + mock_blink_api: MagicMock, + device_registry: dr.DeviceRegistry, + mock_blink_auth_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test update service calls with non blink device.""" + + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + other_domain = "NotBlink" + other_config_id = "555" + other_mock_config_entry = MockConfigEntry( + title="Not Blink", domain=other_domain, entry_id=other_config_id + ) + await hass.config_entries.async_add(other_mock_config_entry) + device_entry = device_registry.async_get_or_create( config_entry_id=other_config_id, identifiers={ @@ -311,67 +158,68 @@ async def test_service_called_with_non_blink_device( mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()} parameters = {ATTR_DEVICE_ID: [device_entry.id]} - parameters.update(params) - with pytest.raises(ServiceValidationError): + with pytest.raises(HomeAssistantError): await hass.services.async_call( DOMAIN, - service, + SERVICE_REFRESH, parameters, blocking=True, ) -@pytest.mark.parametrize( - ("service", "params"), - [ - (SERVICE_SEND_PIN, {CONF_PIN: PIN}), - ( - SERVICE_SAVE_RECENT_CLIPS, - { - CONF_NAME: CAMERA_NAME, - CONF_FILE_PATH: FILENAME, - }, - ), - ( - SERVICE_SAVE_VIDEO, - { - CONF_NAME: CAMERA_NAME, - CONF_FILENAME: FILENAME, - }, - ), - ], -) -async def test_service_called_with_unloaded_entry( +async def test_service_pin_called_with_unloaded_entry( + hass: HomeAssistant, + mock_blink_api: MagicMock, + mock_blink_auth_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test pin service calls with not ready config entry.""" + + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + mock_config_entry.state = ConfigEntryState.SETUP_ERROR + hass.config.is_allowed_path = Mock(return_value=True) + mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()} + + parameters = {ATTR_CONFIG_ENTRY_ID: [mock_config_entry.entry_id], CONF_PIN: PIN} + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_SEND_PIN, + parameters, + blocking=True, + ) + + +async def test_service_update_called_with_unloaded_entry( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_blink_api: MagicMock, mock_blink_auth_api: MagicMock, mock_config_entry: MockConfigEntry, - service, - params, ) -> None: - """Test service calls with unloaded config entry.""" + """Test update service calls with not ready config entry.""" mock_config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - await mock_config_entry.async_unload(hass) - - device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")}) - - assert device_entry + mock_config_entry.state = ConfigEntryState.SETUP_ERROR hass.config.is_allowed_path = Mock(return_value=True) mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()} + device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "12345")}) + assert device_entry + parameters = {ATTR_DEVICE_ID: [device_entry.id]} - parameters.update(params) with pytest.raises(HomeAssistantError): await hass.services.async_call( DOMAIN, - service, + SERVICE_REFRESH, parameters, blocking=True, ) From 924e47c2a85730ed0abb21b83833cf98eaf94d93 Mon Sep 17 00:00:00 2001 From: Bart Janssens Date: Thu, 28 Dec 2023 09:31:35 +0100 Subject: [PATCH 003/112] Skip activating/deactivating Vicare standby preset (#106476) --- homeassistant/components/vicare/climate.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index c14f940ffe6..0b8e3cab865 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -311,8 +311,11 @@ class ViCareClimate(ViCareEntity, ClimateEntity): ) _LOGGER.debug("Current preset %s", self._current_program) - if self._current_program and self._current_program != VICARE_PROGRAM_NORMAL: - # We can't deactivate "normal" + if self._current_program and self._current_program not in [ + VICARE_PROGRAM_NORMAL, + VICARE_PROGRAM_STANDBY, + ]: + # We can't deactivate "normal" or "standby" _LOGGER.debug("deactivating %s", self._current_program) try: self._circuit.deactivateProgram(self._current_program) @@ -326,8 +329,11 @@ class ViCareClimate(ViCareEntity, ClimateEntity): ) from err _LOGGER.debug("Setting preset to %s / %s", preset_mode, target_program) - if target_program != VICARE_PROGRAM_NORMAL: - # And we can't explicitly activate "normal", either + if target_program not in [ + VICARE_PROGRAM_NORMAL, + VICARE_PROGRAM_STANDBY, + ]: + # And we can't explicitly activate "normal" or "standby", either _LOGGER.debug("activating %s", target_program) try: self._circuit.activateProgram(target_program) From b685584b91908707e84ac1bad0fc81e93f6c83ce Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:35:39 +0100 Subject: [PATCH 004/112] Handle AttributeError in ViCare integration (#106470) --- homeassistant/components/vicare/utils.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/vicare/utils.py b/homeassistant/components/vicare/utils.py index 5b3fb38337f..a084eee383b 100644 --- a/homeassistant/components/vicare/utils.py +++ b/homeassistant/components/vicare/utils.py @@ -21,13 +21,12 @@ def is_supported( try: entity_description.value_getter(vicare_device) _LOGGER.debug("Found entity %s", name) + return True except PyViCareNotSupportedFeatureError: - _LOGGER.info("Feature not supported %s", name) - return False + _LOGGER.debug("Feature not supported %s", name) except AttributeError as error: - _LOGGER.debug("Attribute Error %s: %s", name, error) - return False - return True + _LOGGER.debug("Feature not supported %s: %s", name, error) + return False def get_burners(device: PyViCareDevice) -> list[PyViCareHeatingDeviceComponent]: @@ -36,6 +35,8 @@ def get_burners(device: PyViCareDevice) -> list[PyViCareHeatingDeviceComponent]: return device.burners except PyViCareNotSupportedFeatureError: _LOGGER.debug("No burners found") + except AttributeError as error: + _LOGGER.debug("No burners found: %s", error) return [] @@ -45,6 +46,8 @@ def get_circuits(device: PyViCareDevice) -> list[PyViCareHeatingDeviceComponent] return device.circuits except PyViCareNotSupportedFeatureError: _LOGGER.debug("No circuits found") + except AttributeError as error: + _LOGGER.debug("No circuits found: %s", error) return [] @@ -54,4 +57,6 @@ def get_compressors(device: PyViCareDevice) -> list[PyViCareHeatingDeviceCompone return device.compressors except PyViCareNotSupportedFeatureError: _LOGGER.debug("No compressors found") + except AttributeError as error: + _LOGGER.debug("No compressors found: %s", error) return [] From b8ddd61b26016e5bdf968db2708bc443a440d4f5 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Thu, 28 Dec 2023 11:17:13 +0100 Subject: [PATCH 005/112] Avoid changing state of reduced preset in ViCare integration (#105642) --- homeassistant/components/vicare/climate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 0b8e3cab865..2bb0a19924e 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -313,9 +313,10 @@ class ViCareClimate(ViCareEntity, ClimateEntity): _LOGGER.debug("Current preset %s", self._current_program) if self._current_program and self._current_program not in [ VICARE_PROGRAM_NORMAL, + VICARE_PROGRAM_REDUCED, VICARE_PROGRAM_STANDBY, ]: - # We can't deactivate "normal" or "standby" + # We can't deactivate "normal", "reduced" or "standby" _LOGGER.debug("deactivating %s", self._current_program) try: self._circuit.deactivateProgram(self._current_program) @@ -331,9 +332,10 @@ class ViCareClimate(ViCareEntity, ClimateEntity): _LOGGER.debug("Setting preset to %s / %s", preset_mode, target_program) if target_program not in [ VICARE_PROGRAM_NORMAL, + VICARE_PROGRAM_REDUCED, VICARE_PROGRAM_STANDBY, ]: - # And we can't explicitly activate "normal" or "standby", either + # And we can't explicitly activate "normal", "reduced" or "standby", either _LOGGER.debug("activating %s", target_program) try: self._circuit.activateProgram(target_program) From 50acf85f48e1b539c2d780c0d482c8052e5127d5 Mon Sep 17 00:00:00 2001 From: Thomas Hollstegge Date: Thu, 28 Dec 2023 20:30:26 +0100 Subject: [PATCH 006/112] Use correct state for emulated_hue covers (#106516) --- .../components/emulated_hue/hue_api.py | 15 ++++++++--- tests/components/emulated_hue/test_hue_api.py | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index ad6b0541cd6..05e5c1ece07 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -57,6 +57,7 @@ from homeassistant.const import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_SET, + STATE_CLOSED, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, @@ -73,6 +74,7 @@ from homeassistant.util.network import is_local from .config import Config _LOGGER = logging.getLogger(__name__) +_OFF_STATES: dict[str, str] = {cover.DOMAIN: STATE_CLOSED} # How long to wait for a state change to happen STATE_CHANGE_WAIT_TIMEOUT = 5.0 @@ -394,7 +396,7 @@ class HueOneLightChangeView(HomeAssistantView): return self.json_message("Bad request", HTTPStatus.BAD_REQUEST) parsed[STATE_ON] = request_json[HUE_API_STATE_ON] else: - parsed[STATE_ON] = entity.state != STATE_OFF + parsed[STATE_ON] = _hass_to_hue_state(entity) for key, attr in ( (HUE_API_STATE_BRI, STATE_BRIGHTNESS), @@ -585,7 +587,7 @@ class HueOneLightChangeView(HomeAssistantView): ) if service is not None: - state_will_change = parsed[STATE_ON] != (entity.state != STATE_OFF) + state_will_change = parsed[STATE_ON] != _hass_to_hue_state(entity) hass.async_create_task( hass.services.async_call(domain, service, data, blocking=True) @@ -643,7 +645,7 @@ def get_entity_state_dict(config: Config, entity: State) -> dict[str, Any]: cached_state = entry_state elif time.time() - entry_time < STATE_CACHED_TIMEOUT and entry_state[ STATE_ON - ] == (entity.state != STATE_OFF): + ] == _hass_to_hue_state(entity): # We only want to use the cache if the actual state of the entity # is in sync so that it can be detected as an error by Alexa. cached_state = entry_state @@ -676,7 +678,7 @@ def get_entity_state_dict(config: Config, entity: State) -> dict[str, Any]: @lru_cache(maxsize=512) def _build_entity_state_dict(entity: State) -> dict[str, Any]: """Build a state dict for an entity.""" - is_on = entity.state != STATE_OFF + is_on = _hass_to_hue_state(entity) data: dict[str, Any] = { STATE_ON: is_on, STATE_BRIGHTNESS: None, @@ -891,6 +893,11 @@ def hass_to_hue_brightness(value: int) -> int: return max(1, round((value / 255) * HUE_API_STATE_BRI_MAX)) +def _hass_to_hue_state(entity: State) -> bool: + """Convert hass entity states to simple True/False on/off state for Hue.""" + return entity.state != _OFF_STATES.get(entity.domain, STATE_OFF) + + async def wait_for_state_change_or_timeout( hass: core.HomeAssistant, entity_id: str, timeout: float ) -> None: diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 3febc42730b..167562578f2 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1019,6 +1019,12 @@ async def test_set_position_cover(hass_hue, hue_client) -> None: cover_test = hass_hue.states.get(cover_id) assert cover_test.state == "closed" + cover_json = await perform_get_light_state( + hue_client, "cover.living_room_window", HTTPStatus.OK + ) + assert cover_json["state"][HUE_API_STATE_ON] is False + assert cover_json["state"][HUE_API_STATE_BRI] == 1 + level = 20 brightness = round(level / 100 * 254) @@ -1095,6 +1101,7 @@ async def test_put_light_state_fan(hass_hue, hue_client) -> None: fan_json = await perform_get_light_state( hue_client, "fan.living_room_fan", HTTPStatus.OK ) + assert fan_json["state"][HUE_API_STATE_ON] is True assert round(fan_json["state"][HUE_API_STATE_BRI] * 100 / 254) == 33 await perform_put_light_state( @@ -1112,6 +1119,7 @@ async def test_put_light_state_fan(hass_hue, hue_client) -> None: fan_json = await perform_get_light_state( hue_client, "fan.living_room_fan", HTTPStatus.OK ) + assert fan_json["state"][HUE_API_STATE_ON] is True assert ( round(fan_json["state"][HUE_API_STATE_BRI] * 100 / 254) == 66 ) # small rounding error in inverse operation @@ -1132,8 +1140,27 @@ async def test_put_light_state_fan(hass_hue, hue_client) -> None: fan_json = await perform_get_light_state( hue_client, "fan.living_room_fan", HTTPStatus.OK ) + assert fan_json["state"][HUE_API_STATE_ON] is True assert round(fan_json["state"][HUE_API_STATE_BRI] * 100 / 254) == 100 + await perform_put_light_state( + hass_hue, + hue_client, + "fan.living_room_fan", + False, + brightness=0, + ) + assert ( + hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_PERCENTAGE] == 0 + ) + with patch.object(hue_api, "STATE_CACHED_TIMEOUT", 0.000001): + await asyncio.sleep(0.000001) + fan_json = await perform_get_light_state( + hue_client, "fan.living_room_fan", HTTPStatus.OK + ) + assert fan_json["state"][HUE_API_STATE_ON] is False + assert fan_json["state"][HUE_API_STATE_BRI] == 1 + async def test_put_with_form_urlencoded_content_type(hass_hue, hue_client) -> None: """Test the form with urlencoded content.""" From 42ffb51b76a1270d7dbcfe9f99a473b379b83413 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 28 Dec 2023 18:00:34 +1000 Subject: [PATCH 007/112] Fix Tessie honk button (#106518) --- homeassistant/components/tessie/button.py | 2 +- tests/components/tessie/test_button.py | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tessie/button.py b/homeassistant/components/tessie/button.py index 817bdb3a87c..86065d389a4 100644 --- a/homeassistant/components/tessie/button.py +++ b/homeassistant/components/tessie/button.py @@ -35,7 +35,7 @@ DESCRIPTIONS: tuple[TessieButtonEntityDescription, ...] = ( TessieButtonEntityDescription( key="flash_lights", func=lambda: flash_lights, icon="mdi:flashlight" ), - TessieButtonEntityDescription(key="honk", func=honk, icon="mdi:bullhorn"), + TessieButtonEntityDescription(key="honk", func=lambda: honk, icon="mdi:bullhorn"), TessieButtonEntityDescription( key="trigger_homelink", func=lambda: trigger_homelink, icon="mdi:garage" ), diff --git a/tests/components/tessie/test_button.py b/tests/components/tessie/test_button.py index 72e458cb5d6..153171c8b9f 100644 --- a/tests/components/tessie/test_button.py +++ b/tests/components/tessie/test_button.py @@ -1,6 +1,8 @@ """Test the Tessie button platform.""" from unittest.mock import patch +import pytest + from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant @@ -8,19 +10,30 @@ from homeassistant.core import HomeAssistant from .common import setup_platform -async def test_buttons(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("entity_id", "func"), + [ + ("button.test_wake", "wake"), + ("button.test_flash_lights", "flash_lights"), + ("button.test_honk_horn", "honk"), + ("button.test_homelink", "trigger_homelink"), + ("button.test_keyless_driving", "enable_keyless_driving"), + ("button.test_play_fart", "boombox"), + ], +) +async def test_buttons(hass: HomeAssistant, entity_id, func) -> None: """Tests that the button entities are correct.""" await setup_platform(hass) # Test wake button with patch( - "homeassistant.components.tessie.button.wake", + f"homeassistant.components.tessie.button.{func}", ) as mock_wake: await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: ["button.test_wake"]}, + {ATTR_ENTITY_ID: [entity_id]}, blocking=True, ) mock_wake.assert_called_once() From 0de6030911fa2819c91312fdc08d8bb023e131eb Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 28 Dec 2023 18:02:04 +1000 Subject: [PATCH 008/112] Fix run errors in Tessie (#106521) --- homeassistant/components/tessie/entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py index fc6e8939da9..be80caf50cb 100644 --- a/homeassistant/components/tessie/entity.py +++ b/homeassistant/components/tessie/entity.py @@ -52,7 +52,7 @@ class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]): return self.coordinator.data.get(key or self.key, default) async def run( - self, func: Callable[..., Awaitable[dict[str, bool]]], **kargs: Any + self, func: Callable[..., Awaitable[dict[str, bool | str]]], **kargs: Any ) -> None: """Run a tessie_api function and handle exceptions.""" try: @@ -66,7 +66,7 @@ class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]): raise HomeAssistantError from e if response["result"] is False: raise HomeAssistantError( - response.get("reason"), "An unknown issue occurred" + response.get("reason", "An unknown issue occurred") ) def set(self, *args: Any) -> None: From 227a69da651d5890d96bf9c0ad506ede38d63cab Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 28 Dec 2023 17:45:21 +0100 Subject: [PATCH 009/112] Add missing disks to Systemmonitor (#106541) --- homeassistant/components/systemmonitor/util.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/systemmonitor/util.py b/homeassistant/components/systemmonitor/util.py index bb81d0c9715..27c4c449634 100644 --- a/homeassistant/components/systemmonitor/util.py +++ b/homeassistant/components/systemmonitor/util.py @@ -11,14 +11,16 @@ _LOGGER = logging.getLogger(__name__) def get_all_disk_mounts() -> list[str]: """Return all disk mount points on system.""" disks: list[str] = [] - for part in psutil.disk_partitions(all=False): + for part in psutil.disk_partitions(all=True): if os.name == "nt": if "cdrom" in part.opts or part.fstype == "": # skip cd-rom drives with no disk in it; they may raise # ENOENT, pop-up a Windows GUI error for a non-ready # partition or just hang. continue - disks.append(part.mountpoint) + usage = psutil.disk_usage(part.mountpoint) + if usage.total > 0 and part.device != "": + disks.append(part.mountpoint) _LOGGER.debug("Adding disks: %s", ", ".join(disks)) return disks From 571ba0efb093e3e629b506613903ed183fadbd99 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 28 Dec 2023 16:05:52 +0100 Subject: [PATCH 010/112] Bump python-holidays to 0.39 (#106550) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 50536bc201d..7417cc5cd51 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.38", "babel==2.13.1"] + "requirements": ["holidays==0.39", "babel==2.13.1"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 92face1ecdb..ae7c42c1868 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.38"] + "requirements": ["holidays==0.39"] } diff --git a/requirements_all.txt b/requirements_all.txt index ef621c9ce94..1f4b7d030ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1035,7 +1035,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.38 +holidays==0.39 # homeassistant.components.frontend home-assistant-frontend==20231227.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3e3256385fc..16e0897c412 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -825,7 +825,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.38 +holidays==0.39 # homeassistant.components.frontend home-assistant-frontend==20231227.0 From 1d0fafcf2df537dc01675ce8e427021083644346 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 28 Dec 2023 14:20:56 +0100 Subject: [PATCH 011/112] Remove default value from modbus retries (#106551) Solve retries issue. --- homeassistant/components/modbus/__init__.py | 2 +- homeassistant/components/modbus/modbus.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 74a1de48c0a..141f2b0cca6 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -374,7 +374,7 @@ MODBUS_SCHEMA = vol.Schema( vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, vol.Optional(CONF_CLOSE_COMM_ON_ERROR): cv.boolean, vol.Optional(CONF_DELAY, default=0): cv.positive_int, - vol.Optional(CONF_RETRIES, default=3): cv.positive_int, + vol.Optional(CONF_RETRIES): cv.positive_int, vol.Optional(CONF_RETRY_ON_EMPTY): cv.boolean, vol.Optional(CONF_MSG_WAIT): cv.positive_int, vol.Optional(CONF_BINARY_SENSORS): vol.All( diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 1d755adace7..95c0cd45332 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -278,6 +278,8 @@ class ModbusHub: _LOGGER.warning( "`retries`: is deprecated and will be removed in version 2024.7" ) + else: + client_config[CONF_RETRIES] = 3 if CONF_CLOSE_COMM_ON_ERROR in client_config: async_create_issue( hass, From d7a697faf45b48ce806bcb77ddf111eb404adf03 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 28 Dec 2023 16:10:27 +0100 Subject: [PATCH 012/112] Fix holiday HA language not supported (#106554) --- .../components/holiday/config_flow.py | 16 +++++++++++++--- tests/components/holiday/test_config_flow.py | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/holiday/config_flow.py b/homeassistant/components/holiday/config_flow.py index 1ba4a2a0c26..842849a7c57 100644 --- a/homeassistant/components/holiday/config_flow.py +++ b/homeassistant/components/holiday/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from babel import Locale +from babel import Locale, UnknownLocaleError from holidays import list_supported_countries import voluptuous as vol @@ -46,7 +46,12 @@ class HolidayConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_COUNTRY: user_input[CONF_COUNTRY]}) - locale = Locale(self.hass.config.language) + try: + locale = Locale(self.hass.config.language) + except UnknownLocaleError: + # Default to (US) English if language not recognized by babel + # Mainly an issue with English flavors such as "en-GB" + locale = Locale("en") title = locale.territories[selected_country] return self.async_create_entry(title=title, data=user_input) @@ -81,7 +86,12 @@ class HolidayConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } ) - locale = Locale(self.hass.config.language) + try: + locale = Locale(self.hass.config.language) + except UnknownLocaleError: + # Default to (US) English if language not recognized by babel + # Mainly an issue with English flavors such as "en-GB" + locale = Locale("en") province_str = f", {province}" if province else "" name = f"{locale.territories[country]}{province_str}" diff --git a/tests/components/holiday/test_config_flow.py b/tests/components/holiday/test_config_flow.py index e99d310762e..c88d66d843b 100644 --- a/tests/components/holiday/test_config_flow.py +++ b/tests/components/holiday/test_config_flow.py @@ -126,3 +126,22 @@ async def test_single_combination_country_province(hass: HomeAssistant) -> None: ) assert result_de_step2["type"] == FlowResultType.ABORT assert result_de_step2["reason"] == "already_configured" + + +async def test_form_babel_unresolved_language(hass: HomeAssistant) -> None: + """Test the config flow if using not babel supported language.""" + hass.config.language = "en-GB" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_COUNTRY: "SE", + }, + ) + await hass.async_block_till_done() + + assert result2["title"] == "Sweden" From 285bb5632d64422d0c616a9702fac580d14fcc25 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 28 Dec 2023 16:05:11 +0100 Subject: [PATCH 013/112] Update frontend to 20231228.0 (#106556) --- 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 9c631b4cfd5..227fa96edf7 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==20231227.0"] + "requirements": ["home-assistant-frontend==20231228.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 18a8b14b9d5..a6c59c98dc0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ habluetooth==2.0.0 hass-nabucasa==0.75.1 hassil==1.5.1 home-assistant-bluetooth==1.11.0 -home-assistant-frontend==20231227.0 +home-assistant-frontend==20231228.0 home-assistant-intents==2023.12.05 httpx==0.26.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1f4b7d030ac..c2ba8cccf52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1038,7 +1038,7 @@ hole==0.8.0 holidays==0.39 # homeassistant.components.frontend -home-assistant-frontend==20231227.0 +home-assistant-frontend==20231228.0 # homeassistant.components.conversation home-assistant-intents==2023.12.05 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 16e0897c412..1171fbf1970 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -828,7 +828,7 @@ hole==0.8.0 holidays==0.39 # homeassistant.components.frontend -home-assistant-frontend==20231227.0 +home-assistant-frontend==20231228.0 # homeassistant.components.conversation home-assistant-intents==2023.12.05 From d24a923a7308b1e519690d06eb303182f522dd67 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 28 Dec 2023 20:16:14 +0100 Subject: [PATCH 014/112] Replace dash in language if needed (#106559) * Replace dash in language if needed * Add tests --- .../components/holiday/config_flow.py | 4 +- tests/components/holiday/test_config_flow.py | 79 ++++++++++++++++++- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/holiday/config_flow.py b/homeassistant/components/holiday/config_flow.py index 842849a7c57..33268de92b6 100644 --- a/homeassistant/components/holiday/config_flow.py +++ b/homeassistant/components/holiday/config_flow.py @@ -47,7 +47,7 @@ class HolidayConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_COUNTRY: user_input[CONF_COUNTRY]}) try: - locale = Locale(self.hass.config.language) + locale = Locale(self.hass.config.language.replace("-", "_")) except UnknownLocaleError: # Default to (US) English if language not recognized by babel # Mainly an issue with English flavors such as "en-GB" @@ -87,7 +87,7 @@ class HolidayConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) try: - locale = Locale(self.hass.config.language) + locale = Locale(self.hass.config.language.replace("-", "_")) except UnknownLocaleError: # Default to (US) English if language not recognized by babel # Mainly an issue with English flavors such as "en-GB" diff --git a/tests/components/holiday/test_config_flow.py b/tests/components/holiday/test_config_flow.py index c88d66d843b..7dce6131616 100644 --- a/tests/components/holiday/test_config_flow.py +++ b/tests/components/holiday/test_config_flow.py @@ -130,13 +130,13 @@ async def test_single_combination_country_province(hass: HomeAssistant) -> None: async def test_form_babel_unresolved_language(hass: HomeAssistant) -> None: """Test the config flow if using not babel supported language.""" - hass.config.language = "en-GB" + hass.config.language = "en-XX" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_COUNTRY: "SE", @@ -144,4 +144,77 @@ async def test_form_babel_unresolved_language(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["title"] == "Sweden" + assert result["title"] == "Sweden" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_COUNTRY: "DE", + }, + ) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PROVINCE: "BW", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Germany, BW" + assert result["data"] == { + "country": "DE", + "province": "BW", + } + + +async def test_form_babel_replace_dash_with_underscore(hass: HomeAssistant) -> None: + """Test the config flow if using language with dash.""" + hass.config.language = "en-GB" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_COUNTRY: "SE", + }, + ) + await hass.async_block_till_done() + + assert result["title"] == "Sweden" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_COUNTRY: "DE", + }, + ) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PROVINCE: "BW", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Germany, BW" + assert result["data"] == { + "country": "DE", + "province": "BW", + } From 0e0cd8e7deb57edeb2bff37f3db6eef7dea13d6a Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 28 Dec 2023 17:37:48 +0100 Subject: [PATCH 015/112] Remove default value for modbus lazy_error (#106561) --- homeassistant/components/modbus/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 141f2b0cca6..89a50862b6c 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -157,7 +157,7 @@ BASE_COMPONENT_SCHEMA = vol.Schema( vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL ): cv.positive_int, - vol.Optional(CONF_LAZY_ERROR, default=0): cv.positive_int, + vol.Optional(CONF_LAZY_ERROR): cv.positive_int, vol.Optional(CONF_UNIQUE_ID): cv.string, } ) From a111e35026795857d1f58495bef9999423c22b56 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 28 Dec 2023 20:20:59 +0100 Subject: [PATCH 016/112] Only check known attributes in significant change support (#106572) only check known attributes --- .../alarm_control_panel/significant_change.py | 13 +++++++----- .../components/climate/significant_change.py | 12 ++++++----- .../components/cover/significant_change.py | 11 +++++----- .../components/fan/significant_change.py | 20 ++++++++++++------- .../humidifier/significant_change.py | 11 +++++----- .../media_player/significant_change.py | 19 +++++++++++++----- .../components/vacuum/significant_change.py | 11 +++++----- .../water_heater/significant_change.py | 11 +++++----- .../components/weather/significant_change.py | 11 +++++----- 9 files changed, 72 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/significant_change.py b/homeassistant/components/alarm_control_panel/significant_change.py index d33347a67f1..bde6d151393 100644 --- a/homeassistant/components/alarm_control_panel/significant_change.py +++ b/homeassistant/components/alarm_control_panel/significant_change.py @@ -26,13 +26,16 @@ def async_check_significant_change( if old_state != new_state: return True - old_attrs_s = set(old_attrs.items()) - new_attrs_s = set(new_attrs.items()) + old_attrs_s = set( + {k: v for k, v in old_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) + new_attrs_s = set( + {k: v for k, v in new_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) changed_attrs: set[str] = {item[0] for item in old_attrs_s ^ new_attrs_s} - for attr_name in changed_attrs: - if attr_name in SIGNIFICANT_ATTRIBUTES: - return True + if changed_attrs: + return True # no significant attribute change detected return False diff --git a/homeassistant/components/climate/significant_change.py b/homeassistant/components/climate/significant_change.py index 01d3ef98558..7198153f9af 100644 --- a/homeassistant/components/climate/significant_change.py +++ b/homeassistant/components/climate/significant_change.py @@ -52,15 +52,17 @@ def async_check_significant_change( if old_state != new_state: return True - old_attrs_s = set(old_attrs.items()) - new_attrs_s = set(new_attrs.items()) + old_attrs_s = set( + {k: v for k, v in old_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) + new_attrs_s = set( + {k: v for k, v in new_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) + changed_attrs: set[str] = {item[0] for item in old_attrs_s ^ new_attrs_s} ha_unit = hass.config.units.temperature_unit for attr_name in changed_attrs: - if attr_name not in SIGNIFICANT_ATTRIBUTES: - continue - if attr_name in [ ATTR_AUX_HEAT, ATTR_FAN_MODE, diff --git a/homeassistant/components/cover/significant_change.py b/homeassistant/components/cover/significant_change.py index 8762af496c8..ca822c5e9e1 100644 --- a/homeassistant/components/cover/significant_change.py +++ b/homeassistant/components/cover/significant_change.py @@ -30,14 +30,15 @@ def async_check_significant_change( if old_state != new_state: return True - old_attrs_s = set(old_attrs.items()) - new_attrs_s = set(new_attrs.items()) + old_attrs_s = set( + {k: v for k, v in old_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) + new_attrs_s = set( + {k: v for k, v in new_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) changed_attrs: set[str] = {item[0] for item in old_attrs_s ^ new_attrs_s} for attr_name in changed_attrs: - if attr_name not in SIGNIFICANT_ATTRIBUTES: - continue - old_attr_value = old_attrs.get(attr_name) new_attr_value = new_attrs.get(attr_name) if new_attr_value is None or not check_valid_float(new_attr_value): diff --git a/homeassistant/components/fan/significant_change.py b/homeassistant/components/fan/significant_change.py index 19c43522f35..b8038b93f79 100644 --- a/homeassistant/components/fan/significant_change.py +++ b/homeassistant/components/fan/significant_change.py @@ -9,9 +9,14 @@ from homeassistant.helpers.significant_change import ( check_valid_float, ) -from . import ATTR_PERCENTAGE, ATTR_PERCENTAGE_STEP +from . import ATTR_DIRECTION, ATTR_OSCILLATING, ATTR_PERCENTAGE, ATTR_PRESET_MODE -INSIGNIFICANT_ATTRIBUTES: set[str] = {ATTR_PERCENTAGE_STEP} +SIGNIFICANT_ATTRIBUTES: set[str] = { + ATTR_DIRECTION, + ATTR_OSCILLATING, + ATTR_PERCENTAGE, + ATTR_PRESET_MODE, +} @callback @@ -27,14 +32,15 @@ def async_check_significant_change( if old_state != new_state: return True - old_attrs_s = set(old_attrs.items()) - new_attrs_s = set(new_attrs.items()) + old_attrs_s = set( + {k: v for k, v in old_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) + new_attrs_s = set( + {k: v for k, v in new_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) changed_attrs: set[str] = {item[0] for item in old_attrs_s ^ new_attrs_s} for attr_name in changed_attrs: - if attr_name in INSIGNIFICANT_ATTRIBUTES: - continue - if attr_name != ATTR_PERCENTAGE: return True diff --git a/homeassistant/components/humidifier/significant_change.py b/homeassistant/components/humidifier/significant_change.py index 7acc1033d3f..cc279a9fa41 100644 --- a/homeassistant/components/humidifier/significant_change.py +++ b/homeassistant/components/humidifier/significant_change.py @@ -32,14 +32,15 @@ def async_check_significant_change( if old_state != new_state: return True - old_attrs_s = set(old_attrs.items()) - new_attrs_s = set(new_attrs.items()) + old_attrs_s = set( + {k: v for k, v in old_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) + new_attrs_s = set( + {k: v for k, v in new_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) changed_attrs: set[str] = {item[0] for item in old_attrs_s ^ new_attrs_s} for attr_name in changed_attrs: - if attr_name not in SIGNIFICANT_ATTRIBUTES: - continue - if attr_name in [ATTR_ACTION, ATTR_MODE]: return True diff --git a/homeassistant/components/media_player/significant_change.py b/homeassistant/components/media_player/significant_change.py index b2a2e57d84f..3e11cbdb9cd 100644 --- a/homeassistant/components/media_player/significant_change.py +++ b/homeassistant/components/media_player/significant_change.py @@ -43,14 +43,23 @@ def async_check_significant_change( if old_state != new_state: return True - old_attrs_s = set(old_attrs.items()) - new_attrs_s = set(new_attrs.items()) + old_attrs_s = set( + { + k: v + for k, v in old_attrs.items() + if k in SIGNIFICANT_ATTRIBUTES - INSIGNIFICANT_ATTRIBUTES + }.items() + ) + new_attrs_s = set( + { + k: v + for k, v in new_attrs.items() + if k in SIGNIFICANT_ATTRIBUTES - INSIGNIFICANT_ATTRIBUTES + }.items() + ) changed_attrs: set[str] = {item[0] for item in old_attrs_s ^ new_attrs_s} for attr_name in changed_attrs: - if attr_name not in SIGNIFICANT_ATTRIBUTES - INSIGNIFICANT_ATTRIBUTES: - continue - if attr_name != ATTR_MEDIA_VOLUME_LEVEL: return True diff --git a/homeassistant/components/vacuum/significant_change.py b/homeassistant/components/vacuum/significant_change.py index 3031d60305a..5699050c7cb 100644 --- a/homeassistant/components/vacuum/significant_change.py +++ b/homeassistant/components/vacuum/significant_change.py @@ -30,14 +30,15 @@ def async_check_significant_change( if old_state != new_state: return True - old_attrs_s = set(old_attrs.items()) - new_attrs_s = set(new_attrs.items()) + old_attrs_s = set( + {k: v for k, v in old_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) + new_attrs_s = set( + {k: v for k, v in new_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) changed_attrs: set[str] = {item[0] for item in old_attrs_s ^ new_attrs_s} for attr_name in changed_attrs: - if attr_name not in SIGNIFICANT_ATTRIBUTES: - continue - if attr_name != ATTR_BATTERY_LEVEL: return True diff --git a/homeassistant/components/water_heater/significant_change.py b/homeassistant/components/water_heater/significant_change.py index 903c80bb714..bacb0232ee3 100644 --- a/homeassistant/components/water_heater/significant_change.py +++ b/homeassistant/components/water_heater/significant_change.py @@ -42,15 +42,16 @@ def async_check_significant_change( if old_state != new_state: return True - old_attrs_s = set(old_attrs.items()) - new_attrs_s = set(new_attrs.items()) + old_attrs_s = set( + {k: v for k, v in old_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) + new_attrs_s = set( + {k: v for k, v in new_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) changed_attrs: set[str] = {item[0] for item in old_attrs_s ^ new_attrs_s} ha_unit = hass.config.units.temperature_unit for attr_name in changed_attrs: - if attr_name not in SIGNIFICANT_ATTRIBUTES: - continue - if attr_name in [ATTR_OPERATION_MODE, ATTR_AWAY_MODE]: return True diff --git a/homeassistant/components/weather/significant_change.py b/homeassistant/components/weather/significant_change.py index 4bb67c54e19..87e1246ce85 100644 --- a/homeassistant/components/weather/significant_change.py +++ b/homeassistant/components/weather/significant_change.py @@ -88,14 +88,15 @@ def async_check_significant_change( if old_state != new_state: return True - old_attrs_s = set(old_attrs.items()) - new_attrs_s = set(new_attrs.items()) + old_attrs_s = set( + {k: v for k, v in old_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) + new_attrs_s = set( + {k: v for k, v in new_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() + ) changed_attrs: set[str] = {item[0] for item in old_attrs_s ^ new_attrs_s} for attr_name in changed_attrs: - if attr_name not in SIGNIFICANT_ATTRIBUTES: - continue - old_attr_value = old_attrs.get(attr_name) new_attr_value = new_attrs.get(attr_name) absolute_change: float | None = None From e1e697c16eb103b304043c400fd7f45cba3833b0 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 28 Dec 2023 13:36:57 -0500 Subject: [PATCH 017/112] Bump plexapi to 4.15.7 (#106576) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 6dbd6118d7c..8fc01140787 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["plexapi", "plexwebsocket"], "requirements": [ - "PlexAPI==4.15.6", + "PlexAPI==4.15.7", "plexauth==0.0.6", "plexwebsocket==0.0.14" ], diff --git a/requirements_all.txt b/requirements_all.txt index c2ba8cccf52..ed173f9b472 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -45,7 +45,7 @@ Mastodon.py==1.5.1 Pillow==10.1.0 # homeassistant.components.plex -PlexAPI==4.15.6 +PlexAPI==4.15.7 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1171fbf1970..8cdd44e284e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -39,7 +39,7 @@ HATasmota==0.8.0 Pillow==10.1.0 # homeassistant.components.plex -PlexAPI==4.15.6 +PlexAPI==4.15.7 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 From 104039e7327bdf82eb91432d705a576155756d28 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Dec 2023 19:53:56 +0100 Subject: [PATCH 018/112] Revert "Set volume_step in aquostv media_player" (#106577) Revert "Set volume_step in aquostv media_player (#105665)" This reverts commit bb8dce6187b93ea17bf04902574b9c133a887e05. --- .../components/aquostv/media_player.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index cd93ddf9e15..34d5e4161fb 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -112,7 +112,6 @@ class SharpAquosTVDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.PLAY ) - _attr_volume_step = 2 / 60 def __init__( self, name: str, remote: sharp_aquos_rc.TV, power_on_enabled: bool = False @@ -157,6 +156,22 @@ class SharpAquosTVDevice(MediaPlayerEntity): """Turn off tvplayer.""" self._remote.power(0) + @_retry + def volume_up(self) -> None: + """Volume up the media player.""" + if self.volume_level is None: + _LOGGER.debug("Unknown volume in volume_up") + return + self._remote.volume(int(self.volume_level * 60) + 2) + + @_retry + def volume_down(self) -> None: + """Volume down media player.""" + if self.volume_level is None: + _LOGGER.debug("Unknown volume in volume_down") + return + self._remote.volume(int(self.volume_level * 60) - 2) + @_retry def set_volume_level(self, volume: float) -> None: """Set Volume media player.""" From 72dd60e66724b29c15beeb1cc4ae8f4cb5bdff84 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Dec 2023 19:54:10 +0100 Subject: [PATCH 019/112] Revert "Set volume_step in clementine media_player" (#106578) Revert "Set volume_step in clementine media_player (#105666)" This reverts commit 36eeb15feedac126b3465d3c522a858a9cc9ac2e. --- homeassistant/components/clementine/media_player.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/clementine/media_player.py b/homeassistant/components/clementine/media_player.py index eb0da23d360..770f19e9970 100644 --- a/homeassistant/components/clementine/media_player.py +++ b/homeassistant/components/clementine/media_player.py @@ -65,7 +65,6 @@ class ClementineDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.PLAY ) - _attr_volume_step = 4 / 100 def __init__(self, client, name): """Initialize the Clementine device.""" @@ -124,6 +123,16 @@ class ClementineDevice(MediaPlayerEntity): return None, None + def volume_up(self) -> None: + """Volume up the media player.""" + newvolume = min(self._client.volume + 4, 100) + self._client.set_volume(newvolume) + + def volume_down(self) -> None: + """Volume down media player.""" + newvolume = max(self._client.volume - 4, 0) + self._client.set_volume(newvolume) + def mute_volume(self, mute: bool) -> None: """Send mute command.""" self._client.set_volume(0) From 925b851366585e8222aa7c2c51b841c6062f5bdc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Dec 2023 19:54:21 +0100 Subject: [PATCH 020/112] Revert "Set volume_step in cmus media_player" (#106579) Revert "Set volume_step in cmus media_player (#105667)" This reverts commit c10b460c6bf71cb0329dca991b7a09fc5cd963c4. --- homeassistant/components/cmus/media_player.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index a242a5a772c..65bfef3a0cb 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -94,7 +94,6 @@ class CmusDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.SEEK | MediaPlayerEntityFeature.PLAY ) - _attr_volume_step = 5 / 100 def __init__(self, device, name, server): """Initialize the CMUS device.""" @@ -154,6 +153,30 @@ class CmusDevice(MediaPlayerEntity): """Set volume level, range 0..1.""" self._remote.cmus.set_volume(int(volume * 100)) + def volume_up(self) -> None: + """Set the volume up.""" + left = self.status["set"].get("vol_left") + right = self.status["set"].get("vol_right") + if left != right: + current_volume = float(left + right) / 2 + else: + current_volume = left + + if current_volume <= 100: + self._remote.cmus.set_volume(int(current_volume) + 5) + + def volume_down(self) -> None: + """Set the volume down.""" + left = self.status["set"].get("vol_left") + right = self.status["set"].get("vol_right") + if left != right: + current_volume = float(left + right) / 2 + else: + current_volume = left + + if current_volume <= 100: + self._remote.cmus.set_volume(int(current_volume) - 5) + def play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: From e953587260eb0a4c13000371b4742bb56b5a8782 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Dec 2023 19:54:36 +0100 Subject: [PATCH 021/112] Revert "Set volume_step in monoprice media_player" (#106580) Revert "Set volume_step in monoprice media_player (#105670)" This reverts commit cffb51ebec5a681878f7acd88d10e1e53e8130ce. --- .../components/monoprice/media_player.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 40ea9f85a7c..92b98abf374 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -127,7 +127,6 @@ class MonopriceZone(MediaPlayerEntity): ) _attr_has_entity_name = True _attr_name = None - _attr_volume_step = 1 / MAX_VOLUME def __init__(self, monoprice, sources, namespace, zone_id): """Initialize new zone.""" @@ -211,3 +210,17 @@ class MonopriceZone(MediaPlayerEntity): def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._monoprice.set_volume(self._zone_id, round(volume * MAX_VOLUME)) + + def volume_up(self) -> None: + """Volume up the media player.""" + if self.volume_level is None: + return + volume = round(self.volume_level * MAX_VOLUME) + self._monoprice.set_volume(self._zone_id, min(volume + 1, MAX_VOLUME)) + + def volume_down(self) -> None: + """Volume down media player.""" + if self.volume_level is None: + return + volume = round(self.volume_level * MAX_VOLUME) + self._monoprice.set_volume(self._zone_id, max(volume - 1, 0)) From 35b9044187b32fd05c617944e2fde3bd8cd2e194 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Dec 2023 19:48:34 +0100 Subject: [PATCH 022/112] Revert "Set volume_step in sonos media_player" (#106581) Revert "Set volume_step in sonos media_player (#105671)" This reverts commit 6dc8c2c37014de201578b5cbe880f7a1bbcecfc4. --- homeassistant/components/sonos/media_player.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 031e4606148..27059bba180 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -67,6 +67,7 @@ _LOGGER = logging.getLogger(__name__) LONG_SERVICE_TIMEOUT = 30.0 UNJOIN_SERVICE_TIMEOUT = 0.1 +VOLUME_INCREMENT = 2 REPEAT_TO_SONOS = { RepeatMode.OFF: False, @@ -211,7 +212,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ) _attr_media_content_type = MediaType.MUSIC _attr_device_class = MediaPlayerDeviceClass.SPEAKER - _attr_volume_step = 2 / 100 def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the media player entity.""" @@ -373,6 +373,16 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Name of the current input source.""" return self.media.source_name or None + @soco_error() + def volume_up(self) -> None: + """Volume up media player.""" + self.soco.volume += VOLUME_INCREMENT + + @soco_error() + def volume_down(self) -> None: + """Volume down media player.""" + self.soco.volume -= VOLUME_INCREMENT + @soco_error() def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" From 5125d8622d22cd040dddf5008b68fb65fde1210b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Dec 2023 19:54:51 +0100 Subject: [PATCH 023/112] Revert "Set volume_step in bluesound media_player" (#106582) Revert "Set volume_step in bluesound media_player (#105672)" This reverts commit 7fa55ffdd29af9d428b0ebd06b59b7be16130e3a. --- .../components/bluesound/media_player.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index cfe2fedebdc..eba03963ebc 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -200,7 +200,6 @@ class BluesoundPlayer(MediaPlayerEntity): """Representation of a Bluesound Player.""" _attr_media_content_type = MediaType.MUSIC - _attr_volume_step = 0.01 def __init__(self, hass, host, port=None, name=None, init_callback=None): """Initialize the media player.""" @@ -1028,6 +1027,20 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command(url) + async def async_volume_up(self) -> None: + """Volume up the media player.""" + current_vol = self.volume_level + if not current_vol or current_vol >= 1: + return + return await self.async_set_volume_level(current_vol + 0.01) + + async def async_volume_down(self) -> None: + """Volume down the media player.""" + current_vol = self.volume_level + if not current_vol or current_vol <= 0: + return + return await self.async_set_volume_level(current_vol - 0.01) + async def async_set_volume_level(self, volume: float) -> None: """Send volume_up command to media player.""" if volume < 0: From df894acefa5bca0e2e221f8b7b3e74ea14005141 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Dec 2023 19:50:57 +0100 Subject: [PATCH 024/112] Revert "Set volume_step in frontier_silicon media_player" (#106583) Revert "Set volume_step in frontier_silicon media_player (#105953)" This reverts commit 3e50ca6cda06a693002e0e7c69cbaa214145d053. --- .../components/frontier_silicon/media_player.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 565ee79b108..223abe26e55 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -152,9 +152,6 @@ class AFSAPIDevice(MediaPlayerEntity): if not self._max_volume: self._max_volume = int(await afsapi.get_volume_steps() or 1) - 1 - if self._max_volume: - self._attr_volume_step = 1 / self._max_volume - if self._attr_state != MediaPlayerState.OFF: info_name = await afsapi.get_play_name() info_text = await afsapi.get_play_text() @@ -242,6 +239,18 @@ class AFSAPIDevice(MediaPlayerEntity): await self.fs_device.set_mute(mute) # volume + async def async_volume_up(self) -> None: + """Send volume up command.""" + volume = await self.fs_device.get_volume() + volume = int(volume or 0) + 1 + await self.fs_device.set_volume(min(volume, self._max_volume)) + + async def async_volume_down(self) -> None: + """Send volume down command.""" + volume = await self.fs_device.get_volume() + volume = int(volume or 0) - 1 + await self.fs_device.set_volume(max(volume, 0)) + async def async_set_volume_level(self, volume: float) -> None: """Set volume command.""" if self._max_volume: # Can't do anything sensible if not set From fa34cbc41450eb74bd33b38ab1252bdf62cac95d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 28 Dec 2023 20:39:39 +0100 Subject: [PATCH 025/112] Systemmonitor always load imported disks (#106546) * Systemmonitor always load legacy disks * loaded_resources --- .../components/systemmonitor/sensor.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 57838c45dc7..2bc1406308c 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -389,6 +389,7 @@ async def async_setup_entry( entities = [] sensor_registry: dict[tuple[str, str], SensorData] = {} legacy_resources: list[str] = entry.options.get("resources", []) + loaded_resources: list[str] = [] disk_arguments = await hass.async_add_executor_job(get_all_disk_mounts) network_arguments = await hass.async_add_executor_job(get_all_network_interfaces) cpu_temperature = await hass.async_add_executor_job(_read_cpu_temperature) @@ -404,6 +405,7 @@ async def async_setup_entry( is_enabled = check_legacy_resource( f"{_type}_{argument}", legacy_resources ) + loaded_resources.append(f"{_type}_{argument}") entities.append( SystemMonitorSensor( sensor_registry, @@ -423,6 +425,7 @@ async def async_setup_entry( is_enabled = check_legacy_resource( f"{_type}_{argument}", legacy_resources ) + loaded_resources.append(f"{_type}_{argument}") entities.append( SystemMonitorSensor( sensor_registry, @@ -446,6 +449,7 @@ async def async_setup_entry( sensor_registry[(_type, argument)] = SensorData( argument, None, None, None, None ) + loaded_resources.append(f"{_type}_{argument}") entities.append( SystemMonitorSensor( sensor_registry, @@ -459,6 +463,7 @@ async def async_setup_entry( sensor_registry[(_type, "")] = SensorData("", None, None, None, None) is_enabled = check_legacy_resource(f"{_type}_", legacy_resources) + loaded_resources.append(f"{_type}_") entities.append( SystemMonitorSensor( sensor_registry, @@ -469,6 +474,31 @@ async def async_setup_entry( ) ) + # Ensure legacy imported disk_* resources are loaded if they are not part + # of mount points automatically discovered + for resource in legacy_resources: + if resource.startswith("disk_"): + _LOGGER.debug( + "Check resource %s already loaded in %s", resource, loaded_resources + ) + if resource not in loaded_resources: + split_index = resource.rfind("_") + _type = resource[:split_index] + argument = resource[split_index + 1 :] + _LOGGER.debug("Loading legacy %s with argument %s", _type, argument) + sensor_registry[(_type, argument)] = SensorData( + argument, None, None, None, None + ) + entities.append( + SystemMonitorSensor( + sensor_registry, + SENSOR_TYPES[_type], + entry.entry_id, + argument, + True, + ) + ) + scan_interval = DEFAULT_SCAN_INTERVAL await async_setup_sensor_registry_updates(hass, sensor_registry, scan_interval) async_add_entities(entities) From 9de482f4295a92bdeafdf1849e41d7f70e9fbe50 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 28 Dec 2023 15:08:55 -0500 Subject: [PATCH 026/112] Cleanup Sonos subscription used during setup (#106575) --- homeassistant/components/sonos/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index e6b328cbcb0..c79856c58b6 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -290,6 +290,17 @@ class SonosDiscoveryManager: sub.callback = _async_subscription_succeeded # Hold lock to prevent concurrent subscription attempts await asyncio.sleep(ZGS_SUBSCRIPTION_TIMEOUT * 2) + try: + # Cancel this subscription as we create an autorenewing + # subscription when setting up the SonosSpeaker instance + await sub.unsubscribe() + except ClientError as ex: + # Will be rejected if already replaced by new subscription + _LOGGER.debug( + "Cleanup unsubscription from %s was rejected: %s", ip_address, ex + ) + except (OSError, Timeout) as ex: + _LOGGER.error("Cleanup unsubscription from %s failed: %s", ip_address, ex) async def _async_stop_event_listener(self, event: Event | None = None) -> None: for speaker in self.data.discovered.values(): From 2ffb033a4618ff37b4733b21dcc32c428a615f99 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Dec 2023 21:08:16 +0100 Subject: [PATCH 027/112] Revert "Set volume_step in enigma2 media_player" (#106584) --- homeassistant/components/enigma2/media_player.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 4c0911b2462..432823d781b 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -119,7 +119,6 @@ class Enigma2Device(MediaPlayerEntity): | MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.SELECT_SOURCE ) - _attr_volume_step = 5 / 100 def __init__(self, name: str, device: OpenWebIfDevice, about: dict) -> None: """Initialize the Enigma2 device.""" @@ -141,6 +140,18 @@ class Enigma2Device(MediaPlayerEntity): """Set volume level, range 0..1.""" await self._device.set_volume(int(volume * 100)) + async def async_volume_up(self) -> None: + """Volume up the media player.""" + if self._attr_volume_level is None: + return + await self._device.set_volume(int(self._attr_volume_level * 100) + 5) + + async def async_volume_down(self) -> None: + """Volume down media player.""" + if self._attr_volume_level is None: + return + await self._device.set_volume(int(self._attr_volume_level * 100) - 5) + async def async_media_stop(self) -> None: """Send stop command.""" await self._device.send_remote_control_action(RemoteControlCodes.STOP) From fc021f863373f7c3e9ce6297d1ce0332a9bd799e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 10:18:05 -1000 Subject: [PATCH 028/112] Bump aiohomekit to 3.1.1 (#106591) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index e6ef6d58df6..edb81c14a72 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.1.0"], + "requirements": ["aiohomekit==3.1.1"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index ed173f9b472..5e22f7a0815 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -257,7 +257,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.1.0 +aiohomekit==3.1.1 # homeassistant.components.http aiohttp-fast-url-dispatcher==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8cdd44e284e..4360fc394c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -233,7 +233,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.1.0 +aiohomekit==3.1.1 # homeassistant.components.http aiohttp-fast-url-dispatcher==0.3.0 From c9f12d45b43a76da7d00b1b15fb159aaadc600b1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 28 Dec 2023 21:19:27 +0100 Subject: [PATCH 029/112] Bump version to 2024.1.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 572aa5d743d..6afa0430ba3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Any, Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -PATCH_VERSION: Final = "0b0" +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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 33b01c3c288..9d0794d3d2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.0b0" +version = "2024.1.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 256e3e059731e3d298563312f63a53b75cc6dcb3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 28 Dec 2023 21:20:55 +0100 Subject: [PATCH 030/112] Revert "Bump version to 2024.1.0" This reverts commit c9f12d45b43a76da7d00b1b15fb159aaadc600b1. --- 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 6afa0430ba3..572aa5d743d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Any, Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -PATCH_VERSION: Final = "0" +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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 9d0794d3d2d..33b01c3c288 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.0" +version = "2024.1.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2b7d37cbc2fa265fae0ea79511d369d15aaf7fd0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 28 Dec 2023 21:21:15 +0100 Subject: [PATCH 031/112] Bump version to 2024.1.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 572aa5d743d..f2387299576 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Any, Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 33b01c3c288..baa6814c75b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.0b0" +version = "2024.1.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From d407b9fca86a39f3e98f6d586cba72b95a9efcfb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 14:10:26 -1000 Subject: [PATCH 032/112] Update platform back-compat for custom components without UpdateEntityFeature (#106528) --- homeassistant/components/update/__init__.py | 21 +++++++++++++++++---- tests/components/update/test_init.py | 20 ++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 43a2a3e785f..40431332aaf 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -263,7 +263,7 @@ class UpdateEntity( return self._attr_entity_category if hasattr(self, "entity_description"): return self.entity_description.entity_category - if UpdateEntityFeature.INSTALL in self.supported_features: + if UpdateEntityFeature.INSTALL in self.supported_features_compat: return EntityCategory.CONFIG return EntityCategory.DIAGNOSTIC @@ -322,6 +322,19 @@ class UpdateEntity( """ return self._attr_title + @property + def supported_features_compat(self) -> UpdateEntityFeature: + """Return the supported features as UpdateEntityFeature. + + Remove this compatibility shim in 2025.1 or later. + """ + features = self.supported_features + if type(features) is int: # noqa: E721 + new_features = UpdateEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features + @final async def async_skip(self) -> None: """Skip the current offered version to update.""" @@ -408,7 +421,7 @@ class UpdateEntity( # If entity supports progress, return the in_progress value. # Otherwise, we use the internal progress value. - if UpdateEntityFeature.PROGRESS in self.supported_features: + if UpdateEntityFeature.PROGRESS in self.supported_features_compat: in_progress = self.in_progress else: in_progress = self.__in_progress @@ -444,7 +457,7 @@ class UpdateEntity( Handles setting the in_progress state in case the entity doesn't support it natively. """ - if UpdateEntityFeature.PROGRESS not in self.supported_features: + if UpdateEntityFeature.PROGRESS not in self.supported_features_compat: self.__in_progress = True self.async_write_ha_state() @@ -490,7 +503,7 @@ async def websocket_release_notes( ) return - if UpdateEntityFeature.RELEASE_NOTES not in entity.supported_features: + if UpdateEntityFeature.RELEASE_NOTES not in entity.supported_features_compat: connection.send_error( msg["id"], websocket_api.const.ERR_NOT_SUPPORTED, diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index 629c6838654..92e63af4b6f 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -865,3 +865,23 @@ async def test_name(hass: HomeAssistant) -> None: state = hass.states.get(entity4.entity_id) assert state assert expected.items() <= state.attributes.items() + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockUpdateEntity(UpdateEntity): + @property + def supported_features(self) -> int: + """Return supported features.""" + return 1 + + entity = MockUpdateEntity() + assert entity.supported_features_compat is UpdateEntityFeature(1) + assert "MockUpdateEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "UpdateEntityFeature.INSTALL" in caplog.text + caplog.clear() + assert entity.supported_features_compat is UpdateEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text From 0623972ee08be7072cb7cf1bee872dec73d4889d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 13:45:35 -1000 Subject: [PATCH 033/112] Camera platform back-compat for custom components without CameraEntityFeature (#106529) --- homeassistant/components/camera/__init__.py | 17 +++++++++++++++-- tests/components/camera/test_init.py | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 9f5ec0a6740..7a56292f7bb 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -530,6 +530,19 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Flag supported features.""" return self._attr_supported_features + @property + def supported_features_compat(self) -> CameraEntityFeature: + """Return the supported features as CameraEntityFeature. + + Remove this compatibility shim in 2025.1 or later. + """ + features = self.supported_features + if type(features) is int: # noqa: E721 + new_features = CameraEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features + @cached_property def is_recording(self) -> bool: """Return true if the device is recording.""" @@ -570,7 +583,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """ if hasattr(self, "_attr_frontend_stream_type"): return self._attr_frontend_stream_type - if CameraEntityFeature.STREAM not in self.supported_features: + if CameraEntityFeature.STREAM not in self.supported_features_compat: return None if self._rtsp_to_webrtc: return StreamType.WEB_RTC @@ -758,7 +771,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): async def _async_use_rtsp_to_webrtc(self) -> bool: """Determine if a WebRTC provider can be used for the camera.""" - if CameraEntityFeature.STREAM not in self.supported_features: + if CameraEntityFeature.STREAM not in self.supported_features_compat: return False if DATA_RTSP_TO_WEB_RTC not in self.hass.data: return False diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index cb9b09a85ab..0e761f2f437 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -993,3 +993,23 @@ def test_deprecated_support_constants( import_and_test_deprecated_constant_enum( caplog, camera, entity_feature, "SUPPORT_", "2025.1" ) + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockCamera(camera.Camera): + @property + def supported_features(self) -> int: + """Return supported features.""" + return 1 + + entity = MockCamera() + assert entity.supported_features_compat is camera.CameraEntityFeature(1) + assert "MockCamera" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "CameraEntityFeature.ON_OFF" in caplog.text + caplog.clear() + assert entity.supported_features_compat is camera.CameraEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text From 55877b0953d8d9071cb1795ebdf45744f4a8c20a Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 28 Dec 2023 13:24:11 -0800 Subject: [PATCH 034/112] Rename domain aepohio to aep_ohio (#106536) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/{aepohio => aep_ohio}/__init__.py | 0 homeassistant/components/{aepohio => aep_ohio}/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename homeassistant/components/{aepohio => aep_ohio}/__init__.py (100%) rename homeassistant/components/{aepohio => aep_ohio}/manifest.json (78%) diff --git a/homeassistant/components/aepohio/__init__.py b/homeassistant/components/aep_ohio/__init__.py similarity index 100% rename from homeassistant/components/aepohio/__init__.py rename to homeassistant/components/aep_ohio/__init__.py diff --git a/homeassistant/components/aepohio/manifest.json b/homeassistant/components/aep_ohio/manifest.json similarity index 78% rename from homeassistant/components/aepohio/manifest.json rename to homeassistant/components/aep_ohio/manifest.json index f659a712016..9b85e537fc8 100644 --- a/homeassistant/components/aepohio/manifest.json +++ b/homeassistant/components/aep_ohio/manifest.json @@ -1,5 +1,5 @@ { - "domain": "aepohio", + "domain": "aep_ohio", "name": "AEP Ohio", "integration_type": "virtual", "supported_by": "opower" diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 995609ec226..884ca88074e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -65,7 +65,7 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "aepohio": { + "aep_ohio": { "name": "AEP Ohio", "integration_type": "virtual", "supported_by": "opower" From 911234ae8f4f864ebeaa0a22d84d24b776bb84d0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Dec 2023 21:58:34 +0100 Subject: [PATCH 035/112] Move aeptexas to aep_texas (#106595) --- homeassistant/components/{aeptexas => aep_texas}/__init__.py | 0 homeassistant/components/{aeptexas => aep_texas}/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename homeassistant/components/{aeptexas => aep_texas}/__init__.py (100%) rename homeassistant/components/{aeptexas => aep_texas}/manifest.json (77%) diff --git a/homeassistant/components/aeptexas/__init__.py b/homeassistant/components/aep_texas/__init__.py similarity index 100% rename from homeassistant/components/aeptexas/__init__.py rename to homeassistant/components/aep_texas/__init__.py diff --git a/homeassistant/components/aeptexas/manifest.json b/homeassistant/components/aep_texas/manifest.json similarity index 77% rename from homeassistant/components/aeptexas/manifest.json rename to homeassistant/components/aep_texas/manifest.json index d6260a2f51a..5de0e0ffd77 100644 --- a/homeassistant/components/aeptexas/manifest.json +++ b/homeassistant/components/aep_texas/manifest.json @@ -1,5 +1,5 @@ { - "domain": "aeptexas", + "domain": "aep_texas", "name": "AEP Texas", "integration_type": "virtual", "supported_by": "opower" diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 884ca88074e..45bcc1788cd 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -70,7 +70,7 @@ "integration_type": "virtual", "supported_by": "opower" }, - "aeptexas": { + "aep_texas": { "name": "AEP Texas", "integration_type": "virtual", "supported_by": "opower" From 982707afe61eff7adbd84f656f5592e5ee1b750c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 28 Dec 2023 16:26:19 -0500 Subject: [PATCH 036/112] Fix Netgear LTE halting startup (#106598) --- homeassistant/components/netgear_lte/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index 00a43282210..9faa2f361b9 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -170,7 +170,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DATA_HASS_CONFIG] = config if lte_config := config.get(DOMAIN): - await hass.async_create_task(import_yaml(hass, lte_config)) + hass.async_create_task(import_yaml(hass, lte_config)) return True From 16192cd7f2594f85f987e8bfc618e9f2e9e2f9fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 12:24:36 -1000 Subject: [PATCH 037/112] Add helper to report deprecated entity supported features magic numbers (#106602) --- homeassistant/helpers/entity.py | 30 +++++++++++++++++++++++++++++- tests/helpers/test_entity.py | 26 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 8f344aff484..fc627f51acf 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -7,7 +7,7 @@ from collections import deque from collections.abc import Callable, Coroutine, Iterable, Mapping, MutableMapping import dataclasses from datetime import timedelta -from enum import Enum, auto +from enum import Enum, IntFlag, auto import functools as ft import logging import math @@ -460,6 +460,9 @@ class Entity( # If we reported if this entity was slow _slow_reported = False + # If we reported deprecated supported features constants + _deprecated_supported_features_reported = False + # If we reported this entity is updated while disabled _disabled_reported = False @@ -1496,6 +1499,31 @@ class Entity( self.hass, integration_domain=platform_name, module=type(self).__module__ ) + @callback + def _report_deprecated_supported_features_values( + self, replacement: IntFlag + ) -> None: + """Report deprecated supported features values.""" + if self._deprecated_supported_features_reported is True: + return + self._deprecated_supported_features_reported = True + report_issue = self._suggest_report_issue() + report_issue += ( + " and reference " + "https://developers.home-assistant.io/blog/2023/12/28/support-feature-magic-numbers-deprecation" + ) + _LOGGER.warning( + ( + "Entity %s (%s) is using deprecated supported features" + " values which will be removed in HA Core 2025.1. Instead it should use" + " %s, please %s" + ), + self.entity_id, + type(self), + repr(replacement), + report_issue, + ) + class ToggleEntityDescription(EntityDescription, frozen_or_thawed=True): """A class that describes toggle entities.""" diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 2bf90660f31..96bbf95a986 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -3,6 +3,7 @@ import asyncio from collections.abc import Iterable import dataclasses from datetime import timedelta +from enum import IntFlag import logging import threading from typing import Any @@ -2025,3 +2026,28 @@ async def test_cached_entity_property_class_attribute(hass: HomeAssistant) -> No for ent in entities: assert getattr(ent[0], property) == values[1] assert getattr(ent[1], property) == values[0] + + +async def test_entity_report_deprecated_supported_features_values( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test reporting deprecated supported feature values only happens once.""" + ent = entity.Entity() + + class MockEntityFeatures(IntFlag): + VALUE1 = 1 + VALUE2 = 2 + + ent._report_deprecated_supported_features_values(MockEntityFeatures(2)) + assert ( + "is using deprecated supported features values which will be removed" + in caplog.text + ) + assert "MockEntityFeatures.VALUE2" in caplog.text + + caplog.clear() + ent._report_deprecated_supported_features_values(MockEntityFeatures(2)) + assert ( + "is using deprecated supported features values which will be removed" + not in caplog.text + ) From 06f06b7595f3f1a32c7f74423055e1db37edc497 Mon Sep 17 00:00:00 2001 From: Joe Neuman Date: Fri, 29 Dec 2023 02:08:40 -0800 Subject: [PATCH 038/112] Fix count bug in qBittorrent (#106603) --- homeassistant/components/qbittorrent/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index a51ff58405c..9373aec8544 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -165,6 +165,9 @@ def count_torrents_in_states( coordinator: QBittorrentDataCoordinator, states: list[str] ) -> int: """Count the number of torrents in specified states.""" + if not states: + return len(coordinator.data["torrents"]) + return len( [ torrent From 4a98a6465e2bd085f6e0dc98f0331ed9dfa75e07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 13:36:28 -1000 Subject: [PATCH 039/112] Climate platform back-compat for custom components without ClimateEntityFeature (#106605) --- homeassistant/components/climate/__init__.py | 17 +++++++++++++++-- tests/components/climate/test_init.py | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 4815b7a1cbb..19e26265f70 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -316,7 +316,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): @property def capability_attributes(self) -> dict[str, Any] | None: """Return the capability attributes.""" - supported_features = self.supported_features + supported_features = self.supported_features_compat temperature_unit = self.temperature_unit precision = self.precision hass = self.hass @@ -349,7 +349,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): @property def state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" - supported_features = self.supported_features + supported_features = self.supported_features_compat temperature_unit = self.temperature_unit precision = self.precision hass = self.hass @@ -665,6 +665,19 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Return the list of supported features.""" return self._attr_supported_features + @property + def supported_features_compat(self) -> ClimateEntityFeature: + """Return the supported features as ClimateEntityFeature. + + Remove this compatibility shim in 2025.1 or later. + """ + features = self.supported_features + if type(features) is int: # noqa: E721 + new_features = ClimateEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features + @cached_property def min_temp(self) -> float: """Return the minimum temperature.""" diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index f46e0902c66..8fc82365c23 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -333,3 +333,23 @@ async def test_preset_mode_validation( ) assert str(exc.value) == "The fan_mode invalid is not a valid fan_mode: auto, off" assert exc.value.translation_key == "not_valid_fan_mode" + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockClimateEntity(ClimateEntity): + @property + def supported_features(self) -> int: + """Return supported features.""" + return 1 + + entity = MockClimateEntity() + assert entity.supported_features_compat is ClimateEntityFeature(1) + assert "MockClimateEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "ClimateEntityFeature.TARGET_TEMPERATURE" in caplog.text + caplog.clear() + assert entity.supported_features_compat is ClimateEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text From 04fe8260abe8e2bc2cd423a80913c6663cb10ed9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 13:36:15 -1000 Subject: [PATCH 040/112] Fan platform back-compat for custom components without FanEntityFeature (#106607) --- homeassistant/components/fan/__init__.py | 17 +++++++++++++++-- tests/components/fan/test_init.py | 21 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 1bacc6d8dac..dedaedfe600 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -400,7 +400,7 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def capability_attributes(self) -> dict[str, list[str] | None]: """Return capability attributes.""" attrs = {} - supported_features = self.supported_features + supported_features = self.supported_features_compat if ( FanEntityFeature.SET_SPEED in supported_features @@ -415,7 +415,7 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def state_attributes(self) -> dict[str, float | str | None]: """Return optional state attributes.""" data: dict[str, float | str | None] = {} - supported_features = self.supported_features + supported_features = self.supported_features_compat if FanEntityFeature.DIRECTION in supported_features: data[ATTR_DIRECTION] = self.current_direction @@ -439,6 +439,19 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Flag supported features.""" return self._attr_supported_features + @property + def supported_features_compat(self) -> FanEntityFeature: + """Return the supported features as FanEntityFeature. + + Remove this compatibility shim in 2025.1 or later. + """ + features = self.supported_features + if type(features) is int: # noqa: E721 + new_features = FanEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features + @cached_property def preset_mode(self) -> str | None: """Return the current preset mode, e.g., auto, smart, interval, favorite. diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index e6a3ab546cc..828c13b6f16 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -8,6 +8,7 @@ from homeassistant.components.fan import ( DOMAIN, SERVICE_SET_PRESET_MODE, FanEntity, + FanEntityFeature, NotValidPresetModeError, ) from homeassistant.core import HomeAssistant @@ -156,3 +157,23 @@ def test_deprecated_constants( ) -> None: """Test deprecated constants.""" import_and_test_deprecated_constant_enum(caplog, fan, enum, "SUPPORT_", "2025.1") + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockFan(FanEntity): + @property + def supported_features(self) -> int: + """Return supported features.""" + return 1 + + entity = MockFan() + assert entity.supported_features_compat is FanEntityFeature(1) + assert "MockFan" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "FanEntityFeature.SET_SPEED" in caplog.text + caplog.clear() + assert entity.supported_features_compat is FanEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text From 6224e630ac9c4cd0b5539e0295e4d9b10a5802a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 13:16:02 -1000 Subject: [PATCH 041/112] Water heater platform back-compat for custom components without WaterHeaterEntityFeature (#106608) --- .../components/water_heater/__init__.py | 13 ++++++++++++ tests/components/water_heater/test_init.py | 20 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index ddef4e7366c..f2744416900 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -401,6 +401,19 @@ class WaterHeaterEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Return the list of supported features.""" return self._attr_supported_features + @property + def supported_features_compat(self) -> WaterHeaterEntityFeature: + """Return the supported features as WaterHeaterEntityFeature. + + Remove this compatibility shim in 2025.1 or later. + """ + features = self.supported_features + if type(features) is int: # noqa: E721 + new_features = WaterHeaterEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features + async def async_service_away_mode( entity: WaterHeaterEntity, service: ServiceCall diff --git a/tests/components/water_heater/test_init.py b/tests/components/water_heater/test_init.py index 8a7d76bd891..0d33f3a9e93 100644 --- a/tests/components/water_heater/test_init.py +++ b/tests/components/water_heater/test_init.py @@ -115,3 +115,23 @@ def test_deprecated_constants( import_and_test_deprecated_constant_enum( caplog, water_heater, enum, "SUPPORT_", "2025.1" ) + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockWaterHeaterEntity(WaterHeaterEntity): + @property + def supported_features(self) -> int: + """Return supported features.""" + return 1 + + entity = MockWaterHeaterEntity() + assert entity.supported_features_compat is WaterHeaterEntityFeature(1) + assert "MockWaterHeaterEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "WaterHeaterEntityFeature.TARGET_TEMPERATURE" in caplog.text + caplog.clear() + assert entity.supported_features_compat is WaterHeaterEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text From aa6e904e86f7bb70b0b345437a82628e8ac2ba91 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 13:15:48 -1000 Subject: [PATCH 042/112] Remote platform back-compat for custom components without RemoteEntityFeature (#106609) --- homeassistant/components/remote/__init__.py | 15 ++++++++++++++- tests/components/remote/test_init.py | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 8c3d094710e..7e9ebfe12b9 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -200,6 +200,19 @@ class RemoteEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_) """Flag supported features.""" return self._attr_supported_features + @property + def supported_features_compat(self) -> RemoteEntityFeature: + """Return the supported features as RemoteEntityFeature. + + Remove this compatibility shim in 2025.1 or later. + """ + features = self.supported_features + if type(features) is int: # noqa: E721 + new_features = RemoteEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features + @cached_property def current_activity(self) -> str | None: """Active activity.""" @@ -214,7 +227,7 @@ class RemoteEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_) @property def state_attributes(self) -> dict[str, Any] | None: """Return optional state attributes.""" - if RemoteEntityFeature.ACTIVITY not in self.supported_features: + if RemoteEntityFeature.ACTIVITY not in self.supported_features_compat: return None return { diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py index b185b229cd2..a75ff858483 100644 --- a/tests/components/remote/test_init.py +++ b/tests/components/remote/test_init.py @@ -150,3 +150,23 @@ def test_deprecated_constants( ) -> None: """Test deprecated constants.""" import_and_test_deprecated_constant_enum(caplog, remote, enum, "SUPPORT_", "2025.1") + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockRemote(remote.RemoteEntity): + @property + def supported_features(self) -> int: + """Return supported features.""" + return 1 + + entity = MockRemote() + assert entity.supported_features_compat is remote.RemoteEntityFeature(1) + assert "MockRemote" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "RemoteEntityFeature.LEARN_COMMAND" in caplog.text + caplog.clear() + assert entity.supported_features_compat is remote.RemoteEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text From f03bb4a2dac90005db4554cd13dde767dc6373d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 14:10:46 -1000 Subject: [PATCH 043/112] Humidifier platform back-compat for custom components without HumidifierEntityFeature (#106613) --- .../components/humidifier/__init__.py | 15 ++++++++++- tests/components/humidifier/test_init.py | 25 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 821cc8c4f37..75d4f0fd225 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -185,7 +185,7 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT ATTR_MAX_HUMIDITY: self.max_humidity, } - if HumidifierEntityFeature.MODES in self.supported_features: + if HumidifierEntityFeature.MODES in self.supported_features_compat: data[ATTR_AVAILABLE_MODES] = self.available_modes return data @@ -280,3 +280,16 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT def supported_features(self) -> HumidifierEntityFeature: """Return the list of supported features.""" return self._attr_supported_features + + @property + def supported_features_compat(self) -> HumidifierEntityFeature: + """Return the supported features as HumidifierEntityFeature. + + Remove this compatibility shim in 2025.1 or later. + """ + features = self.supported_features + if type(features) is int: # noqa: E721 + new_features = HumidifierEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features diff --git a/tests/components/humidifier/test_init.py b/tests/components/humidifier/test_init.py index da45e1f1661..45da5ba750f 100644 --- a/tests/components/humidifier/test_init.py +++ b/tests/components/humidifier/test_init.py @@ -6,7 +6,10 @@ from unittest.mock import MagicMock import pytest from homeassistant.components import humidifier -from homeassistant.components.humidifier import HumidifierEntity +from homeassistant.components.humidifier import ( + HumidifierEntity, + HumidifierEntityFeature, +) from homeassistant.core import HomeAssistant from tests.common import import_and_test_deprecated_constant_enum @@ -66,3 +69,23 @@ def test_deprecated_constants( import_and_test_deprecated_constant_enum( caplog, module, enum, constant_prefix, "2025.1" ) + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockHumidifierEntity(HumidifierEntity): + @property + def supported_features(self) -> int: + """Return supported features.""" + return 1 + + entity = MockHumidifierEntity() + assert entity.supported_features_compat is HumidifierEntityFeature(1) + assert "MockHumidifierEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "HumidifierEntityFeature.MODES" in caplog.text + caplog.clear() + assert entity.supported_features_compat is HumidifierEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text From 70842f197e0aa8b602a152a012955bbcf3df9e60 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 14:06:25 -1000 Subject: [PATCH 044/112] Vacuum platform back-compat for custom components without VacuumEntityFeature (#106614) --- homeassistant/components/vacuum/__init__.py | 19 ++++++++++++--- tests/components/vacuum/test_init.py | 26 ++++++++++++++++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 3ff29ec4e47..9a10da23824 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -258,6 +258,19 @@ class _BaseVacuum(Entity, cached_properties=BASE_CACHED_PROPERTIES_WITH_ATTR_): """Flag vacuum cleaner features that are supported.""" return self._attr_supported_features + @property + def supported_features_compat(self) -> VacuumEntityFeature: + """Return the supported features as VacuumEntityFeature. + + Remove this compatibility shim in 2025.1 or later. + """ + features = self.supported_features + if type(features) is int: # noqa: E721 + new_features = VacuumEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features + @cached_property def battery_level(self) -> int | None: """Return the battery level of the vacuum cleaner.""" @@ -281,7 +294,7 @@ class _BaseVacuum(Entity, cached_properties=BASE_CACHED_PROPERTIES_WITH_ATTR_): @property def capability_attributes(self) -> Mapping[str, Any] | None: """Return capability attributes.""" - if VacuumEntityFeature.FAN_SPEED in self.supported_features: + if VacuumEntityFeature.FAN_SPEED in self.supported_features_compat: return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} return None @@ -289,7 +302,7 @@ class _BaseVacuum(Entity, cached_properties=BASE_CACHED_PROPERTIES_WITH_ATTR_): def state_attributes(self) -> dict[str, Any]: """Return the state attributes of the vacuum cleaner.""" data: dict[str, Any] = {} - supported_features = self.supported_features + supported_features = self.supported_features_compat if VacuumEntityFeature.BATTERY in supported_features: data[ATTR_BATTERY_LEVEL] = self.battery_level @@ -471,7 +484,7 @@ class VacuumEntity( """Return the state attributes of the vacuum cleaner.""" data = super().state_attributes - if VacuumEntityFeature.STATUS in self.supported_features: + if VacuumEntityFeature.STATUS in self.supported_features_compat: data[ATTR_STATUS] = self.status return data diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index 3cf77d4f420..0b44476989b 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -5,7 +5,11 @@ from collections.abc import Generator import pytest -from homeassistant.components.vacuum import DOMAIN as VACUUM_DOMAIN, VacuumEntity +from homeassistant.components.vacuum import ( + DOMAIN as VACUUM_DOMAIN, + VacuumEntity, + VacuumEntityFeature, +) from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir @@ -121,3 +125,23 @@ async def test_deprecated_base_class( issue.translation_placeholders == {"platform": "test"} | translation_placeholders_extra ) + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockVacuumEntity(VacuumEntity): + @property + def supported_features(self) -> int: + """Return supported features.""" + return 1 + + entity = MockVacuumEntity() + assert entity.supported_features_compat is VacuumEntityFeature(1) + assert "MockVacuumEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "VacuumEntityFeature.TURN_ON" in caplog.text + caplog.clear() + assert entity.supported_features_compat is VacuumEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text From 5d9177d6e6e427d85d83b65a53c47554121d9ec5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 14:32:44 -1000 Subject: [PATCH 045/112] Media player platform back-compat for custom components without MediaPlayerEntityFeature (#106616) --- .../components/media_player/__init__.py | 49 ++++++++++++------- tests/components/media_player/test_init.py | 22 +++++++++ 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index a4439c9c68e..113048421e1 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -766,6 +766,19 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Flag media player features that are supported.""" return self._attr_supported_features + @property + def supported_features_compat(self) -> MediaPlayerEntityFeature: + """Return the supported features as MediaPlayerEntityFeature. + + Remove this compatibility shim in 2025.1 or later. + """ + features = self.supported_features + if type(features) is int: # noqa: E721 + new_features = MediaPlayerEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features + def turn_on(self) -> None: """Turn the media player on.""" raise NotImplementedError() @@ -905,85 +918,87 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): @property def support_play(self) -> bool: """Boolean if play is supported.""" - return MediaPlayerEntityFeature.PLAY in self.supported_features + return MediaPlayerEntityFeature.PLAY in self.supported_features_compat @final @property def support_pause(self) -> bool: """Boolean if pause is supported.""" - return MediaPlayerEntityFeature.PAUSE in self.supported_features + return MediaPlayerEntityFeature.PAUSE in self.supported_features_compat @final @property def support_stop(self) -> bool: """Boolean if stop is supported.""" - return MediaPlayerEntityFeature.STOP in self.supported_features + return MediaPlayerEntityFeature.STOP in self.supported_features_compat @final @property def support_seek(self) -> bool: """Boolean if seek is supported.""" - return MediaPlayerEntityFeature.SEEK in self.supported_features + return MediaPlayerEntityFeature.SEEK in self.supported_features_compat @final @property def support_volume_set(self) -> bool: """Boolean if setting volume is supported.""" - return MediaPlayerEntityFeature.VOLUME_SET in self.supported_features + return MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat @final @property def support_volume_mute(self) -> bool: """Boolean if muting volume is supported.""" - return MediaPlayerEntityFeature.VOLUME_MUTE in self.supported_features + return MediaPlayerEntityFeature.VOLUME_MUTE in self.supported_features_compat @final @property def support_previous_track(self) -> bool: """Boolean if previous track command supported.""" - return MediaPlayerEntityFeature.PREVIOUS_TRACK in self.supported_features + return MediaPlayerEntityFeature.PREVIOUS_TRACK in self.supported_features_compat @final @property def support_next_track(self) -> bool: """Boolean if next track command supported.""" - return MediaPlayerEntityFeature.NEXT_TRACK in self.supported_features + return MediaPlayerEntityFeature.NEXT_TRACK in self.supported_features_compat @final @property def support_play_media(self) -> bool: """Boolean if play media command supported.""" - return MediaPlayerEntityFeature.PLAY_MEDIA in self.supported_features + return MediaPlayerEntityFeature.PLAY_MEDIA in self.supported_features_compat @final @property def support_select_source(self) -> bool: """Boolean if select source command supported.""" - return MediaPlayerEntityFeature.SELECT_SOURCE in self.supported_features + return MediaPlayerEntityFeature.SELECT_SOURCE in self.supported_features_compat @final @property def support_select_sound_mode(self) -> bool: """Boolean if select sound mode command supported.""" - return MediaPlayerEntityFeature.SELECT_SOUND_MODE in self.supported_features + return ( + MediaPlayerEntityFeature.SELECT_SOUND_MODE in self.supported_features_compat + ) @final @property def support_clear_playlist(self) -> bool: """Boolean if clear playlist command supported.""" - return MediaPlayerEntityFeature.CLEAR_PLAYLIST in self.supported_features + return MediaPlayerEntityFeature.CLEAR_PLAYLIST in self.supported_features_compat @final @property def support_shuffle_set(self) -> bool: """Boolean if shuffle is supported.""" - return MediaPlayerEntityFeature.SHUFFLE_SET in self.supported_features + return MediaPlayerEntityFeature.SHUFFLE_SET in self.supported_features_compat @final @property def support_grouping(self) -> bool: """Boolean if player grouping is supported.""" - return MediaPlayerEntityFeature.GROUPING in self.supported_features + return MediaPlayerEntityFeature.GROUPING in self.supported_features_compat async def async_toggle(self) -> None: """Toggle the power on the media player.""" @@ -1012,7 +1027,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if ( self.volume_level is not None and self.volume_level < 1 - and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features + and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat ): await self.async_set_volume_level( min(1, self.volume_level + self.volume_step) @@ -1030,7 +1045,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if ( self.volume_level is not None and self.volume_level > 0 - and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features + and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat ): await self.async_set_volume_level( max(0, self.volume_level - self.volume_step) @@ -1073,7 +1088,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" data: dict[str, Any] = {} - supported_features = self.supported_features + supported_features = self.supported_features_compat if ( source_list := self.source_list diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 377cdd32748..b4228d1ee69 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -10,6 +10,8 @@ from homeassistant.components.media_player import ( BrowseMedia, MediaClass, MediaPlayerEnqueue, + MediaPlayerEntity, + MediaPlayerEntityFeature, ) from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF @@ -327,3 +329,23 @@ async def test_get_async_get_browse_image_quoting( url = player.get_browse_image_url("album", media_content_id) await client.get(url) mock_browse_image.assert_called_with("album", media_content_id, None) + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockMediaPlayerEntity(MediaPlayerEntity): + @property + def supported_features(self) -> int: + """Return supported features.""" + return 1 + + entity = MockMediaPlayerEntity() + assert entity.supported_features_compat is MediaPlayerEntityFeature(1) + assert "MockMediaPlayerEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "MediaPlayerEntityFeature.PAUSE" in caplog.text + caplog.clear() + assert entity.supported_features_compat is MediaPlayerEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text From 9f4790902a4244275fd818d4aadff68aa4122d74 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 21:34:08 -1000 Subject: [PATCH 046/112] Add deprecation warning for cover supported features when using magic numbers (#106618) --- homeassistant/components/cover/__init__.py | 8 ++++++-- tests/components/cover/test_init.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 1a21908860a..3e438fb4ca1 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -340,8 +340,12 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): @property def supported_features(self) -> CoverEntityFeature: """Flag supported features.""" - if self._attr_supported_features is not None: - return self._attr_supported_features + if (features := self._attr_supported_features) is not None: + if type(features) is int: # noqa: E721 + new_features = CoverEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features supported_features = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py index 062440e6b39..1b08658d983 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_init.py @@ -141,3 +141,20 @@ def test_deprecated_constants( import_and_test_deprecated_constant_enum( caplog, cover, enum, constant_prefix, "2025.1" ) + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockCoverEntity(cover.CoverEntity): + _attr_supported_features = 1 + + entity = MockCoverEntity() + assert entity.supported_features is cover.CoverEntityFeature(1) + assert "MockCoverEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "CoverEntityFeature.OPEN" in caplog.text + caplog.clear() + assert entity.supported_features is cover.CoverEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text From 024d689b94aca1f952f5e9e9712695f66ebfba98 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 15:04:06 -1000 Subject: [PATCH 047/112] Add deprecation warning for alarm_control_panel supported features when using magic numbers (#106619) --- .../alarm_control_panel/__init__.py | 7 +++++- .../alarm_control_panel/test_init.py | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index dd42c6c7072..9c53f2b7fd0 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -233,7 +233,12 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A @cached_property def supported_features(self) -> AlarmControlPanelEntityFeature: """Return the list of supported features.""" - return self._attr_supported_features + features = self._attr_supported_features + if type(features) is int: # noqa: E721 + new_features = AlarmControlPanelEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features @final @property diff --git a/tests/components/alarm_control_panel/test_init.py b/tests/components/alarm_control_panel/test_init.py index c447119c119..1e6fce6def6 100644 --- a/tests/components/alarm_control_panel/test_init.py +++ b/tests/components/alarm_control_panel/test_init.py @@ -45,3 +45,26 @@ def test_deprecated_support_alarm_constants( import_and_test_deprecated_constant_enum( caplog, module, entity_feature, "SUPPORT_ALARM_", "2025.1" ) + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockAlarmControlPanelEntity(alarm_control_panel.AlarmControlPanelEntity): + _attr_supported_features = 1 + + entity = MockAlarmControlPanelEntity() + assert ( + entity.supported_features + is alarm_control_panel.AlarmControlPanelEntityFeature(1) + ) + assert "MockAlarmControlPanelEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "AlarmControlPanelEntityFeature.ARM_HOME" in caplog.text + caplog.clear() + assert ( + entity.supported_features + is alarm_control_panel.AlarmControlPanelEntityFeature(1) + ) + assert "is using deprecated supported features values" not in caplog.text From af9f6a2b12a741567194d99bd98ba864899c484c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 15:03:14 -1000 Subject: [PATCH 048/112] Add deprecation warning for lock supported features when using magic numbers (#106620) --- homeassistant/components/lock/__init__.py | 7 ++++++- tests/components/lock/test_init.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 9a2466e22dd..a9370f8d092 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -278,7 +278,12 @@ class LockEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): @cached_property def supported_features(self) -> LockEntityFeature: """Return the list of supported features.""" - return self._attr_supported_features + features = self._attr_supported_features + if type(features) is int: # noqa: E721 + new_features = LockEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features async def async_internal_added_to_hass(self) -> None: """Call when the sensor entity is added to hass.""" diff --git a/tests/components/lock/test_init.py b/tests/components/lock/test_init.py index c4337c367a9..854b89fd1d8 100644 --- a/tests/components/lock/test_init.py +++ b/tests/components/lock/test_init.py @@ -378,3 +378,20 @@ def test_deprecated_constants( ) -> None: """Test deprecated constants.""" import_and_test_deprecated_constant_enum(caplog, lock, enum, "SUPPORT_", "2025.1") + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockLockEntity(lock.LockEntity): + _attr_supported_features = 1 + + entity = MockLockEntity() + assert entity.supported_features is lock.LockEntityFeature(1) + assert "MockLockEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "LockEntityFeature.OPEN" in caplog.text + caplog.clear() + assert entity.supported_features is lock.LockEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text From 2147df4418fc1f1253892742a03f4b3c8e42c281 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 15:45:27 -1000 Subject: [PATCH 049/112] Add deprecation warning for siren supported features when using magic numbers (#106621) --- homeassistant/components/siren/__init__.py | 7 ++++++- tests/components/siren/test_init.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index 263c6697df6..29ad238ac00 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -212,4 +212,9 @@ class SirenEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): @cached_property def supported_features(self) -> SirenEntityFeature: """Return the list of supported features.""" - return self._attr_supported_features + features = self._attr_supported_features + if type(features) is int: # noqa: E721 + new_features = SirenEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features diff --git a/tests/components/siren/test_init.py b/tests/components/siren/test_init.py index ee007f6f1f5..abc5b0fac38 100644 --- a/tests/components/siren/test_init.py +++ b/tests/components/siren/test_init.py @@ -119,3 +119,20 @@ def test_deprecated_constants( ) -> None: """Test deprecated constants.""" import_and_test_deprecated_constant_enum(caplog, module, enum, "SUPPORT_", "2025.1") + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockSirenEntity(siren.SirenEntity): + _attr_supported_features = 1 + + entity = MockSirenEntity() + assert entity.supported_features is siren.SirenEntityFeature(1) + assert "MockSirenEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "SirenEntityFeature.TURN_ON" in caplog.text + caplog.clear() + assert entity.supported_features is siren.SirenEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text From f84d865c51239ecd54efa69116497830e3ed95d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Dec 2023 15:45:06 -1000 Subject: [PATCH 050/112] Migrate light entity to use contains for LightEntityFeature with deprecation warnings (#106622) --- homeassistant/components/light/__init__.py | 67 ++++++++++++++++------ tests/components/light/test_init.py | 21 +++++++ 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index c66562a53af..ebd3696d61f 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -345,11 +345,11 @@ def filter_turn_off_params( light: LightEntity, params: dict[str, Any] ) -> dict[str, Any]: """Filter out params not used in turn off or not supported by the light.""" - supported_features = light.supported_features + supported_features = light.supported_features_compat - if not supported_features & LightEntityFeature.FLASH: + if LightEntityFeature.FLASH not in supported_features: params.pop(ATTR_FLASH, None) - if not supported_features & LightEntityFeature.TRANSITION: + if LightEntityFeature.TRANSITION not in supported_features: params.pop(ATTR_TRANSITION, None) return {k: v for k, v in params.items() if k in (ATTR_TRANSITION, ATTR_FLASH)} @@ -357,13 +357,13 @@ def filter_turn_off_params( def filter_turn_on_params(light: LightEntity, params: dict[str, Any]) -> dict[str, Any]: """Filter out params not supported by the light.""" - supported_features = light.supported_features + supported_features = light.supported_features_compat - if not supported_features & LightEntityFeature.EFFECT: + if LightEntityFeature.EFFECT not in supported_features: params.pop(ATTR_EFFECT, None) - if not supported_features & LightEntityFeature.FLASH: + if LightEntityFeature.FLASH not in supported_features: params.pop(ATTR_FLASH, None) - if not supported_features & LightEntityFeature.TRANSITION: + if LightEntityFeature.TRANSITION not in supported_features: params.pop(ATTR_TRANSITION, None) supported_color_modes = ( @@ -989,7 +989,7 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" data: dict[str, Any] = {} - supported_features = self.supported_features + supported_features = self.supported_features_compat supported_color_modes = self._light_internal_supported_color_modes if ColorMode.COLOR_TEMP in supported_color_modes: @@ -1007,7 +1007,7 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): data[ATTR_MAX_MIREDS] = color_util.color_temperature_kelvin_to_mired( self.min_color_temp_kelvin ) - if supported_features & LightEntityFeature.EFFECT: + if LightEntityFeature.EFFECT in supported_features: data[ATTR_EFFECT_LIST] = self.effect_list data[ATTR_SUPPORTED_COLOR_MODES] = sorted(supported_color_modes) @@ -1061,8 +1061,9 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def state_attributes(self) -> dict[str, Any] | None: """Return state attributes.""" data: dict[str, Any] = {} - supported_features = self.supported_features + supported_features = self.supported_features_compat supported_color_modes = self._light_internal_supported_color_modes + supported_features_value = supported_features.value color_mode = self._light_internal_color_mode if self.is_on else None if color_mode and color_mode not in supported_color_modes: @@ -1081,7 +1082,7 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): data[ATTR_BRIGHTNESS] = self.brightness else: data[ATTR_BRIGHTNESS] = None - elif supported_features & SUPPORT_BRIGHTNESS: + elif supported_features_value & SUPPORT_BRIGHTNESS: # Backwards compatibility for ambiguous / incomplete states # Add warning in 2021.6, remove in 2021.10 if self.is_on: @@ -1103,7 +1104,7 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): else: data[ATTR_COLOR_TEMP_KELVIN] = None data[ATTR_COLOR_TEMP] = None - elif supported_features & SUPPORT_COLOR_TEMP: + elif supported_features_value & SUPPORT_COLOR_TEMP: # Backwards compatibility # Add warning in 2021.6, remove in 2021.10 if self.is_on: @@ -1133,7 +1134,7 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if color_mode: data.update(self._light_internal_convert_color(color_mode)) - if supported_features & LightEntityFeature.EFFECT: + if LightEntityFeature.EFFECT in supported_features: data[ATTR_EFFECT] = self.effect if self.is_on else None return data @@ -1146,14 +1147,15 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): # Backwards compatibility for supported_color_modes added in 2021.4 # Add warning in 2021.6, remove in 2021.10 - supported_features = self.supported_features + supported_features = self.supported_features_compat + supported_features_value = supported_features.value supported_color_modes: set[ColorMode] = set() - if supported_features & SUPPORT_COLOR_TEMP: + if supported_features_value & SUPPORT_COLOR_TEMP: supported_color_modes.add(ColorMode.COLOR_TEMP) - if supported_features & SUPPORT_COLOR: + if supported_features_value & SUPPORT_COLOR: supported_color_modes.add(ColorMode.HS) - if supported_features & SUPPORT_BRIGHTNESS and not supported_color_modes: + if not supported_color_modes and supported_features_value & SUPPORT_BRIGHTNESS: supported_color_modes = {ColorMode.BRIGHTNESS} if not supported_color_modes: @@ -1170,3 +1172,34 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def supported_features(self) -> LightEntityFeature: """Flag supported features.""" return self._attr_supported_features + + @property + def supported_features_compat(self) -> LightEntityFeature: + """Return the supported features as LightEntityFeature. + + Remove this compatibility shim in 2025.1 or later. + """ + features = self.supported_features + if type(features) is not int: # noqa: E721 + return features + new_features = LightEntityFeature(features) + if self._deprecated_supported_features_reported is True: + return new_features + self._deprecated_supported_features_reported = True + report_issue = self._suggest_report_issue() + report_issue += ( + " and reference " + "https://developers.home-assistant.io/blog/2023/12/28/support-feature-magic-numbers-deprecation" + ) + _LOGGER.warning( + ( + "Entity %s (%s) is using deprecated supported features" + " values which will be removed in HA Core 2025.1. Instead it should use" + " %s and color modes, please %s" + ), + self.entity_id, + type(self), + repr(new_features), + report_issue, + ) + return new_features diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 962c5500f06..903002063e8 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -2589,3 +2589,24 @@ def test_filter_supported_color_modes() -> None: # ColorMode.BRIGHTNESS has priority over ColorMode.ONOFF supported = {light.ColorMode.ONOFF, light.ColorMode.BRIGHTNESS} assert light.filter_supported_color_modes(supported) == {light.ColorMode.BRIGHTNESS} + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockLightEntityEntity(light.LightEntity): + @property + def supported_features(self) -> int: + """Return supported features.""" + return 1 + + entity = MockLightEntityEntity() + assert entity.supported_features_compat is light.LightEntityFeature(1) + assert "MockLightEntityEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "LightEntityFeature" in caplog.text + assert "and color modes" in caplog.text + caplog.clear() + assert entity.supported_features_compat is light.LightEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text From bb6f3bc830d663ab02d4a014e8194382fdff238d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 29 Dec 2023 10:04:16 +0100 Subject: [PATCH 051/112] Fix missing await when running shutdown jobs (#106632) --- homeassistant/core.py | 2 +- tests/test_core.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 72287fb81ce..51cb3d4e496 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -889,7 +889,7 @@ class HomeAssistant: continue tasks.append(task_or_none) if tasks: - asyncio.gather(*tasks, return_exceptions=True) + await asyncio.gather(*tasks, return_exceptions=True) except asyncio.TimeoutError: _LOGGER.warning( "Timed out waiting for shutdown jobs to complete, the shutdown will" diff --git a/tests/test_core.py b/tests/test_core.py index 5f5be1b05db..90b87068a5d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2605,6 +2605,9 @@ async def test_shutdown_job(hass: HomeAssistant) -> None: evt = asyncio.Event() async def shutdown_func() -> None: + # Sleep to ensure core is waiting for the task to finish + await asyncio.sleep(0.01) + # Set the event evt.set() job = HassJob(shutdown_func, "shutdown_job") From c54af00ce98d616d1bd9f6cb932071108c03a6fc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Dec 2023 11:29:50 +0100 Subject: [PATCH 052/112] Bump version to 2024.1.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 f2387299576..940a410a778 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Any, Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index baa6814c75b..66e4c973339 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.0b1" +version = "2024.1.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f9150b78b3877debc95de5955b0462494bd293b4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 31 Dec 2023 18:54:34 +0100 Subject: [PATCH 053/112] Ensure it's safe to call Entity.__repr__ on non added entity (#106032) --- homeassistant/components/sensor/__init__.py | 11 ----------- homeassistant/helpers/entity.py | 7 ++++++- tests/helpers/test_entity.py | 15 +++++++++++++-- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 5fca119d5b5..d7c5cddc5db 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -734,17 +734,6 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return value - def __repr__(self) -> str: - """Return the representation. - - Entity.__repr__ includes the state in the generated string, this fails if we're - called before self.hass is set. - """ - if not self.hass: - return f"" - - return super().__repr__() - def _suggested_precision_or_none(self) -> int | None: """Return suggested display precision, or None if not set.""" assert self.registry_entry diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index fc627f51acf..b7ed7e3c095 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1476,7 +1476,12 @@ class Entity( self.async_on_remove(self._async_unsubscribe_device_updates) def __repr__(self) -> str: - """Return the representation.""" + """Return the representation. + + If the entity is not added to a platform it's not safe to call _stringify_state. + """ + if self._platform_state != EntityPlatformState.ADDED: + return f"" return f"" async def async_request_call(self, coro: Coroutine[Any, Any, _T]) -> _T: diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 96bbf95a986..4fca7ed4c23 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1392,8 +1392,8 @@ async def test_translation_key(hass: HomeAssistant) -> None: assert mock_entity2.translation_key == "from_entity_description" -async def test_repr_using_stringify_state() -> None: - """Test that repr uses stringify state.""" +async def test_repr(hass) -> None: + """Test Entity.__repr__.""" class MyEntity(MockEntity): """Mock entity.""" @@ -1403,9 +1403,20 @@ async def test_repr_using_stringify_state() -> None: """Return the state.""" raise ValueError("Boom") + platform = MockEntityPlatform(hass, domain="hello") my_entity = MyEntity(entity_id="test.test", available=False) + + # Not yet added + assert str(my_entity) == "" + + # Added + await platform.async_add_entities([my_entity]) assert str(my_entity) == "" + # Removed + await platform.async_remove_entity(my_entity.entity_id) + assert str(my_entity) == "" + async def test_warn_using_async_update_ha_state( hass: HomeAssistant, caplog: pytest.LogCaptureFixture From 3cd5f0568ac110e475631ec3e22668bec743f8f7 Mon Sep 17 00:00:00 2001 From: Jirka Date: Fri, 29 Dec 2023 12:01:23 +0100 Subject: [PATCH 054/112] Fix typo in Blink strings (#106641) Update strings.json Fixed typo. --- homeassistant/components/blink/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index 87e2fc68c20..a875fb3e343 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -106,7 +106,7 @@ }, "exceptions": { "integration_not_found": { - "message": "Integraion '{target}' not found in registry" + "message": "Integration '{target}' not found in registry" }, "no_path": { "message": "Can't write to directory {target}, no access to path!" From c1e37a4cc3125e24ea92fccbe698cee29018d1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Hansl=C3=ADk?= Date: Fri, 29 Dec 2023 18:37:46 +0100 Subject: [PATCH 055/112] Fixed native apparent temperature in WeatherEntity (#106645) --- homeassistant/components/weather/__init__.py | 2 +- tests/components/smhi/snapshots/test_weather.ambr | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index fa832ca8c32..bdc8ae4d514 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -430,7 +430,7 @@ class WeatherEntity(Entity, PostInit, cached_properties=CACHED_PROPERTIES_WITH_A @cached_property def native_apparent_temperature(self) -> float | None: """Return the apparent temperature in native units.""" - return self._attr_native_temperature + return self._attr_native_apparent_temperature @cached_property def native_temperature(self) -> float | None: diff --git a/tests/components/smhi/snapshots/test_weather.ambr b/tests/components/smhi/snapshots/test_weather.ambr index fa9d76c68ba..eb7378b5cba 100644 --- a/tests/components/smhi/snapshots/test_weather.ambr +++ b/tests/components/smhi/snapshots/test_weather.ambr @@ -669,7 +669,6 @@ # --- # name: test_setup_hass ReadOnlyDict({ - 'apparent_temperature': 18.0, 'attribution': 'Swedish weather institute (SMHI)', 'cloud_coverage': 100, 'forecast': list([ From 5f3389b8e41138b4eb42aaa003471af8f59a897e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 29 Dec 2023 13:22:06 +0100 Subject: [PATCH 056/112] Fix yolink entity descriptions (#106649) --- homeassistant/components/yolink/sensor.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 4ac9379d763..ace13353341 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -48,21 +48,13 @@ from .coordinator import YoLinkCoordinator from .entity import YoLinkEntity -@dataclass(frozen=True) -class YoLinkSensorEntityDescriptionMixin: - """Mixin for device type.""" - - exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True - - -@dataclass(frozen=True) -class YoLinkSensorEntityDescription( - YoLinkSensorEntityDescriptionMixin, SensorEntityDescription -): +@dataclass(frozen=True, kw_only=True) +class YoLinkSensorEntityDescription(SensorEntityDescription): """YoLink SensorEntityDescription.""" - value: Callable = lambda state: state + exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True should_update_entity: Callable = lambda state: True + value: Callable = lambda state: state SENSOR_DEVICE_TYPE = [ From 767c55fbac207399603e94a890d5c5bf0b84feee Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 29 Dec 2023 13:21:08 +0100 Subject: [PATCH 057/112] Use set instead of list in Systemmonitor (#106650) --- .../components/systemmonitor/config_flow.py | 2 +- .../components/systemmonitor/sensor.py | 14 +++++++------- homeassistant/components/systemmonitor/util.py | 18 +++++++++--------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/systemmonitor/config_flow.py b/homeassistant/components/systemmonitor/config_flow.py index 3dc45480aee..6d9787a39f5 100644 --- a/homeassistant/components/systemmonitor/config_flow.py +++ b/homeassistant/components/systemmonitor/config_flow.py @@ -86,7 +86,7 @@ async def validate_import_sensor_setup( async def get_sensor_setup_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: """Return process sensor setup schema.""" hass = handler.parent_handler.hass - processes = await hass.async_add_executor_job(get_all_running_processes) + processes = list(await hass.async_add_executor_job(get_all_running_processes)) return vol.Schema( { vol.Required(CONF_PROCESS): SelectSelector( diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 2bc1406308c..28929d07a7c 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -267,7 +267,7 @@ def check_required_arg(value: Any) -> Any: return value -def check_legacy_resource(resource: str, resources: list[str]) -> bool: +def check_legacy_resource(resource: str, resources: set[str]) -> bool: """Return True if legacy resource was configured.""" # This function to check legacy resources can be removed # once we are removing the import from YAML @@ -388,8 +388,8 @@ async def async_setup_entry( """Set up System Montor sensors based on a config entry.""" entities = [] sensor_registry: dict[tuple[str, str], SensorData] = {} - legacy_resources: list[str] = entry.options.get("resources", []) - loaded_resources: list[str] = [] + legacy_resources: set[str] = set(entry.options.get("resources", [])) + loaded_resources: set[str] = set() disk_arguments = await hass.async_add_executor_job(get_all_disk_mounts) network_arguments = await hass.async_add_executor_job(get_all_network_interfaces) cpu_temperature = await hass.async_add_executor_job(_read_cpu_temperature) @@ -405,7 +405,7 @@ async def async_setup_entry( is_enabled = check_legacy_resource( f"{_type}_{argument}", legacy_resources ) - loaded_resources.append(f"{_type}_{argument}") + loaded_resources.add(f"{_type}_{argument}") entities.append( SystemMonitorSensor( sensor_registry, @@ -425,7 +425,7 @@ async def async_setup_entry( is_enabled = check_legacy_resource( f"{_type}_{argument}", legacy_resources ) - loaded_resources.append(f"{_type}_{argument}") + loaded_resources.add(f"{_type}_{argument}") entities.append( SystemMonitorSensor( sensor_registry, @@ -449,7 +449,7 @@ async def async_setup_entry( sensor_registry[(_type, argument)] = SensorData( argument, None, None, None, None ) - loaded_resources.append(f"{_type}_{argument}") + loaded_resources.add(f"{_type}_{argument}") entities.append( SystemMonitorSensor( sensor_registry, @@ -463,7 +463,7 @@ async def async_setup_entry( sensor_registry[(_type, "")] = SensorData("", None, None, None, None) is_enabled = check_legacy_resource(f"{_type}_", legacy_resources) - loaded_resources.append(f"{_type}_") + loaded_resources.add(f"{_type}_") entities.append( SystemMonitorSensor( sensor_registry, diff --git a/homeassistant/components/systemmonitor/util.py b/homeassistant/components/systemmonitor/util.py index 27c4c449634..2baacb9d16f 100644 --- a/homeassistant/components/systemmonitor/util.py +++ b/homeassistant/components/systemmonitor/util.py @@ -8,9 +8,9 @@ import psutil _LOGGER = logging.getLogger(__name__) -def get_all_disk_mounts() -> list[str]: +def get_all_disk_mounts() -> set[str]: """Return all disk mount points on system.""" - disks: list[str] = [] + disks: set[str] = set() for part in psutil.disk_partitions(all=True): if os.name == "nt": if "cdrom" in part.opts or part.fstype == "": @@ -20,25 +20,25 @@ def get_all_disk_mounts() -> list[str]: continue usage = psutil.disk_usage(part.mountpoint) if usage.total > 0 and part.device != "": - disks.append(part.mountpoint) + disks.add(part.mountpoint) _LOGGER.debug("Adding disks: %s", ", ".join(disks)) return disks -def get_all_network_interfaces() -> list[str]: +def get_all_network_interfaces() -> set[str]: """Return all network interfaces on system.""" - interfaces: list[str] = [] + interfaces: set[str] = set() for interface, _ in psutil.net_if_addrs().items(): - interfaces.append(interface) + interfaces.add(interface) _LOGGER.debug("Adding interfaces: %s", ", ".join(interfaces)) return interfaces -def get_all_running_processes() -> list[str]: +def get_all_running_processes() -> set[str]: """Return all running processes on system.""" - processes: list[str] = [] + processes: set[str] = set() for proc in psutil.process_iter(["name"]): if proc.name() not in processes: - processes.append(proc.name()) + processes.add(proc.name()) _LOGGER.debug("Running processes: %s", ", ".join(processes)) return processes From 494dd2ef0767ef93d1ad22f8640dfe8589cfadb4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 29 Dec 2023 13:21:36 +0100 Subject: [PATCH 058/112] Handle no permission for disks in Systemmonitor (#106653) --- homeassistant/components/systemmonitor/util.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/systemmonitor/util.py b/homeassistant/components/systemmonitor/util.py index 2baacb9d16f..25b8aa2eb1d 100644 --- a/homeassistant/components/systemmonitor/util.py +++ b/homeassistant/components/systemmonitor/util.py @@ -18,7 +18,13 @@ def get_all_disk_mounts() -> set[str]: # ENOENT, pop-up a Windows GUI error for a non-ready # partition or just hang. continue - usage = psutil.disk_usage(part.mountpoint) + try: + usage = psutil.disk_usage(part.mountpoint) + except PermissionError: + _LOGGER.debug( + "No permission for running user to access %s", part.mountpoint + ) + continue if usage.total > 0 and part.device != "": disks.add(part.mountpoint) _LOGGER.debug("Adding disks: %s", ", ".join(disks)) From 362e5ca09a63134288bcd633c39633834e20a1a8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 30 Dec 2023 08:34:21 +0100 Subject: [PATCH 059/112] Fix changed_variables in automation traces (#106665) --- homeassistant/helpers/script.py | 19 +++++++++---- homeassistant/helpers/trace.py | 26 ++++++++++------- tests/helpers/test_script.py | 49 ++++++++++++++------------------- 3 files changed, 50 insertions(+), 44 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index a1d045eb542..07f10e13dbf 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Mapping, Sequence +from collections.abc import AsyncGenerator, Callable, Mapping, Sequence from contextlib import asynccontextmanager, suppress from contextvars import ContextVar from copy import copy @@ -157,7 +157,12 @@ def action_trace_append(variables, path): @asynccontextmanager -async def trace_action(hass, script_run, stop, variables): +async def trace_action( + hass: HomeAssistant, + script_run: _ScriptRun, + stop: asyncio.Event, + variables: dict[str, Any], +) -> AsyncGenerator[TraceElement, None]: """Trace action execution.""" path = trace_path_get() trace_element = action_trace_append(variables, path) @@ -362,6 +367,8 @@ class _StopScript(_HaltScript): class _ScriptRun: """Manage Script sequence run.""" + _action: dict[str, Any] + def __init__( self, hass: HomeAssistant, @@ -376,7 +383,6 @@ class _ScriptRun: self._context = context self._log_exceptions = log_exceptions self._step = -1 - self._action: dict[str, Any] | None = None self._stop = asyncio.Event() self._stopped = asyncio.Event() @@ -446,11 +452,13 @@ class _ScriptRun: return ScriptRunResult(response, self._variables) - async def _async_step(self, log_exceptions): + async def _async_step(self, log_exceptions: bool) -> None: continue_on_error = self._action.get(CONF_CONTINUE_ON_ERROR, False) with trace_path(str(self._step)): - async with trace_action(self._hass, self, self._stop, self._variables): + async with trace_action( + self._hass, self, self._stop, self._variables + ) as trace_element: if self._stop.is_set(): return @@ -466,6 +474,7 @@ class _ScriptRun: try: handler = f"_async_{action}_step" await getattr(self, handler)() + trace_element.update_variables(self._variables) except Exception as ex: # pylint: disable=broad-except self._handle_exception( ex, continue_on_error, self._log_exceptions or log_exceptions diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index fd7a3081f7a..6c7d6cf0a7a 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -21,6 +21,7 @@ class TraceElement: "_child_key", "_child_run_id", "_error", + "_last_variables", "path", "_result", "reuse_by_child", @@ -38,16 +39,8 @@ class TraceElement: self.reuse_by_child = False self._timestamp = dt_util.utcnow() - if variables is None: - variables = {} - last_variables = variables_cv.get() or {} - variables_cv.set(dict(variables)) - changed_variables = { - key: value - for key, value in variables.items() - if key not in last_variables or last_variables[key] != value - } - self._variables = changed_variables + self._last_variables = variables_cv.get() or {} + self.update_variables(variables) def __repr__(self) -> str: """Container for trace data.""" @@ -71,6 +64,19 @@ class TraceElement: old_result = self._result or {} self._result = {**old_result, **kwargs} + def update_variables(self, variables: TemplateVarsType) -> None: + """Update variables.""" + if variables is None: + variables = {} + last_variables = self._last_variables + variables_cv.set(dict(variables)) + changed_variables = { + key: value + for key, value in variables.items() + if key not in last_variables or last_variables[key] != value + } + self._variables = changed_variables + def as_dict(self) -> dict[str, Any]: """Return dictionary version of this TraceElement.""" result: dict[str, Any] = {"path": self.path, "timestamp": self._timestamp} diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index c2bad6287ab..1ea602f7cda 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -386,7 +386,10 @@ async def test_calling_service_response_data( "target": {}, }, "running_script": False, - } + }, + "variables": { + "my_response": {"data": "value-12345"}, + }, } ], "1": [ @@ -399,10 +402,7 @@ async def test_calling_service_response_data( "target": {}, }, "running_script": False, - }, - "variables": { - "my_response": {"data": "value-12345"}, - }, + } } ], } @@ -1163,13 +1163,13 @@ async def test_wait_template_not_schedule(hass: HomeAssistant) -> None: assert_action_trace( { "0": [{"result": {"event": "test_event", "event_data": {}}}], - "1": [{"result": {"wait": {"completed": True, "remaining": None}}}], - "2": [ + "1": [ { - "result": {"event": "test_event", "event_data": {}}, + "result": {"wait": {"completed": True, "remaining": None}}, "variables": {"wait": {"completed": True, "remaining": None}}, } ], + "2": [{"result": {"event": "test_event", "event_data": {}}}], } ) @@ -1230,13 +1230,13 @@ async def test_wait_timeout( else: variable_wait = {"wait": {"trigger": None, "remaining": 0.0}} expected_trace = { - "0": [{"result": variable_wait}], - "1": [ + "0": [ { - "result": {"event": "test_event", "event_data": {}}, + "result": variable_wait, "variables": variable_wait, } ], + "1": [{"result": {"event": "test_event", "event_data": {}}}], } assert_action_trace(expected_trace) @@ -1291,19 +1291,14 @@ async def test_wait_continue_on_timeout( else: variable_wait = {"wait": {"trigger": None, "remaining": 0.0}} expected_trace = { - "0": [{"result": variable_wait}], + "0": [{"result": variable_wait, "variables": variable_wait}], } if continue_on_timeout is False: expected_trace["0"][0]["result"]["timeout"] = True expected_trace["0"][0]["error_type"] = asyncio.TimeoutError expected_script_execution = "aborted" else: - expected_trace["1"] = [ - { - "result": {"event": "test_event", "event_data": {}}, - "variables": variable_wait, - } - ] + expected_trace["1"] = [{"result": {"event": "test_event", "event_data": {}}}] expected_script_execution = "finished" assert_action_trace(expected_trace, expected_script_execution) @@ -3269,12 +3264,12 @@ async def test_parallel(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - "description": "state of switch.trigger", }, } - } + }, + "variables": {"wait": {"remaining": None}}, } ], "0/parallel/1/sequence/0": [ { - "variables": {}, "result": { "event": "test_event", "event_data": {"hello": "from action 2", "what": "world"}, @@ -3283,7 +3278,6 @@ async def test_parallel(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - ], "0/parallel/0/sequence/1": [ { - "variables": {"wait": {"remaining": None}}, "result": { "event": "test_event", "event_data": {"hello": "from action 1", "what": "world"}, @@ -4462,7 +4456,7 @@ async def test_set_variable( assert f"Executing step {alias}" in caplog.text expected_trace = { - "0": [{}], + "0": [{"variables": {"variable": "value"}}], "1": [ { "result": { @@ -4474,7 +4468,6 @@ async def test_set_variable( }, "running_script": False, }, - "variables": {"variable": "value"}, } ], } @@ -4504,7 +4497,7 @@ async def test_set_redefines_variable( assert mock_calls[1].data["value"] == 2 expected_trace = { - "0": [{}], + "0": [{"variables": {"variable": "1"}}], "1": [ { "result": { @@ -4515,11 +4508,10 @@ async def test_set_redefines_variable( "target": {}, }, "running_script": False, - }, - "variables": {"variable": "1"}, + } } ], - "2": [{}], + "2": [{"variables": {"variable": 2}}], "3": [ { "result": { @@ -4530,8 +4522,7 @@ async def test_set_redefines_variable( "target": {}, }, "running_script": False, - }, - "variables": {"variable": 2}, + } } ], } From 84da1638e85c7a37333b4765a7661e8de750e08c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Dec 2023 23:33:59 -1000 Subject: [PATCH 060/112] Bump thermobeacon-ble to 0.6.2 (#106676) changelog: https://github.com/Bluetooth-Devices/thermobeacon-ble/compare/v0.6.0...v0.6.2 --- homeassistant/components/thermobeacon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/thermobeacon/manifest.json b/homeassistant/components/thermobeacon/manifest.json index 772c565e9d2..29443acaa3d 100644 --- a/homeassistant/components/thermobeacon/manifest.json +++ b/homeassistant/components/thermobeacon/manifest.json @@ -42,5 +42,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/thermobeacon", "iot_class": "local_push", - "requirements": ["thermobeacon-ble==0.6.0"] + "requirements": ["thermobeacon-ble==0.6.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5e22f7a0815..f1df817bc7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2635,7 +2635,7 @@ tessie-api==0.0.9 # tf-models-official==2.5.0 # homeassistant.components.thermobeacon -thermobeacon-ble==0.6.0 +thermobeacon-ble==0.6.2 # homeassistant.components.thermopro thermopro-ble==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4360fc394c7..4c57d9cfd9f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1982,7 +1982,7 @@ tesla-wall-connector==1.0.2 tessie-api==0.0.9 # homeassistant.components.thermobeacon -thermobeacon-ble==0.6.0 +thermobeacon-ble==0.6.2 # homeassistant.components.thermopro thermopro-ble==0.5.0 From 3dd998b622665f1ea605a6c1536a31336239c4c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Dec 2023 13:45:27 -1000 Subject: [PATCH 061/112] Bump roombapy to 1.6.10 (#106678) changelog: https://github.com/pschmitt/roombapy/compare/1.6.8...1.6.10 fixes #105323 --- homeassistant/components/roomba/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 8e6b92732eb..fbe6c925438 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -24,7 +24,7 @@ "documentation": "https://www.home-assistant.io/integrations/roomba", "iot_class": "local_push", "loggers": ["paho_mqtt", "roombapy"], - "requirements": ["roombapy==1.6.8"], + "requirements": ["roombapy==1.6.10"], "zeroconf": [ { "type": "_amzn-alexa._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index f1df817bc7f..21226355623 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2400,7 +2400,7 @@ rocketchat-API==0.6.1 rokuecp==0.18.1 # homeassistant.components.roomba -roombapy==1.6.8 +roombapy==1.6.10 # homeassistant.components.roon roonapi==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c57d9cfd9f..7906924b1cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1807,7 +1807,7 @@ ring-doorbell[listen]==0.8.5 rokuecp==0.18.1 # homeassistant.components.roomba -roombapy==1.6.8 +roombapy==1.6.10 # homeassistant.components.roon roonapi==0.1.6 From 8dfbe6849e0dcfb4566d5352757af3ed8d10ecfd Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Fri, 29 Dec 2023 19:45:04 -0500 Subject: [PATCH 062/112] Bump asyncsleepiq to v1.4.1 (#106682) Update asyncsleepiq to v1.4.1 --- homeassistant/components/sleepiq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index d58c20b14b8..62bd3930c77 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/sleepiq", "iot_class": "cloud_polling", "loggers": ["asyncsleepiq"], - "requirements": ["asyncsleepiq==1.4.0"] + "requirements": ["asyncsleepiq==1.4.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 21226355623..13d2597420f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -478,7 +478,7 @@ asyncinotify==4.0.2 asyncpysupla==0.0.5 # homeassistant.components.sleepiq -asyncsleepiq==1.4.0 +asyncsleepiq==1.4.1 # homeassistant.components.aten_pe # atenpdu==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7906924b1cc..4a6ba2b0d1f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -424,7 +424,7 @@ arcam-fmj==1.4.0 async-upnp-client==0.38.0 # homeassistant.components.sleepiq -asyncsleepiq==1.4.0 +asyncsleepiq==1.4.1 # homeassistant.components.aurora auroranoaa==0.0.3 From 456cb20fcd0bda094552a7b1b5f16671de4f9613 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Dec 2023 14:12:19 -1000 Subject: [PATCH 063/112] Fix missed cached_property for hvac_mode in climate (#106692) --- homeassistant/components/climate/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 19e26265f70..78cb92944cb 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -227,6 +227,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = { "temperature_unit", "current_humidity", "target_humidity", + "hvac_mode", "hvac_modes", "hvac_action", "current_temperature", @@ -414,7 +415,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Return the humidity we try to reach.""" return self._attr_target_humidity - @property + @cached_property def hvac_mode(self) -> HVACMode | None: """Return hvac operation ie. heat, cool mode.""" return self._attr_hvac_mode From 2255f6737c4a14a42351bca6f9b5f06770a8c580 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Dec 2023 00:29:19 -1000 Subject: [PATCH 064/112] Pin lxml to 4.9.4 (#106694) --- homeassistant/components/scrape/manifest.json | 2 +- homeassistant/package_constraints.txt | 4 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 4 ++++ 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 26603603198..708ecc14d16 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -6,5 +6,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/scrape", "iot_class": "cloud_polling", - "requirements": ["beautifulsoup4==4.12.2", "lxml==4.9.3"] + "requirements": ["beautifulsoup4==4.12.2", "lxml==4.9.4"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a6c59c98dc0..a3d84b8ce08 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -185,3 +185,7 @@ get-mac==1000000000.0.0 # They are build with mypyc, but causes issues with our wheel builder. # In order to do so, we need to constrain the version. charset-normalizer==3.2.0 + +# lxml 5.0.0 currently does not build on alpine 3.18 +# https://bugs.launchpad.net/lxml/+bug/2047718 +lxml==4.9.4 diff --git a/requirements_all.txt b/requirements_all.txt index 13d2597420f..b1ffc9d5e40 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1225,7 +1225,7 @@ lupupy==0.3.1 lw12==0.9.2 # homeassistant.components.scrape -lxml==4.9.3 +lxml==4.9.4 # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a6ba2b0d1f..c32d93814d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -958,7 +958,7 @@ loqedAPI==2.1.8 luftdaten==0.7.4 # homeassistant.components.scrape -lxml==4.9.3 +lxml==4.9.4 # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.12 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index bcd19b97e08..101c9294706 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -179,6 +179,10 @@ get-mac==1000000000.0.0 # They are build with mypyc, but causes issues with our wheel builder. # In order to do so, we need to constrain the version. charset-normalizer==3.2.0 + +# lxml 5.0.0 currently does not build on alpine 3.18 +# https://bugs.launchpad.net/lxml/+bug/2047718 +lxml==4.9.4 """ GENERATED_MESSAGE = ( From 2179d4de3d0c6d979fdb5e10e8f4d48d756ef137 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 30 Dec 2023 23:16:06 +0100 Subject: [PATCH 065/112] Add missing vacuum toggle service description (#106729) --- homeassistant/components/vacuum/services.yaml | 8 ++++++++ homeassistant/components/vacuum/strings.json | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index aab35b42077..25f3822bd35 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -14,6 +14,14 @@ turn_off: supported_features: - vacuum.VacuumEntityFeature.TURN_OFF +toggle: + target: + entity: + domain: vacuum + supported_features: + - vacuum.VacuumEntityFeature.TURN_OFF + - vacuum.VacuumEntityFeature.TURN_ON + stop: target: entity: diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 3c018fc1a89..15ba2076060 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -48,6 +48,10 @@ "name": "[%key:common::action::turn_off%]", "description": "Stops the current cleaning task and returns to its dock." }, + "toggle": { + "name": "[%key:common::action::toggle%]", + "description": "Toggles the vacuum cleaner on/off." + }, "stop": { "name": "[%key:common::action::stop%]", "description": "Stops the current cleaning task." From 3d75603b4f6cfbf06252a1bcfbe955e514f49823 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 31 Dec 2023 12:12:06 -0500 Subject: [PATCH 066/112] Fix Zlinky energy polling in ZHA (#106738) --- .../components/zha/core/cluster_handlers/smartenergy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/core/cluster_handlers/smartenergy.py b/homeassistant/components/zha/core/cluster_handlers/smartenergy.py index 8fd38425dff..2ceaeaf1013 100644 --- a/homeassistant/components/zha/core/cluster_handlers/smartenergy.py +++ b/homeassistant/components/zha/core/cluster_handlers/smartenergy.py @@ -195,9 +195,9 @@ class Metering(ClusterHandler): ) # 1 digit to the right, 15 digits to the left self._summa_format = self.get_formatting(fmting) - async def async_force_update(self) -> None: + async def async_update(self) -> None: """Retrieve latest state.""" - self.debug("async_force_update") + self.debug("async_update") attrs = [ a["attr"] From 05768f5fbd380d184277ce773b048aff4f4a2c15 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 31 Dec 2023 00:45:05 +0100 Subject: [PATCH 067/112] Bump reolink_aio to 0.8.5 (#106747) --- homeassistant/components/reolink/manifest.json | 2 +- homeassistant/components/reolink/media_source.py | 11 ++++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index e687fc5d9b1..d5116af0071 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.8.4"] + "requirements": ["reolink-aio==0.8.5"] } diff --git a/homeassistant/components/reolink/media_source.py b/homeassistant/components/reolink/media_source.py index 6a350e13836..2a1eee9e97d 100644 --- a/homeassistant/components/reolink/media_source.py +++ b/homeassistant/components/reolink/media_source.py @@ -5,6 +5,8 @@ from __future__ import annotations import datetime as dt import logging +from reolink_aio.enums import VodRequestType + from homeassistant.components.camera import DOMAIN as CAM_DOMAIN, DynamicStreamSettings from homeassistant.components.media_player import MediaClass, MediaType from homeassistant.components.media_source.error import Unresolvable @@ -56,7 +58,14 @@ class ReolinkVODMediaSource(MediaSource): channel = int(channel_str) host = self.data[config_entry_id].host - mime_type, url = await host.api.get_vod_source(channel, filename, stream_res) + + vod_type = VodRequestType.RTMP + if host.api.is_nvr: + vod_type = VodRequestType.FLV + + mime_type, url = await host.api.get_vod_source( + channel, filename, stream_res, vod_type + ) if _LOGGER.isEnabledFor(logging.DEBUG): url_log = f"{url.split('&user=')[0]}&user=xxxxx&password=xxxxx" _LOGGER.debug( diff --git a/requirements_all.txt b/requirements_all.txt index b1ffc9d5e40..839fddddebd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2376,7 +2376,7 @@ renault-api==0.2.1 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.8.4 +reolink-aio==0.8.5 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c32d93814d7..b7d7650691d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1795,7 +1795,7 @@ renault-api==0.2.1 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.8.4 +reolink-aio==0.8.5 # homeassistant.components.rflink rflink==0.0.65 From a11fd2aaa665b186d80ed8503bdbd890c1036621 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Dec 2023 22:44:12 -1000 Subject: [PATCH 068/112] Bump pyunifiprotect to 4.22.4 (#106749) changelog: https://github.com/AngellusMortis/pyunifiprotect/compare/v4.22.3...v4.22.4 --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index cd38f50bf6d..c74097c3c17 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.22.3", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.22.4", "unifi-discovery==1.1.7"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 839fddddebd..ed18f3659f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2280,7 +2280,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.22.3 +pyunifiprotect==4.22.4 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7d7650691d..77abafcfc66 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1723,7 +1723,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.22.3 +pyunifiprotect==4.22.4 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 3dca39d0f9c4cadb880039409f9bf9a847df1854 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Dec 2023 06:44:55 -1000 Subject: [PATCH 069/112] Bump habluetooth to 2.0.1 (#106750) fixes switching scanners to quickly since the manager failed to account for jitter in the auto discovered advertising interval replaces and closes #96531 changelog: https://github.com/Bluetooth-Devices/habluetooth/compare/v2.0.0...v2.0.1 --- .../components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/test_manager.py | 86 ++++++++++++++++++- 5 files changed, 89 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 33404a762b9..19199e4b1c6 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,6 +20,6 @@ "bluetooth-auto-recovery==1.2.3", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.0", - "habluetooth==2.0.0" + "habluetooth==2.0.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a3d84b8ce08..842bff950fc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -24,7 +24,7 @@ dbus-fast==2.21.0 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.1.0 -habluetooth==2.0.0 +habluetooth==2.0.1 hass-nabucasa==0.75.1 hassil==1.5.1 home-assistant-bluetooth==1.11.0 diff --git a/requirements_all.txt b/requirements_all.txt index ed18f3659f3..f9111763365 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -998,7 +998,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.0.0 +habluetooth==2.0.1 # homeassistant.components.cloud hass-nabucasa==0.75.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77abafcfc66..73f636b5692 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -800,7 +800,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.0.0 +habluetooth==2.0.1 # homeassistant.components.cloud hass-nabucasa==0.75.1 diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index 212f45bb5f0..4726c12f681 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -7,11 +7,12 @@ from unittest.mock import patch from bleak.backends.scanner import AdvertisementData, BLEDevice from bluetooth_adapters import AdvertisementHistory -from habluetooth.manager import FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS +from habluetooth.advertisement_tracker import TRACKER_BUFFERING_WOBBLE_SECONDS import pytest from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, MONOTONIC_TIME, BaseHaRemoteScanner, BluetoothChange, @@ -315,6 +316,89 @@ async def test_switching_adapters_based_on_stale( ) +async def test_switching_adapters_based_on_stale_with_discovered_interval( + hass: HomeAssistant, + enable_bluetooth: None, + register_hci0_scanner: None, + register_hci1_scanner: None, +) -> None: + """Test switching with discovered interval.""" + + address = "44:44:33:11:23:41" + start_time_monotonic = 50.0 + + switchbot_device_poor_signal_hci0 = generate_ble_device( + address, "wohand_poor_signal_hci0" + ) + switchbot_adv_poor_signal_hci0 = generate_advertisement_data( + local_name="wohand_poor_signal_hci0", service_uuids=[], rssi=-100 + ) + inject_advertisement_with_time_and_source( + hass, + switchbot_device_poor_signal_hci0, + switchbot_adv_poor_signal_hci0, + start_time_monotonic, + "hci0", + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal_hci0 + ) + + bluetooth.async_set_fallback_availability_interval(hass, address, 10) + + switchbot_device_poor_signal_hci1 = generate_ble_device( + address, "wohand_poor_signal_hci1" + ) + switchbot_adv_poor_signal_hci1 = generate_advertisement_data( + local_name="wohand_poor_signal_hci1", service_uuids=[], rssi=-99 + ) + inject_advertisement_with_time_and_source( + hass, + switchbot_device_poor_signal_hci1, + switchbot_adv_poor_signal_hci1, + start_time_monotonic, + "hci1", + ) + + # Should not switch adapters until the advertisement is stale + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal_hci0 + ) + + inject_advertisement_with_time_and_source( + hass, + switchbot_device_poor_signal_hci1, + switchbot_adv_poor_signal_hci1, + start_time_monotonic + 10 + 1, + "hci1", + ) + + # Should not switch yet since we are not within the + # wobble period + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal_hci0 + ) + + inject_advertisement_with_time_and_source( + hass, + switchbot_device_poor_signal_hci1, + switchbot_adv_poor_signal_hci1, + start_time_monotonic + 10 + TRACKER_BUFFERING_WOBBLE_SECONDS + 1, + "hci1", + ) + # Should switch to hci1 since the previous advertisement is stale + # even though the signal is poor because the device is now + # likely unreachable via hci0 + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal_hci1 + ) + + async def test_restore_history_from_dbus( hass: HomeAssistant, one_adapter: None, disable_new_discovery_flows ) -> None: From 99d575261d260a03d4d1e2409cdbc84d627bcc79 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 31 Dec 2023 04:54:09 -0500 Subject: [PATCH 070/112] Bump ZHA dependencies (#106756) * Bump ZHA dependencies * Revert "Remove bellows thread, as it has been removed upstream" This reverts commit c28053f4bf2539eb6150d35af19687610aaeac5e. --- homeassistant/components/zha/core/const.py | 1 + homeassistant/components/zha/core/gateway.py | 10 +++++ homeassistant/components/zha/manifest.json | 6 +-- homeassistant/components/zha/radio_manager.py | 2 + requirements_all.txt | 6 +-- requirements_test_all.txt | 6 +-- tests/components/zha/test_gateway.py | 45 ++++++++++++++++++- 7 files changed, 66 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 7e591a596e5..ecbd347a621 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -139,6 +139,7 @@ CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" CONF_ENABLE_QUIRKS = "enable_quirks" CONF_RADIO_TYPE = "radio_type" CONF_USB_PATH = "usb_path" +CONF_USE_THREAD = "use_thread" CONF_ZIGPY = "zigpy_config" CONF_CONSIDER_UNAVAILABLE_MAINS = "consider_unavailable_mains" diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 1308abb3d37..12e439f1059 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -46,6 +46,7 @@ from .const import ( ATTR_SIGNATURE, ATTR_TYPE, CONF_RADIO_TYPE, + CONF_USE_THREAD, CONF_ZIGPY, DATA_ZHA, DEBUG_COMP_BELLOWS, @@ -158,6 +159,15 @@ class ZHAGateway: if CONF_NWK_VALIDATE_SETTINGS not in app_config: app_config[CONF_NWK_VALIDATE_SETTINGS] = True + # The bellows UART thread sometimes propagates a cancellation into the main Core + # event loop, when a connection to a TCP coordinator fails in a specific way + if ( + CONF_USE_THREAD not in app_config + and radio_type is RadioType.ezsp + and app_config[CONF_DEVICE][CONF_DEVICE_PATH].startswith("socket://") + ): + app_config[CONF_USE_THREAD] = False + # Local import to avoid circular dependencies # pylint: disable-next=import-outside-toplevel from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 6a14a3064a6..db5939123e4 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,12 +21,12 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.37.4", + "bellows==0.37.6", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.109", - "zigpy-deconz==0.22.3", - "zigpy==0.60.2", + "zigpy-deconz==0.22.4", + "zigpy==0.60.3", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 92a90e0e13a..d3ca03de8d8 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -10,6 +10,7 @@ import logging import os from typing import Any, Self +from bellows.config import CONF_USE_THREAD import voluptuous as vol from zigpy.application import ControllerApplication import zigpy.backups @@ -174,6 +175,7 @@ class ZhaRadioManager: app_config[CONF_DATABASE] = database_path app_config[CONF_DEVICE] = self.device_settings app_config[CONF_NWK_BACKUP_ENABLED] = False + app_config[CONF_USE_THREAD] = False app_config = self.radio_type.controller.SCHEMA(app_config) app = await self.radio_type.controller.new( diff --git a/requirements_all.txt b/requirements_all.txt index f9111763365..b4d0c6f304f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -526,7 +526,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.37.4 +bellows==0.37.6 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -2875,7 +2875,7 @@ zhong-hong-hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.22.3 +zigpy-deconz==0.22.4 # homeassistant.components.zha zigpy-xbee==0.20.1 @@ -2887,7 +2887,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.2 +zigpy==0.60.3 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73f636b5692..8d7c57932d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -448,7 +448,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.37.4 +bellows==0.37.6 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -2171,7 +2171,7 @@ zeversolar==0.3.1 zha-quirks==0.0.109 # homeassistant.components.zha -zigpy-deconz==0.22.3 +zigpy-deconz==0.22.4 # homeassistant.components.zha zigpy-xbee==0.20.1 @@ -2183,7 +2183,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.2 +zigpy==0.60.3 # homeassistant.components.zwave_js zwave-js-server-python==0.55.2 diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 1d9042daa4a..4f520920704 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,8 +1,9 @@ """Test ZHA Gateway.""" import asyncio -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest +from zigpy.application import ControllerApplication import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting @@ -222,6 +223,48 @@ async def test_gateway_create_group_with_id( assert zha_group.group_id == 0x1234 +@patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_devices", + MagicMock(), +) +@patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_groups", + MagicMock(), +) +@pytest.mark.parametrize( + ("device_path", "thread_state", "config_override"), + [ + ("/dev/ttyUSB0", True, {}), + ("socket://192.168.1.123:9999", False, {}), + ("socket://192.168.1.123:9999", True, {"use_thread": True}), + ], +) +async def test_gateway_initialize_bellows_thread( + device_path: str, + thread_state: bool, + config_override: dict, + hass: HomeAssistant, + zigpy_app_controller: ControllerApplication, + config_entry: MockConfigEntry, +) -> None: + """Test ZHA disabling the UART thread when connecting to a TCP coordinator.""" + config_entry.data = dict(config_entry.data) + config_entry.data["device"]["path"] = device_path + config_entry.add_to_hass(hass) + + zha_gateway = ZHAGateway(hass, {"zigpy_config": config_override}, config_entry) + + with patch( + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ) as mock_new: + await zha_gateway.async_initialize() + + mock_new.mock_calls[-1].kwargs["config"]["use_thread"] is thread_state + + await zha_gateway.shutdown() + + @pytest.mark.parametrize( ("device_path", "config_override", "expected_channel"), [ From c06df1957fe0c88ceba85c3be9ef57e392ea2a34 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 31 Dec 2023 10:04:42 +0100 Subject: [PATCH 071/112] Bump pyatmo to v8.0.2 (#106758) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index f5f2d67947f..aee63e60016 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["pyatmo"], - "requirements": ["pyatmo==8.0.1"] + "requirements": ["pyatmo==8.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index b4d0c6f304f..587fa1ac599 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1648,7 +1648,7 @@ pyasuswrt==0.1.21 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==8.0.1 +pyatmo==8.0.2 # homeassistant.components.apple_tv pyatv==0.14.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d7c57932d3..0f6ca00186e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1265,7 +1265,7 @@ pyasuswrt==0.1.21 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==8.0.1 +pyatmo==8.0.2 # homeassistant.components.apple_tv pyatv==0.14.3 From a7d11120fadd8e89633a324a692f689fa02defc4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Dec 2023 18:57:11 +0100 Subject: [PATCH 072/112] Bump version to 2024.1.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 940a410a778..87f6f9fd7d0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Any, Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 66e4c973339..8976143bf0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.0b2" +version = "2024.1.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 77286e8f596e5edd2915dfc7a400611872cd9b0d Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Mon, 1 Jan 2024 23:00:17 -0500 Subject: [PATCH 073/112] Constrain dacite to at least 1.7.0 (#105709) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 842bff950fc..eb264561000 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -189,3 +189,7 @@ charset-normalizer==3.2.0 # lxml 5.0.0 currently does not build on alpine 3.18 # https://bugs.launchpad.net/lxml/+bug/2047718 lxml==4.9.4 + +# dacite: Ensure we have a version that is able to handle type unions for +# Roborock, NAM, Brother, and GIOS. +dacite>=1.7.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 101c9294706..3cecff68fb0 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -183,6 +183,10 @@ charset-normalizer==3.2.0 # lxml 5.0.0 currently does not build on alpine 3.18 # https://bugs.launchpad.net/lxml/+bug/2047718 lxml==4.9.4 + +# dacite: Ensure we have a version that is able to handle type unions for +# Roborock, NAM, Brother, and GIOS. +dacite>=1.7.0 """ GENERATED_MESSAGE = ( From fedb63720cf456557eafd1e8c3ce7135bd1c4942 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Tue, 2 Jan 2024 06:46:39 -0500 Subject: [PATCH 074/112] Fix Hydrawise data not refreshing (#105923) Co-authored-by: Robert Resch --- .../components/hydrawise/binary_sensor.py | 2 +- .../components/hydrawise/coordinator.py | 27 ++++++++++++++++--- homeassistant/components/hydrawise/entity.py | 3 +++ homeassistant/components/hydrawise/sensor.py | 2 +- homeassistant/components/hydrawise/switch.py | 2 +- 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index 65355a1829f..0b12fcb3ddb 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -70,7 +70,7 @@ async def async_setup_entry( config_entry.entry_id ] entities = [] - for controller in coordinator.data.controllers: + for controller in coordinator.data.controllers.values(): entities.append( HydrawiseBinarySensor(coordinator, BINARY_SENSOR_STATUS, controller) ) diff --git a/homeassistant/components/hydrawise/coordinator.py b/homeassistant/components/hydrawise/coordinator.py index 412108f859f..71922928651 100644 --- a/homeassistant/components/hydrawise/coordinator.py +++ b/homeassistant/components/hydrawise/coordinator.py @@ -2,10 +2,11 @@ from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta from pydrawise import HydrawiseBase -from pydrawise.schema import User +from pydrawise.schema import Controller, User, Zone from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -13,9 +14,20 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, LOGGER -class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[User]): +@dataclass +class HydrawiseData: + """Container for data fetched from the Hydrawise API.""" + + user: User + controllers: dict[int, Controller] + zones: dict[int, Zone] + + +class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]): """The Hydrawise Data Update Coordinator.""" + api: HydrawiseBase + def __init__( self, hass: HomeAssistant, api: HydrawiseBase, scan_interval: timedelta ) -> None: @@ -23,6 +35,13 @@ class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[User]): super().__init__(hass, LOGGER, name=DOMAIN, update_interval=scan_interval) self.api = api - async def _async_update_data(self) -> User: + async def _async_update_data(self) -> HydrawiseData: """Fetch the latest data from Hydrawise.""" - return await self.api.get_user() + user = await self.api.get_user() + controllers = {} + zones = {} + for controller in user.controllers: + controllers[controller.id] = controller + for zone in controller.zones: + zones[zone.id] = zone + return HydrawiseData(user=user, controllers=controllers, zones=zones) diff --git a/homeassistant/components/hydrawise/entity.py b/homeassistant/components/hydrawise/entity.py index c707690ce95..887de6ba648 100644 --- a/homeassistant/components/hydrawise/entity.py +++ b/homeassistant/components/hydrawise/entity.py @@ -48,5 +48,8 @@ class HydrawiseEntity(CoordinatorEntity[HydrawiseDataUpdateCoordinator]): @callback def _handle_coordinator_update(self) -> None: """Get the latest data and updates the state.""" + self.controller = self.coordinator.data.controllers[self.controller.id] + if self.zone: + self.zone = self.coordinator.data.zones[self.zone.id] self._update_attrs() super()._handle_coordinator_update() diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index 79a318f778f..f8490ad00e1 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -76,7 +76,7 @@ async def async_setup_entry( ] async_add_entities( HydrawiseSensor(coordinator, description, controller, zone) - for controller in coordinator.data.controllers + for controller in coordinator.data.controllers.values() for zone in controller.zones for description in SENSOR_TYPES ) diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py index 5a3a3a62895..8a92a56975a 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -81,7 +81,7 @@ async def async_setup_entry( ] async_add_entities( HydrawiseSwitch(coordinator, description, controller, zone) - for controller in coordinator.data.controllers + for controller in coordinator.data.controllers.values() for zone in controller.zones for description in SWITCH_TYPES ) From e6d2721d1b01d206dfd0c817fb6800e3c7b60bff Mon Sep 17 00:00:00 2001 From: Benjamin Richter Date: Tue, 2 Jan 2024 09:59:13 +0100 Subject: [PATCH 075/112] Fix fints account type check (#106082) --- homeassistant/components/fints/sensor.py | 8 +- requirements_test_all.txt | 3 + tests/components/fints/__init__.py | 1 + tests/components/fints/test_client.py | 95 ++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 tests/components/fints/__init__.py create mode 100644 tests/components/fints/test_client.py diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index fafe1fcf2bf..c969adfe637 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -168,8 +168,8 @@ class FinTsClient: if not account_information: return False - if 1 <= account_information["type"] <= 9: - return True + if account_type := account_information.get("type"): + return 1 <= account_type <= 9 if ( account_information["iban"] in self.account_config @@ -188,8 +188,8 @@ class FinTsClient: if not account_information: return False - if 30 <= account_information["type"] <= 39: - return True + if account_type := account_information.get("type"): + return 30 <= account_type <= 39 if ( account_information["iban"] in self.holdings_config diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f6ca00186e..0603750b0d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -659,6 +659,9 @@ feedparser==6.0.11 # homeassistant.components.file file-read-backwards==2.0.0 +# homeassistant.components.fints +fints==3.1.0 + # homeassistant.components.fitbit fitbit==0.3.1 diff --git a/tests/components/fints/__init__.py b/tests/components/fints/__init__.py new file mode 100644 index 00000000000..6a2b1d96d20 --- /dev/null +++ b/tests/components/fints/__init__.py @@ -0,0 +1 @@ +"""Tests for FinTS component.""" diff --git a/tests/components/fints/test_client.py b/tests/components/fints/test_client.py new file mode 100644 index 00000000000..429d391b07e --- /dev/null +++ b/tests/components/fints/test_client.py @@ -0,0 +1,95 @@ +"""Tests for the FinTS client.""" + +from typing import Optional + +from fints.client import BankIdentifier, FinTSOperations +import pytest + +from homeassistant.components.fints.sensor import ( + BankCredentials, + FinTsClient, + SEPAAccount, +) + +BANK_INFORMATION = { + "bank_identifier": BankIdentifier(country_identifier="280", bank_code="50010517"), + "currency": "EUR", + "customer_id": "0815", + "owner_name": ["SURNAME, FIRSTNAME"], + "subaccount_number": None, + "supported_operations": { + FinTSOperations.GET_BALANCE: True, + FinTSOperations.GET_CREDIT_CARD_TRANSACTIONS: False, + FinTSOperations.GET_HOLDINGS: False, + FinTSOperations.GET_SCHEDULED_DEBITS_MULTIPLE: False, + FinTSOperations.GET_SCHEDULED_DEBITS_SINGLE: False, + FinTSOperations.GET_SEPA_ACCOUNTS: True, + FinTSOperations.GET_STATEMENT: False, + FinTSOperations.GET_STATEMENT_PDF: False, + FinTSOperations.GET_TRANSACTIONS: True, + FinTSOperations.GET_TRANSACTIONS_XML: False, + }, +} + + +@pytest.mark.parametrize( + ( + "account_number", + "iban", + "product_name", + "account_type", + "expected_balance_result", + "expected_holdings_result", + ), + [ + ("GIRO1", "GIRO1", "Valid balance account", 5, True, False), + (None, None, "Invalid account", None, False, False), + ("GIRO2", "GIRO2", "Account without type", None, False, False), + ("GIRO3", "GIRO3", "Balance account from fallback", None, True, False), + ("DEPOT1", "DEPOT1", "Valid holdings account", 33, False, True), + ("DEPOT2", "DEPOT2", "Holdings account from fallback", None, False, True), + ], +) +async def test_account_type( + account_number: Optional[str], + iban: Optional[str], + product_name: str, + account_type: Optional[int], + expected_balance_result: bool, + expected_holdings_result: bool, +) -> None: + """Check client methods is_balance_account and is_holdings_account.""" + credentials = BankCredentials( + blz=1234, login="test", pin="0000", url="https://example.com" + ) + account_config = {"GIRO3": True} + holdings_config = {"DEPOT2": True} + + client = FinTsClient( + credentials=credentials, + name="test", + account_config=account_config, + holdings_config=holdings_config, + ) + + client._account_information_fetched = True + client._account_information = { + iban: BANK_INFORMATION + | { + "account_number": account_number, + "iban": iban, + "product_name": product_name, + "type": account_type, + } + } + + sepa_account = SEPAAccount( + iban=iban, + bic="BANCODELTEST", + accountnumber=account_number, + subaccount=None, + blz="12345", + ) + + assert client.is_balance_account(sepa_account) == expected_balance_result + assert client.is_holdings_account(sepa_account) == expected_holdings_result From 39960caf36abd935f82f11003f3ca73e7d5d4b0a Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 31 Dec 2023 14:26:21 -0500 Subject: [PATCH 076/112] Bump pyunifiprotect to v4.22.5 (#106781) --- .../components/unifiprotect/binary_sensor.py | 11 +++++++++++ homeassistant/components/unifiprotect/button.py | 7 +++++++ homeassistant/components/unifiprotect/data.py | 2 ++ homeassistant/components/unifiprotect/manifest.json | 2 +- .../components/unifiprotect/media_player.py | 11 +++++++++++ homeassistant/components/unifiprotect/number.py | 12 ++++++++++++ homeassistant/components/unifiprotect/select.py | 11 +++++++++++ homeassistant/components/unifiprotect/sensor.py | 9 +++++++++ homeassistant/components/unifiprotect/switch.py | 11 +++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 11 files changed, 77 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index f32b53a5d7a..1104ecb98e1 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -643,4 +643,15 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity): or self._attr_extra_state_attributes != previous_extra_state_attributes or self._attr_available != previous_available ): + _LOGGER.debug( + "Updating state [%s (%s)] %s (%s, %s) -> %s (%s, %s)", + device.name, + device.mac, + previous_is_on, + previous_available, + previous_extra_state_attributes, + self._attr_is_on, + self._attr_available, + self._attr_extra_state_attributes, + ) self.async_write_ha_state() diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 01bde0d9248..b69fbb95970 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -206,4 +206,11 @@ class ProtectButton(ProtectDeviceEntity, ButtonEntity): previous_available = self._attr_available self._async_update_device_from_protect(device) if self._attr_available != previous_available: + _LOGGER.debug( + "Updating state [%s (%s)] %s -> %s", + device.name, + device.mac, + previous_available, + self._attr_available, + ) self.async_write_ha_state() diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 73d05f1be1d..8b8ec80c5ba 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -228,6 +228,8 @@ class ProtectData: # trigger updates for camera that the event references elif isinstance(obj, Event): # type: ignore[unreachable] + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug("event WS msg: %s", obj.dict()) if obj.type in SMART_EVENTS: if obj.camera is not None: if obj.end is None: diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index c74097c3c17..2fbf8f31071 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.22.4", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.22.5", "unifi-discovery==1.1.7"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index df5ea40d4a9..b2376277e6f 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -133,6 +133,17 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): or self._attr_volume_level != previous_volume_level or self._attr_available != previous_available ): + _LOGGER.debug( + "Updating state [%s (%s)] %s (%s, %s) -> %s (%s, %s)", + device.name, + device.mac, + previous_state, + previous_available, + previous_volume_level, + self._attr_state, + self._attr_available, + self._attr_volume_level, + ) self.async_write_ha_state() async def async_set_volume_level(self, volume: float) -> None: diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 7fed79499d2..c02753a9401 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta +import logging from pyunifiprotect.data import ( Camera, @@ -25,6 +26,8 @@ from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd +_LOGGER = logging.getLogger(__name__) + @dataclass(frozen=True) class NumberKeysMixin: @@ -285,4 +288,13 @@ class ProtectNumbers(ProtectDeviceEntity, NumberEntity): self._attr_native_value != previous_value or self._attr_available != previous_available ): + _LOGGER.debug( + "Updating state [%s (%s)] %s (%s) -> %s (%s)", + device.name, + device.mac, + previous_value, + previous_available, + self._attr_native_value, + self._attr_available, + ) self.async_write_ha_state() diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 649c77bed5b..dfc3be2d4a1 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -420,4 +420,15 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity): or self._attr_options != previous_options or self._attr_available != previous_available ): + _LOGGER.debug( + "Updating state [%s (%s)] %s (%s, %s) -> %s (%s, %s)", + device.name, + device.mac, + previous_option, + previous_available, + previous_options, + self._attr_current_option, + self._attr_available, + self._attr_options, + ) self.async_write_ha_state() diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 6344b852b63..3e2bd6ee858 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -730,6 +730,15 @@ class ProtectDeviceSensor(ProtectDeviceEntity, SensorEntity): self._attr_native_value != previous_value or self._attr_available != previous_available ): + _LOGGER.debug( + "Updating state [%s (%s)] %s (%s) -> %s (%s)", + device.name, + device.mac, + previous_value, + previous_available, + self._attr_native_value, + self._attr_available, + ) self.async_write_ha_state() diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index c57546be8d0..d8a3fc1c5bc 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +import logging from typing import Any from pyunifiprotect.data import ( @@ -27,6 +28,7 @@ from .entity import ProtectDeviceEntity, ProtectNVREntity, async_all_device_enti from .models import PermRequired, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd +_LOGGER = logging.getLogger(__name__) ATTR_PREV_MIC = "prev_mic_level" ATTR_PREV_RECORD = "prev_record_mode" @@ -458,6 +460,15 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): self._attr_is_on != previous_is_on or self._attr_available != previous_available ): + _LOGGER.debug( + "Updating state [%s (%s)] %s (%s) -> %s (%s)", + device.name, + device.mac, + previous_is_on, + previous_available, + self._attr_is_on, + self._attr_available, + ) self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 587fa1ac599..bf0bbf67f8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2280,7 +2280,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.22.4 +pyunifiprotect==4.22.5 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0603750b0d9..1cf739dd4f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1726,7 +1726,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.22.4 +pyunifiprotect==4.22.5 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 16d3d88fa3dfb5efb3593bbc01eb9a560231fe84 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Sun, 31 Dec 2023 14:17:51 -0500 Subject: [PATCH 077/112] Bump pyschlage to 2023.12.1 (#106782) --- homeassistant/components/schlage/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json index e14a5bc706e..72d5ad54565 100644 --- a/homeassistant/components/schlage/manifest.json +++ b/homeassistant/components/schlage/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/schlage", "iot_class": "cloud_polling", - "requirements": ["pyschlage==2023.12.0"] + "requirements": ["pyschlage==2023.12.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index bf0bbf67f8f..9a34d99b55d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2058,7 +2058,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2023.12.0 +pyschlage==2023.12.1 # homeassistant.components.sensibo pysensibo==1.0.36 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1cf739dd4f2..dfef509ec99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1567,7 +1567,7 @@ pyrympro==0.0.7 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2023.12.0 +pyschlage==2023.12.1 # homeassistant.components.sensibo pysensibo==1.0.36 From b1a55e9b19a1ceb58093c467d24016f0cf94003a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Jan 2024 01:51:05 -1000 Subject: [PATCH 078/112] Fix emulated_hue brightness check (#106783) --- .../components/emulated_hue/hue_api.py | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 05e5c1ece07..0730eced60c 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable from functools import lru_cache import hashlib from http import HTTPStatus @@ -41,6 +42,7 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, + ColorMode, LightEntityFeature, ) from homeassistant.components.media_player import ( @@ -115,12 +117,19 @@ UNAUTHORIZED_USER = [ {"error": {"address": "/", "description": "unauthorized user", "type": "1"}} ] -DIMMABLE_SUPPORT_FEATURES = ( - CoverEntityFeature.SET_POSITION - | FanEntityFeature.SET_SPEED - | MediaPlayerEntityFeature.VOLUME_SET - | ClimateEntityFeature.TARGET_TEMPERATURE -) +DIMMABLE_SUPPORTED_FEATURES_BY_DOMAIN = { + cover.DOMAIN: CoverEntityFeature.SET_POSITION, + fan.DOMAIN: FanEntityFeature.SET_SPEED, + media_player.DOMAIN: MediaPlayerEntityFeature.VOLUME_SET, + climate.DOMAIN: ClimateEntityFeature.TARGET_TEMPERATURE, +} + +ENTITY_FEATURES_BY_DOMAIN = { + cover.DOMAIN: CoverEntityFeature, + fan.DOMAIN: FanEntityFeature, + media_player.DOMAIN: MediaPlayerEntityFeature, + climate.DOMAIN: ClimateEntityFeature, +} @lru_cache(maxsize=32) @@ -756,7 +765,6 @@ def _entity_unique_id(entity_id: str) -> str: def state_to_json(config: Config, state: State) -> dict[str, Any]: """Convert an entity to its Hue bridge JSON representation.""" - entity_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) color_modes = state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) unique_id = _entity_unique_id(state.entity_id) state_dict = get_entity_state_dict(config, state) @@ -773,9 +781,9 @@ def state_to_json(config: Config, state: State) -> dict[str, Any]: "manufacturername": "Home Assistant", "swversion": "123", } - - color_supported = light.color_supported(color_modes) - color_temp_supported = light.color_temp_supported(color_modes) + is_light = state.domain == light.DOMAIN + color_supported = is_light and light.color_supported(color_modes) + color_temp_supported = is_light and light.color_temp_supported(color_modes) if color_supported and color_temp_supported: # Extended Color light (Zigbee Device ID: 0x0210) # Same as Color light, but which supports additional setting of color temperature @@ -820,9 +828,7 @@ def state_to_json(config: Config, state: State) -> dict[str, Any]: HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS], } ) - elif entity_features & DIMMABLE_SUPPORT_FEATURES or light.brightness_supported( - color_modes - ): + elif state_supports_hue_brightness(state, color_modes): # Dimmable light (Zigbee Device ID: 0x0100) # Supports groups, scenes, on/off and dimming retval["type"] = "Dimmable light" @@ -845,6 +851,21 @@ def state_to_json(config: Config, state: State) -> dict[str, Any]: return retval +def state_supports_hue_brightness( + state: State, color_modes: Iterable[ColorMode] +) -> bool: + """Return True if the state supports brightness.""" + domain = state.domain + if domain == light.DOMAIN: + return light.brightness_supported(color_modes) + if not (required_feature := DIMMABLE_SUPPORTED_FEATURES_BY_DOMAIN.get(domain)): + return False + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + enum = ENTITY_FEATURES_BY_DOMAIN[domain] + features = enum(features) if type(features) is int else features # noqa: E721 + return required_feature in features + + def create_hue_success_response( entity_number: str, attr: str, value: str ) -> dict[str, Any]: From 6ca3c7a6733e64d18090b06ab09b2aa2cac2464a Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 1 Jan 2024 23:45:31 +0100 Subject: [PATCH 079/112] Bump pyduotecno to 2024.1.1 (#106801) * Bump pyduotecno to 2024.0.1 * Bump pyduotecno to 2024.1.0 * small update --- homeassistant/components/duotecno/climate.py | 2 +- homeassistant/components/duotecno/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/duotecno/climate.py b/homeassistant/components/duotecno/climate.py index 8e23e742c04..dc10e0a61d9 100644 --- a/homeassistant/components/duotecno/climate.py +++ b/homeassistant/components/duotecno/climate.py @@ -52,7 +52,7 @@ class DuotecnoClimate(DuotecnoEntity, ClimateEntity): _attr_translation_key = "duotecno" @property - def current_temperature(self) -> int | None: + def current_temperature(self) -> float | None: """Get the current temperature.""" return self._unit.get_cur_temp() diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json index 2f221929178..9f6d082cae8 100644 --- a/homeassistant/components/duotecno/manifest.json +++ b/homeassistant/components/duotecno/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["pyduotecno", "pyduotecno-node", "pyduotecno-unit"], "quality_scale": "silver", - "requirements": ["pyDuotecno==2023.11.1"] + "requirements": ["pyDuotecno==2024.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9a34d99b55d..803212d03b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1599,7 +1599,7 @@ pyCEC==0.5.2 pyControl4==1.1.0 # homeassistant.components.duotecno -pyDuotecno==2023.11.1 +pyDuotecno==2024.1.1 # homeassistant.components.electrasmart pyElectra==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dfef509ec99..67b3eeb8e1c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1234,7 +1234,7 @@ pyCEC==0.5.2 pyControl4==1.1.0 # homeassistant.components.duotecno -pyDuotecno==2023.11.1 +pyDuotecno==2024.1.1 # homeassistant.components.electrasmart pyElectra==1.2.0 From 448e98eac59003f98fe1b86449595adb979f3732 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Jan 2024 05:15:34 +0100 Subject: [PATCH 080/112] Update frontend to 20240101.0 (#106808) --- 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 227fa96edf7..02a311a42ce 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==20231228.0"] + "requirements": ["home-assistant-frontend==20240101.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index eb264561000..e1166ed5f0a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ habluetooth==2.0.1 hass-nabucasa==0.75.1 hassil==1.5.1 home-assistant-bluetooth==1.11.0 -home-assistant-frontend==20231228.0 +home-assistant-frontend==20240101.0 home-assistant-intents==2023.12.05 httpx==0.26.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 803212d03b6..c5031a97cbc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1038,7 +1038,7 @@ hole==0.8.0 holidays==0.39 # homeassistant.components.frontend -home-assistant-frontend==20231228.0 +home-assistant-frontend==20240101.0 # homeassistant.components.conversation home-assistant-intents==2023.12.05 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67b3eeb8e1c..1b76d8bc191 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -831,7 +831,7 @@ hole==0.8.0 holidays==0.39 # homeassistant.components.frontend -home-assistant-frontend==20231228.0 +home-assistant-frontend==20240101.0 # homeassistant.components.conversation home-assistant-intents==2023.12.05 From 54a87cf04759827427b6e5b0e06fdbe3b1327775 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Jan 2024 12:16:17 -1000 Subject: [PATCH 081/112] Bump bleak-retry-connector to 3.4.0 (#106831) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 19199e4b1c6..c5dec12fe40 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -15,7 +15,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.21.1", - "bleak-retry-connector==3.3.0", + "bleak-retry-connector==3.4.0", "bluetooth-adapters==0.16.2", "bluetooth-auto-recovery==1.2.3", "bluetooth-data-tools==1.19.0", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e1166ed5f0a..4f63f79aac5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ atomicwrites-homeassistant==1.4.1 attrs==23.1.0 awesomeversion==23.11.0 bcrypt==4.0.1 -bleak-retry-connector==3.3.0 +bleak-retry-connector==3.4.0 bleak==0.21.1 bluetooth-adapters==0.16.2 bluetooth-auto-recovery==1.2.3 diff --git a/requirements_all.txt b/requirements_all.txt index c5031a97cbc..71520486027 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -538,7 +538,7 @@ bizkaibus==0.1.1 bleak-esphome==0.4.0 # homeassistant.components.bluetooth -bleak-retry-connector==3.3.0 +bleak-retry-connector==3.4.0 # homeassistant.components.bluetooth bleak==0.21.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b76d8bc191..42649400e4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -457,7 +457,7 @@ bimmer-connected[china]==0.14.6 bleak-esphome==0.4.0 # homeassistant.components.bluetooth -bleak-retry-connector==3.3.0 +bleak-retry-connector==3.4.0 # homeassistant.components.bluetooth bleak==0.21.1 From 38b8a1f95d06f01f4450332918ee2289ef659b17 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Jan 2024 00:39:04 -1000 Subject: [PATCH 082/112] Bump pySwitchbot to 0.43.0 (#106833) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index e835a2f4aca..d3d84d2cd48 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -39,5 +39,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.40.1"] + "requirements": ["PySwitchbot==0.43.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 71520486027..ba7221b554c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -96,7 +96,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.40.1 +PySwitchbot==0.43.0 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42649400e4a..020678ecc8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.40.1 +PySwitchbot==0.43.0 # homeassistant.components.syncthru PySyncThru==0.7.10 From 8c25e2610e87237fa2de04deae09b8093920dd54 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Jan 2024 00:41:56 -1000 Subject: [PATCH 083/112] Bump yalexs-ble to 2.4.0 (#106834) --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index aacebb4bb5c..d0f2a27522d 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==1.10.0", "yalexs-ble==2.3.2"] + "requirements": ["yalexs==1.10.0", "yalexs-ble==2.4.0"] } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index be388ec563c..dcd7e57ce1f 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "iot_class": "local_push", - "requirements": ["yalexs-ble==2.3.2"] + "requirements": ["yalexs-ble==2.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ba7221b554c..1584b61146f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2830,7 +2830,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.3.2 +yalexs-ble==2.4.0 # homeassistant.components.august yalexs==1.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 020678ecc8b..0ce1394a889 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2141,7 +2141,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.3.2 +yalexs-ble==2.4.0 # homeassistant.components.august yalexs==1.10.0 From 59bed57d482358c36de94325e71ff7264e43155f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Jan 2024 00:37:51 -1000 Subject: [PATCH 084/112] Fix incorrect state in Yale Access Bluetooth when lock status is unknown (#106851) --- homeassistant/components/yalexs_ble/lock.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/lock.py b/homeassistant/components/yalexs_ble/lock.py index d457784a038..f6fa1917d7e 100644 --- a/homeassistant/components/yalexs_ble/lock.py +++ b/homeassistant/components/yalexs_ble/lock.py @@ -40,17 +40,19 @@ class YaleXSBLELock(YALEXSBLEEntity, LockEntity): self._attr_is_unlocking = False self._attr_is_jammed = False lock_state = new_state.lock - if lock_state == LockStatus.LOCKED: + if lock_state is LockStatus.LOCKED: self._attr_is_locked = True - elif lock_state == LockStatus.LOCKING: + elif lock_state is LockStatus.LOCKING: self._attr_is_locking = True - elif lock_state == LockStatus.UNLOCKING: + elif lock_state is LockStatus.UNLOCKING: self._attr_is_unlocking = True elif lock_state in ( LockStatus.UNKNOWN_01, LockStatus.UNKNOWN_06, ): self._attr_is_jammed = True + elif lock_state is LockStatus.UNKNOWN: + self._attr_is_locked = None super()._async_update_state(new_state, lock_info, connection_info) async def async_unlock(self, **kwargs: Any) -> None: From e604bc8c9b54078174a43095baa7178ba78571fb Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Mon, 1 Jan 2024 19:58:12 -0800 Subject: [PATCH 085/112] Map missing preset mapping for heat mode "ready" in smarttub (#106856) --- homeassistant/components/smarttub/climate.py | 2 ++ tests/components/smarttub/test_climate.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py index b2d4fbf17c4..9f1802e7327 100644 --- a/homeassistant/components/smarttub/climate.py +++ b/homeassistant/components/smarttub/climate.py @@ -23,11 +23,13 @@ from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, SMARTTUB_CONTROLL from .entity import SmartTubEntity PRESET_DAY = "day" +PRESET_READY = "ready" PRESET_MODES = { Spa.HeatMode.AUTO: PRESET_NONE, Spa.HeatMode.ECONOMY: PRESET_ECO, Spa.HeatMode.DAY: PRESET_DAY, + Spa.HeatMode.READY: PRESET_READY, } HEAT_MODES = {v: k for k, v in PRESET_MODES.items()} diff --git a/tests/components/smarttub/test_climate.py b/tests/components/smarttub/test_climate.py index 601015ca681..40e3c05b509 100644 --- a/tests/components/smarttub/test_climate.py +++ b/tests/components/smarttub/test_climate.py @@ -58,7 +58,7 @@ async def test_thermostat_update( assert state.attributes[ATTR_TEMPERATURE] == 39 assert state.attributes[ATTR_MAX_TEMP] == DEFAULT_MAX_TEMP assert state.attributes[ATTR_MIN_TEMP] == DEFAULT_MIN_TEMP - assert state.attributes[ATTR_PRESET_MODES] == ["none", "eco", "day"] + assert state.attributes[ATTR_PRESET_MODES] == ["none", "eco", "day", "ready"] await hass.services.async_call( CLIMATE_DOMAIN, From 056b06de13caa79e894043bb142adda01b5fbf0e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Jan 2024 11:35:16 +0100 Subject: [PATCH 086/112] Don't use entity_id in __repr__ of not added entity (#106861) --- homeassistant/helpers/entity.py | 2 +- tests/helpers/test_entity.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index b7ed7e3c095..3c3c8474e67 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1481,7 +1481,7 @@ class Entity( If the entity is not added to a platform it's not safe to call _stringify_state. """ if self._platform_state != EntityPlatformState.ADDED: - return f"" + return f"" return f"" async def async_request_call(self, coro: Coroutine[Any, Any, _T]) -> _T: diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 4fca7ed4c23..a18d8963947 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1407,7 +1407,7 @@ async def test_repr(hass) -> None: my_entity = MyEntity(entity_id="test.test", available=False) # Not yet added - assert str(my_entity) == "" + assert str(my_entity) == "" # Added await platform.async_add_entities([my_entity]) @@ -1415,7 +1415,7 @@ async def test_repr(hass) -> None: # Removed await platform.async_remove_entity(my_entity.entity_id) - assert str(my_entity) == "" + assert str(my_entity) == "" async def test_warn_using_async_update_ha_state( From fc66dead64413b807d0f6ba08cbfb045ae6e7c51 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Jan 2024 12:59:23 +0100 Subject: [PATCH 087/112] Bump version to 2024.1.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 87f6f9fd7d0..525c2db95ca 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Any, Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 8976143bf0d..c7f9622faa1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.0b3" +version = "2024.1.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 35fc26457b116a98968494bc448e414ab252286a Mon Sep 17 00:00:00 2001 From: Robert Groot <8398505+iamrgroot@users.noreply.github.com> Date: Tue, 2 Jan 2024 13:24:17 +0100 Subject: [PATCH 088/112] Changed setup of EnergyZero services (#106224) * Changed setup of energyzero services * PR review updates * Dict access instead of get Co-authored-by: Martin Hjelmare * Added tests for unloaded state --------- Co-authored-by: Martin Hjelmare --- .../components/energyzero/__init__.py | 15 +++- .../components/energyzero/services.py | 49 +++++++++-- .../components/energyzero/services.yaml | 10 +++ .../components/energyzero/strings.json | 14 ++++ tests/components/energyzero/test_services.py | 82 +++++++++++++++++-- 5 files changed, 155 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/energyzero/__init__.py b/homeassistant/components/energyzero/__init__.py index 0eac874f1ed..8878a99e562 100644 --- a/homeassistant/components/energyzero/__init__.py +++ b/homeassistant/components/energyzero/__init__.py @@ -5,12 +5,23 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN from .coordinator import EnergyZeroDataUpdateCoordinator -from .services import async_register_services +from .services import async_setup_services PLATFORMS = [Platform.SENSOR] +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up EnergyZero services.""" + + async_setup_services(hass) + + return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -27,8 +38,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - async_register_services(hass, coordinator) - return True diff --git a/homeassistant/components/energyzero/services.py b/homeassistant/components/energyzero/services.py index fb451c40401..d8e548c22f8 100644 --- a/homeassistant/components/energyzero/services.py +++ b/homeassistant/components/energyzero/services.py @@ -9,6 +9,7 @@ from typing import Final from energyzero import Electricity, Gas, VatOption import voluptuous as vol +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import ( HomeAssistant, ServiceCall, @@ -17,11 +18,13 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import selector from homeassistant.util import dt as dt_util from .const import DOMAIN from .coordinator import EnergyZeroDataUpdateCoordinator +ATTR_CONFIG_ENTRY: Final = "config_entry" ATTR_START: Final = "start" ATTR_END: Final = "end" ATTR_INCL_VAT: Final = "incl_vat" @@ -30,6 +33,11 @@ GAS_SERVICE_NAME: Final = "get_gas_prices" ENERGY_SERVICE_NAME: Final = "get_energy_prices" SERVICE_SCHEMA: Final = vol.Schema( { + vol.Required(ATTR_CONFIG_ENTRY): selector.ConfigEntrySelector( + { + "integration": DOMAIN, + } + ), vol.Required(ATTR_INCL_VAT): bool, vol.Optional(ATTR_START): str, vol.Optional(ATTR_END): str, @@ -75,12 +83,43 @@ def __serialize_prices(prices: Electricity | Gas) -> ServiceResponse: } +def __get_coordinator( + hass: HomeAssistant, call: ServiceCall +) -> EnergyZeroDataUpdateCoordinator: + """Get the coordinator from the entry.""" + entry_id: str = call.data[ATTR_CONFIG_ENTRY] + entry: ConfigEntry | None = hass.config_entries.async_get_entry(entry_id) + + if not entry: + raise ServiceValidationError( + f"Invalid config entry: {entry_id}", + translation_domain=DOMAIN, + translation_key="invalid_config_entry", + translation_placeholders={ + "config_entry": entry_id, + }, + ) + if entry.state != ConfigEntryState.LOADED: + raise ServiceValidationError( + f"{entry.title} is not loaded", + translation_domain=DOMAIN, + translation_key="unloaded_config_entry", + translation_placeholders={ + "config_entry": entry.title, + }, + ) + + return hass.data[DOMAIN][entry_id] + + async def __get_prices( call: ServiceCall, *, - coordinator: EnergyZeroDataUpdateCoordinator, + hass: HomeAssistant, price_type: PriceType, ) -> ServiceResponse: + coordinator = __get_coordinator(hass, call) + start = __get_date(call.data.get(ATTR_START)) end = __get_date(call.data.get(ATTR_END)) @@ -108,22 +147,20 @@ async def __get_prices( @callback -def async_register_services( - hass: HomeAssistant, coordinator: EnergyZeroDataUpdateCoordinator -): +def async_setup_services(hass: HomeAssistant) -> None: """Set up EnergyZero services.""" hass.services.async_register( DOMAIN, GAS_SERVICE_NAME, - partial(__get_prices, coordinator=coordinator, price_type=PriceType.GAS), + partial(__get_prices, hass=hass, price_type=PriceType.GAS), schema=SERVICE_SCHEMA, supports_response=SupportsResponse.ONLY, ) hass.services.async_register( DOMAIN, ENERGY_SERVICE_NAME, - partial(__get_prices, coordinator=coordinator, price_type=PriceType.ENERGY), + partial(__get_prices, hass=hass, price_type=PriceType.ENERGY), schema=SERVICE_SCHEMA, supports_response=SupportsResponse.ONLY, ) diff --git a/homeassistant/components/energyzero/services.yaml b/homeassistant/components/energyzero/services.yaml index 1bcc5ae34be..dc8df9aa6d0 100644 --- a/homeassistant/components/energyzero/services.yaml +++ b/homeassistant/components/energyzero/services.yaml @@ -1,5 +1,10 @@ get_gas_prices: fields: + config_entry: + required: true + selector: + config_entry: + integration: energyzero incl_vat: required: true default: true @@ -17,6 +22,11 @@ get_gas_prices: datetime: get_energy_prices: fields: + config_entry: + required: true + selector: + config_entry: + integration: energyzero incl_vat: required: true default: true diff --git a/homeassistant/components/energyzero/strings.json b/homeassistant/components/energyzero/strings.json index 81f54f4222a..9858838aff7 100644 --- a/homeassistant/components/energyzero/strings.json +++ b/homeassistant/components/energyzero/strings.json @@ -12,6 +12,12 @@ "exceptions": { "invalid_date": { "message": "Invalid date provided. Got {date}" + }, + "invalid_config_entry": { + "message": "Invalid config entry provided. Got {config_entry}" + }, + "unloaded_config_entry": { + "message": "Invalid config entry provided. {config_entry} is not loaded." } }, "entity": { @@ -50,6 +56,10 @@ "name": "Get gas prices", "description": "Request gas prices from EnergyZero.", "fields": { + "config_entry": { + "name": "Config Entry", + "description": "The config entry to use for this service." + }, "incl_vat": { "name": "Including VAT", "description": "Include VAT in the prices." @@ -68,6 +78,10 @@ "name": "Get energy prices", "description": "Request energy prices from EnergyZero.", "fields": { + "config_entry": { + "name": "[%key:component::energyzero::services::get_gas_prices::fields::config_entry::name%]", + "description": "[%key:component::energyzero::services::get_gas_prices::fields::config_entry::description%]" + }, "incl_vat": { "name": "[%key:component::energyzero::services::get_gas_prices::fields::incl_vat::name%]", "description": "[%key:component::energyzero::services::get_gas_prices::fields::incl_vat::description%]" diff --git a/tests/components/energyzero/test_services.py b/tests/components/energyzero/test_services.py index 7939b06ce8e..c0b54729e03 100644 --- a/tests/components/energyzero/test_services.py +++ b/tests/components/energyzero/test_services.py @@ -6,12 +6,15 @@ import voluptuous as vol from homeassistant.components.energyzero.const import DOMAIN from homeassistant.components.energyzero.services import ( + ATTR_CONFIG_ENTRY, ENERGY_SERVICE_NAME, GAS_SERVICE_NAME, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError +from tests.common import MockConfigEntry + @pytest.mark.usefixtures("init_integration") async def test_has_services( @@ -29,6 +32,7 @@ async def test_has_services( @pytest.mark.parametrize("end", [{"end": "2023-01-01 00:00:00"}, {}]) async def test_service( hass: HomeAssistant, + mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, service: str, incl_vat: dict[str, bool], @@ -36,8 +40,9 @@ async def test_service( end: dict[str, str], ) -> None: """Test the EnergyZero Service.""" + entry = {ATTR_CONFIG_ENTRY: mock_config_entry.entry_id} - data = incl_vat | start | end + data = entry | incl_vat | start | end assert snapshot == await hass.services.async_call( DOMAIN, @@ -48,32 +53,72 @@ async def test_service( ) +@pytest.fixture +def config_entry_data( + mock_config_entry: MockConfigEntry, request: pytest.FixtureRequest +) -> dict[str, str]: + """Fixture for the config entry.""" + if "config_entry" in request.param and request.param["config_entry"] is True: + return {"config_entry": mock_config_entry.entry_id} + + return request.param + + @pytest.mark.usefixtures("init_integration") @pytest.mark.parametrize("service", [GAS_SERVICE_NAME, ENERGY_SERVICE_NAME]) @pytest.mark.parametrize( - ("service_data", "error", "error_message"), + ("config_entry_data", "service_data", "error", "error_message"), [ - ({}, vol.er.Error, "required key not provided .+"), + ({}, {}, vol.er.Error, "required key not provided .+"), ( + {"config_entry": True}, + {}, + vol.er.Error, + "required key not provided .+", + ), + ( + {}, + {"incl_vat": True}, + vol.er.Error, + "required key not provided .+", + ), + ( + {"config_entry": True}, {"incl_vat": "incorrect vat"}, vol.er.Error, "expected bool for dictionary value .+", ), ( - {"incl_vat": True, "start": "incorrect date"}, + {"config_entry": "incorrect entry"}, + {"incl_vat": True}, + ServiceValidationError, + "Invalid config entry.+", + ), + ( + {"config_entry": True}, + { + "incl_vat": True, + "start": "incorrect date", + }, ServiceValidationError, "Invalid datetime provided.", ), ( - {"incl_vat": True, "end": "incorrect date"}, + {"config_entry": True}, + { + "incl_vat": True, + "end": "incorrect date", + }, ServiceValidationError, "Invalid datetime provided.", ), ], + indirect=["config_entry_data"], ) async def test_service_validation( hass: HomeAssistant, service: str, + config_entry_data: dict[str, str], service_data: dict[str, str], error: type[Exception], error_message: str, @@ -84,7 +129,32 @@ async def test_service_validation( await hass.services.async_call( DOMAIN, service, - service_data, + config_entry_data | service_data, + blocking=True, + return_response=True, + ) + + +@pytest.mark.usefixtures("init_integration") +@pytest.mark.parametrize("service", [GAS_SERVICE_NAME, ENERGY_SERVICE_NAME]) +async def test_service_called_with_unloaded_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + service: str, +) -> None: + """Test service calls with unloaded config entry.""" + + await mock_config_entry.async_unload(hass) + + data = {"config_entry": mock_config_entry.entry_id, "incl_vat": True} + + with pytest.raises( + ServiceValidationError, match=f"{mock_config_entry.title} is not loaded" + ): + await hass.services.async_call( + DOMAIN, + service, + data, blocking=True, return_response=True, ) From 3419b8d0824a595d22260c27b94910c6e24835dc Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Tue, 2 Jan 2024 13:34:19 +0100 Subject: [PATCH 089/112] Move urllib3 constraint to pyproject.toml (#106768) --- homeassistant/package_constraints.txt | 6 +----- pyproject.toml | 4 ++++ requirements.txt | 1 + script/gen_requirements_all.py | 5 ----- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4f63f79aac5..7ba565c4057 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -55,6 +55,7 @@ scapy==2.5.0 SQLAlchemy==2.0.23 typing-extensions>=4.9.0,<5.0 ulid-transform==0.9.0 +urllib3>=1.26.5,<2 voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 @@ -65,11 +66,6 @@ zeroconf==0.131.0 # see https://github.com/home-assistant/core/pull/16238 pycryptodome>=3.6.6 -# Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 -# Temporary setting an upper bound, to prevent compat issues with urllib3>=2 -# https://github.com/home-assistant/core/issues/97248 -urllib3>=1.26.5,<2 - # Constrain httplib2 to protect against GHSA-93xj-8mrv-444m # https://github.com/advisories/GHSA-93xj-8mrv-444m httplib2>=0.19.0 diff --git a/pyproject.toml b/pyproject.toml index c7f9622faa1..067275eaedb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,10 @@ dependencies = [ "requests==2.31.0", "typing-extensions>=4.9.0,<5.0", "ulid-transform==0.9.0", + # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 + # Temporary setting an upper bound, to prevent compat issues with urllib3>=2 + # https://github.com/home-assistant/core/issues/97248 + "urllib3>=1.26.5,<2", "voluptuous==0.13.1", "voluptuous-serialize==2.6.0", "yarl==1.9.4", diff --git a/requirements.txt b/requirements.txt index 2cac92b4972..55cbdc31730 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,6 +30,7 @@ PyYAML==6.0.1 requests==2.31.0 typing-extensions>=4.9.0,<5.0 ulid-transform==0.9.0 +urllib3>=1.26.5,<2 voluptuous==0.13.1 voluptuous-serialize==2.6.0 yarl==1.9.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3cecff68fb0..7f652b14302 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -59,11 +59,6 @@ CONSTRAINT_BASE = """ # see https://github.com/home-assistant/core/pull/16238 pycryptodome>=3.6.6 -# Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 -# Temporary setting an upper bound, to prevent compat issues with urllib3>=2 -# https://github.com/home-assistant/core/issues/97248 -urllib3>=1.26.5,<2 - # Constrain httplib2 to protect against GHSA-93xj-8mrv-444m # https://github.com/advisories/GHSA-93xj-8mrv-444m httplib2>=0.19.0 From 26cf30fc3a20cd701c9f245c083bd4e268e84a4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Jan 2024 09:44:17 -1000 Subject: [PATCH 090/112] Update switchbot to use close_stale_connections_by_address (#106835) --- homeassistant/components/switchbot/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 445920ad276..6bad3c25142 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -98,6 +98,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # connectable means we can make connections to the device connectable = switchbot_model in CONNECTABLE_SUPPORTED_MODEL_TYPES address: str = entry.data[CONF_ADDRESS] + + await switchbot.close_stale_connections_by_address(address) + ble_device = bluetooth.async_ble_device_from_address( hass, address.upper(), connectable ) @@ -106,7 +109,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Could not find Switchbot {sensor_type} with address {address}" ) - await switchbot.close_stale_connections(ble_device) cls = CLASS_BY_DEVICE.get(sensor_type, switchbot.SwitchbotDevice) if cls is switchbot.SwitchbotLock: try: From 5877fe135cc5b5b5b045e9a33448c4b330bd3bc5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Jan 2024 06:50:10 -1000 Subject: [PATCH 091/112] Close stale connections in yalexs_ble to ensure setup can proceed (#106842) --- homeassistant/components/yalexs_ble/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index 11516015b6c..b5683777c24 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -10,6 +10,7 @@ from yalexs_ble import ( LockState, PushLock, YaleXSBLEError, + close_stale_connections_by_address, local_name_is_unique, ) @@ -47,6 +48,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: id_ = local_name if has_unique_local_name else address push_lock.set_name(f"{entry.title} ({id_})") + # Ensure any lingering connections are closed since the device may not be + # advertising when its connected to another client which will prevent us + # from setting the device and setup will fail. + await close_stale_connections_by_address(address) + @callback def _async_update_ble( service_info: bluetooth.BluetoothServiceInfoBleak, From 596f855eab5bda941ce299f64dc0594cd3db648c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 2 Jan 2024 08:59:45 -0500 Subject: [PATCH 092/112] Bump Zigpy to 0.60.4 (#106870) --- 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 db5939123e4..06ebfaaa6a0 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -26,7 +26,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.109", "zigpy-deconz==0.22.4", - "zigpy==0.60.3", + "zigpy==0.60.4", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", diff --git a/requirements_all.txt b/requirements_all.txt index 1584b61146f..e5a205c29e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2887,7 +2887,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.3 +zigpy==0.60.4 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ce1394a889..9e67c0fed70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2186,7 +2186,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.3 +zigpy==0.60.4 # homeassistant.components.zwave_js zwave-js-server-python==0.55.2 From 6f18a29241421d38a6b0fa11a911f2a9236b0d94 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 2 Jan 2024 08:51:15 -0800 Subject: [PATCH 093/112] Improve fitbit authentication error handling (#106885) --- .../components/fitbit/application_credentials.py | 2 ++ tests/components/fitbit/test_init.py | 11 +++++++++-- tests/components/fitbit/test_sensor.py | 10 +++++++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fitbit/application_credentials.py b/homeassistant/components/fitbit/application_credentials.py index caa47351f45..bbd7af09183 100644 --- a/homeassistant/components/fitbit/application_credentials.py +++ b/homeassistant/components/fitbit/application_credentials.py @@ -69,6 +69,8 @@ class FitbitOAuth2Implementation(AuthImplementation): ) if err.status == HTTPStatus.UNAUTHORIZED: raise FitbitAuthException(f"Unauthorized error: {err}") from err + if err.status == HTTPStatus.BAD_REQUEST: + raise FitbitAuthException(f"Bad Request error: {err}") from err raise FitbitApiException(f"Server error response: {err}") from err except aiohttp.ClientError as err: raise FitbitApiException(f"Client connection error: {err}") from err diff --git a/tests/components/fitbit/test_init.py b/tests/components/fitbit/test_init.py index 3ed3695ff3d..74312348af1 100644 --- a/tests/components/fitbit/test_init.py +++ b/tests/components/fitbit/test_init.py @@ -106,7 +106,13 @@ async def test_token_refresh_success( ) -@pytest.mark.parametrize("token_expiration_time", [12345]) +@pytest.mark.parametrize( + ("token_expiration_time", "server_status"), + [ + (12345, HTTPStatus.UNAUTHORIZED), + (12345, HTTPStatus.BAD_REQUEST), + ], +) @pytest.mark.parametrize("closing", [True, False]) async def test_token_requires_reauth( hass: HomeAssistant, @@ -114,13 +120,14 @@ async def test_token_requires_reauth( config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, setup_credentials: None, + server_status: HTTPStatus, closing: bool, ) -> None: """Test where token is expired and the refresh attempt requires reauth.""" aioclient_mock.post( OAUTH2_TOKEN, - status=HTTPStatus.UNAUTHORIZED, + status=server_status, closing=closing, ) diff --git a/tests/components/fitbit/test_sensor.py b/tests/components/fitbit/test_sensor.py index 871088eae63..91aafd944b0 100644 --- a/tests/components/fitbit/test_sensor.py +++ b/tests/components/fitbit/test_sensor.py @@ -599,21 +599,25 @@ async def test_settings_scope_config_entry( @pytest.mark.parametrize( - ("scopes"), - [(["heartrate"])], + ("scopes", "server_status"), + [ + (["heartrate"], HTTPStatus.INTERNAL_SERVER_ERROR), + (["heartrate"], HTTPStatus.BAD_REQUEST), + ], ) async def test_sensor_update_failed( hass: HomeAssistant, setup_credentials: None, integration_setup: Callable[[], Awaitable[bool]], requests_mock: Mocker, + server_status: HTTPStatus, ) -> None: """Test a failed sensor update when talking to the API.""" requests_mock.register_uri( "GET", TIMESERIES_API_URL_FORMAT.format(resource="activities/heart"), - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + status_code=server_status, ) assert await integration_setup() From b5b8bc3102843ed6e8c4674cc3963d31ad9a4bd2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 2 Jan 2024 10:50:28 -0800 Subject: [PATCH 094/112] Improve To-do service error handling (#106886) --- homeassistant/components/todo/__init__.py | 23 ++++++++++++++++----- homeassistant/components/todo/strings.json | 8 +++++++ tests/components/shopping_list/test_todo.py | 3 ++- tests/components/todo/test_init.py | 14 ++++++------- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/todo/__init__.py b/homeassistant/components/todo/__init__.py index 0f39d38eb46..afcb8e28f74 100644 --- a/homeassistant/components/todo/__init__.py +++ b/homeassistant/components/todo/__init__.py @@ -19,7 +19,7 @@ from homeassistant.core import ( SupportsResponse, callback, ) -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -106,8 +106,11 @@ def _validate_supported_features( if desc.service_field not in call_data: continue if not supported_features or not supported_features & desc.required_feature: - raise ValueError( - f"Entity does not support setting field '{desc.service_field}'" + raise ServiceValidationError( + f"Entity does not support setting field '{desc.service_field}'", + translation_domain=DOMAIN, + translation_key="update_field_not_supported", + translation_placeholders={"service_field": desc.service_field}, ) @@ -481,7 +484,12 @@ async def _async_update_todo_item(entity: TodoListEntity, call: ServiceCall) -> item = call.data["item"] found = _find_by_uid_or_summary(item, entity.todo_items) if not found: - raise ValueError(f"Unable to find To-do item '{item}'") + raise ServiceValidationError( + f"Unable to find To-do item '{item}'", + translation_domain=DOMAIN, + translation_key="item_not_found", + translation_placeholders={"item": item}, + ) _validate_supported_features(entity.supported_features, call.data) @@ -509,7 +517,12 @@ async def _async_remove_todo_items(entity: TodoListEntity, call: ServiceCall) -> for item in call.data.get("item", []): found = _find_by_uid_or_summary(item, entity.todo_items) if not found or not found.uid: - raise ValueError(f"Unable to find To-do item '{item}") + raise ServiceValidationError( + f"Unable to find To-do item '{item}'", + translation_domain=DOMAIN, + translation_key="item_not_found", + translation_placeholders={"item": item}, + ) uids.append(found.uid) await entity.async_delete_todo_items(uids=uids) diff --git a/homeassistant/components/todo/strings.json b/homeassistant/components/todo/strings.json index 3da921a8f47..5ef7a5fe35b 100644 --- a/homeassistant/components/todo/strings.json +++ b/homeassistant/components/todo/strings.json @@ -90,5 +90,13 @@ "completed": "Completed" } } + }, + "exceptions": { + "item_not_found": { + "message": "Unable to find To-do item: {item}" + }, + "update_field_not_supported": { + "message": "Entity does not support setting field: {service_field}" + } } } diff --git a/tests/components/shopping_list/test_todo.py b/tests/components/shopping_list/test_todo.py index 7722bd8b6da..373c449497c 100644 --- a/tests/components/shopping_list/test_todo.py +++ b/tests/components/shopping_list/test_todo.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components.todo import DOMAIN as TODO_DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from tests.typing import WebSocketGenerator @@ -338,7 +339,7 @@ async def test_update_invalid_item( ) -> None: """Test updating a todo item that does not exist.""" - with pytest.raises(ValueError, match="Unable to find"): + with pytest.raises(ServiceValidationError, match="Unable to find"): await hass.services.async_call( TODO_DOMAIN, "update_item", diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index e1440b292ee..5a8f6183cbb 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -20,7 +20,7 @@ from homeassistant.components.todo import ( from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import intent from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -347,12 +347,12 @@ async def test_add_item_service_raises( ({"item": ""}, vol.Invalid, "length of value must be at least 1"), ( {"item": "Submit forms", "description": "Submit tax forms"}, - ValueError, + ServiceValidationError, "does not support setting field 'description'", ), ( {"item": "Submit forms", "due_date": "2023-11-17"}, - ValueError, + ServiceValidationError, "does not support setting field 'due_date'", ), ( @@ -360,7 +360,7 @@ async def test_add_item_service_raises( "item": "Submit forms", "due_datetime": f"2023-11-17T17:00:00{TEST_OFFSET}", }, - ValueError, + ServiceValidationError, "does not support setting field 'due_datetime'", ), ], @@ -622,7 +622,7 @@ async def test_update_todo_item_service_by_summary_not_found( await create_mock_platform(hass, [test_entity]) - with pytest.raises(ValueError, match="Unable to find"): + with pytest.raises(ServiceValidationError, match="Unable to find"): await hass.services.async_call( DOMAIN, "update_item", @@ -681,7 +681,7 @@ async def test_update_todo_item_field_unsupported( await create_mock_platform(hass, [test_entity]) - with pytest.raises(ValueError, match="does not support"): + with pytest.raises(ServiceValidationError, match="does not support"): await hass.services.async_call( DOMAIN, "update_item", @@ -931,7 +931,7 @@ async def test_remove_todo_item_service_by_summary_not_found( await create_mock_platform(hass, [test_entity]) - with pytest.raises(ValueError, match="Unable to find"): + with pytest.raises(ServiceValidationError, match="Unable to find"): await hass.services.async_call( DOMAIN, "remove_item", From 5100ba252f5661024641ff5756edaedd83b80b84 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Jan 2024 20:42:27 +0100 Subject: [PATCH 095/112] Update frontend to 20240102.0 (#106898) --- 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 02a311a42ce..7579426e1e1 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==20240101.0"] + "requirements": ["home-assistant-frontend==20240102.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7ba565c4057..ae76e0afa6c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ habluetooth==2.0.1 hass-nabucasa==0.75.1 hassil==1.5.1 home-assistant-bluetooth==1.11.0 -home-assistant-frontend==20240101.0 +home-assistant-frontend==20240102.0 home-assistant-intents==2023.12.05 httpx==0.26.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index e5a205c29e5..d6f956cb734 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1038,7 +1038,7 @@ hole==0.8.0 holidays==0.39 # homeassistant.components.frontend -home-assistant-frontend==20240101.0 +home-assistant-frontend==20240102.0 # homeassistant.components.conversation home-assistant-intents==2023.12.05 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e67c0fed70..5d5f4763da9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -831,7 +831,7 @@ hole==0.8.0 holidays==0.39 # homeassistant.components.frontend -home-assistant-frontend==20240101.0 +home-assistant-frontend==20240102.0 # homeassistant.components.conversation home-assistant-intents==2023.12.05 From 77cdc10883a0ba2c612b8aa619b80d6f9b5f89c5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Jan 2024 20:59:49 +0100 Subject: [PATCH 096/112] Bump version to 2024.1.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 525c2db95ca..a56835f1fbc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Any, Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 067275eaedb..e4d9a6b0c9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.0b4" +version = "2024.1.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 5eb1073b4a01bf417c3992859402a9ce4a84a51d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 2 Jan 2024 13:37:58 +0100 Subject: [PATCH 097/112] Apply late review comments on media player (#106727) --- .../components/media_player/significant_change.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/media_player/significant_change.py b/homeassistant/components/media_player/significant_change.py index 3e11cbdb9cd..adc96fc8b83 100644 --- a/homeassistant/components/media_player/significant_change.py +++ b/homeassistant/components/media_player/significant_change.py @@ -27,7 +27,7 @@ SIGNIFICANT_ATTRIBUTES: set[str] = { ATTR_ENTITY_PICTURE_LOCAL, ATTR_GROUP_MEMBERS, *ATTR_TO_PROPERTY, -} +} - INSIGNIFICANT_ATTRIBUTES @callback @@ -44,18 +44,10 @@ def async_check_significant_change( return True old_attrs_s = set( - { - k: v - for k, v in old_attrs.items() - if k in SIGNIFICANT_ATTRIBUTES - INSIGNIFICANT_ATTRIBUTES - }.items() + {k: v for k, v in old_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() ) new_attrs_s = set( - { - k: v - for k, v in new_attrs.items() - if k in SIGNIFICANT_ATTRIBUTES - INSIGNIFICANT_ATTRIBUTES - }.items() + {k: v for k, v in new_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items() ) changed_attrs: set[str] = {item[0] for item in old_attrs_s ^ new_attrs_s} From 527d9fbb6b0fab85fef73e7f752125aa2d38b2d6 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Wed, 3 Jan 2024 09:15:39 +0100 Subject: [PATCH 098/112] Add try-catch for invalid auth to Tado (#106774) Co-authored-by: Martin Hjelmare --- homeassistant/components/tado/config_flow.py | 3 +++ .../components/tado/device_tracker.py | 2 ++ homeassistant/components/tado/strings.json | 6 ++++- tests/components/tado/test_config_flow.py | 22 +++++++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index 3e183b0a9b5..f9f4f80bde1 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from typing import Any +import PyTado from PyTado.interface import Tado import requests.exceptions import voluptuous as vol @@ -136,6 +137,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) except exceptions.HomeAssistantError: return self.async_abort(reason="import_failed") + except PyTado.exceptions.TadoWrongCredentialsException: + return self.async_abort(reason="import_failed_invalid_auth") home_id = validate_result[UNIQUE_ID] await self.async_set_unique_id(home_id) diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index 426c7d9ed5d..c10ab118060 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -55,6 +55,8 @@ async def async_get_scanner( translation_key = "import_aborted" if import_result.get("reason") == "import_failed": translation_key = "import_failed" + if import_result.get("reason") == "import_failed_invalid_auth": + translation_key = "import_failed_invalid_auth" async_create_issue( hass, diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index 157b98e33ea..d50d1490566 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -133,9 +133,13 @@ "title": "Import aborted", "description": "Configuring the Tado Device Tracker using YAML is being removed.\n The import was aborted, due to an existing config entry being the same as the data being imported in the YAML. Remove the YAML device tracker configuration and restart Home Assistant. Please use the UI to configure Tado." }, - "failed_to_import": { + "import_failed": { "title": "Failed to import", "description": "Failed to import the configuration for the Tado Device Tracker. Please use the UI to configure Tado. Don't forget to delete the YAML configuration." + }, + "import_failed_invalid_auth": { + "title": "Failed to import, invalid credentials", + "description": "Failed to import the configuration for the Tado Device Tracker, due to invalid credentials. Please fix the YAML configuration and restart Home Assistant. Alternatively you can use the UI to configure Tado. Don't forget to delete the YAML configuration, once the import is successful." } } } diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index d83a4b22efc..ac04777dc1c 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -3,6 +3,7 @@ from http import HTTPStatus from ipaddress import ip_address from unittest.mock import MagicMock, patch +import PyTado import pytest import requests @@ -346,6 +347,27 @@ async def test_import_step_validation_failed(hass: HomeAssistant) -> None: assert result["reason"] == "import_failed" +async def test_import_step_device_authentication_failed(hass: HomeAssistant) -> None: + """Test import step with device tracker authentication failed.""" + with patch( + "homeassistant.components.tado.config_flow.Tado", + side_effect=PyTado.exceptions.TadoWrongCredentialsException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "username": "test-username", + "password": "test-password", + "home_id": 1, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "import_failed_invalid_auth" + + async def test_import_step_unique_id_configured(hass: HomeAssistant) -> None: """Test import step with unique ID already configured.""" entry = MockConfigEntry( From 95ef2dd7f9077951965701b66437932187e3cdc8 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 2 Jan 2024 15:35:48 -0600 Subject: [PATCH 099/112] Bump intents to 2024.1.2 (#106909) --- .../components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../conversation/snapshots/test_init.ambr | 8 ++--- tests/components/conversation/test_init.py | 30 +++++++++---------- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index cb03499d8e4..5f0c7b171ae 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.5.1", "home-assistant-intents==2023.12.05"] + "requirements": ["hassil==1.5.1", "home-assistant-intents==2024.1.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ae76e0afa6c..77aa0c699cf 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ hass-nabucasa==0.75.1 hassil==1.5.1 home-assistant-bluetooth==1.11.0 home-assistant-frontend==20240102.0 -home-assistant-intents==2023.12.05 +home-assistant-intents==2024.1.2 httpx==0.26.0 ifaddr==0.2.0 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index d6f956cb734..122592994ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1041,7 +1041,7 @@ holidays==0.39 home-assistant-frontend==20240102.0 # homeassistant.components.conversation -home-assistant-intents==2023.12.05 +home-assistant-intents==2024.1.2 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d5f4763da9..45846bbfc2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -834,7 +834,7 @@ holidays==0.39 home-assistant-frontend==20240102.0 # homeassistant.components.conversation -home-assistant-intents==2023.12.05 +home-assistant-intents==2024.1.2 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index f7145a9ab56..35d967f37da 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -249,7 +249,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Turned on light', + 'speech': 'Turned on the light', }), }), }), @@ -279,7 +279,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Turned on light', + 'speech': 'Turned on the light', }), }), }), @@ -309,7 +309,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Turned on light', + 'speech': 'Turned on the light', }), }), }), @@ -339,7 +339,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Turned on light', + 'speech': 'Turned on the light', }), }), }), diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index fdbf10b0c7f..0f47f9ac3d9 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -85,7 +85,7 @@ async def test_http_processing_intent( "speech": { "plain": { "extra_data": None, - "speech": "Turned on light", + "speech": "Turned on the light", } }, "language": hass.config.language, @@ -135,7 +135,7 @@ async def test_http_processing_intent_target_ha_agent( "speech": { "plain": { "extra_data": None, - "speech": "Turned on light", + "speech": "Turned on the light", } }, "language": hass.config.language, @@ -186,7 +186,7 @@ async def test_http_processing_intent_entity_added_removed( "speech": { "plain": { "extra_data": None, - "speech": "Turned on light", + "speech": "Turned on the light", } }, "language": hass.config.language, @@ -222,7 +222,7 @@ async def test_http_processing_intent_entity_added_removed( "speech": { "plain": { "extra_data": None, - "speech": "Turned on light", + "speech": "Turned on the light", } }, "language": hass.config.language, @@ -255,7 +255,7 @@ async def test_http_processing_intent_entity_added_removed( "speech": { "plain": { "extra_data": None, - "speech": "Turned on light", + "speech": "Turned on the light", } }, "language": hass.config.language, @@ -331,7 +331,7 @@ async def test_http_processing_intent_alias_added_removed( "speech": { "plain": { "extra_data": None, - "speech": "Turned on light", + "speech": "Turned on the light", } }, "language": hass.config.language, @@ -364,7 +364,7 @@ async def test_http_processing_intent_alias_added_removed( "speech": { "plain": { "extra_data": None, - "speech": "Turned on light", + "speech": "Turned on the light", } }, "language": hass.config.language, @@ -449,7 +449,7 @@ async def test_http_processing_intent_entity_renamed( "speech": { "plain": { "extra_data": None, - "speech": "Turned on light", + "speech": "Turned on the light", } }, "language": hass.config.language, @@ -483,7 +483,7 @@ async def test_http_processing_intent_entity_renamed( "speech": { "plain": { "extra_data": None, - "speech": "Turned on light", + "speech": "Turned on the light", } }, "language": hass.config.language, @@ -540,7 +540,7 @@ async def test_http_processing_intent_entity_renamed( "speech": { "plain": { "extra_data": None, - "speech": "Turned on light", + "speech": "Turned on the light", } }, "language": hass.config.language, @@ -624,7 +624,7 @@ async def test_http_processing_intent_entity_exposed( "speech": { "plain": { "extra_data": None, - "speech": "Turned on light", + "speech": "Turned on the light", } }, "language": hass.config.language, @@ -656,7 +656,7 @@ async def test_http_processing_intent_entity_exposed( "speech": { "plain": { "extra_data": None, - "speech": "Turned on light", + "speech": "Turned on the light", } }, "language": hass.config.language, @@ -740,7 +740,7 @@ async def test_http_processing_intent_entity_exposed( "speech": { "plain": { "extra_data": None, - "speech": "Turned on light", + "speech": "Turned on the light", } }, "language": hass.config.language, @@ -769,7 +769,7 @@ async def test_http_processing_intent_entity_exposed( "speech": { "plain": { "extra_data": None, - "speech": "Turned on light", + "speech": "Turned on the light", } }, "language": hass.config.language, @@ -855,7 +855,7 @@ async def test_http_processing_intent_conversion_not_expose_new( "speech": { "plain": { "extra_data": None, - "speech": "Turned on light", + "speech": "Turned on the light", } }, "language": hass.config.language, From 5986967db7c73af6f6e183322d0dc4172aa65aec Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 3 Jan 2024 06:40:42 +0100 Subject: [PATCH 100/112] Avoid triggering ping device tracker `home` after restore (#106913) --- homeassistant/components/ping/device_tracker.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index d627082a499..6b904043b30 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -136,7 +136,7 @@ async def async_setup_entry( class PingDeviceTracker(CoordinatorEntity[PingUpdateCoordinator], ScannerEntity): """Representation of a Ping device tracker.""" - _first_offline: datetime | None = None + _last_seen: datetime | None = None def __init__( self, config_entry: ConfigEntry, coordinator: PingUpdateCoordinator @@ -171,14 +171,12 @@ class PingDeviceTracker(CoordinatorEntity[PingUpdateCoordinator], ScannerEntity) def is_connected(self) -> bool: """Return true if ping returns is_alive or considered home.""" if self.coordinator.data.is_alive: - self._first_offline = None - return True + self._last_seen = dt_util.utcnow() - now = dt_util.utcnow() - if self._first_offline is None: - self._first_offline = now - - return (self._first_offline + self._consider_home_interval) > now + return ( + self._last_seen is not None + and (dt_util.utcnow() - self._last_seen) < self._consider_home_interval + ) @property def entity_registry_enabled_default(self) -> bool: From 0226b3f10c28b535101184a579e9856a1c84ef7c Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 2 Jan 2024 23:47:32 +0100 Subject: [PATCH 101/112] Remove group_members from significant attributes in media player (#106916) --- homeassistant/components/media_player/significant_change.py | 2 -- tests/components/media_player/test_significant_change.py | 6 +++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/significant_change.py b/homeassistant/components/media_player/significant_change.py index adc96fc8b83..43a253d9220 100644 --- a/homeassistant/components/media_player/significant_change.py +++ b/homeassistant/components/media_player/significant_change.py @@ -11,7 +11,6 @@ from homeassistant.helpers.significant_change import ( from . import ( ATTR_ENTITY_PICTURE_LOCAL, - ATTR_GROUP_MEMBERS, ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT, ATTR_MEDIA_VOLUME_LEVEL, @@ -25,7 +24,6 @@ INSIGNIFICANT_ATTRIBUTES: set[str] = { SIGNIFICANT_ATTRIBUTES: set[str] = { ATTR_ENTITY_PICTURE_LOCAL, - ATTR_GROUP_MEMBERS, *ATTR_TO_PROPERTY, } - INSIGNIFICANT_ATTRIBUTES diff --git a/tests/components/media_player/test_significant_change.py b/tests/components/media_player/test_significant_change.py index 1b0ac6fe5aa..233f133c342 100644 --- a/tests/components/media_player/test_significant_change.py +++ b/tests/components/media_player/test_significant_change.py @@ -51,7 +51,11 @@ async def test_significant_state_change() -> None: {ATTR_ENTITY_PICTURE_LOCAL: "new_value"}, True, ), - ({ATTR_GROUP_MEMBERS: "old_value"}, {ATTR_GROUP_MEMBERS: "new_value"}, True), + ( + {ATTR_GROUP_MEMBERS: ["old1", "old2"]}, + {ATTR_GROUP_MEMBERS: ["old1", "new"]}, + False, + ), ({ATTR_INPUT_SOURCE: "old_value"}, {ATTR_INPUT_SOURCE: "new_value"}, True), ( {ATTR_MEDIA_ALBUM_ARTIST: "old_value"}, From f98bbf88b19bc2cebf7dd8f2351f0ec06a3fec0a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Jan 2024 09:56:28 +0100 Subject: [PATCH 102/112] Bump version to 2024.1.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 a56835f1fbc..f5e7f480427 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Any, Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index e4d9a6b0c9a..b604280d8c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.0b5" +version = "2024.1.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 3295722e70708d44bb428419da73e0f72358aa6e Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Wed, 3 Jan 2024 10:30:32 +0100 Subject: [PATCH 103/112] Change Tado deprecation version to 2024.7.0 (#106938) Change version to 2024.7.0 --- homeassistant/components/tado/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index c10ab118060..9c50318639d 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -62,7 +62,7 @@ async def async_get_scanner( hass, DOMAIN, "deprecated_yaml_import_device_tracker", - breaks_in_ha_version="2024.6.0", + breaks_in_ha_version="2024.7.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key=translation_key, From 2b43f5fcdae3fb4028a914df8f58b6f856238fc8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Jan 2024 11:32:48 +0100 Subject: [PATCH 104/112] Update frontend to 20240103.0 (#106942) --- 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 7579426e1e1..42cd3eb1f33 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==20240102.0"] + "requirements": ["home-assistant-frontend==20240103.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 77aa0c699cf..9a389e61811 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ habluetooth==2.0.1 hass-nabucasa==0.75.1 hassil==1.5.1 home-assistant-bluetooth==1.11.0 -home-assistant-frontend==20240102.0 +home-assistant-frontend==20240103.0 home-assistant-intents==2024.1.2 httpx==0.26.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 122592994ba..a05ae658de6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1038,7 +1038,7 @@ hole==0.8.0 holidays==0.39 # homeassistant.components.frontend -home-assistant-frontend==20240102.0 +home-assistant-frontend==20240103.0 # homeassistant.components.conversation home-assistant-intents==2024.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45846bbfc2a..7f7eaa8aaa7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -831,7 +831,7 @@ hole==0.8.0 holidays==0.39 # homeassistant.components.frontend -home-assistant-frontend==20240102.0 +home-assistant-frontend==20240103.0 # homeassistant.components.conversation home-assistant-intents==2024.1.2 From 2be72fd891be0831a8df54b4a4e08a7946628ad5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Jan 2024 11:35:43 +0100 Subject: [PATCH 105/112] Bump version to 2024.1.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 f5e7f480427..7cdcd452385 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Any, Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index b604280d8c0..022046f3e51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.0b6" +version = "2024.1.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e74554243199ac2a5325055112d0d764b38689d5 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 3 Jan 2024 12:29:05 +0100 Subject: [PATCH 106/112] Fix creating cloud hook twice for mobile_app (#106945) --- homeassistant/components/mobile_app/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 94d268f9412..cb5c0ae5c3d 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -118,7 +118,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await create_cloud_hook() if ( - CONF_CLOUDHOOK_URL not in registration + CONF_CLOUDHOOK_URL not in entry.data and cloud.async_active_subscription(hass) and cloud.async_is_connected(hass) ): From 4595c3edaab3572299fc3b056ae6e5a13c54c5f6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Jan 2024 13:18:09 +0100 Subject: [PATCH 107/112] Update frontend to 20240103.1 (#106948) --- 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 42cd3eb1f33..9a753edd059 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==20240103.0"] + "requirements": ["home-assistant-frontend==20240103.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9a389e61811..62a10157d97 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ habluetooth==2.0.1 hass-nabucasa==0.75.1 hassil==1.5.1 home-assistant-bluetooth==1.11.0 -home-assistant-frontend==20240103.0 +home-assistant-frontend==20240103.1 home-assistant-intents==2024.1.2 httpx==0.26.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index a05ae658de6..013a53dfd47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1038,7 +1038,7 @@ hole==0.8.0 holidays==0.39 # homeassistant.components.frontend -home-assistant-frontend==20240103.0 +home-assistant-frontend==20240103.1 # homeassistant.components.conversation home-assistant-intents==2024.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f7eaa8aaa7..dc1c6c28bca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -831,7 +831,7 @@ hole==0.8.0 holidays==0.39 # homeassistant.components.frontend -home-assistant-frontend==20240103.0 +home-assistant-frontend==20240103.1 # homeassistant.components.conversation home-assistant-intents==2024.1.2 From 9d697c502663a89c8c994fb4d93a2bbb7e31c445 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 3 Jan 2024 13:42:55 +0100 Subject: [PATCH 108/112] Only set precision in modbus if not configured. (#106952) Only set precision if not configured. --- homeassistant/components/modbus/__init__.py | 2 +- homeassistant/components/modbus/base_platform.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 89a50862b6c..cc1b3c74356 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -190,7 +190,7 @@ BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend( vol.Optional(CONF_STRUCTURE): cv.string, vol.Optional(CONF_SCALE, default=1): number_validator, vol.Optional(CONF_OFFSET, default=0): number_validator, - vol.Optional(CONF_PRECISION, default=0): cv.positive_int, + vol.Optional(CONF_PRECISION): cv.positive_int, vol.Optional( CONF_SWAP, ): vol.In( diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 1c7c8f65140..d3ec06bbdd7 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -185,10 +185,8 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): self._swap = config[CONF_SWAP] self._data_type = config[CONF_DATA_TYPE] self._structure: str = config[CONF_STRUCTURE] - self._precision = config[CONF_PRECISION] self._scale = config[CONF_SCALE] - if self._scale < 1 and not self._precision: - self._precision = 2 + self._precision = config.get(CONF_PRECISION, 2 if self._scale < 1 else 0) self._offset = config[CONF_OFFSET] self._slave_count = config.get(CONF_SLAVE_COUNT, None) or config.get( CONF_VIRTUAL_COUNT, 0 From 015752ff118e6e0ccccf6062e12e885098060de8 Mon Sep 17 00:00:00 2001 From: Jonas Fors Lellky Date: Wed, 3 Jan 2024 14:43:17 +0100 Subject: [PATCH 109/112] Set precision to halves in flexit_bacnet (#106959) flexit_bacnet: set precision to halves for target temperature --- homeassistant/components/flexit_bacnet/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/flexit_bacnet/climate.py b/homeassistant/components/flexit_bacnet/climate.py index 28f4a6ae178..c15cb59a6f3 100644 --- a/homeassistant/components/flexit_bacnet/climate.py +++ b/homeassistant/components/flexit_bacnet/climate.py @@ -19,7 +19,7 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo @@ -65,7 +65,7 @@ class FlexitClimateEntity(ClimateEntity): ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE ) - _attr_target_temperature_step = PRECISION_WHOLE + _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = UnitOfTemperature.CELSIUS def __init__(self, device: FlexitBACnet) -> None: From cd8d95a04d5b76a682e7bc2fa731bc3002ebc6fe Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Jan 2024 15:28:22 +0100 Subject: [PATCH 110/112] Update frontend to 20240103.3 (#106963) --- 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 9a753edd059..52f3932237b 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==20240103.1"] + "requirements": ["home-assistant-frontend==20240103.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 62a10157d97..0f069a0e0b5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ habluetooth==2.0.1 hass-nabucasa==0.75.1 hassil==1.5.1 home-assistant-bluetooth==1.11.0 -home-assistant-frontend==20240103.1 +home-assistant-frontend==20240103.3 home-assistant-intents==2024.1.2 httpx==0.26.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 013a53dfd47..46b89f491a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1038,7 +1038,7 @@ hole==0.8.0 holidays==0.39 # homeassistant.components.frontend -home-assistant-frontend==20240103.1 +home-assistant-frontend==20240103.3 # homeassistant.components.conversation home-assistant-intents==2024.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc1c6c28bca..ee1a9b2ac35 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -831,7 +831,7 @@ hole==0.8.0 holidays==0.39 # homeassistant.components.frontend -home-assistant-frontend==20240103.1 +home-assistant-frontend==20240103.3 # homeassistant.components.conversation home-assistant-intents==2024.1.2 From 8cf47c4925071fea3fe97f5429f7609e1aa6c55c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Jan 2024 15:29:59 +0100 Subject: [PATCH 111/112] Bump version to 2024.1.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 7cdcd452385..cf2eb18a4d9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Any, Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 022046f3e51..004b5b1a35a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.0b7" +version = "2024.1.0b8" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 15cecbd4a4316184b797181ae2af22c205e89d13 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Jan 2024 17:13:22 +0100 Subject: [PATCH 112/112] Bump version to 2024.1.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 cf2eb18a4d9..6afa0430ba3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Any, Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -PATCH_VERSION: Final = "0b8" +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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 004b5b1a35a..ec313a5bcf6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.0b8" +version = "2024.1.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"