From a42c0230c9796d5401e131d7eeeface98c3f80e0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 1 Dec 2024 18:26:29 +0100 Subject: [PATCH 01/17] Simplify recorder RecorderRunsManager (#131785) --- .../recorder/table_managers/recorder_runs.py | 73 +++---------------- .../table_managers/test_recorder_runs.py | 32 ++------ 2 files changed, 15 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/recorder/table_managers/recorder_runs.py b/homeassistant/components/recorder/table_managers/recorder_runs.py index b0b9818118ba0..4ca0aa18b88ea 100644 --- a/homeassistant/components/recorder/table_managers/recorder_runs.py +++ b/homeassistant/components/recorder/table_managers/recorder_runs.py @@ -2,8 +2,6 @@ from __future__ import annotations -import bisect -from dataclasses import dataclass from datetime import datetime from sqlalchemy.orm.session import Session @@ -11,34 +9,6 @@ import homeassistant.util.dt as dt_util from ..db_schema import RecorderRuns -from ..models import process_timestamp - - -def _find_recorder_run_for_start_time( - run_history: _RecorderRunsHistory, start: datetime -) -> RecorderRuns | None: - """Find the recorder run for a start time in _RecorderRunsHistory.""" - run_timestamps = run_history.run_timestamps - runs_by_timestamp = run_history.runs_by_timestamp - - # bisect_left tells us were we would insert - # a value in the list of runs after the start timestamp. - # - # The run before that (idx-1) is when the run started - # - # If idx is 0, history never ran before the start timestamp - # - if idx := bisect.bisect_left(run_timestamps, start.timestamp()): - return runs_by_timestamp[run_timestamps[idx - 1]] - return None - - -@dataclass(frozen=True) -class _RecorderRunsHistory: - """Bisectable history of RecorderRuns.""" - - run_timestamps: list[int] - runs_by_timestamp: dict[int, RecorderRuns] class RecorderRunsManager: @@ -48,7 +18,7 @@ def __init__(self) -> None: """Track recorder run history.""" self._recording_start = dt_util.utcnow() self._current_run_info: RecorderRuns | None = None - self._run_history = _RecorderRunsHistory([], {}) + self._first_run: RecorderRuns | None = None @property def recording_start(self) -> datetime: @@ -58,9 +28,7 @@ def recording_start(self) -> datetime: @property def first(self) -> RecorderRuns: """Get the first run.""" - if runs_by_timestamp := self._run_history.runs_by_timestamp: - return next(iter(runs_by_timestamp.values())) - return self.current + return self._first_run or self.current @property def current(self) -> RecorderRuns: @@ -78,15 +46,6 @@ def active(self) -> bool: """Return if a run is active.""" return self._current_run_info is not None - def get(self, start: datetime) -> RecorderRuns | None: - """Return the recorder run that started before or at start. - - If the first run started after the start, return None - """ - if start >= self.recording_start: - return self.current - return _find_recorder_run_for_start_time(self._run_history, start) - def start(self, session: Session) -> None: """Start a new run. @@ -122,31 +81,17 @@ def load_from_db(self, session: Session) -> None: Must run in the recorder thread. """ - run_timestamps: list[int] = [] - runs_by_timestamp: dict[int, RecorderRuns] = {} - - for run in session.query(RecorderRuns).order_by(RecorderRuns.start.asc()).all(): + if ( + run := session.query(RecorderRuns) + .order_by(RecorderRuns.start.asc()) + .first() + ): session.expunge(run) - if run_dt := process_timestamp(run.start): - # Not sure if this is correct or runs_by_timestamp annotation should be changed - timestamp = int(run_dt.timestamp()) - run_timestamps.append(timestamp) - runs_by_timestamp[timestamp] = run - - # - # self._run_history is accessed in get() - # which is allowed to be called from any thread - # - # We use a dataclass to ensure that when we update - # run_timestamps and runs_by_timestamp - # are never out of sync with each other. - # - self._run_history = _RecorderRunsHistory(run_timestamps, runs_by_timestamp) + self._first_run = run def clear(self) -> None: """Clear the current run after ending it. Must run in the recorder thread. """ - if self._current_run_info: - self._current_run_info = None + self._current_run_info = None diff --git a/tests/components/recorder/table_managers/test_recorder_runs.py b/tests/components/recorder/table_managers/test_recorder_runs.py index 41f3a8fef4da9..e79def01badb7 100644 --- a/tests/components/recorder/table_managers/test_recorder_runs.py +++ b/tests/components/recorder/table_managers/test_recorder_runs.py @@ -21,6 +21,11 @@ async def test_run_history(recorder_mock: Recorder, hass: HomeAssistant) -> None two_days_ago = now - timedelta(days=2) one_day_ago = now - timedelta(days=1) + # Test that the first run falls back to the current run + assert process_timestamp( + instance.recorder_runs_manager.first.start + ) == process_timestamp(instance.recorder_runs_manager.current.start) + with instance.get_session() as session: session.add(RecorderRuns(start=three_days_ago, created=three_days_ago)) session.add(RecorderRuns(start=two_days_ago, created=two_days_ago)) @@ -29,32 +34,7 @@ async def test_run_history(recorder_mock: Recorder, hass: HomeAssistant) -> None instance.recorder_runs_manager.load_from_db(session) assert ( - process_timestamp( - instance.recorder_runs_manager.get( - three_days_ago + timedelta(microseconds=1) - ).start - ) - == three_days_ago - ) - assert ( - process_timestamp( - instance.recorder_runs_manager.get( - two_days_ago + timedelta(microseconds=1) - ).start - ) - == two_days_ago - ) - assert ( - process_timestamp( - instance.recorder_runs_manager.get( - one_day_ago + timedelta(microseconds=1) - ).start - ) - == one_day_ago - ) - assert ( - process_timestamp(instance.recorder_runs_manager.get(now).start) - == instance.recorder_runs_manager.recording_start + process_timestamp(instance.recorder_runs_manager.first.start) == three_days_ago ) From cce7b9ac3448bf97d05dac898855124a29060940 Mon Sep 17 00:00:00 2001 From: rappenze Date: Sun, 15 Dec 2024 11:02:26 +0100 Subject: [PATCH 02/17] Fix fibaro climate hvac mode (#132508) --- homeassistant/components/fibaro/climate.py | 6 +- tests/components/fibaro/conftest.py | 56 +++++++++ tests/components/fibaro/test_climate.py | 134 +++++++++++++++++++++ 3 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 tests/components/fibaro/test_climate.py diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 0bfc22233173f..6948dc9122f77 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -274,7 +274,9 @@ def hvac_mode(self) -> HVACMode | None: if isinstance(fibaro_operation_mode, str): with suppress(ValueError): return HVACMode(fibaro_operation_mode.lower()) - elif fibaro_operation_mode in OPMODES_HVAC: + # when the mode cannot be instantiated a preset_mode is selected + return HVACMode.AUTO + if fibaro_operation_mode in OPMODES_HVAC: return OPMODES_HVAC[fibaro_operation_mode] return None @@ -282,8 +284,6 @@ def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target operation mode.""" if not self._op_mode_device: return - if self.preset_mode: - return if "setOperatingMode" in self._op_mode_device.fibaro_device.actions: self._op_mode_device.action("setOperatingMode", HA_OPMODES_HVAC[hvac_mode]) diff --git a/tests/components/fibaro/conftest.py b/tests/components/fibaro/conftest.py index 1976a8f310b2e..583c44a41e640 100644 --- a/tests/components/fibaro/conftest.py +++ b/tests/components/fibaro/conftest.py @@ -129,6 +129,62 @@ def mock_light() -> Mock: return light +@pytest.fixture +def mock_thermostat() -> Mock: + """Fixture for a thermostat.""" + climate = Mock() + climate.fibaro_id = 4 + climate.parent_fibaro_id = 0 + climate.name = "Test climate" + climate.room_id = 1 + climate.dead = False + climate.visible = True + climate.enabled = True + climate.type = "com.fibaro.thermostatDanfoss" + climate.base_type = "com.fibaro.device" + climate.properties = {"manufacturer": ""} + climate.actions = {"setThermostatMode": 1} + climate.supported_features = {} + climate.has_supported_thermostat_modes = True + climate.supported_thermostat_modes = ["Off", "Heat", "CustomerSpecific"] + climate.has_operating_mode = False + climate.has_thermostat_mode = True + climate.thermostat_mode = "CustomerSpecific" + value_mock = Mock() + value_mock.has_value = True + value_mock.int_value.return_value = 20 + climate.value = value_mock + return climate + + +@pytest.fixture +def mock_thermostat_with_operating_mode() -> Mock: + """Fixture for a thermostat.""" + climate = Mock() + climate.fibaro_id = 4 + climate.parent_fibaro_id = 0 + climate.name = "Test climate" + climate.room_id = 1 + climate.dead = False + climate.visible = True + climate.enabled = True + climate.type = "com.fibaro.thermostatDanfoss" + climate.base_type = "com.fibaro.device" + climate.properties = {"manufacturer": ""} + climate.actions = {"setOperationMode": 1} + climate.supported_features = {} + climate.has_supported_operating_modes = True + climate.supported_operating_modes = [0, 1, 15] + climate.has_operating_mode = True + climate.operating_mode = 15 + climate.has_thermostat_mode = False + value_mock = Mock() + value_mock.has_value = True + value_mock.int_value.return_value = 20 + climate.value = value_mock + return climate + + @pytest.fixture def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: """Return the default mocked config entry.""" diff --git a/tests/components/fibaro/test_climate.py b/tests/components/fibaro/test_climate.py new file mode 100644 index 0000000000000..31022e19a0819 --- /dev/null +++ b/tests/components/fibaro/test_climate.py @@ -0,0 +1,134 @@ +"""Test the Fibaro climate platform.""" + +from unittest.mock import Mock, patch + +from homeassistant.components.climate import ClimateEntityFeature, HVACMode +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import init_integration + +from tests.common import MockConfigEntry + + +async def test_climate_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_fibaro_client: Mock, + mock_config_entry: MockConfigEntry, + mock_thermostat: Mock, + mock_room: Mock, +) -> None: + """Test that the climate creates an entity.""" + + # Arrange + mock_fibaro_client.read_rooms.return_value = [mock_room] + mock_fibaro_client.read_devices.return_value = [mock_thermostat] + + with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.CLIMATE]): + # Act + await init_integration(hass, mock_config_entry) + # Assert + entry = entity_registry.async_get("climate.room_1_test_climate_4") + assert entry + assert entry.unique_id == "hc2_111111.4" + assert entry.original_name == "Room 1 Test climate" + assert entry.supported_features == ( + ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.PRESET_MODE + ) + + +async def test_hvac_mode_preset( + hass: HomeAssistant, + mock_fibaro_client: Mock, + mock_config_entry: MockConfigEntry, + mock_thermostat: Mock, + mock_room: Mock, +) -> None: + """Test that the climate state is auto when a preset is selected.""" + + # Arrange + mock_fibaro_client.read_rooms.return_value = [mock_room] + mock_fibaro_client.read_devices.return_value = [mock_thermostat] + + with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.CLIMATE]): + # Act + await init_integration(hass, mock_config_entry) + # Assert + state = hass.states.get("climate.room_1_test_climate_4") + assert state.state == HVACMode.AUTO + assert state.attributes["preset_mode"] == "CustomerSpecific" + + +async def test_hvac_mode_heat( + hass: HomeAssistant, + mock_fibaro_client: Mock, + mock_config_entry: MockConfigEntry, + mock_thermostat: Mock, + mock_room: Mock, +) -> None: + """Test that the preset mode is None if a hvac mode is active.""" + + # Arrange + mock_thermostat.thermostat_mode = "Heat" + mock_fibaro_client.read_rooms.return_value = [mock_room] + mock_fibaro_client.read_devices.return_value = [mock_thermostat] + + with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.CLIMATE]): + # Act + await init_integration(hass, mock_config_entry) + # Assert + state = hass.states.get("climate.room_1_test_climate_4") + assert state.state == HVACMode.HEAT + assert state.attributes["preset_mode"] is None + + +async def test_set_hvac_mode( + hass: HomeAssistant, + mock_fibaro_client: Mock, + mock_config_entry: MockConfigEntry, + mock_thermostat: Mock, + mock_room: Mock, +) -> None: + """Test that set_hvac_mode() works.""" + + # Arrange + mock_fibaro_client.read_rooms.return_value = [mock_room] + mock_fibaro_client.read_devices.return_value = [mock_thermostat] + + with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.CLIMATE]): + # Act + await init_integration(hass, mock_config_entry) + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": "climate.room_1_test_climate_4", "hvac_mode": HVACMode.HEAT}, + blocking=True, + ) + + # Assert + mock_thermostat.execute_action.assert_called_once() + + +async def test_hvac_mode_with_operation_mode_support( + hass: HomeAssistant, + mock_fibaro_client: Mock, + mock_config_entry: MockConfigEntry, + mock_thermostat_with_operating_mode: Mock, + mock_room: Mock, +) -> None: + """Test that operating mode works.""" + + # Arrange + mock_fibaro_client.read_rooms.return_value = [mock_room] + mock_fibaro_client.read_devices.return_value = [mock_thermostat_with_operating_mode] + + with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.CLIMATE]): + # Act + await init_integration(hass, mock_config_entry) + # Assert + state = hass.states.get("climate.room_1_test_climate_4") + assert state.state == HVACMode.AUTO From 8286ec9e603334840e2dbd8ccf323a820aee5364 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 13 Dec 2024 13:30:22 +0100 Subject: [PATCH 03/17] Bump yt-dlp to 2024.12.13 (#133129) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 195dc678bc2ca..21c0760757371 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp[default]==2024.12.06"], + "requirements": ["yt-dlp[default]==2024.12.13"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 38239d22af2e2..984e1b1374c08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3066,7 +3066,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp[default]==2024.12.06 +yt-dlp[default]==2024.12.13 # homeassistant.components.zamg zamg==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c76684a4a1e9..72399d331bb0c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2452,7 +2452,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp[default]==2024.12.06 +yt-dlp[default]==2024.12.13 # homeassistant.components.zamg zamg==0.3.6 From cdea9b5d3a387a3d39e90aa3736aca937685e52e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 17 Dec 2024 12:41:18 +0100 Subject: [PATCH 04/17] Fix strptime in python_script (#133159) Co-authored-by: Erik Montnemery --- .../components/python_script/__init__.py | 17 +++++++++++++ tests/components/python_script/test_init.py | 24 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 70e9c5b0d29a7..af7732780293d 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -1,5 +1,6 @@ """Component to allow running Python scripts.""" +from collections.abc import Mapping, Sequence import datetime import glob import logging @@ -7,6 +8,7 @@ import operator import os import time +import types from typing import Any from RestrictedPython import ( @@ -167,6 +169,20 @@ def python_script_service_handler(call: ServiceCall) -> ServiceResponse: } +def guarded_import( + name: str, + globals: Mapping[str, object] | None = None, + locals: Mapping[str, object] | None = None, + fromlist: Sequence[str] = (), + level: int = 0, +) -> types.ModuleType: + """Guard imports.""" + # Allow import of _strptime needed by datetime.datetime.strptime + if name == "_strptime": + return __import__(name, globals, locals, fromlist, level) + raise ScriptError(f"Not allowed to import {name}") + + def guarded_inplacevar(op: str, target: Any, operand: Any) -> Any: """Implement augmented-assign (+=, -=, etc.) operators for restricted code. @@ -232,6 +248,7 @@ def protected_getattr(obj, name, default=None): return getattr(obj, name, default) extra_builtins = { + "__import__": guarded_import, "datetime": datetime, "sorted": sorted, "time": TimeWrapper(), diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index c4dc00c448aae..2d151b4b81e6a 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -688,3 +688,27 @@ async def test_prohibited_augmented_assignment_operations( hass.async_add_executor_job(execute, hass, "aug_assign_prohibited.py", case, {}) await hass.async_block_till_done(wait_background_tasks=True) assert error in caplog.text + + +async def test_import_allow_strptime( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test calling datetime.datetime.strptime works.""" + source = """ +test_date = datetime.datetime.strptime('2024-04-01', '%Y-%m-%d') +logger.info(f'Date {test_date}') + """ + hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_block_till_done(wait_background_tasks=True) + assert "Error executing script: Not allowed to import _strptime" not in caplog.text + assert "Date 2024-04-01 00:00:00" in caplog.text + + +async def test_no_other_imports_allowed( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test imports are not allowed.""" + source = "import sys" + hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_block_till_done(wait_background_tasks=True) + assert "Error executing script: Not allowed to import sys" in caplog.text From 223817a7fbbaa597e837d648b89b3d9326479b9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Dec 2024 16:37:26 -0500 Subject: [PATCH 05/17] Bump yalexs-ble to 2.5.4 (#133172) --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yale/manifest.json | 2 +- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 99dbbc0ed9c8d..ed2c8007ee87b 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==8.10.0", "yalexs-ble==2.5.2"] + "requirements": ["yalexs==8.10.0", "yalexs-ble==2.5.4"] } diff --git a/homeassistant/components/yale/manifest.json b/homeassistant/components/yale/manifest.json index 474ed36e90cfb..2ed1f4b5c4397 100644 --- a/homeassistant/components/yale/manifest.json +++ b/homeassistant/components/yale/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/yale", "iot_class": "cloud_push", "loggers": ["socketio", "engineio", "yalexs"], - "requirements": ["yalexs==8.10.0", "yalexs-ble==2.5.2"] + "requirements": ["yalexs==8.10.0", "yalexs-ble==2.5.4"] } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 95d28cd53721d..1472f9035eac3 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.5.2"] + "requirements": ["yalexs-ble==2.5.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 984e1b1374c08..5536c7723f528 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3044,7 +3044,7 @@ yalesmartalarmclient==0.4.3 # homeassistant.components.august # homeassistant.components.yale # homeassistant.components.yalexs_ble -yalexs-ble==2.5.2 +yalexs-ble==2.5.4 # homeassistant.components.august # homeassistant.components.yale diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72399d331bb0c..e3df6637649c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2433,7 +2433,7 @@ yalesmartalarmclient==0.4.3 # homeassistant.components.august # homeassistant.components.yale # homeassistant.components.yalexs_ble -yalexs-ble==2.5.2 +yalexs-ble==2.5.4 # homeassistant.components.august # homeassistant.components.yale From 9b02db008e389e39eff0821609ac1de940fe2954 Mon Sep 17 00:00:00 2001 From: Conor Eager Date: Mon, 16 Dec 2024 05:43:21 +1300 Subject: [PATCH 06/17] Bump starlink-grpc-core to 1.2.1 to fix missing ping (#133183) --- homeassistant/components/starlink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/starlink/manifest.json b/homeassistant/components/starlink/manifest.json index 070cbf1b44c22..15bad3ebc2e92 100644 --- a/homeassistant/components/starlink/manifest.json +++ b/homeassistant/components/starlink/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/starlink", "iot_class": "local_polling", - "requirements": ["starlink-grpc-core==1.2.0"] + "requirements": ["starlink-grpc-core==1.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5536c7723f528..8b1846afc4ff2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2734,7 +2734,7 @@ starline==0.1.5 starlingbank==3.2 # homeassistant.components.starlink -starlink-grpc-core==1.2.0 +starlink-grpc-core==1.2.2 # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3df6637649c4..07d6a80270ff7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2183,7 +2183,7 @@ srpenergy==1.3.6 starline==0.1.5 # homeassistant.components.starlink -starlink-grpc-core==1.2.0 +starlink-grpc-core==1.2.2 # homeassistant.components.statsd statsd==3.2.1 From 9b0a4897539c2b1e8551953372c1730531a6414c Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 15 Dec 2024 20:24:41 +1100 Subject: [PATCH 07/17] Bump aiolifx to 1.1.2 and add new HomeKit product prefixes (#133191) Signed-off-by: Avi Miller --- homeassistant/components/lifx/manifest.json | 5 ++++- homeassistant/generated/zeroconf.py | 12 ++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index c7d8a27a1c713..2e16eb2082b67 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -23,6 +23,7 @@ "LIFX Ceiling", "LIFX Clean", "LIFX Color", + "LIFX Colour", "LIFX DLCOL", "LIFX Dlight", "LIFX DLWW", @@ -35,12 +36,14 @@ "LIFX Neon", "LIFX Nightvision", "LIFX PAR38", + "LIFX Permanent Outdoor", "LIFX Pls", "LIFX Plus", "LIFX Round", "LIFX Square", "LIFX String", "LIFX Tile", + "LIFX Tube", "LIFX White", "LIFX Z" ] @@ -48,7 +51,7 @@ "iot_class": "local_polling", "loggers": ["aiolifx", "aiolifx_effects", "bitstring"], "requirements": [ - "aiolifx==1.1.1", + "aiolifx==1.1.2", "aiolifx-effects==0.3.2", "aiolifx-themes==0.5.5" ] diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 5f7161a8245f1..749c1acfb1533 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -92,6 +92,10 @@ "always_discover": True, "domain": "lifx", }, + "LIFX Colour": { + "always_discover": True, + "domain": "lifx", + }, "LIFX DLCOL": { "always_discover": True, "domain": "lifx", @@ -140,6 +144,10 @@ "always_discover": True, "domain": "lifx", }, + "LIFX Permanent Outdoor": { + "always_discover": True, + "domain": "lifx", + }, "LIFX Pls": { "always_discover": True, "domain": "lifx", @@ -164,6 +172,10 @@ "always_discover": True, "domain": "lifx", }, + "LIFX Tube": { + "always_discover": True, + "domain": "lifx", + }, "LIFX White": { "always_discover": True, "domain": "lifx", diff --git a/requirements_all.txt b/requirements_all.txt index 8b1846afc4ff2..6929deaf14e57 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -286,7 +286,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.5.5 # homeassistant.components.lifx -aiolifx==1.1.1 +aiolifx==1.1.2 # homeassistant.components.livisi aiolivisi==0.0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07d6a80270ff7..83070eb803085 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -268,7 +268,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.5.5 # homeassistant.components.lifx -aiolifx==1.1.1 +aiolifx==1.1.2 # homeassistant.components.livisi aiolivisi==0.0.19 From ca47253d81e3774a95166323b2001e50f3d0be8d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 14 Dec 2024 14:21:19 +0100 Subject: [PATCH 08/17] Revert "Simplify recorder RecorderRunsManager" (#133201) Revert "Simplify recorder RecorderRunsManager (#131785)" This reverts commit cf0ee635077114961f6e508be56ce7620c718c18. --- .../recorder/table_managers/recorder_runs.py | 73 ++++++++++++++++--- .../table_managers/test_recorder_runs.py | 32 ++++++-- 2 files changed, 90 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/recorder/table_managers/recorder_runs.py b/homeassistant/components/recorder/table_managers/recorder_runs.py index 4ca0aa18b88ea..b0b9818118ba0 100644 --- a/homeassistant/components/recorder/table_managers/recorder_runs.py +++ b/homeassistant/components/recorder/table_managers/recorder_runs.py @@ -2,6 +2,8 @@ from __future__ import annotations +import bisect +from dataclasses import dataclass from datetime import datetime from sqlalchemy.orm.session import Session @@ -9,6 +11,34 @@ import homeassistant.util.dt as dt_util from ..db_schema import RecorderRuns +from ..models import process_timestamp + + +def _find_recorder_run_for_start_time( + run_history: _RecorderRunsHistory, start: datetime +) -> RecorderRuns | None: + """Find the recorder run for a start time in _RecorderRunsHistory.""" + run_timestamps = run_history.run_timestamps + runs_by_timestamp = run_history.runs_by_timestamp + + # bisect_left tells us were we would insert + # a value in the list of runs after the start timestamp. + # + # The run before that (idx-1) is when the run started + # + # If idx is 0, history never ran before the start timestamp + # + if idx := bisect.bisect_left(run_timestamps, start.timestamp()): + return runs_by_timestamp[run_timestamps[idx - 1]] + return None + + +@dataclass(frozen=True) +class _RecorderRunsHistory: + """Bisectable history of RecorderRuns.""" + + run_timestamps: list[int] + runs_by_timestamp: dict[int, RecorderRuns] class RecorderRunsManager: @@ -18,7 +48,7 @@ def __init__(self) -> None: """Track recorder run history.""" self._recording_start = dt_util.utcnow() self._current_run_info: RecorderRuns | None = None - self._first_run: RecorderRuns | None = None + self._run_history = _RecorderRunsHistory([], {}) @property def recording_start(self) -> datetime: @@ -28,7 +58,9 @@ def recording_start(self) -> datetime: @property def first(self) -> RecorderRuns: """Get the first run.""" - return self._first_run or self.current + if runs_by_timestamp := self._run_history.runs_by_timestamp: + return next(iter(runs_by_timestamp.values())) + return self.current @property def current(self) -> RecorderRuns: @@ -46,6 +78,15 @@ def active(self) -> bool: """Return if a run is active.""" return self._current_run_info is not None + def get(self, start: datetime) -> RecorderRuns | None: + """Return the recorder run that started before or at start. + + If the first run started after the start, return None + """ + if start >= self.recording_start: + return self.current + return _find_recorder_run_for_start_time(self._run_history, start) + def start(self, session: Session) -> None: """Start a new run. @@ -81,17 +122,31 @@ def load_from_db(self, session: Session) -> None: Must run in the recorder thread. """ - if ( - run := session.query(RecorderRuns) - .order_by(RecorderRuns.start.asc()) - .first() - ): + run_timestamps: list[int] = [] + runs_by_timestamp: dict[int, RecorderRuns] = {} + + for run in session.query(RecorderRuns).order_by(RecorderRuns.start.asc()).all(): session.expunge(run) - self._first_run = run + if run_dt := process_timestamp(run.start): + # Not sure if this is correct or runs_by_timestamp annotation should be changed + timestamp = int(run_dt.timestamp()) + run_timestamps.append(timestamp) + runs_by_timestamp[timestamp] = run + + # + # self._run_history is accessed in get() + # which is allowed to be called from any thread + # + # We use a dataclass to ensure that when we update + # run_timestamps and runs_by_timestamp + # are never out of sync with each other. + # + self._run_history = _RecorderRunsHistory(run_timestamps, runs_by_timestamp) def clear(self) -> None: """Clear the current run after ending it. Must run in the recorder thread. """ - self._current_run_info = None + if self._current_run_info: + self._current_run_info = None diff --git a/tests/components/recorder/table_managers/test_recorder_runs.py b/tests/components/recorder/table_managers/test_recorder_runs.py index e79def01badb7..41f3a8fef4da9 100644 --- a/tests/components/recorder/table_managers/test_recorder_runs.py +++ b/tests/components/recorder/table_managers/test_recorder_runs.py @@ -21,11 +21,6 @@ async def test_run_history(recorder_mock: Recorder, hass: HomeAssistant) -> None two_days_ago = now - timedelta(days=2) one_day_ago = now - timedelta(days=1) - # Test that the first run falls back to the current run - assert process_timestamp( - instance.recorder_runs_manager.first.start - ) == process_timestamp(instance.recorder_runs_manager.current.start) - with instance.get_session() as session: session.add(RecorderRuns(start=three_days_ago, created=three_days_ago)) session.add(RecorderRuns(start=two_days_ago, created=two_days_ago)) @@ -34,7 +29,32 @@ async def test_run_history(recorder_mock: Recorder, hass: HomeAssistant) -> None instance.recorder_runs_manager.load_from_db(session) assert ( - process_timestamp(instance.recorder_runs_manager.first.start) == three_days_ago + process_timestamp( + instance.recorder_runs_manager.get( + three_days_ago + timedelta(microseconds=1) + ).start + ) + == three_days_ago + ) + assert ( + process_timestamp( + instance.recorder_runs_manager.get( + two_days_ago + timedelta(microseconds=1) + ).start + ) + == two_days_ago + ) + assert ( + process_timestamp( + instance.recorder_runs_manager.get( + one_day_ago + timedelta(microseconds=1) + ).start + ) + == one_day_ago + ) + assert ( + process_timestamp(instance.recorder_runs_manager.get(now).start) + == instance.recorder_runs_manager.recording_start ) From 3b0ab421b0fa1b94326eea67c280a87ec7b8250f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 15 Dec 2024 12:28:32 +0100 Subject: [PATCH 09/17] Revert "Improve recorder history queries (#131702)" (#133203) --- homeassistant/components/history/__init__.py | 7 ++-- homeassistant/components/history/helpers.py | 13 ++++---- .../components/history/websocket_api.py | 7 ++-- homeassistant/components/recorder/core.py | 1 - .../components/recorder/history/legacy.py | 18 ++++++----- .../components/recorder/history/modern.py | 31 +++++++++--------- homeassistant/components/recorder/purge.py | 3 -- homeassistant/components/recorder/queries.py | 9 ------ .../recorder/table_managers/states.py | 32 ------------------- homeassistant/components/recorder/tasks.py | 2 ++ tests/components/recorder/test_purge.py | 17 ---------- 11 files changed, 38 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 7241e1fac9ad7..365be06fd2db6 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -22,7 +22,7 @@ from . import websocket_api from .const import DOMAIN -from .helpers import entities_may_have_state_changes_after, has_states_before +from .helpers import entities_may_have_state_changes_after, has_recorder_run_after CONF_ORDER = "use_include_order" @@ -107,10 +107,7 @@ async def get( no_attributes = "no_attributes" in request.query if ( - # has_states_before will return True if there are states older than - # end_time. If it's false, we know there are no states in the - # database up until end_time. - (end_time and not has_states_before(hass, end_time)) + (end_time and not has_recorder_run_after(hass, end_time)) or not include_start_time_state and entity_ids and not entities_may_have_state_changes_after( diff --git a/homeassistant/components/history/helpers.py b/homeassistant/components/history/helpers.py index 2010b7373ffa9..bd477e7e4ed43 100644 --- a/homeassistant/components/history/helpers.py +++ b/homeassistant/components/history/helpers.py @@ -6,6 +6,7 @@ from datetime import datetime as dt from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.models import process_timestamp from homeassistant.core import HomeAssistant @@ -25,10 +26,8 @@ def entities_may_have_state_changes_after( return False -def has_states_before(hass: HomeAssistant, run_time: dt) -> bool: - """Check if the recorder has states as old or older than run_time. - - Returns True if there may be such states. - """ - oldest_ts = get_instance(hass).states_manager.oldest_ts - return oldest_ts is not None and run_time.timestamp() >= oldest_ts +def has_recorder_run_after(hass: HomeAssistant, run_time: dt) -> bool: + """Check if the recorder has any runs after a specific time.""" + return run_time >= process_timestamp( + get_instance(hass).recorder_runs_manager.first.start + ) diff --git a/homeassistant/components/history/websocket_api.py b/homeassistant/components/history/websocket_api.py index 35f8ed5f1acdc..c85d975c3c97d 100644 --- a/homeassistant/components/history/websocket_api.py +++ b/homeassistant/components/history/websocket_api.py @@ -39,7 +39,7 @@ import homeassistant.util.dt as dt_util from .const import EVENT_COALESCE_TIME, MAX_PENDING_HISTORY_STATES -from .helpers import entities_may_have_state_changes_after, has_states_before +from .helpers import entities_may_have_state_changes_after, has_recorder_run_after _LOGGER = logging.getLogger(__name__) @@ -142,10 +142,7 @@ async def ws_get_history_during_period( no_attributes = msg["no_attributes"] if ( - # has_states_before will return True if there are states older than - # end_time. If it's false, we know there are no states in the - # database up until end_time. - (end_time and not has_states_before(hass, end_time)) + (end_time and not has_recorder_run_after(hass, end_time)) or not include_start_time_state and entity_ids and not entities_may_have_state_changes_after( diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 0c61f8a955ec9..0db677ac2af62 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -1431,7 +1431,6 @@ def _setup_run(self) -> None: with session_scope(session=self.get_session()) as session: end_incomplete_runs(session, self.recorder_runs_manager.recording_start) self.recorder_runs_manager.start(session) - self.states_manager.load_from_db(session) self._open_event_session() diff --git a/homeassistant/components/recorder/history/legacy.py b/homeassistant/components/recorder/history/legacy.py index 3a0fe79455b57..b59fc43c3d0b9 100644 --- a/homeassistant/components/recorder/history/legacy.py +++ b/homeassistant/components/recorder/history/legacy.py @@ -22,9 +22,9 @@ from homeassistant.helpers.recorder import get_instance import homeassistant.util.dt as dt_util -from ..db_schema import StateAttributes, States +from ..db_schema import RecorderRuns, StateAttributes, States from ..filters import Filters -from ..models import process_timestamp_to_utc_isoformat +from ..models import process_timestamp, process_timestamp_to_utc_isoformat from ..models.legacy import LegacyLazyState, legacy_row_to_compressed_state from ..util import execute_stmt_lambda_element, session_scope from .const import ( @@ -436,7 +436,7 @@ def get_last_state_changes( def _get_states_for_entities_stmt( - run_start_ts: float, + run_start: datetime, utc_point_in_time: datetime, entity_ids: list[str], no_attributes: bool, @@ -447,6 +447,7 @@ def _get_states_for_entities_stmt( ) # We got an include-list of entities, accelerate the query by filtering already # in the inner query. + run_start_ts = process_timestamp(run_start).timestamp() utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time) stmt += lambda q: q.join( ( @@ -482,7 +483,7 @@ def _get_rows_with_session( session: Session, utc_point_in_time: datetime, entity_ids: list[str], - *, + run: RecorderRuns | None = None, no_attributes: bool = False, ) -> Iterable[Row]: """Return the states at a specific point in time.""" @@ -494,16 +495,17 @@ def _get_rows_with_session( ), ) - oldest_ts = get_instance(hass).states_manager.oldest_ts + if run is None: + run = get_instance(hass).recorder_runs_manager.get(utc_point_in_time) - if oldest_ts is None or oldest_ts > utc_point_in_time.timestamp(): - # We don't have any states for the requested time + if run is None or process_timestamp(run.start) > utc_point_in_time: + # History did not run before utc_point_in_time return [] # We have more than one entity to look at so we need to do a query on states # since the last recorder run started. stmt = _get_states_for_entities_stmt( - oldest_ts, utc_point_in_time, entity_ids, no_attributes + run.start, utc_point_in_time, entity_ids, no_attributes ) return execute_stmt_lambda_element(session, stmt) diff --git a/homeassistant/components/recorder/history/modern.py b/homeassistant/components/recorder/history/modern.py index 902f1b5dc24b2..b44bec0d0ee13 100644 --- a/homeassistant/components/recorder/history/modern.py +++ b/homeassistant/components/recorder/history/modern.py @@ -34,6 +34,7 @@ LazyState, datetime_to_timestamp_or_none, extract_metadata_ids, + process_timestamp, row_to_compressed_state, ) from ..util import execute_stmt_lambda_element, session_scope @@ -245,9 +246,9 @@ def get_significant_states_with_session( if metadata_id is not None and split_entity_id(entity_id)[0] in SIGNIFICANT_DOMAINS ] - oldest_ts: float | None = None + run_start_ts: float | None = None if include_start_time_state and not ( - oldest_ts := _get_oldest_possible_ts(hass, start_time) + run_start_ts := _get_run_start_ts_for_utc_point_in_time(hass, start_time) ): include_start_time_state = False start_time_ts = dt_util.utc_to_timestamp(start_time) @@ -263,7 +264,7 @@ def get_significant_states_with_session( significant_changes_only, no_attributes, include_start_time_state, - oldest_ts, + run_start_ts, ), track_on=[ bool(single_metadata_id), @@ -410,9 +411,9 @@ def state_changes_during_period( entity_id_to_metadata_id: dict[str, int | None] = { entity_id: single_metadata_id } - oldest_ts: float | None = None + run_start_ts: float | None = None if include_start_time_state and not ( - oldest_ts := _get_oldest_possible_ts(hass, start_time) + run_start_ts := _get_run_start_ts_for_utc_point_in_time(hass, start_time) ): include_start_time_state = False start_time_ts = dt_util.utc_to_timestamp(start_time) @@ -425,7 +426,7 @@ def state_changes_during_period( no_attributes, limit, include_start_time_state, - oldest_ts, + run_start_ts, has_last_reported, ), track_on=[ @@ -599,17 +600,17 @@ def _get_start_time_state_for_entities_stmt( ) -def _get_oldest_possible_ts( +def _get_run_start_ts_for_utc_point_in_time( hass: HomeAssistant, utc_point_in_time: datetime ) -> float | None: - """Return the oldest possible timestamp. - - Returns None if there are no states as old as utc_point_in_time. - """ - - oldest_ts = get_instance(hass).states_manager.oldest_ts - if oldest_ts is not None and oldest_ts < utc_point_in_time.timestamp(): - return oldest_ts + """Return the start time of a run.""" + run = get_instance(hass).recorder_runs_manager.get(utc_point_in_time) + if ( + run is not None + and (run_start := process_timestamp(run.start)) < utc_point_in_time + ): + return run_start.timestamp() + # History did not run before utc_point_in_time but we still return None diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 28a5a2ed32d16..329f48e5455c4 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -123,9 +123,6 @@ def purge_old_data( _purge_old_entity_ids(instance, session) _purge_old_recorder_runs(instance, session, purge_before) - with session_scope(session=instance.get_session(), read_only=True) as session: - instance.recorder_runs_manager.load_from_db(session) - instance.states_manager.load_from_db(session) if repack: repack_database(instance) return True diff --git a/homeassistant/components/recorder/queries.py b/homeassistant/components/recorder/queries.py index 8ca7bef269115..2e4b588a0b095 100644 --- a/homeassistant/components/recorder/queries.py +++ b/homeassistant/components/recorder/queries.py @@ -637,15 +637,6 @@ def find_states_to_purge( ) -def find_oldest_state() -> StatementLambdaElement: - """Find the last_updated_ts of the oldest state.""" - return lambda_stmt( - lambda: select(States.last_updated_ts).where( - States.state_id.in_(select(func.min(States.state_id))) - ) - ) - - def find_short_term_statistics_to_purge( purge_before: datetime, max_bind_vars: int ) -> StatementLambdaElement: diff --git a/homeassistant/components/recorder/table_managers/states.py b/homeassistant/components/recorder/table_managers/states.py index fafcfa0ea61c6..d5cef759c5423 100644 --- a/homeassistant/components/recorder/table_managers/states.py +++ b/homeassistant/components/recorder/table_managers/states.py @@ -2,15 +2,7 @@ from __future__ import annotations -from collections.abc import Sequence -from typing import Any, cast - -from sqlalchemy.engine.row import Row -from sqlalchemy.orm.session import Session - from ..db_schema import States -from ..queries import find_oldest_state -from ..util import execute_stmt_lambda_element class StatesManager: @@ -21,12 +13,6 @@ def __init__(self) -> None: self._pending: dict[str, States] = {} self._last_committed_id: dict[str, int] = {} self._last_reported: dict[int, float] = {} - self._oldest_ts: float | None = None - - @property - def oldest_ts(self) -> float | None: - """Return the oldest timestamp.""" - return self._oldest_ts def pop_pending(self, entity_id: str) -> States | None: """Pop a pending state. @@ -58,8 +44,6 @@ def add_pending(self, entity_id: str, state: States) -> None: recorder thread. """ self._pending[entity_id] = state - if self._oldest_ts is None: - self._oldest_ts = state.last_updated_ts def update_pending_last_reported( self, state_id: int, last_reported_timestamp: float @@ -90,22 +74,6 @@ def reset(self) -> None: """ self._last_committed_id.clear() self._pending.clear() - self._oldest_ts = None - - def load_from_db(self, session: Session) -> None: - """Update the cache. - - Must run in the recorder thread. - """ - result = cast( - Sequence[Row[Any]], - execute_stmt_lambda_element(session, find_oldest_state()), - ) - if not result: - ts = None - else: - ts = result[0].last_updated_ts - self._oldest_ts = ts def evict_purged_state_ids(self, purged_state_ids: set[int]) -> None: """Evict purged states from the committed states. diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index fa10c12aa6889..783f0a80b8e31 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -120,6 +120,8 @@ def run(self, instance: Recorder) -> None: if purge.purge_old_data( instance, self.purge_before, self.repack, self.apply_filter ): + with instance.get_session() as session: + instance.recorder_runs_manager.load_from_db(session) # We always need to do the db cleanups after a purge # is finished to ensure the WAL checkpoint and other # tasks happen after a vacuum. diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index f721a260c14e0..ca160e5201b02 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -112,9 +112,6 @@ async def test_purge_big_database(hass: HomeAssistant, recorder_mock: Recorder) async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> None: """Test deleting old states.""" - assert recorder_mock.states_manager.oldest_ts is None - oldest_ts = recorder_mock.states_manager.oldest_ts - await _add_test_states(hass) # make sure we start with 6 states @@ -130,10 +127,6 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> events = session.query(Events).filter(Events.event_type == "state_changed") assert events.count() == 0 - assert recorder_mock.states_manager.oldest_ts != oldest_ts - assert recorder_mock.states_manager.oldest_ts == states[0].last_updated_ts - oldest_ts = recorder_mock.states_manager.oldest_ts - assert "test.recorder2" in recorder_mock.states_manager._last_committed_id purge_before = dt_util.utcnow() - timedelta(days=4) @@ -147,8 +140,6 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> repack=False, ) assert not finished - # states_manager.oldest_ts is not updated until after the purge is complete - assert recorder_mock.states_manager.oldest_ts == oldest_ts with session_scope(hass=hass) as session: states = session.query(States) @@ -171,8 +162,6 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> finished = purge_old_data(recorder_mock, purge_before, repack=False) assert finished - # states_manager.oldest_ts should now be updated - assert recorder_mock.states_manager.oldest_ts != oldest_ts with session_scope(hass=hass) as session: states = session.query(States) @@ -180,10 +169,6 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> assert states.count() == 2 assert state_attributes.count() == 1 - assert recorder_mock.states_manager.oldest_ts != oldest_ts - assert recorder_mock.states_manager.oldest_ts == states[0].last_updated_ts - oldest_ts = recorder_mock.states_manager.oldest_ts - assert "test.recorder2" in recorder_mock.states_manager._last_committed_id # run purge_old_data again @@ -196,8 +181,6 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> repack=False, ) assert not finished - # states_manager.oldest_ts is not updated until after the purge is complete - assert recorder_mock.states_manager.oldest_ts == oldest_ts with session_scope(hass=hass) as session: assert states.count() == 0 From e93256951efa7b908e7accb54588cbd2d178f356 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 14 Dec 2024 20:51:30 +0100 Subject: [PATCH 10/17] Bump incomfort-client to v0.6.4 (#133205) --- homeassistant/components/incomfort/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 40c93012eefcb..f404f33b970a9 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/incomfort", "iot_class": "local_polling", "loggers": ["incomfortclient"], - "requirements": ["incomfort-client==0.6.3-1"] + "requirements": ["incomfort-client==0.6.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6929deaf14e57..bf353472fba58 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1192,7 +1192,7 @@ ihcsdk==2.8.5 imgw_pib==1.0.6 # homeassistant.components.incomfort -incomfort-client==0.6.3-1 +incomfort-client==0.6.4 # homeassistant.components.influxdb influxdb-client==1.24.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83070eb803085..2b851df1979bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1003,7 +1003,7 @@ ifaddr==0.2.0 imgw_pib==1.0.6 # homeassistant.components.incomfort -incomfort-client==0.6.3-1 +incomfort-client==0.6.4 # homeassistant.components.influxdb influxdb-client==1.24.0 From eb86b00dd40fa4f0009b15464128dcd4a2268705 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Dec 2024 15:06:26 -0600 Subject: [PATCH 11/17] Bump yalexs-ble to 2.5.5 (#133229) changelog: https://github.com/bdraco/yalexs-ble/compare/v2.5.4...v2.5.5 --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yale/manifest.json | 2 +- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index ed2c8007ee87b..d0b41411c96b8 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==8.10.0", "yalexs-ble==2.5.4"] + "requirements": ["yalexs==8.10.0", "yalexs-ble==2.5.5"] } diff --git a/homeassistant/components/yale/manifest.json b/homeassistant/components/yale/manifest.json index 2ed1f4b5c4397..7b7edfac77ba7 100644 --- a/homeassistant/components/yale/manifest.json +++ b/homeassistant/components/yale/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/yale", "iot_class": "cloud_push", "loggers": ["socketio", "engineio", "yalexs"], - "requirements": ["yalexs==8.10.0", "yalexs-ble==2.5.4"] + "requirements": ["yalexs==8.10.0", "yalexs-ble==2.5.5"] } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 1472f9035eac3..b2c331397b349 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.5.4"] + "requirements": ["yalexs-ble==2.5.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index bf353472fba58..9c69b88313667 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3044,7 +3044,7 @@ yalesmartalarmclient==0.4.3 # homeassistant.components.august # homeassistant.components.yale # homeassistant.components.yalexs_ble -yalexs-ble==2.5.4 +yalexs-ble==2.5.5 # homeassistant.components.august # homeassistant.components.yale diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b851df1979bc..3205ed5a2909f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2433,7 +2433,7 @@ yalesmartalarmclient==0.4.3 # homeassistant.components.august # homeassistant.components.yale # homeassistant.components.yalexs_ble -yalexs-ble==2.5.4 +yalexs-ble==2.5.5 # homeassistant.components.august # homeassistant.components.yale From a48a5adc81aea979f64a0c8460fd9b84746abc61 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Dec 2024 12:28:29 -0600 Subject: [PATCH 12/17] Set code_arm_required to False for homekit_controller (#133284) --- .../components/homekit_controller/alarm_control_panel.py | 1 + tests/components/homekit_controller/snapshots/test_init.ambr | 4 ++-- .../components/homekit_controller/test_alarm_control_panel.py | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index 3cb80f2c817ce..b17f122dfa5c8 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -69,6 +69,7 @@ class HomeKitAlarmControlPanelEntity(HomeKitEntity, AlarmControlPanelEntity): | AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_NIGHT ) + _attr_code_arm_required = False def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index b96da507adfc2..2bd5e7faf75b8 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -1474,7 +1474,7 @@ 'state': dict({ 'attributes': dict({ 'changed_by': None, - 'code_arm_required': True, + 'code_arm_required': False, 'code_format': None, 'friendly_name': 'Aqara-Hub-E1-00A0 Security System', 'supported_features': , @@ -1848,7 +1848,7 @@ 'state': dict({ 'attributes': dict({ 'changed_by': None, - 'code_arm_required': True, + 'code_arm_required': False, 'code_format': None, 'friendly_name': 'Aqara Hub-1563 Security System', 'supported_features': , diff --git a/tests/components/homekit_controller/test_alarm_control_panel.py b/tests/components/homekit_controller/test_alarm_control_panel.py index 1e9f023fc4647..3ab9dc82e4126 100644 --- a/tests/components/homekit_controller/test_alarm_control_panel.py +++ b/tests/components/homekit_controller/test_alarm_control_panel.py @@ -6,6 +6,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes +from homeassistant.components.alarm_control_panel import ATTR_CODE_ARM_REQUIRED from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -106,6 +107,7 @@ async def test_switch_read_alarm_state( state = await helper.poll_and_get_state() assert state.state == "armed_home" assert state.attributes["battery_level"] == 50 + assert state.attributes[ATTR_CODE_ARM_REQUIRED] is False await helper.async_update( ServicesTypes.SECURITY_SYSTEM, From 97f22b3a3d04a50d8f9496a163191b04d58dd690 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 15 Dec 2024 19:26:46 +0100 Subject: [PATCH 13/17] Allow load_verify_locations with only cadata passed (#133299) --- homeassistant/block_async_io.py | 8 +++++++- tests/test_block_async_io.py | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index 7a68b2515e995..767716dbe27d5 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -50,6 +50,12 @@ def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool: return False +def _check_load_verify_locations_call_allowed(mapped_args: dict[str, Any]) -> bool: + # If only cadata is passed, we can ignore it + kwargs = mapped_args.get("kwargs") + return bool(kwargs and len(kwargs) == 1 and "cadata" in kwargs) + + @dataclass(slots=True, frozen=True) class BlockingCall: """Class to hold information about a blocking call.""" @@ -158,7 +164,7 @@ class BlockingCall: original_func=SSLContext.load_verify_locations, object=SSLContext, function="load_verify_locations", - check_allowed=None, + check_allowed=_check_load_verify_locations_call_allowed, strict=False, strict_core=False, skip_for_tests=True, diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index dc2b096f5958d..dd23d4e9709bc 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -429,6 +429,12 @@ async def test_protect_loop_load_verify_locations( context.load_verify_locations("/dev/null") assert "Detected blocking call to load_verify_locations" in caplog.text + # ignore with only cadata + caplog.clear() + with pytest.raises(ssl.SSLError): + context.load_verify_locations(cadata="xxx") + assert "Detected blocking call to load_verify_locations" not in caplog.text + async def test_protect_loop_load_cert_chain( hass: HomeAssistant, caplog: pytest.LogCaptureFixture From 2bc917c8426ddf87b4197b256b20f8b9a5c4fd38 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 16 Dec 2024 18:06:06 +0000 Subject: [PATCH 14/17] Bump `imgw-pib` to version 1.0.7 (#133364) --- homeassistant/components/imgw_pib/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index b5c35f3f1eb62..ce3bc14d37b9f 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", - "requirements": ["imgw_pib==1.0.6"] + "requirements": ["imgw_pib==1.0.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9c69b88313667..3ec404c849001 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1189,7 +1189,7 @@ iglo==1.2.7 ihcsdk==2.8.5 # homeassistant.components.imgw_pib -imgw_pib==1.0.6 +imgw_pib==1.0.7 # homeassistant.components.incomfort incomfort-client==0.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3205ed5a2909f..26966480444e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1000,7 +1000,7 @@ idasen-ha==2.6.2 ifaddr==0.2.0 # homeassistant.components.imgw_pib -imgw_pib==1.0.6 +imgw_pib==1.0.7 # homeassistant.components.incomfort incomfort-client==0.6.4 From a56ad0273b3c29e54d1d96b301ab870a34ee9db9 Mon Sep 17 00:00:00 2001 From: Jonas Fors Lellky Date: Tue, 17 Dec 2024 11:36:45 +0100 Subject: [PATCH 15/17] Fix fan setpoints for flexit_bacnet (#133388) --- .../components/flexit_bacnet/number.py | 52 ++++++++------ tests/components/flexit_bacnet/conftest.py | 20 +++--- .../flexit_bacnet/snapshots/test_number.ambr | 68 +++++++++---------- tests/components/flexit_bacnet/test_number.py | 8 +-- 4 files changed, 80 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/flexit_bacnet/number.py b/homeassistant/components/flexit_bacnet/number.py index 6e6e2eea980e9..029ce896445c0 100644 --- a/homeassistant/components/flexit_bacnet/number.py +++ b/homeassistant/components/flexit_bacnet/number.py @@ -29,6 +29,8 @@ class FlexitNumberEntityDescription(NumberEntityDescription): """Describes a Flexit number entity.""" native_value_fn: Callable[[FlexitBACnet], float] + native_max_value_fn: Callable[[FlexitBACnet], int] + native_min_value_fn: Callable[[FlexitBACnet], int] set_native_value_fn: Callable[[FlexitBACnet], Callable[[int], Awaitable[None]]] @@ -37,121 +39,121 @@ class FlexitNumberEntityDescription(NumberEntityDescription): key="away_extract_fan_setpoint", translation_key="away_extract_fan_setpoint", device_class=NumberDeviceClass.POWER_FACTOR, - native_min_value=0, - native_max_value=100, native_step=1, mode=NumberMode.SLIDER, native_value_fn=lambda device: device.fan_setpoint_extract_air_away, set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_away, native_unit_of_measurement=PERCENTAGE, + native_max_value_fn=lambda device: int(device.fan_setpoint_extract_air_home), + native_min_value_fn=lambda _: 30, ), FlexitNumberEntityDescription( key="away_supply_fan_setpoint", translation_key="away_supply_fan_setpoint", device_class=NumberDeviceClass.POWER_FACTOR, - native_min_value=0, - native_max_value=100, native_step=1, mode=NumberMode.SLIDER, native_value_fn=lambda device: device.fan_setpoint_supply_air_away, set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_away, native_unit_of_measurement=PERCENTAGE, + native_max_value_fn=lambda device: int(device.fan_setpoint_supply_air_home), + native_min_value_fn=lambda _: 30, ), FlexitNumberEntityDescription( key="cooker_hood_extract_fan_setpoint", translation_key="cooker_hood_extract_fan_setpoint", device_class=NumberDeviceClass.POWER_FACTOR, - native_min_value=0, - native_max_value=100, native_step=1, mode=NumberMode.SLIDER, native_value_fn=lambda device: device.fan_setpoint_extract_air_cooker, set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_cooker, native_unit_of_measurement=PERCENTAGE, + native_max_value_fn=lambda _: 100, + native_min_value_fn=lambda _: 30, ), FlexitNumberEntityDescription( key="cooker_hood_supply_fan_setpoint", translation_key="cooker_hood_supply_fan_setpoint", device_class=NumberDeviceClass.POWER_FACTOR, - native_min_value=0, - native_max_value=100, native_step=1, mode=NumberMode.SLIDER, native_value_fn=lambda device: device.fan_setpoint_supply_air_cooker, set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_cooker, native_unit_of_measurement=PERCENTAGE, + native_max_value_fn=lambda _: 100, + native_min_value_fn=lambda _: 30, ), FlexitNumberEntityDescription( key="fireplace_extract_fan_setpoint", translation_key="fireplace_extract_fan_setpoint", device_class=NumberDeviceClass.POWER_FACTOR, - native_min_value=0, - native_max_value=100, native_step=1, mode=NumberMode.SLIDER, native_value_fn=lambda device: device.fan_setpoint_extract_air_fire, set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_fire, native_unit_of_measurement=PERCENTAGE, + native_max_value_fn=lambda _: 100, + native_min_value_fn=lambda _: 30, ), FlexitNumberEntityDescription( key="fireplace_supply_fan_setpoint", translation_key="fireplace_supply_fan_setpoint", device_class=NumberDeviceClass.POWER_FACTOR, - native_min_value=0, - native_max_value=100, native_step=1, mode=NumberMode.SLIDER, native_value_fn=lambda device: device.fan_setpoint_supply_air_fire, set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_fire, native_unit_of_measurement=PERCENTAGE, + native_max_value_fn=lambda _: 100, + native_min_value_fn=lambda _: 30, ), FlexitNumberEntityDescription( key="high_extract_fan_setpoint", translation_key="high_extract_fan_setpoint", device_class=NumberDeviceClass.POWER_FACTOR, - native_min_value=0, - native_max_value=100, native_step=1, mode=NumberMode.SLIDER, native_value_fn=lambda device: device.fan_setpoint_extract_air_high, set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_high, native_unit_of_measurement=PERCENTAGE, + native_max_value_fn=lambda _: 100, + native_min_value_fn=lambda device: int(device.fan_setpoint_extract_air_home), ), FlexitNumberEntityDescription( key="high_supply_fan_setpoint", translation_key="high_supply_fan_setpoint", device_class=NumberDeviceClass.POWER_FACTOR, - native_min_value=0, - native_max_value=100, native_step=1, mode=NumberMode.SLIDER, native_value_fn=lambda device: device.fan_setpoint_supply_air_high, set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_high, native_unit_of_measurement=PERCENTAGE, + native_max_value_fn=lambda _: 100, + native_min_value_fn=lambda device: int(device.fan_setpoint_supply_air_home), ), FlexitNumberEntityDescription( key="home_extract_fan_setpoint", translation_key="home_extract_fan_setpoint", device_class=NumberDeviceClass.POWER_FACTOR, - native_min_value=0, - native_max_value=100, native_step=1, mode=NumberMode.SLIDER, native_value_fn=lambda device: device.fan_setpoint_extract_air_home, set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_home, native_unit_of_measurement=PERCENTAGE, + native_max_value_fn=lambda _: 100, + native_min_value_fn=lambda device: int(device.fan_setpoint_extract_air_away), ), FlexitNumberEntityDescription( key="home_supply_fan_setpoint", translation_key="home_supply_fan_setpoint", device_class=NumberDeviceClass.POWER_FACTOR, - native_min_value=0, - native_max_value=100, native_step=1, mode=NumberMode.SLIDER, native_value_fn=lambda device: device.fan_setpoint_supply_air_home, set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_home, native_unit_of_measurement=PERCENTAGE, + native_max_value_fn=lambda _: 100, + native_min_value_fn=lambda device: int(device.fan_setpoint_supply_air_away), ), ) @@ -192,6 +194,16 @@ def native_value(self) -> float: """Return the state of the number.""" return self.entity_description.native_value_fn(self.coordinator.device) + @property + def native_max_value(self) -> float: + """Return the native max value of the number.""" + return self.entity_description.native_max_value_fn(self.coordinator.device) + + @property + def native_min_value(self) -> float: + """Return the native min value of the number.""" + return self.entity_description.native_min_value_fn(self.coordinator.device) + async def async_set_native_value(self, value: float) -> None: """Update the current value.""" set_native_value_fn = self.entity_description.set_native_value_fn( diff --git a/tests/components/flexit_bacnet/conftest.py b/tests/components/flexit_bacnet/conftest.py index cc7c9fa0570b5..c12559ef3aee7 100644 --- a/tests/components/flexit_bacnet/conftest.py +++ b/tests/components/flexit_bacnet/conftest.py @@ -68,16 +68,16 @@ def mock_flexit_bacnet() -> Generator[AsyncMock]: flexit_bacnet.electric_heater = True # Mock fan setpoints - flexit_bacnet.fan_setpoint_extract_air_fire = 10 - flexit_bacnet.fan_setpoint_supply_air_fire = 20 - flexit_bacnet.fan_setpoint_extract_air_away = 30 - flexit_bacnet.fan_setpoint_supply_air_away = 40 - flexit_bacnet.fan_setpoint_extract_air_home = 50 - flexit_bacnet.fan_setpoint_supply_air_home = 60 - flexit_bacnet.fan_setpoint_extract_air_high = 70 - flexit_bacnet.fan_setpoint_supply_air_high = 80 - flexit_bacnet.fan_setpoint_extract_air_cooker = 90 - flexit_bacnet.fan_setpoint_supply_air_cooker = 100 + flexit_bacnet.fan_setpoint_extract_air_fire = 56 + flexit_bacnet.fan_setpoint_supply_air_fire = 77 + flexit_bacnet.fan_setpoint_extract_air_away = 40 + flexit_bacnet.fan_setpoint_supply_air_away = 42 + flexit_bacnet.fan_setpoint_extract_air_home = 70 + flexit_bacnet.fan_setpoint_supply_air_home = 74 + flexit_bacnet.fan_setpoint_extract_air_high = 100 + flexit_bacnet.fan_setpoint_supply_air_high = 100 + flexit_bacnet.fan_setpoint_extract_air_cooker = 50 + flexit_bacnet.fan_setpoint_supply_air_cooker = 70 yield flexit_bacnet diff --git a/tests/components/flexit_bacnet/snapshots/test_number.ambr b/tests/components/flexit_bacnet/snapshots/test_number.ambr index c4fb1e7c43444..78eefd083455b 100644 --- a/tests/components/flexit_bacnet/snapshots/test_number.ambr +++ b/tests/components/flexit_bacnet/snapshots/test_number.ambr @@ -5,8 +5,8 @@ }), 'area_id': None, 'capabilities': dict({ - 'max': 100, - 'min': 0, + 'max': 70, + 'min': 30, 'mode': , 'step': 1, }), @@ -42,8 +42,8 @@ 'attributes': ReadOnlyDict({ 'device_class': 'power_factor', 'friendly_name': 'Device Name Away extract fan setpoint', - 'max': 100, - 'min': 0, + 'max': 70, + 'min': 30, 'mode': , 'step': 1, 'unit_of_measurement': '%', @@ -53,7 +53,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '30', + 'state': '40', }) # --- # name: test_numbers[number.device_name_away_supply_fan_setpoint-entry] @@ -62,8 +62,8 @@ }), 'area_id': None, 'capabilities': dict({ - 'max': 100, - 'min': 0, + 'max': 74, + 'min': 30, 'mode': , 'step': 1, }), @@ -99,8 +99,8 @@ 'attributes': ReadOnlyDict({ 'device_class': 'power_factor', 'friendly_name': 'Device Name Away supply fan setpoint', - 'max': 100, - 'min': 0, + 'max': 74, + 'min': 30, 'mode': , 'step': 1, 'unit_of_measurement': '%', @@ -110,7 +110,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '40', + 'state': '42', }) # --- # name: test_numbers[number.device_name_cooker_hood_extract_fan_setpoint-entry] @@ -120,7 +120,7 @@ 'area_id': None, 'capabilities': dict({ 'max': 100, - 'min': 0, + 'min': 30, 'mode': , 'step': 1, }), @@ -157,7 +157,7 @@ 'device_class': 'power_factor', 'friendly_name': 'Device Name Cooker hood extract fan setpoint', 'max': 100, - 'min': 0, + 'min': 30, 'mode': , 'step': 1, 'unit_of_measurement': '%', @@ -167,7 +167,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '90', + 'state': '50', }) # --- # name: test_numbers[number.device_name_cooker_hood_supply_fan_setpoint-entry] @@ -177,7 +177,7 @@ 'area_id': None, 'capabilities': dict({ 'max': 100, - 'min': 0, + 'min': 30, 'mode': , 'step': 1, }), @@ -214,7 +214,7 @@ 'device_class': 'power_factor', 'friendly_name': 'Device Name Cooker hood supply fan setpoint', 'max': 100, - 'min': 0, + 'min': 30, 'mode': , 'step': 1, 'unit_of_measurement': '%', @@ -224,7 +224,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '100', + 'state': '70', }) # --- # name: test_numbers[number.device_name_fireplace_extract_fan_setpoint-entry] @@ -234,7 +234,7 @@ 'area_id': None, 'capabilities': dict({ 'max': 100, - 'min': 0, + 'min': 30, 'mode': , 'step': 1, }), @@ -271,7 +271,7 @@ 'device_class': 'power_factor', 'friendly_name': 'Device Name Fireplace extract fan setpoint', 'max': 100, - 'min': 0, + 'min': 30, 'mode': , 'step': 1, 'unit_of_measurement': '%', @@ -281,7 +281,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '10', + 'state': '56', }) # --- # name: test_numbers[number.device_name_fireplace_supply_fan_setpoint-entry] @@ -291,7 +291,7 @@ 'area_id': None, 'capabilities': dict({ 'max': 100, - 'min': 0, + 'min': 30, 'mode': , 'step': 1, }), @@ -328,7 +328,7 @@ 'device_class': 'power_factor', 'friendly_name': 'Device Name Fireplace supply fan setpoint', 'max': 100, - 'min': 0, + 'min': 30, 'mode': , 'step': 1, 'unit_of_measurement': '%', @@ -338,7 +338,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '20', + 'state': '77', }) # --- # name: test_numbers[number.device_name_high_extract_fan_setpoint-entry] @@ -348,7 +348,7 @@ 'area_id': None, 'capabilities': dict({ 'max': 100, - 'min': 0, + 'min': 70, 'mode': , 'step': 1, }), @@ -385,7 +385,7 @@ 'device_class': 'power_factor', 'friendly_name': 'Device Name High extract fan setpoint', 'max': 100, - 'min': 0, + 'min': 70, 'mode': , 'step': 1, 'unit_of_measurement': '%', @@ -395,7 +395,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '70', + 'state': '100', }) # --- # name: test_numbers[number.device_name_high_supply_fan_setpoint-entry] @@ -405,7 +405,7 @@ 'area_id': None, 'capabilities': dict({ 'max': 100, - 'min': 0, + 'min': 74, 'mode': , 'step': 1, }), @@ -442,7 +442,7 @@ 'device_class': 'power_factor', 'friendly_name': 'Device Name High supply fan setpoint', 'max': 100, - 'min': 0, + 'min': 74, 'mode': , 'step': 1, 'unit_of_measurement': '%', @@ -452,7 +452,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '80', + 'state': '100', }) # --- # name: test_numbers[number.device_name_home_extract_fan_setpoint-entry] @@ -462,7 +462,7 @@ 'area_id': None, 'capabilities': dict({ 'max': 100, - 'min': 0, + 'min': 40, 'mode': , 'step': 1, }), @@ -499,7 +499,7 @@ 'device_class': 'power_factor', 'friendly_name': 'Device Name Home extract fan setpoint', 'max': 100, - 'min': 0, + 'min': 40, 'mode': , 'step': 1, 'unit_of_measurement': '%', @@ -509,7 +509,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '50', + 'state': '70', }) # --- # name: test_numbers[number.device_name_home_supply_fan_setpoint-entry] @@ -519,7 +519,7 @@ 'area_id': None, 'capabilities': dict({ 'max': 100, - 'min': 0, + 'min': 42, 'mode': , 'step': 1, }), @@ -556,7 +556,7 @@ 'device_class': 'power_factor', 'friendly_name': 'Device Name Home supply fan setpoint', 'max': 100, - 'min': 0, + 'min': 42, 'mode': , 'step': 1, 'unit_of_measurement': '%', @@ -566,6 +566,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '60', + 'state': '74', }) # --- diff --git a/tests/components/flexit_bacnet/test_number.py b/tests/components/flexit_bacnet/test_number.py index ad49908fa96d4..f566b623f124f 100644 --- a/tests/components/flexit_bacnet/test_number.py +++ b/tests/components/flexit_bacnet/test_number.py @@ -64,21 +64,21 @@ async def test_numbers_implementation( assert len(mocked_method.mock_calls) == 1 assert hass.states.get(ENTITY_ID).state == "60" - mock_flexit_bacnet.fan_setpoint_supply_air_fire = 10 + mock_flexit_bacnet.fan_setpoint_supply_air_fire = 40 await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, { ATTR_ENTITY_ID: ENTITY_ID, - ATTR_VALUE: 10, + ATTR_VALUE: 40, }, blocking=True, ) mocked_method = getattr(mock_flexit_bacnet, "set_fan_setpoint_supply_air_fire") assert len(mocked_method.mock_calls) == 2 - assert hass.states.get(ENTITY_ID).state == "10" + assert hass.states.get(ENTITY_ID).state == "40" # Error recovery, when setting the value mock_flexit_bacnet.set_fan_setpoint_supply_air_fire.side_effect = DecodingError @@ -89,7 +89,7 @@ async def test_numbers_implementation( SERVICE_SET_VALUE, { ATTR_ENTITY_ID: ENTITY_ID, - ATTR_VALUE: 10, + ATTR_VALUE: 40, }, blocking=True, ) From b4015805f7fd0408aa4e047769de69e5c6e74651 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 17 Dec 2024 11:10:38 +0100 Subject: [PATCH 16/17] Bump holidays to 0.63 (#133391) --- 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 7edc140da1128..33cae2315956e 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.62", "babel==2.15.0"] + "requirements": ["holidays==0.63", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 842c6f1f1ad3b..de9cbe694d899 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.62"] + "requirements": ["holidays==0.63"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3ec404c849001..2858c92d18282 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1127,7 +1127,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.62 +holidays==0.63 # homeassistant.components.frontend home-assistant-frontend==20241127.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26966480444e0..f8565afc4b6c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -953,7 +953,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.62 +holidays==0.63 # homeassistant.components.frontend home-assistant-frontend==20241127.8 From 517f3faa0ac8697b53a36c9b8849509db4fd2ebd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 Dec 2024 12:14:26 +0000 Subject: [PATCH 17/17] Bump version to 2024.12.4 --- 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 391a02d07b43d..21f805bae72f7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index ef8ce79f8946b..6b640bce4d02a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.12.3" +version = "2024.12.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"