From 1df82f39c14db5eb382748e4da292adf598f5de7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Feb 2023 16:58:34 -0600 Subject: [PATCH 01/28] Speed up purge time with newer MariaDB versions (#87409) * Speed up purge time with newer MariaDB versions * fix * document * document * document * rename * self review * Update homeassistant/components/recorder/util.py * fixes --- homeassistant/components/recorder/core.py | 8 +- homeassistant/components/recorder/models.py | 28 ++++++ homeassistant/components/recorder/purge.py | 76 ++++++++-------- homeassistant/components/recorder/queries.py | 4 +- .../recorder/system_health/__init__.py | 4 +- homeassistant/components/recorder/util.py | 88 +++++++++++-------- tests/components/recorder/test_util.py | 48 ++++++++-- 7 files changed, 169 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index ddca32e6970..497ae364b64 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -14,7 +14,6 @@ import time from typing import Any, TypeVar, cast import async_timeout -from awesomeversion import AwesomeVersion from lru import LRU # pylint: disable=no-name-in-module from sqlalchemy import create_engine, event as sqlalchemy_event, exc, func, select from sqlalchemy.engine import Engine @@ -67,6 +66,7 @@ from .db_schema import ( ) from .executor import DBInterruptibleThreadPoolExecutor from .models import ( + DatabaseEngine, StatisticData, StatisticMetaData, UnsupportedDialect, @@ -173,7 +173,7 @@ class Recorder(threading.Thread): self.db_url = uri self.db_max_retries = db_max_retries self.db_retry_wait = db_retry_wait - self.engine_version: AwesomeVersion | None = None + self.database_engine: DatabaseEngine | None = None # Database connection is ready, but non-live migration may be in progress db_connected: asyncio.Future[bool] = hass.data[DOMAIN].db_connected self.async_db_connected: asyncio.Future[bool] = db_connected @@ -1125,13 +1125,13 @@ class Recorder(threading.Thread): ) -> None: """Dbapi specific connection settings.""" assert self.engine is not None - if version := setup_connection_for_dialect( + if database_engine := setup_connection_for_dialect( self, self.engine.dialect.name, dbapi_connection, not self._completed_first_database_setup, ): - self.engine_version = version + self.database_engine = database_engine self._completed_first_database_setup = True if self.db_url == SQLITE_URL_PREFIX or ":memory:" in self.db_url: diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 3bbd9f173a3..b071a686761 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,10 +1,12 @@ """Models for Recorder.""" from __future__ import annotations +from dataclasses import dataclass from datetime import datetime, timedelta import logging from typing import Any, Literal, TypedDict, overload +from awesomeversion import AwesomeVersion from sqlalchemy.engine.row import Row from homeassistant.const import ( @@ -17,6 +19,8 @@ from homeassistant.core import Context, State from homeassistant.helpers.json import json_loads import homeassistant.util.dt as dt_util +from .const import SupportedDialect + # pylint: disable=invalid-name _LOGGER = logging.getLogger(__name__) @@ -443,3 +447,27 @@ class StatisticPeriod(TypedDict, total=False): calendar: CalendarStatisticPeriod fixed_period: FixedStatisticPeriod rolling_window: RollingWindowStatisticPeriod + + +@dataclass +class DatabaseEngine: + """Properties of the database engine.""" + + dialect: SupportedDialect + optimizer: DatabaseOptimizer + version: AwesomeVersion | None + + +@dataclass +class DatabaseOptimizer: + """Properties of the database optimizer for the configured database engine.""" + + # Some MariaDB versions have a bug that causes a slow query when using + # a range in a select statement with an IN clause. + # + # https://jira.mariadb.org/browse/MDEV-25020 + # + # Historically, we have applied this logic to PostgreSQL as well, but + # it may not be necessary. We should revisit this in the future + # when we have more data. + slow_range_in_select: bool diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 4fb52dabb21..fa380e5a7e2 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -14,13 +14,14 @@ from sqlalchemy.sql.expression import distinct from homeassistant.const import EVENT_STATE_CHANGED import homeassistant.util.dt as dt_util -from .const import MAX_ROWS_TO_PURGE, SupportedDialect +from .const import MAX_ROWS_TO_PURGE from .db_schema import Events, StateAttributes, States +from .models import DatabaseEngine from .queries import ( attributes_ids_exist_in_states, - attributes_ids_exist_in_states_sqlite, + attributes_ids_exist_in_states_with_fast_in_distinct, data_ids_exist_in_events, - data_ids_exist_in_events_sqlite, + data_ids_exist_in_events_with_fast_in_distinct, delete_event_data_rows, delete_event_rows, delete_recorder_runs_rows, @@ -83,8 +84,6 @@ def purge_old_data( "Purging states and events before target %s", purge_before.isoformat(sep=" ", timespec="seconds"), ) - using_sqlite = instance.dialect_name == SupportedDialect.SQLITE - with session_scope(session=instance.get_session()) as session: # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record has_more_to_purge = False @@ -93,9 +92,7 @@ def purge_old_data( "Purge running in legacy format as there are states with event_id" " remaining" ) - has_more_to_purge |= _purge_legacy_format( - instance, session, purge_before, using_sqlite - ) + has_more_to_purge |= _purge_legacy_format(instance, session, purge_before) else: _LOGGER.debug( "Purge running in new format as there are NO states with event_id" @@ -103,10 +100,10 @@ def purge_old_data( ) # Once we are done purging legacy rows, we use the new method has_more_to_purge |= _purge_states_and_attributes_ids( - instance, session, states_batch_size, purge_before, using_sqlite + instance, session, states_batch_size, purge_before ) has_more_to_purge |= _purge_events_and_data_ids( - instance, session, events_batch_size, purge_before, using_sqlite + instance, session, events_batch_size, purge_before ) statistics_runs = _select_statistics_runs_to_purge(session, purge_before) @@ -140,7 +137,7 @@ def _purging_legacy_format(session: Session) -> bool: def _purge_legacy_format( - instance: Recorder, session: Session, purge_before: datetime, using_sqlite: bool + instance: Recorder, session: Session, purge_before: datetime ) -> bool: """Purge rows that are still linked by the event_ids.""" ( @@ -153,10 +150,10 @@ def _purge_legacy_format( ) if state_ids: _purge_state_ids(instance, session, state_ids) - _purge_unused_attributes_ids(instance, session, attributes_ids, using_sqlite) + _purge_unused_attributes_ids(instance, session, attributes_ids) if event_ids: _purge_event_ids(session, event_ids) - _purge_unused_data_ids(instance, session, data_ids, using_sqlite) + _purge_unused_data_ids(instance, session, data_ids) return bool(event_ids or state_ids or attributes_ids or data_ids) @@ -165,12 +162,13 @@ def _purge_states_and_attributes_ids( session: Session, states_batch_size: int, purge_before: datetime, - using_sqlite: bool, ) -> bool: """Purge states and linked attributes id in a batch. Returns true if there are more states to purge. """ + database_engine = instance.database_engine + assert database_engine is not None has_remaining_state_ids_to_purge = True # There are more states relative to attributes_ids so # we purge enough state_ids to try to generate a full @@ -187,7 +185,7 @@ def _purge_states_and_attributes_ids( _purge_state_ids(instance, session, state_ids) attributes_ids_batch = attributes_ids_batch | attributes_ids - _purge_unused_attributes_ids(instance, session, attributes_ids_batch, using_sqlite) + _purge_unused_attributes_ids(instance, session, attributes_ids_batch) _LOGGER.debug( "After purging states and attributes_ids remaining=%s", has_remaining_state_ids_to_purge, @@ -200,7 +198,6 @@ def _purge_events_and_data_ids( session: Session, events_batch_size: int, purge_before: datetime, - using_sqlite: bool, ) -> bool: """Purge states and linked attributes id in a batch. @@ -220,7 +217,7 @@ def _purge_events_and_data_ids( _purge_event_ids(session, event_ids) data_ids_batch = data_ids_batch | data_ids - _purge_unused_data_ids(instance, session, data_ids_batch, using_sqlite) + _purge_unused_data_ids(instance, session, data_ids_batch) _LOGGER.debug( "After purging event and data_ids remaining=%s", has_remaining_event_ids_to_purge, @@ -267,13 +264,13 @@ def _select_event_data_ids_to_purge( def _select_unused_attributes_ids( - session: Session, attributes_ids: set[int], using_sqlite: bool + session: Session, attributes_ids: set[int], database_engine: DatabaseEngine ) -> set[int]: """Return a set of attributes ids that are not used by any states in the db.""" if not attributes_ids: return set() - if using_sqlite: + if not database_engine.optimizer.slow_range_in_select: # # SQLite has a superior query optimizer for the distinct query below as it uses # the covering index without having to examine the rows directly for both of the @@ -290,7 +287,7 @@ def _select_unused_attributes_ids( seen_ids = { state[0] for state in session.execute( - attributes_ids_exist_in_states_sqlite(attributes_ids) + attributes_ids_exist_in_states_with_fast_in_distinct(attributes_ids) ).all() } else: @@ -340,16 +337,18 @@ def _purge_unused_attributes_ids( instance: Recorder, session: Session, attributes_ids_batch: set[int], - using_sqlite: bool, ) -> None: + """Purge unused attributes ids.""" + database_engine = instance.database_engine + assert database_engine is not None if unused_attribute_ids_set := _select_unused_attributes_ids( - session, attributes_ids_batch, using_sqlite + session, attributes_ids_batch, database_engine ): _purge_batch_attributes_ids(instance, session, unused_attribute_ids_set) def _select_unused_event_data_ids( - session: Session, data_ids: set[int], using_sqlite: bool + session: Session, data_ids: set[int], database_engine: DatabaseEngine ) -> set[int]: """Return a set of event data ids that are not used by any events in the db.""" if not data_ids: @@ -357,11 +356,11 @@ def _select_unused_event_data_ids( # See _select_unused_attributes_ids for why this function # branches for non-sqlite databases. - if using_sqlite: + if not database_engine.optimizer.slow_range_in_select: seen_ids = { state[0] for state in session.execute( - data_ids_exist_in_events_sqlite(data_ids) + data_ids_exist_in_events_with_fast_in_distinct(data_ids) ).all() } else: @@ -381,10 +380,12 @@ def _select_unused_event_data_ids( def _purge_unused_data_ids( - instance: Recorder, session: Session, data_ids_batch: set[int], using_sqlite: bool + instance: Recorder, session: Session, data_ids_batch: set[int] ) -> None: + database_engine = instance.database_engine + assert database_engine is not None if unused_data_ids_set := _select_unused_event_data_ids( - session, data_ids_batch, using_sqlite + session, data_ids_batch, database_engine ): _purge_batch_data_ids(instance, session, unused_data_ids_set) @@ -582,7 +583,8 @@ def _purge_old_recorder_runs( def _purge_filtered_data(instance: Recorder, session: Session) -> bool: """Remove filtered states and events that shouldn't be in the database.""" _LOGGER.debug("Cleanup filtered data") - using_sqlite = instance.dialect_name == SupportedDialect.SQLITE + database_engine = instance.database_engine + assert database_engine is not None # Check if excluded entity_ids are in database excluded_entity_ids: list[str] = [ @@ -591,7 +593,7 @@ def _purge_filtered_data(instance: Recorder, session: Session) -> bool: if not instance.entity_filter(entity_id) ] if len(excluded_entity_ids) > 0: - _purge_filtered_states(instance, session, excluded_entity_ids, using_sqlite) + _purge_filtered_states(instance, session, excluded_entity_ids, database_engine) return False # Check if excluded event_types are in database @@ -611,7 +613,7 @@ def _purge_filtered_states( instance: Recorder, session: Session, excluded_entity_ids: list[str], - using_sqlite: bool, + database_engine: DatabaseEngine, ) -> None: """Remove filtered states and linked events.""" state_ids: list[int] @@ -632,7 +634,7 @@ def _purge_filtered_states( _purge_state_ids(instance, session, set(state_ids)) _purge_event_ids(session, event_ids) unused_attribute_ids_set = _select_unused_attributes_ids( - session, {id_ for id_ in attributes_ids if id_ is not None}, using_sqlite + session, {id_ for id_ in attributes_ids if id_ is not None}, database_engine ) _purge_batch_attributes_ids(instance, session, unused_attribute_ids_set) @@ -641,7 +643,8 @@ def _purge_filtered_events( instance: Recorder, session: Session, excluded_event_types: list[str] ) -> None: """Remove filtered events and linked states.""" - using_sqlite = instance.dialect_name == SupportedDialect.SQLITE + database_engine = instance.database_engine + assert database_engine is not None event_ids, data_ids = zip( *( session.query(Events.event_id, Events.data_id) @@ -660,7 +663,7 @@ def _purge_filtered_events( _purge_state_ids(instance, session, state_ids) _purge_event_ids(session, event_ids) if unused_data_ids_set := _select_unused_event_data_ids( - session, set(data_ids), using_sqlite + session, set(data_ids), database_engine ): _purge_batch_data_ids(instance, session, unused_data_ids_set) if EVENT_STATE_CHANGED in excluded_event_types: @@ -671,7 +674,8 @@ def _purge_filtered_events( @retryable_database_job("purge") def purge_entity_data(instance: Recorder, entity_filter: Callable[[str], bool]) -> bool: """Purge states and events of specified entities.""" - using_sqlite = instance.dialect_name == SupportedDialect.SQLITE + database_engine = instance.database_engine + assert database_engine is not None with session_scope(session=instance.get_session()) as session: selected_entity_ids: list[str] = [ entity_id @@ -682,7 +686,9 @@ def purge_entity_data(instance: Recorder, entity_filter: Callable[[str], bool]) if len(selected_entity_ids) > 0: # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states # or events record. - _purge_filtered_states(instance, session, selected_entity_ids, using_sqlite) + _purge_filtered_states( + instance, session, selected_entity_ids, database_engine + ) _LOGGER.debug("Purging entity data hasn't fully completed yet") return False diff --git a/homeassistant/components/recorder/queries.py b/homeassistant/components/recorder/queries.py index 0591fda4713..29bac70eef6 100644 --- a/homeassistant/components/recorder/queries.py +++ b/homeassistant/components/recorder/queries.py @@ -45,7 +45,7 @@ def _state_attrs_exist(attr: int | None) -> Select: return select(func.min(States.attributes_id)).where(States.attributes_id == attr) -def attributes_ids_exist_in_states_sqlite( +def attributes_ids_exist_in_states_with_fast_in_distinct( attributes_ids: Iterable[int], ) -> StatementLambdaElement: """Find attributes ids that exist in the states table.""" @@ -268,7 +268,7 @@ def attributes_ids_exist_in_states( ) -def data_ids_exist_in_events_sqlite( +def data_ids_exist_in_events_with_fast_in_distinct( data_ids: Iterable[int], ) -> StatementLambdaElement: """Find data ids that exist in the events table.""" diff --git a/homeassistant/components/recorder/system_health/__init__.py b/homeassistant/components/recorder/system_health/__init__.py index b79f526db2b..da463d38610 100644 --- a/homeassistant/components/recorder/system_health/__init__.py +++ b/homeassistant/components/recorder/system_health/__init__.py @@ -49,8 +49,8 @@ def _async_get_db_engine_info(instance: Recorder) -> dict[str, Any]: db_engine_info: dict[str, Any] = {} if dialect_name := instance.dialect_name: db_engine_info["database_engine"] = dialect_name.value - if engine_version := instance.engine_version: - db_engine_info["database_version"] = str(engine_version) + if database_engine := instance.database_engine: + db_engine_info["database_version"] = str(database_engine.version) return db_engine_info diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 0469a71009a..dde32bf05db 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -36,7 +36,13 @@ from .db_schema import ( TABLES_TO_CHECK, RecorderRuns, ) -from .models import StatisticPeriod, UnsupportedDialect, process_timestamp +from .models import ( + DatabaseEngine, + DatabaseOptimizer, + StatisticPeriod, + UnsupportedDialect, + process_timestamp, +) if TYPE_CHECKING: from . import Recorder @@ -51,44 +57,33 @@ QUERY_RETRY_WAIT = 0.1 SQLITE3_POSTFIXES = ["", "-wal", "-shm"] DEFAULT_YIELD_STATES_ROWS = 32768 + # Our minimum versions for each database # # Older MariaDB suffers https://jira.mariadb.org/browse/MDEV-25020 # which is fixed in 10.5.17, 10.6.9, 10.7.5, 10.8.4 # -MIN_VERSION_MARIA_DB = AwesomeVersion( - "10.3.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -RECOMMENDED_MIN_VERSION_MARIA_DB = AwesomeVersion( - "10.5.17", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -MARIA_DB_106 = AwesomeVersion( - "10.6.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -RECOMMENDED_MIN_VERSION_MARIA_DB_106 = AwesomeVersion( - "10.6.9", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -MARIA_DB_107 = AwesomeVersion( - "10.7.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -RECOMMENDED_MIN_VERSION_MARIA_DB_107 = AwesomeVersion( - "10.7.5", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -MARIA_DB_108 = AwesomeVersion( - "10.8.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -RECOMMENDED_MIN_VERSION_MARIA_DB_108 = AwesomeVersion( - "10.8.4", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -MIN_VERSION_MYSQL = AwesomeVersion( - "8.0.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -MIN_VERSION_PGSQL = AwesomeVersion( - "12.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -MIN_VERSION_SQLITE = AwesomeVersion( - "3.31.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) +def _simple_version(version: str) -> AwesomeVersion: + """Return a simple version.""" + return AwesomeVersion(version, ensure_strategy=AwesomeVersionStrategy.SIMPLEVER) + + +MIN_VERSION_MARIA_DB = _simple_version("10.3.0") +RECOMMENDED_MIN_VERSION_MARIA_DB = _simple_version("10.5.17") +MARIADB_WITH_FIXED_IN_QUERIES_105 = _simple_version("10.5.17") +MARIA_DB_106 = _simple_version("10.6.0") +MARIADB_WITH_FIXED_IN_QUERIES_106 = _simple_version("10.6.9") +RECOMMENDED_MIN_VERSION_MARIA_DB_106 = _simple_version("10.6.9") +MARIA_DB_107 = _simple_version("10.7.0") +RECOMMENDED_MIN_VERSION_MARIA_DB_107 = _simple_version("10.7.5") +MARIADB_WITH_FIXED_IN_QUERIES_107 = _simple_version("10.7.5") +MARIA_DB_108 = _simple_version("10.8.0") +RECOMMENDED_MIN_VERSION_MARIA_DB_108 = _simple_version("10.8.4") +MARIADB_WITH_FIXED_IN_QUERIES_108 = _simple_version("10.8.4") +MIN_VERSION_MYSQL = _simple_version("8.0.0") +MIN_VERSION_PGSQL = _simple_version("12.0") +MIN_VERSION_SQLITE = _simple_version("3.31.0") + # This is the maximum time after the recorder ends the session # before we no longer consider startup to be a "restart" and we @@ -467,10 +462,12 @@ def setup_connection_for_dialect( dialect_name: str, dbapi_connection: Any, first_connection: bool, -) -> AwesomeVersion | None: +) -> DatabaseEngine | None: """Execute statements needed for dialect connection.""" version: AwesomeVersion | None = None + slow_range_in_select = True if dialect_name == SupportedDialect.SQLITE: + slow_range_in_select = False if first_connection: old_isolation = dbapi_connection.isolation_level dbapi_connection.isolation_level = None @@ -536,7 +533,19 @@ def setup_connection_for_dialect( version or version_string, "MySQL", MIN_VERSION_MYSQL ) + slow_range_in_select = bool( + not version + or version < MARIADB_WITH_FIXED_IN_QUERIES_105 + or MARIA_DB_106 <= version < MARIADB_WITH_FIXED_IN_QUERIES_106 + or MARIA_DB_107 <= version < MARIADB_WITH_FIXED_IN_QUERIES_107 + or MARIA_DB_108 <= version < MARIADB_WITH_FIXED_IN_QUERIES_108 + ) elif dialect_name == SupportedDialect.POSTGRESQL: + # Historically we have marked PostgreSQL as having slow range in select + # but this may not be true for all versions. We should investigate + # this further when we have more data and remove this if possible + # in the future so we can use the simpler purge SQL query for + # _select_unused_attributes_ids and _select_unused_events_ids if first_connection: # server_version_num was added in 2006 result = query_on_connection(dbapi_connection, "SHOW server_version") @@ -550,7 +559,14 @@ def setup_connection_for_dialect( else: _fail_unsupported_dialect(dialect_name) - return version + if not first_connection: + return None + + return DatabaseEngine( + dialect=SupportedDialect(dialect_name), + version=version, + optimizer=DatabaseOptimizer(slow_range_in_select=slow_range_in_select), + ) def end_incomplete_runs(session: Session, start_time: datetime) -> None: diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 3f5ba6d40ef..932b23b1152 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -231,7 +231,12 @@ def test_setup_connection_for_dialect_sqlite(sqlite_version): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, True) + assert ( + util.setup_connection_for_dialect( + instance_mock, "sqlite", dbapi_connection, True + ) + is not None + ) assert len(execute_args) == 5 assert execute_args[0] == "PRAGMA journal_mode=WAL" @@ -241,7 +246,12 @@ def test_setup_connection_for_dialect_sqlite(sqlite_version): assert execute_args[4] == "PRAGMA foreign_keys=ON" execute_args = [] - util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, False) + assert ( + util.setup_connection_for_dialect( + instance_mock, "sqlite", dbapi_connection, False + ) + is None + ) assert len(execute_args) == 3 assert execute_args[0] == "PRAGMA cache_size = -16384" @@ -276,7 +286,12 @@ def test_setup_connection_for_dialect_sqlite_zero_commit_interval( dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, True) + assert ( + util.setup_connection_for_dialect( + instance_mock, "sqlite", dbapi_connection, True + ) + is not None + ) assert len(execute_args) == 5 assert execute_args[0] == "PRAGMA journal_mode=WAL" @@ -286,7 +301,12 @@ def test_setup_connection_for_dialect_sqlite_zero_commit_interval( assert execute_args[4] == "PRAGMA foreign_keys=ON" execute_args = [] - util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, False) + assert ( + util.setup_connection_for_dialect( + instance_mock, "sqlite", dbapi_connection, False + ) + is None + ) assert len(execute_args) == 3 assert execute_args[0] == "PRAGMA cache_size = -16384" @@ -444,11 +464,13 @@ def test_supported_pgsql(caplog, pgsql_version): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect( + database_engine = util.setup_connection_for_dialect( instance_mock, "postgresql", dbapi_connection, True ) assert "minimum supported version" not in caplog.text + assert database_engine is not None + assert database_engine.optimizer.slow_range_in_select is True @pytest.mark.parametrize( @@ -525,9 +547,13 @@ def test_supported_sqlite(caplog, sqlite_version): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, True) + database_engine = util.setup_connection_for_dialect( + instance_mock, "sqlite", dbapi_connection, True + ) assert "minimum supported version" not in caplog.text + assert database_engine is not None + assert database_engine.optimizer.slow_range_in_select is False @pytest.mark.parametrize( @@ -599,7 +625,7 @@ async def test_issue_for_mariadb_with_MDEV_25020( dbapi_connection = MagicMock(cursor=_make_cursor_mock) - await hass.async_add_executor_job( + database_engine = await hass.async_add_executor_job( util.setup_connection_for_dialect, instance_mock, "mysql", @@ -613,6 +639,9 @@ async def test_issue_for_mariadb_with_MDEV_25020( assert issue is not None assert issue.translation_placeholders == {"min_version": min_version} + assert database_engine is not None + assert database_engine.optimizer.slow_range_in_select is True + @pytest.mark.parametrize( "mysql_version", @@ -649,7 +678,7 @@ async def test_no_issue_for_mariadb_with_MDEV_25020(hass, caplog, mysql_version) dbapi_connection = MagicMock(cursor=_make_cursor_mock) - await hass.async_add_executor_job( + database_engine = await hass.async_add_executor_job( util.setup_connection_for_dialect, instance_mock, "mysql", @@ -662,6 +691,9 @@ async def test_no_issue_for_mariadb_with_MDEV_25020(hass, caplog, mysql_version) issue = registry.async_get_issue(DOMAIN, "maria_db_range_index_regression") assert issue is None + assert database_engine is not None + assert database_engine.optimizer.slow_range_in_select is False + def test_basic_sanity_check(hass_recorder, recorder_db_url): """Test the basic sanity checks with a missing table.""" From 5b0c7321b5de4287991ac623887385bfbed038cc Mon Sep 17 00:00:00 2001 From: Gregory Haynes Date: Sun, 5 Feb 2023 17:34:37 +0000 Subject: [PATCH 02/28] Add missing name field to emulated_hue config (#87456) Co-authored-by: J. Nick Koston --- homeassistant/components/emulated_hue/hue_api.py | 1 + tests/components/emulated_hue/test_hue_api.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index a81544151ea..b855d0c738d 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -831,6 +831,7 @@ def create_hue_success_response( def create_config_model(config: Config, request: web.Request) -> dict[str, Any]: """Create a config resource.""" return { + "name": "HASS BRIDGE", "mac": "00:00:00:00:00:00", "swversion": "01003542", "apiversion": "1.17.0", diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 58303ce54a6..cfadccc39c4 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -465,8 +465,9 @@ async def test_discover_full_state(hue_client): # Make sure array is correct size assert len(result_json) == 2 - assert len(config_json) == 6 + assert len(config_json) == 7 assert len(lights_json) >= 1 + assert "name" in config_json # Make sure the config wrapper added to the config is there assert "mac" in config_json @@ -505,7 +506,8 @@ async def test_discover_config(hue_client): config_json = await result.json() # Make sure array is correct size - assert len(config_json) == 6 + assert len(config_json) == 7 + assert "name" in config_json # Make sure the config wrapper added to the config is there assert "mac" in config_json From 4a7aee4bde666c799e2c9438488e29fe85cb9978 Mon Sep 17 00:00:00 2001 From: majuss Date: Mon, 6 Feb 2023 13:30:35 +0100 Subject: [PATCH 03/28] Bump lupupy to 0.2.7 (#87469) --- homeassistant/components/lupusec/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index f1403e1b2d7..3e47daa0271 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -2,7 +2,7 @@ "domain": "lupusec", "name": "Lupus Electronics LUPUSEC", "documentation": "https://www.home-assistant.io/integrations/lupusec", - "requirements": ["lupupy==0.2.5"], + "requirements": ["lupupy==0.2.7"], "codeowners": ["@majuss"], "iot_class": "local_polling", "loggers": ["lupupy"] diff --git a/requirements_all.txt b/requirements_all.txt index 73861751456..43f76ad1646 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1081,7 +1081,7 @@ london-tube-status==0.5 luftdaten==0.7.4 # homeassistant.components.lupusec -lupupy==0.2.5 +lupupy==0.2.7 # homeassistant.components.lw12wifi lw12==0.9.2 From b2ccf2e87e66f9f128879fbbd11a791e6dae2ad7 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 5 Feb 2023 20:06:17 +0100 Subject: [PATCH 04/28] Bump py-synologydsm-api to 2.1.4 (#87471) fixes undefined --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index a4b340bd3fa..a6664055bc9 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["py-synologydsm-api==2.1.2"], + "requirements": ["py-synologydsm-api==2.1.4"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 43f76ad1646..0a326a79465 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1442,7 +1442,7 @@ py-schluter==0.1.7 py-sucks==0.9.8 # homeassistant.components.synology_dsm -py-synologydsm-api==2.1.2 +py-synologydsm-api==2.1.4 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4dc297934b..146b2e0e1f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1051,7 +1051,7 @@ py-melissa-climate==2.1.4 py-nightscout==1.2.2 # homeassistant.components.synology_dsm -py-synologydsm-api==2.1.2 +py-synologydsm-api==2.1.4 # homeassistant.components.seventeentrack py17track==2021.12.2 From 423d8f0bca14c4cddb3c6e948e303c6f02394bc7 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 5 Feb 2023 13:28:28 -0500 Subject: [PATCH 05/28] Disable uptime sensor by default in Unifi (#87484) Disable Uptime sensor by default in Unifi --- homeassistant/components/unifi/sensor.py | 1 + tests/components/unifi/test_sensor.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index a88b750fdf7..aec123ca273 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -154,6 +154,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, has_entity_name=True, + entity_registry_enabled_default=False, allowed_fn=lambda controller, _: controller.option_allow_uptime_sensors, api_handler_fn=lambda api: api.clients, available_fn=lambda controller, obj_id: controller.available, diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 3aa4114e829..d3823c2fd9e 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -193,6 +193,7 @@ async def test_uptime_sensors( hass, aioclient_mock, mock_unifi_websocket, + entity_registry_enabled_by_default, initial_uptime, event_uptime, new_uptime, @@ -263,7 +264,9 @@ async def test_uptime_sensors( assert hass.states.get("sensor.client1_uptime") is None -async def test_remove_sensors(hass, aioclient_mock, mock_unifi_websocket): +async def test_remove_sensors( + hass, aioclient_mock, mock_unifi_websocket, entity_registry_enabled_by_default +): """Verify removing of clients work as expected.""" wired_client = { "hostname": "Wired client", From e30ea3e344fbef2337420574e94fb006a58f3555 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 6 Feb 2023 17:11:21 +0100 Subject: [PATCH 06/28] Add the correct loggers to velbus manifest.json (#87488) --- homeassistant/components/velbus/manifest.json | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 9384947cb82..9052e73ba69 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -1,13 +1,19 @@ { "domain": "velbus", "name": "Velbus", - "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2022.12.0"], - "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], + "config_flow": true, "dependencies": ["usb"], + "documentation": "https://www.home-assistant.io/integrations/velbus", "integration_type": "hub", "iot_class": "local_push", + "loggers": [ + "velbus-parser", + "velbus-module", + "velbus-packet", + "velbus-protocol" + ], + "requirements": ["velbus-aio==2022.12.0"], "usb": [ { "vid": "10CF", @@ -25,6 +31,5 @@ "vid": "10CF", "pid": "0518" } - ], - "loggers": ["velbusaio"] + ] } From 8a257df59f17cf749740d6db9a688a5163a853e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Feb 2023 21:12:41 -0600 Subject: [PATCH 07/28] Fix recorder run history during schema migration and startup (#87492) Fix recorder run history during schema migration RunHistory.get and RunHistory.current can be called before RunHistory.start. We need to return a RecorderRuns object with the recording_start time that will be used when start it called to ensure history queries still work as expected. fixes #87112 --- homeassistant/components/recorder/run_history.py | 9 +++++++-- tests/components/recorder/test_run_history.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/run_history.py b/homeassistant/components/recorder/run_history.py index fb87d9a1fa2..02b2df066bd 100644 --- a/homeassistant/components/recorder/run_history.py +++ b/homeassistant/components/recorder/run_history.py @@ -64,8 +64,13 @@ class RunHistory: @property def current(self) -> RecorderRuns: """Get the current run.""" - assert self._current_run_info is not None - return self._current_run_info + # If start has not been called yet because the recorder is + # still starting up we want history to use the current time + # as the created time to ensure we can still return results + # and we do not try to pull data from the previous run. + return self._current_run_info or RecorderRuns( + start=self.recording_start, created=dt_util.utcnow() + ) def get(self, start: datetime) -> RecorderRuns | None: """Return the recorder run that started before or at start. diff --git a/tests/components/recorder/test_run_history.py b/tests/components/recorder/test_run_history.py index 7504404f779..3b5bd7dda6b 100644 --- a/tests/components/recorder/test_run_history.py +++ b/tests/components/recorder/test_run_history.py @@ -45,3 +45,14 @@ async def test_run_history(recorder_mock, hass): process_timestamp(instance.run_history.get(now).start) == instance.run_history.recording_start ) + + +async def test_run_history_during_schema_migration(recorder_mock, hass): + """Test the run history during schema migration.""" + instance = recorder.get_instance(hass) + run_history = instance.run_history + assert run_history.current.start == run_history.recording_start + with instance.get_session() as session: + run_history.start(session) + assert run_history.current.start == run_history.recording_start + assert run_history.current.created >= run_history.recording_start From 1a72b64024af52ed602b02b9a3887089cbaf4448 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Mon, 6 Feb 2023 00:31:21 +0100 Subject: [PATCH 08/28] Bump xiaomi-ble to 0.16.1 (#87496) Co-authored-by: J. Nick Koston --- homeassistant/components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 1f36ac10d10..f3904003411 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -14,7 +14,7 @@ } ], "dependencies": ["bluetooth_adapters"], - "requirements": ["xiaomi-ble==0.15.0"], + "requirements": ["xiaomi-ble==0.16.1"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 0a326a79465..9738e087786 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2637,7 +2637,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.15.0 +xiaomi-ble==0.16.1 # homeassistant.components.knx xknx==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 146b2e0e1f8..ad76811f4c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1862,7 +1862,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.15.0 +xiaomi-ble==0.16.1 # homeassistant.components.knx xknx==2.3.0 From 08c23dd688c9220e8cff08a5d332eaa3da2a96e6 Mon Sep 17 00:00:00 2001 From: snapcase Date: Mon, 6 Feb 2023 11:12:54 -0500 Subject: [PATCH 09/28] Bump jaraco.abode to 3.3.0 (#87498) Fixes https://github.com/home-assistant/core/issues/86765 fixes undefined --- homeassistant/components/abode/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index 6045f8797b4..771dfc581a9 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -3,7 +3,7 @@ "name": "Abode", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", - "requirements": ["jaraco.abode==3.2.1"], + "requirements": ["jaraco.abode==3.3.0"], "codeowners": ["@shred86"], "homekit": { "models": ["Abode", "Iota"] diff --git a/requirements_all.txt b/requirements_all.txt index 9738e087786..3c5fe37a04d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -997,7 +997,7 @@ ismartgate==4.0.4 janus==1.0.0 # homeassistant.components.abode -jaraco.abode==3.2.1 +jaraco.abode==3.3.0 # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ad76811f4c6..772f4a10c07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -753,7 +753,7 @@ ismartgate==4.0.4 janus==1.0.0 # homeassistant.components.abode -jaraco.abode==3.2.1 +jaraco.abode==3.3.0 # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 From f59eb6c277ece964052202d82de51e70df03d5bc Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Mon, 6 Feb 2023 00:34:52 +0100 Subject: [PATCH 10/28] Bump bimmer_connected to 0.12.1 (#87506) Co-authored-by: rikroe fixes undefined --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index c03bdf6a26f..c5fbb192846 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.12.0"], + "requirements": ["bimmer_connected==0.12.1"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 3c5fe37a04d..96c78fd861f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -425,7 +425,7 @@ beautifulsoup4==4.11.1 bellows==0.34.7 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.12.0 +bimmer_connected==0.12.1 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 772f4a10c07..f21105746fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -355,7 +355,7 @@ beautifulsoup4==4.11.1 bellows==0.34.7 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.12.0 +bimmer_connected==0.12.1 # homeassistant.components.bluetooth bleak-retry-connector==2.13.0 From 7fd3f404de22a7e1b5578eac77297a483fc01a26 Mon Sep 17 00:00:00 2001 From: Ben Corrado Date: Mon, 6 Feb 2023 10:00:06 -0800 Subject: [PATCH 11/28] Add LD2410BLE support for new firmware version (#87507) * Updated local_name to reflect the naming scheme in HLK firmware 2.01.23020209 * Adding generated bluetooth file. --- homeassistant/components/ld2410_ble/manifest.json | 9 ++++++++- homeassistant/generated/bluetooth.py | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index df7ddff7018..14af223baf0 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -6,7 +6,14 @@ "requirements": ["bluetooth-data-tools==0.3.1", "ld2410-ble==0.1.1"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@930913"], - "bluetooth": [{ "local_name": "HLK-LD2410B_*" }], + "bluetooth": [ + { + "local_name": "HLK-LD2410B_*" + }, + { + "local_name": "HLK-LD2410_*" + } + ], "integration_type": "device", "iot_class": "local_push" } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index efc05ff2831..ef79445ff32 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -225,6 +225,10 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "ld2410_ble", "local_name": "HLK-LD2410B_*", }, + { + "domain": "ld2410_ble", + "local_name": "HLK-LD2410_*", + }, { "domain": "led_ble", "local_name": "LEDnet*", From bc01df6b52d0a6a48c23eedc7950e11bfb05ad4a Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Mon, 6 Feb 2023 11:05:28 -0500 Subject: [PATCH 12/28] Bump env_canada to 0.5.28 (#87509) Co-authored-by: J. Nick Koston fixes undefined --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index b7d3644d481..f0e27c30f7d 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,7 +2,7 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.5.27"], + "requirements": ["env_canada==0.5.28"], "codeowners": ["@gwww", "@michaeldavie"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 96c78fd861f..fab562589bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env_canada==0.5.27 +env_canada==0.5.28 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f21105746fe..4e87c19e698 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -511,7 +511,7 @@ energyzero==0.3.1 enocean==0.50 # homeassistant.components.environment_canada -env_canada==0.5.27 +env_canada==0.5.28 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 From 590bdc1f496eb804c62ab26f285fae2af6cd3af6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Feb 2023 07:50:44 -0600 Subject: [PATCH 13/28] Optimize history.get_last_state_changes query (#87554) fixes undefined --- homeassistant/components/recorder/history.py | 48 ++++++++++--------- tests/components/recorder/test_history.py | 2 +- .../recorder/test_history_db_schema_30.py | 2 +- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index e4f5a39f1cc..1d3716abc47 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -519,48 +519,52 @@ def state_changes_during_period( def _get_last_state_changes_stmt( - schema_version: int, number_of_states: int, entity_id: str | None + schema_version: int, number_of_states: int, entity_id: str ) -> StatementLambdaElement: stmt, join_attributes = lambda_stmt_and_join_attributes( schema_version, False, include_last_changed=False ) if schema_version >= 31: - stmt += lambda q: q.filter( - (States.last_changed_ts == States.last_updated_ts) - | States.last_changed_ts.is_(None) + stmt += lambda q: q.where( + States.state_id + == ( + select(States.state_id) + .filter(States.entity_id == entity_id) + .order_by(States.last_updated_ts.desc()) + .limit(number_of_states) + .subquery() + ).c.state_id ) else: - stmt += lambda q: q.filter( - (States.last_changed == States.last_updated) | States.last_changed.is_(None) + stmt += lambda q: q.where( + States.state_id + == ( + select(States.state_id) + .filter(States.entity_id == entity_id) + .order_by(States.last_updated.desc()) + .limit(number_of_states) + .subquery() + ).c.state_id ) - if entity_id: - stmt += lambda q: q.filter(States.entity_id == entity_id) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - if schema_version >= 31: - stmt += lambda q: q.order_by( - States.entity_id, States.last_updated_ts.desc() - ).limit(number_of_states) - else: - stmt += lambda q: q.order_by( - States.entity_id, States.last_updated.desc() - ).limit(number_of_states) + + stmt += lambda q: q.order_by(States.state_id.desc()) return stmt def get_last_state_changes( - hass: HomeAssistant, number_of_states: int, entity_id: str | None + hass: HomeAssistant, number_of_states: int, entity_id: str ) -> MutableMapping[str, list[State]]: """Return the last number_of_states.""" - start_time = dt_util.utcnow() - entity_id = entity_id.lower() if entity_id is not None else None - entity_ids = [entity_id] if entity_id is not None else None + entity_id_lower = entity_id.lower() + entity_ids = [entity_id_lower] with session_scope(hass=hass) as session: stmt = _get_last_state_changes_stmt( - _schema_version(hass), number_of_states, entity_id + _schema_version(hass), number_of_states, entity_id_lower ) states = list(execute_stmt_lambda_element(session, stmt)) return cast( @@ -569,7 +573,7 @@ def get_last_state_changes( hass, session, reversed(states), - start_time, + dt_util.utcnow(), entity_ids, include_start_time_state=False, ), diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index c35f0075844..fab2b7fbacd 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -324,7 +324,7 @@ def test_get_last_state_changes(hass_recorder): start = dt_util.utcnow() - timedelta(minutes=2) point = start + timedelta(minutes=1) - point2 = point + timedelta(minutes=1) + point2 = point + timedelta(minutes=1, seconds=1) with patch( "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start diff --git a/tests/components/recorder/test_history_db_schema_30.py b/tests/components/recorder/test_history_db_schema_30.py index 5e944ce454a..f1104145b18 100644 --- a/tests/components/recorder/test_history_db_schema_30.py +++ b/tests/components/recorder/test_history_db_schema_30.py @@ -249,7 +249,7 @@ def test_get_last_state_changes(hass_recorder): start = dt_util.utcnow() - timedelta(minutes=2) point = start + timedelta(minutes=1) - point2 = point + timedelta(minutes=1) + point2 = point + timedelta(minutes=1, seconds=1) with patch( "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start From 5bc49b1757563a6831040dcbf897df04710cdd55 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Feb 2023 23:57:08 -0500 Subject: [PATCH 14/28] OpenAI: Ignore devices without a name (#87558) Ignore devices without a name --- homeassistant/components/openai_conversation/const.py | 2 +- tests/components/openai_conversation/test_init.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index 378548173b0..b5644915d91 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -9,7 +9,7 @@ An overview of the areas and the devices in this smart home: {%- for area in areas %} {%- set area_info = namespace(printed=false) %} {%- for device in area_devices(area.name) -%} - {%- if not device_attr(device, "disabled_by") and not device_attr(device, "entry_type") %} + {%- if not device_attr(device, "disabled_by") and not device_attr(device, "entry_type") and device_attr(device, "name") %} {%- if not area_info.printed %} {{ area.name }}: diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index 551d493df8e..b64f3322895 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -67,6 +67,13 @@ async def test_default_prompt(hass, mock_init_component): device_reg.async_update_device( device.id, disabled_by=device_registry.DeviceEntryDisabler.USER ) + device = device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "9876-no-name")}, + manufacturer="Test Manufacturer NoName", + model="Test Model NoName", + suggested_area="Test Area 2", + ) with patch("openai.Completion.create") as mock_create: result = await conversation.async_converse(hass, "hello", None, Context()) From 9657567280f254bfd9f444c0a5f0c7812b8875ca Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 6 Feb 2023 14:28:53 -0500 Subject: [PATCH 15/28] Bump oralb-ble to 0.17.4 (#87570) closes undefined --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index 08b257c1869..e2f1749eb76 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.17.2"], + "requirements": ["oralb-ble==0.17.4"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco", "@Lash-L"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index fab562589bf..616ef1fd78e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1299,7 +1299,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.17.2 +oralb-ble==0.17.4 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e87c19e698..341b5a0d269 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -947,7 +947,7 @@ openai==0.26.2 openerz-api==0.2.0 # homeassistant.components.oralb -oralb-ble==0.17.2 +oralb-ble==0.17.4 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From d06e640889bd61f7292f1e3fd5de9dce961be8a2 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 6 Feb 2023 22:41:52 +0100 Subject: [PATCH 16/28] Fix matter remove config entry device (#87571) --- homeassistant/components/matter/__init__.py | 20 ++--- .../components/matter/diagnostics.py | 21 +---- homeassistant/components/matter/helpers.py | 42 ++++++++- tests/components/matter/test_helpers.py | 50 ++++++++++- tests/components/matter/test_init.py | 85 ++++++++++++++++++- 5 files changed, 180 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/matter/__init__.py b/homeassistant/components/matter/__init__.py index a0e4dcf7483..6a11e5ded44 100644 --- a/homeassistant/components/matter/__init__.py +++ b/homeassistant/components/matter/__init__.py @@ -32,7 +32,7 @@ from .addon import get_addon_manager from .api import async_register_api from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON, DOMAIN, LOGGER from .device_platform import DEVICE_PLATFORM -from .helpers import MatterEntryData, get_matter +from .helpers import MatterEntryData, get_matter, get_node_from_device_entry CONNECT_TIMEOUT = 10 LISTEN_READY_TIMEOUT = 30 @@ -192,23 +192,13 @@ async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove a config entry from a device.""" - unique_id = None + node = await get_node_from_device_entry(hass, device_entry) - for ident in device_entry.identifiers: - if ident[0] == DOMAIN: - unique_id = ident[1] - break - - if not unique_id: + if node is None: return True - matter_entry_data: MatterEntryData = hass.data[DOMAIN][config_entry.entry_id] - matter_client = matter_entry_data.adapter.matter_client - - for node in await matter_client.get_nodes(): - if node.unique_id == unique_id: - await matter_client.remove_node(node.node_id) - break + matter = get_matter(hass) + await matter.matter_client.remove_node(node.node_id) return True diff --git a/homeassistant/components/matter/diagnostics.py b/homeassistant/components/matter/diagnostics.py index 77234ab48b5..571523f7f0c 100644 --- a/homeassistant/components/matter/diagnostics.py +++ b/homeassistant/components/matter/diagnostics.py @@ -11,8 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from .const import DOMAIN, ID_TYPE_DEVICE_ID -from .helpers import get_device_id, get_matter +from .helpers import get_matter, get_node_from_device_entry ATTRIBUTES_TO_REDACT = {"chip.clusters.Objects.BasicInformation.Attributes.Location"} @@ -53,28 +52,14 @@ async def async_get_device_diagnostics( ) -> dict[str, Any]: """Return diagnostics for a device.""" matter = get_matter(hass) - device_id_type_prefix = f"{ID_TYPE_DEVICE_ID}_" - device_id_full = next( - identifier[1] - for identifier in device.identifiers - if identifier[0] == DOMAIN and identifier[1].startswith(device_id_type_prefix) - ) - device_id = device_id_full.lstrip(device_id_type_prefix) - server_diagnostics = await matter.matter_client.get_diagnostics() - - node = next( - node - for node in await matter.matter_client.get_nodes() - for node_device in node.node_devices - if get_device_id(server_diagnostics.info, node_device) == device_id - ) + node = await get_node_from_device_entry(hass, device) return { "server_info": remove_serialization_type( dataclass_to_dict(server_diagnostics.info) ), "node": redact_matter_attributes( - remove_serialization_type(dataclass_to_dict(node)) + remove_serialization_type(dataclass_to_dict(node) if node else {}) ), } diff --git a/homeassistant/components/matter/helpers.py b/homeassistant/components/matter/helpers.py index 5abf81ee608..ef42f9354ca 100644 --- a/homeassistant/components/matter/helpers.py +++ b/homeassistant/components/matter/helpers.py @@ -6,8 +6,9 @@ from dataclasses import dataclass from typing import TYPE_CHECKING from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr -from .const import DOMAIN +from .const import DOMAIN, ID_TYPE_DEVICE_ID if TYPE_CHECKING: from matter_server.common.models.node import MatterNode @@ -58,3 +59,42 @@ def get_device_id( # Append nodedevice(type) to differentiate between a root node # and bridge within Home Assistant devices. return f"{operational_instance_id}-{node_device.__class__.__name__}" + + +async def get_node_from_device_entry( + hass: HomeAssistant, device: dr.DeviceEntry +) -> MatterNode | None: + """Return MatterNode from device entry.""" + matter = get_matter(hass) + device_id_type_prefix = f"{ID_TYPE_DEVICE_ID}_" + device_id_full = next( + ( + identifier[1] + for identifier in device.identifiers + if identifier[0] == DOMAIN + and identifier[1].startswith(device_id_type_prefix) + ), + None, + ) + + if device_id_full is None: + raise ValueError(f"Device {device.id} is not a Matter device") + + device_id = device_id_full.lstrip(device_id_type_prefix) + matter_client = matter.matter_client + server_info = matter_client.server_info + + if server_info is None: + raise RuntimeError("Matter server information is not available") + + node = next( + ( + node + for node in await matter_client.get_nodes() + for node_device in node.node_devices + if get_device_id(server_info, node_device) == device_id + ), + None, + ) + + return node diff --git a/tests/components/matter/test_helpers.py b/tests/components/matter/test_helpers.py index 3da0a26b7ee..8f849c85941 100644 --- a/tests/components/matter/test_helpers.py +++ b/tests/components/matter/test_helpers.py @@ -3,11 +3,20 @@ from __future__ import annotations from unittest.mock import MagicMock -from homeassistant.components.matter.helpers import get_device_id +import pytest + +from homeassistant.components.matter.const import DOMAIN +from homeassistant.components.matter.helpers import ( + get_device_id, + get_node_from_device_entry, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from .common import setup_integration_with_node_fixture +from tests.common import MockConfigEntry + async def test_get_device_id( hass: HomeAssistant, @@ -20,3 +29,42 @@ async def test_get_device_id( device_id = get_device_id(matter_client.server_info, node.node_devices[0]) assert device_id == "00000000000004D2-0000000000000005-MatterNodeDevice" + + +async def test_get_node_from_device_entry( + hass: HomeAssistant, + matter_client: MagicMock, +) -> None: + """Test get_node_from_device_entry.""" + device_registry = dr.async_get(hass) + other_domain = "other_domain" + other_config_entry = MockConfigEntry(domain=other_domain) + other_device_entry = device_registry.async_get_or_create( + config_entry_id=other_config_entry.entry_id, + identifiers={(other_domain, "1234")}, + ) + node = await setup_integration_with_node_fixture( + hass, "device_diagnostics", matter_client + ) + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + device_entry = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + )[0] + assert device_entry + node_from_device_entry = await get_node_from_device_entry(hass, device_entry) + + assert node_from_device_entry is node + + with pytest.raises(ValueError) as value_error: + await get_node_from_device_entry(hass, other_device_entry) + + assert f"Device {other_device_entry.id} is not a Matter device" in str( + value_error.value + ) + + matter_client.server_info = None + + with pytest.raises(RuntimeError) as runtime_error: + node_from_device_entry = await get_node_from_device_entry(hass, device_entry) + + assert "Matter server information is not available" in str(runtime_error.value) diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index a3febe799a5..ce2e1010f64 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio -from collections.abc import Generator +from collections.abc import Awaitable, Callable, Generator from unittest.mock import AsyncMock, MagicMock, call, patch +from aiohttp import ClientWebSocketResponse from matter_server.client.exceptions import CannotConnect, InvalidServerVersion from matter_server.common.helpers.util import dataclass_from_dict from matter_server.common.models.error import MatterError @@ -16,9 +17,14 @@ from homeassistant.components.matter.const import DOMAIN from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir +from homeassistant.helpers import ( + device_registry as dr, + entity_registry as er, + issue_registry as ir, +) +from homeassistant.setup import async_setup_component -from .common import load_and_parse_node_fixture +from .common import load_and_parse_node_fixture, setup_integration_with_node_fixture from tests.common import MockConfigEntry @@ -587,3 +593,76 @@ async def test_remove_entry( assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to uninstall the Matter Server add-on" in caplog.text + + +async def test_remove_config_entry_device( + hass: HomeAssistant, + matter_client: MagicMock, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test that a device can be removed ok.""" + assert await async_setup_component(hass, "config", {}) + await setup_integration_with_node_fixture(hass, "device_diagnostics", matter_client) + await hass.async_block_till_done() + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + device_registry = dr.async_get(hass) + device_entry = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + )[0] + entity_registry = er.async_get(hass) + entity_id = "light.m5stamp_lighting_app" + + assert device_entry + assert entity_registry.async_get(entity_id) + assert hass.states.get(entity_id) + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry.entry_id, + "device_id": device_entry.id, + } + ) + response = await client.receive_json() + assert response["success"] + await hass.async_block_till_done() + + assert not device_registry.async_get(device_entry.id) + assert not entity_registry.async_get(entity_id) + assert not hass.states.get(entity_id) + + +async def test_remove_config_entry_device_no_node( + hass: HomeAssistant, + matter_client: MagicMock, + integration: MockConfigEntry, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test that a device can be removed ok without an existing node.""" + assert await async_setup_component(hass, "config", {}) + config_entry = integration + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={ + (DOMAIN, "deviceid_00000000000004D2-0000000000000005-MatterNodeDevice") + }, + ) + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry.entry_id, + "device_id": device_entry.id, + } + ) + response = await client.receive_json() + assert response["success"] + await hass.async_block_till_done() + + assert not device_registry.async_get(device_entry.id) From 0d9393ca793f68e59cc5ec3ed5a27012082366dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Feb 2023 22:59:54 -0600 Subject: [PATCH 17/28] Fix indent on slow_range_in_select for MySQL/MariaDB (#87581) We would calculate this value and throw it away because it was only used on first_connection --- homeassistant/components/recorder/util.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index dde32bf05db..ff6f9f1e7e1 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -533,13 +533,13 @@ def setup_connection_for_dialect( version or version_string, "MySQL", MIN_VERSION_MYSQL ) - slow_range_in_select = bool( - not version - or version < MARIADB_WITH_FIXED_IN_QUERIES_105 - or MARIA_DB_106 <= version < MARIADB_WITH_FIXED_IN_QUERIES_106 - or MARIA_DB_107 <= version < MARIADB_WITH_FIXED_IN_QUERIES_107 - or MARIA_DB_108 <= version < MARIADB_WITH_FIXED_IN_QUERIES_108 - ) + slow_range_in_select = bool( + not version + or version < MARIADB_WITH_FIXED_IN_QUERIES_105 + or MARIA_DB_106 <= version < MARIADB_WITH_FIXED_IN_QUERIES_106 + or MARIA_DB_107 <= version < MARIADB_WITH_FIXED_IN_QUERIES_107 + or MARIA_DB_108 <= version < MARIADB_WITH_FIXED_IN_QUERIES_108 + ) elif dialect_name == SupportedDialect.POSTGRESQL: # Historically we have marked PostgreSQL as having slow range in select # but this may not be true for all versions. We should investigate From e0a6a6cfa6cd2f5b449f1edf450db3fa23aef3d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Feb 2023 12:38:48 -0600 Subject: [PATCH 18/28] Fix LD2410 BLE detection with passive scans (#87584) --- homeassistant/components/ld2410_ble/manifest.json | 5 +++++ homeassistant/generated/bluetooth.py | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index 14af223baf0..9d133876a7d 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -12,6 +12,11 @@ }, { "local_name": "HLK-LD2410_*" + }, + { + "manufacturer_id": 256, + "manufacturer_data_start": [7, 1], + "service_uuid": "0000af30-0000-1000-8000-00805f9b34fb" } ], "integration_type": "device", diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index ef79445ff32..031791ca106 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -229,6 +229,15 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "ld2410_ble", "local_name": "HLK-LD2410_*", }, + { + "domain": "ld2410_ble", + "manufacturer_data_start": [ + 7, + 1, + ], + "manufacturer_id": 256, + "service_uuid": "0000af30-0000-1000-8000-00805f9b34fb", + }, { "domain": "led_ble", "local_name": "LEDnet*", From be837535144b2abee4223f8013f4bee2c62e81ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Feb 2023 22:59:21 -0600 Subject: [PATCH 19/28] Bump inkbird-ble to 0.5.6 (#87590) changelog: https://github.com/Bluetooth-Devices/inkbird-ble/compare/v0.5.5...v0.5.6 fixes #85432 --- homeassistant/components/inkbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index e9984284c72..90bb44101af 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -10,7 +10,7 @@ { "local_name": "xBBQ*", "connectable": false }, { "local_name": "tps", "connectable": false } ], - "requirements": ["inkbird-ble==0.5.5"], + "requirements": ["inkbird-ble==0.5.6"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 616ef1fd78e..59551eeaf53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -976,7 +976,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.5 +inkbird-ble==0.5.6 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 341b5a0d269..4a590176ec8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -735,7 +735,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.5 +inkbird-ble==0.5.6 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 From ab14671dbc97c16cdb49956e1e9dabbdcfca9740 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Feb 2023 22:59:17 -0600 Subject: [PATCH 20/28] Bump sensorpro-ble to 0.5.3 (#87591) changelog: https://github.com/Bluetooth-Devices/sensorpro-ble/compare/v0.5.1...v0.5.3 same fix as #85432 but for sensorpro --- homeassistant/components/sensorpro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensorpro/manifest.json b/homeassistant/components/sensorpro/manifest.json index 07499609133..603c29b539c 100644 --- a/homeassistant/components/sensorpro/manifest.json +++ b/homeassistant/components/sensorpro/manifest.json @@ -15,7 +15,7 @@ "connectable": false } ], - "requirements": ["sensorpro-ble==0.5.1"], + "requirements": ["sensorpro-ble==0.5.3"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 59551eeaf53..b4ef3784405 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2312,7 +2312,7 @@ sense_energy==0.11.1 sensirion-ble==0.0.1 # homeassistant.components.sensorpro -sensorpro-ble==0.5.1 +sensorpro-ble==0.5.3 # homeassistant.components.sensorpush sensorpush-ble==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a590176ec8..1bcd3c41d9d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1627,7 +1627,7 @@ sense_energy==0.11.1 sensirion-ble==0.0.1 # homeassistant.components.sensorpro -sensorpro-ble==0.5.1 +sensorpro-ble==0.5.3 # homeassistant.components.sensorpush sensorpush-ble==1.5.2 From c4470fc36dc738f12f2a18ea5bfdc3d54a7a5b11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Feb 2023 22:59:13 -0600 Subject: [PATCH 21/28] Bump thermopro-ble to 0.4.5 (#87592) changelog: https://github.com/Bluetooth-Devices/thermopro-ble/compare/v0.4.3..v0.4.5 same fix as #85432 but for thermopro --- homeassistant/components/thermopro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/thermopro/manifest.json b/homeassistant/components/thermopro/manifest.json index e9135a44324..fc9efdf81b2 100644 --- a/homeassistant/components/thermopro/manifest.json +++ b/homeassistant/components/thermopro/manifest.json @@ -8,7 +8,7 @@ { "local_name": "TP39*", "connectable": false } ], "dependencies": ["bluetooth_adapters"], - "requirements": ["thermopro-ble==0.4.3"], + "requirements": ["thermopro-ble==0.4.5"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index b4ef3784405..a5e05b06d3c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2481,7 +2481,7 @@ tesla-wall-connector==1.0.2 thermobeacon-ble==0.6.0 # homeassistant.components.thermopro -thermopro-ble==0.4.3 +thermopro-ble==0.4.5 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1bcd3c41d9d..dcb47b8ba80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1745,7 +1745,7 @@ tesla-wall-connector==1.0.2 thermobeacon-ble==0.6.0 # homeassistant.components.thermopro -thermopro-ble==0.4.3 +thermopro-ble==0.4.5 # homeassistant.components.tilt_ble tilt-ble==0.2.3 From e67d9988fd5887f3d8f23ffaaa2898c9289d9f31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Feb 2023 22:59:09 -0600 Subject: [PATCH 22/28] Bump bluemaestro-ble to 0.2.3 (#87594) changelog: https://github.com/Bluetooth-Devices/bluemaestro-ble/compare/v0.2.1..v0.2.3 same fix as #85432 but for bluemaestro --- homeassistant/components/bluemaestro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluemaestro/manifest.json b/homeassistant/components/bluemaestro/manifest.json index 277e02ab488..d4a9890efae 100644 --- a/homeassistant/components/bluemaestro/manifest.json +++ b/homeassistant/components/bluemaestro/manifest.json @@ -9,7 +9,7 @@ "connectable": false } ], - "requirements": ["bluemaestro-ble==0.2.1"], + "requirements": ["bluemaestro-ble==0.2.3"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index a5e05b06d3c..c7674ea4029 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ blinkstick==1.2.0 blockchain==1.4.4 # homeassistant.components.bluemaestro -bluemaestro-ble==0.2.1 +bluemaestro-ble==0.2.3 # homeassistant.components.decora # homeassistant.components.zengge diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dcb47b8ba80..742a765a8bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ blebox_uniapi==2.1.4 blinkpy==0.19.2 # homeassistant.components.bluemaestro -bluemaestro-ble==0.2.1 +bluemaestro-ble==0.2.3 # homeassistant.components.bluetooth bluetooth-adapters==0.15.2 From a2c9f9242097944bd09bd6551bf97671b5df485f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Feb 2023 22:59:05 -0600 Subject: [PATCH 23/28] Bump sensorpush-ble to 1.5.5 (#87595) * Bump sensorpush-ble to 1.5.4 changelog: https://github.com/Bluetooth-Devices/sensorpush-ble/compare/v1.5.2..v1.5.4 same fix as #85432 but for sensorpush * bump again to fix parser with passive scans --- homeassistant/components/sensorpush/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensorpush/manifest.json b/homeassistant/components/sensorpush/manifest.json index 7046837e45f..7e68628c91e 100644 --- a/homeassistant/components/sensorpush/manifest.json +++ b/homeassistant/components/sensorpush/manifest.json @@ -9,7 +9,7 @@ "connectable": false } ], - "requirements": ["sensorpush-ble==1.5.2"], + "requirements": ["sensorpush-ble==1.5.5"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index c7674ea4029..c28ee421fa3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2315,7 +2315,7 @@ sensirion-ble==0.0.1 sensorpro-ble==0.5.3 # homeassistant.components.sensorpush -sensorpush-ble==1.5.2 +sensorpush-ble==1.5.5 # homeassistant.components.sentry sentry-sdk==1.13.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 742a765a8bd..25409f3a692 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1630,7 +1630,7 @@ sensirion-ble==0.0.1 sensorpro-ble==0.5.3 # homeassistant.components.sensorpush -sensorpush-ble==1.5.2 +sensorpush-ble==1.5.5 # homeassistant.components.sentry sentry-sdk==1.13.0 From 2e541e7ef64eed70a1cd9287fb7013c3f0ffd897 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 6 Feb 2023 19:57:16 -0800 Subject: [PATCH 24/28] Improve rainbird device reliability by sending requests serially (#87603) Send rainbird requests serially --- .../components/rainbird/coordinator.py | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/rainbird/coordinator.py b/homeassistant/components/rainbird/coordinator.py index ddb2b70324d..14598921a61 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from dataclasses import dataclass import datetime import logging @@ -84,27 +83,18 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): raise UpdateFailed(f"Error communicating with Device: {err}") from err async def _fetch_data(self) -> RainbirdDeviceState: - """Fetch data from the Rain Bird device.""" - (zones, states, rain, rain_delay) = await asyncio.gather( - self._fetch_zones(), - self._controller.get_zone_states(), - self._controller.get_rain_sensor_state(), - self._controller.get_rain_delay(), - ) + """Fetch data from the Rain Bird device. + + Rainbird devices can only reliably handle a single request at a time, + so the requests are sent serially. + """ + available_stations = await self._controller.get_available_stations() + states = await self._controller.get_zone_states() + rain = await self._controller.get_rain_sensor_state() + rain_delay = await self._controller.get_rain_delay() return RainbirdDeviceState( - zones=set(zones), - active_zones={zone for zone in zones if states.active(zone)}, + zones=available_stations.active_set, + active_zones=states.active_set, rain=rain, rain_delay=rain_delay, ) - - async def _fetch_zones(self) -> set[int]: - """Fetch the zones from the device, caching the results.""" - if self._zones is None: - available_stations = await self._controller.get_available_stations() - self._zones = { - zone - for zone in range(1, available_stations.stations.count + 1) - if available_stations.stations.active(zone) - } - return self._zones From 354d77d26b49f21939b848e802535f4c4c255a4a Mon Sep 17 00:00:00 2001 From: Thomas Hollstegge Date: Tue, 7 Feb 2023 19:39:08 +0100 Subject: [PATCH 25/28] Do not return cached values for entity states in emulated_hue (#87642) * Do not return cached values for state and brightness * Move building the uncached state dict behind a lru_cache (_build_entity_state_dict) --------- Co-authored-by: J. Nick Koston --- .../components/emulated_hue/hue_api.py | 146 ++++++++++-------- tests/components/emulated_hue/test_hue_api.py | 13 +- 2 files changed, 92 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index b855d0c738d..41c25943a77 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -634,77 +634,87 @@ def get_entity_state_dict(config: Config, entity: State) -> dict[str, Any]: # Remove the now stale cached entry. config.cached_states.pop(entity.entity_id) + if cached_state is None: + return _build_entity_state_dict(entity) + + data: dict[str, Any] = cached_state + # Make sure brightness is valid + if data[STATE_BRIGHTNESS] is None: + data[STATE_BRIGHTNESS] = HUE_API_STATE_BRI_MAX if data[STATE_ON] else 0 + + # Make sure hue/saturation are valid + if (data[STATE_HUE] is None) or (data[STATE_SATURATION] is None): + data[STATE_HUE] = 0 + data[STATE_SATURATION] = 0 + + # If the light is off, set the color to off + if data[STATE_BRIGHTNESS] == 0: + data[STATE_HUE] = 0 + data[STATE_SATURATION] = 0 + + _clamp_values(data) + return data + + +@lru_cache(maxsize=512) +def _build_entity_state_dict(entity: State) -> dict[str, Any]: + """Build a state dict for an entity.""" data: dict[str, Any] = { - STATE_ON: False, + STATE_ON: entity.state != STATE_OFF, STATE_BRIGHTNESS: None, STATE_HUE: None, STATE_SATURATION: None, STATE_COLOR_TEMP: None, } - - if cached_state is None: - data[STATE_ON] = entity.state != STATE_OFF - - if data[STATE_ON]: - data[STATE_BRIGHTNESS] = hass_to_hue_brightness( - entity.attributes.get(ATTR_BRIGHTNESS, 0) - ) - hue_sat = entity.attributes.get(ATTR_HS_COLOR) - if hue_sat is not None: - hue = hue_sat[0] - sat = hue_sat[1] - # Convert hass hs values back to hue hs values - data[STATE_HUE] = int((hue / 360.0) * HUE_API_STATE_HUE_MAX) - data[STATE_SATURATION] = int((sat / 100.0) * HUE_API_STATE_SAT_MAX) - else: - data[STATE_HUE] = HUE_API_STATE_HUE_MIN - data[STATE_SATURATION] = HUE_API_STATE_SAT_MIN - data[STATE_COLOR_TEMP] = entity.attributes.get(ATTR_COLOR_TEMP, 0) - + if data[STATE_ON]: + data[STATE_BRIGHTNESS] = hass_to_hue_brightness( + entity.attributes.get(ATTR_BRIGHTNESS, 0) + ) + hue_sat = entity.attributes.get(ATTR_HS_COLOR) + if hue_sat is not None: + hue = hue_sat[0] + sat = hue_sat[1] + # Convert hass hs values back to hue hs values + data[STATE_HUE] = int((hue / 360.0) * HUE_API_STATE_HUE_MAX) + data[STATE_SATURATION] = int((sat / 100.0) * HUE_API_STATE_SAT_MAX) else: - data[STATE_BRIGHTNESS] = 0 - data[STATE_HUE] = 0 - data[STATE_SATURATION] = 0 - data[STATE_COLOR_TEMP] = 0 + data[STATE_HUE] = HUE_API_STATE_HUE_MIN + data[STATE_SATURATION] = HUE_API_STATE_SAT_MIN + data[STATE_COLOR_TEMP] = entity.attributes.get(ATTR_COLOR_TEMP, 0) - if entity.domain == climate.DOMAIN: - temperature = entity.attributes.get(ATTR_TEMPERATURE, 0) - # Convert 0-100 to 0-254 - data[STATE_BRIGHTNESS] = round(temperature * HUE_API_STATE_BRI_MAX / 100) - elif entity.domain == humidifier.DOMAIN: - humidity = entity.attributes.get(ATTR_HUMIDITY, 0) - # Convert 0-100 to 0-254 - data[STATE_BRIGHTNESS] = round(humidity * HUE_API_STATE_BRI_MAX / 100) - elif entity.domain == media_player.DOMAIN: - level = entity.attributes.get( - ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0 - ) - # Convert 0.0-1.0 to 0-254 - data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX) - elif entity.domain == fan.DOMAIN: - percentage = entity.attributes.get(ATTR_PERCENTAGE) or 0 - # Convert 0-100 to 0-254 - data[STATE_BRIGHTNESS] = round(percentage * HUE_API_STATE_BRI_MAX / 100) - elif entity.domain == cover.DOMAIN: - level = entity.attributes.get(ATTR_CURRENT_POSITION, 0) - data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX) else: - data = cached_state - # Make sure brightness is valid - if data[STATE_BRIGHTNESS] is None: - data[STATE_BRIGHTNESS] = HUE_API_STATE_BRI_MAX if data[STATE_ON] else 0 + data[STATE_BRIGHTNESS] = 0 + data[STATE_HUE] = 0 + data[STATE_SATURATION] = 0 + data[STATE_COLOR_TEMP] = 0 - # Make sure hue/saturation are valid - if (data[STATE_HUE] is None) or (data[STATE_SATURATION] is None): - data[STATE_HUE] = 0 - data[STATE_SATURATION] = 0 + if entity.domain == climate.DOMAIN: + temperature = entity.attributes.get(ATTR_TEMPERATURE, 0) + # Convert 0-100 to 0-254 + data[STATE_BRIGHTNESS] = round(temperature * HUE_API_STATE_BRI_MAX / 100) + elif entity.domain == humidifier.DOMAIN: + humidity = entity.attributes.get(ATTR_HUMIDITY, 0) + # Convert 0-100 to 0-254 + data[STATE_BRIGHTNESS] = round(humidity * HUE_API_STATE_BRI_MAX / 100) + elif entity.domain == media_player.DOMAIN: + level = entity.attributes.get( + ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0 + ) + # Convert 0.0-1.0 to 0-254 + data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX) + elif entity.domain == fan.DOMAIN: + percentage = entity.attributes.get(ATTR_PERCENTAGE) or 0 + # Convert 0-100 to 0-254 + data[STATE_BRIGHTNESS] = round(percentage * HUE_API_STATE_BRI_MAX / 100) + elif entity.domain == cover.DOMAIN: + level = entity.attributes.get(ATTR_CURRENT_POSITION, 0) + data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX) + _clamp_values(data) + return data - # If the light is off, set the color to off - if data[STATE_BRIGHTNESS] == 0: - data[STATE_HUE] = 0 - data[STATE_SATURATION] = 0 - # Clamp brightness, hue, saturation, and color temp to valid values +def _clamp_values(data: dict[str, Any]) -> None: + """Clamp brightness, hue, saturation, and color temp to valid values.""" for key, v_min, v_max in ( (STATE_BRIGHTNESS, HUE_API_STATE_BRI_MIN, HUE_API_STATE_BRI_MAX), (STATE_HUE, HUE_API_STATE_HUE_MIN, HUE_API_STATE_HUE_MAX), @@ -714,8 +724,6 @@ def get_entity_state_dict(config: Config, entity: State) -> dict[str, Any]: if data[key] is not None: data[key] = max(v_min, min(data[key], v_max)) - return data - @lru_cache(maxsize=1024) def _entity_unique_id(entity_id: str) -> str: @@ -843,10 +851,18 @@ def create_config_model(config: Config, request: web.Request) -> dict[str, Any]: def create_list_of_entities(config: Config, request: web.Request) -> dict[str, Any]: """Create a list of all entities.""" - json_response: dict[str, Any] = { - config.entity_id_to_number(state.entity_id): state_to_json(config, state) - for state in config.get_exposed_states() - } + hass: core.HomeAssistant = request.app["hass"] + + json_response: dict[str, Any] = {} + for cached_state in config.get_exposed_states(): + entity_id = cached_state.entity_id + state = hass.states.get(entity_id) + assert state is not None + + json_response[config.entity_id_to_number(entity_id)] = state_to_json( + config, state + ) + return json_response diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index cfadccc39c4..3d2d91dfa6d 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -301,6 +301,7 @@ async def test_discover_lights(hass, hue_client): await hass.async_block_till_done() result_json = await async_get_lights(hue_client) + assert "1" not in result_json.keys() devices = {val["uniqueid"] for val in result_json.values()} assert "00:2f:d2:31:ce:c5:55:cc-ee" not in devices # light.ceiling_lights @@ -308,8 +309,16 @@ async def test_discover_lights(hass, hue_client): hass.states.async_set("light.ceiling_lights", STATE_ON) await hass.async_block_till_done() result_json = await async_get_lights(hue_client) - devices = {val["uniqueid"] for val in result_json.values()} - assert "00:2f:d2:31:ce:c5:55:cc-ee" in devices # light.ceiling_lights + device = result_json["1"] # Test that light ID did not change + assert device["uniqueid"] == "00:2f:d2:31:ce:c5:55:cc-ee" # light.ceiling_lights + assert device["state"][HUE_API_STATE_ON] is True + + # Test that returned value is fresh and not cached + hass.states.async_set("light.ceiling_lights", STATE_OFF) + await hass.async_block_till_done() + result_json = await async_get_lights(hue_client) + device = result_json["1"] + assert device["state"][HUE_API_STATE_ON] is False async def test_light_without_brightness_supported(hass_hue, hue_client): From 08fb16a0c28909672f569f0224abb095e97cb794 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Feb 2023 13:40:53 -0500 Subject: [PATCH 26/28] Bumped version to 2023.2.3 --- 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 168bf80aa11..63d691f6ac7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 825c2bc356f..6253e204117 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.2" +version = "2023.2.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From aacd8e044d9c6e8da4e64e2aabc16262a4ac2cf5 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 29 Jan 2023 06:11:56 -0800 Subject: [PATCH 27/28] Bump pyrainbird to 2.0.0 (#86851) --- homeassistant/components/rainbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 50eb11c3fe9..dcecaac81ab 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -3,7 +3,7 @@ "name": "Rain Bird", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainbird", - "requirements": ["pyrainbird==1.1.0"], + "requirements": ["pyrainbird==2.0.0"], "codeowners": ["@konikvranik", "@allenporter"], "iot_class": "local_polling", "loggers": ["pyrainbird"] diff --git a/requirements_all.txt b/requirements_all.txt index c28ee421fa3..9f2558e5aa7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1896,7 +1896,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==1.1.0 +pyrainbird==2.0.0 # homeassistant.components.recswitch pyrecswitch==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 25409f3a692..c3b2ad9d2bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1367,7 +1367,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.rainbird -pyrainbird==1.1.0 +pyrainbird==2.0.0 # homeassistant.components.risco pyrisco==0.5.7 From 8e8a170121d8279354489125f169e6161274d71d Mon Sep 17 00:00:00 2001 From: shbatm Date: Tue, 7 Feb 2023 13:03:27 -0600 Subject: [PATCH 28/28] Bump PyISY to 3.1.13, check portal for network buttons (#87650) --- homeassistant/components/isy994/__init__.py | 8 +++++--- homeassistant/components/isy994/const.py | 1 - homeassistant/components/isy994/manifest.json | 2 +- homeassistant/components/isy994/services.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index c9e4f6ed16e..3612e87f8e6 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -7,6 +7,7 @@ from urllib.parse import urlparse from aiohttp import CookieJar import async_timeout from pyisy import ISY, ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError +from pyisy.constants import CONFIG_NETWORKING, CONFIG_PORTAL import voluptuous as vol from homeassistant import config_entries @@ -43,7 +44,6 @@ from .const import ( ISY_CONF_FIRMWARE, ISY_CONF_MODEL, ISY_CONF_NAME, - ISY_CONF_NETWORKING, MANUFACTURER, PLATFORMS, SCHEME_HTTP, @@ -220,9 +220,11 @@ async def async_setup_entry( numbers = isy_data.variables[Platform.NUMBER] for vtype, _, vid in isy.variables.children: numbers.append(isy.variables[vtype][vid]) - if isy.conf[ISY_CONF_NETWORKING]: + if ( + isy.conf[CONFIG_NETWORKING] or isy.conf[CONFIG_PORTAL] + ) and isy.networking.nobjs: isy_data.devices[CONF_NETWORK] = _create_service_device_info( - isy, name=ISY_CONF_NETWORKING, unique_id=CONF_NETWORK + isy, name=CONFIG_NETWORKING, unique_id=CONF_NETWORK ) for resource in isy.networking.nobjs: isy_data.net_resources.append(resource) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 95e68c5fa11..37ae1a82b91 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -118,7 +118,6 @@ SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"] # (they can turn off, and report their state) ISY_GROUP_PLATFORM = Platform.SWITCH -ISY_CONF_NETWORKING = "Networking Module" ISY_CONF_UUID = "uuid" ISY_CONF_NAME = "name" ISY_CONF_MODEL = "model" diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 8fa77cd126c..b24167e0c78 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Universal Devices ISY/IoX", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.1.11"], + "requirements": ["pyisy==3.1.13"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 759ebfbde0e..05e0425c3f5 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -23,7 +23,7 @@ import homeassistant.helpers.entity_registry as er from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.service import entity_service_call -from .const import _LOGGER, CONF_NETWORK, DOMAIN, ISY_CONF_NAME, ISY_CONF_NETWORKING +from .const import _LOGGER, CONF_NETWORK, DOMAIN, ISY_CONF_NAME from .util import _async_cleanup_registry_entries # Common Services for All Platforms: @@ -233,7 +233,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 isy = isy_data.root if isy_name and isy_name != isy.conf[ISY_CONF_NAME]: continue - if isy.networking is None or not isy.conf[ISY_CONF_NETWORKING]: + if isy.networking is None: continue command = None if address: diff --git a/requirements_all.txt b/requirements_all.txt index 9f2558e5aa7..2c7b26cd923 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1702,7 +1702,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.11 +pyisy==3.1.13 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3b2ad9d2bc..91445d7ad5a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1221,7 +1221,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.11 +pyisy==3.1.13 # homeassistant.components.kaleidescape pykaleidescape==1.0.1