pytest-homeassistant-custom-component 0.13.163__py3-none-any.whl → 0.13.298__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. pytest_homeassistant_custom_component/common.py +289 -46
  2. pytest_homeassistant_custom_component/components/__init__.py +351 -0
  3. pytest_homeassistant_custom_component/components/diagnostics/__init__.py +71 -0
  4. pytest_homeassistant_custom_component/components/recorder/common.py +143 -13
  5. pytest_homeassistant_custom_component/components/recorder/db_schema_0.py +1 -1
  6. pytest_homeassistant_custom_component/const.py +4 -3
  7. pytest_homeassistant_custom_component/patch_json.py +41 -0
  8. pytest_homeassistant_custom_component/patch_recorder.py +1 -1
  9. pytest_homeassistant_custom_component/patch_time.py +66 -0
  10. pytest_homeassistant_custom_component/plugins.py +429 -177
  11. pytest_homeassistant_custom_component/syrupy.py +188 -1
  12. pytest_homeassistant_custom_component/test_util/aiohttp.py +56 -20
  13. pytest_homeassistant_custom_component/typing.py +5 -0
  14. {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/METADATA +54 -29
  15. pytest_homeassistant_custom_component-0.13.298.dist-info/RECORD +28 -0
  16. {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/WHEEL +1 -1
  17. pytest_homeassistant_custom_component-0.13.163.dist-info/RECORD +0 -26
  18. {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/entry_points.txt +0 -0
  19. {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info/licenses}/LICENSE +0 -0
  20. {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info/licenses}/LICENSE_HA_CORE.md +0 -0
  21. {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/top_level.txt +0 -0
@@ -19,7 +19,7 @@ from collections.abc import (
19
19
  )
20
20
  from contextlib import asynccontextmanager, contextmanager, suppress
21
21
  from datetime import UTC, datetime, timedelta
22
- from enum import Enum
22
+ from enum import Enum, StrEnum
23
23
  import functools as ft
24
24
  from functools import lru_cache
25
25
  from io import StringIO
@@ -33,10 +33,11 @@ from types import FrameType, ModuleType
33
33
  from typing import Any, Literal, NoReturn
34
34
  from unittest.mock import AsyncMock, Mock, patch
35
35
 
36
- from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401
36
+ from aiohttp.test_utils import unused_port as get_test_instance_port
37
+ from annotatedyaml import load_yaml_dict, loader as yaml_loader
38
+ import attr
37
39
  import pytest
38
- from syrupy import SnapshotAssertion
39
- from typing_extensions import TypeVar
40
+ from syrupy.assertion import SnapshotAssertion
40
41
  import voluptuous as vol
41
42
 
42
43
  from homeassistant import auth, bootstrap, config_entries, loader
@@ -48,9 +49,14 @@ from homeassistant.auth import (
48
49
  )
49
50
  from homeassistant.auth.permissions import system_policies
50
51
  from homeassistant.components import device_automation, persistent_notification as pn
51
- from homeassistant.components.device_automation import ( # noqa: F401
52
+ from homeassistant.components.device_automation import (
52
53
  _async_get_device_automation_capabilities as async_get_device_automation_capabilities,
53
54
  )
55
+ from homeassistant.components.logger import (
56
+ DOMAIN as LOGGER_DOMAIN,
57
+ SERVICE_SET_LEVEL,
58
+ _clear_logger_overwrites,
59
+ )
54
60
  from homeassistant.config import IntegrationConfigInfo, async_process_component_config
55
61
  from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
56
62
  from homeassistant.const import (
@@ -74,6 +80,7 @@ from homeassistant.core import (
74
80
  from homeassistant.helpers import (
75
81
  area_registry as ar,
76
82
  category_registry as cr,
83
+ condition,
77
84
  device_registry as dr,
78
85
  entity,
79
86
  entity_platform,
@@ -86,21 +93,25 @@ from homeassistant.helpers import (
86
93
  restore_state as rs,
87
94
  storage,
88
95
  translation,
96
+ trigger,
89
97
  )
90
98
  from homeassistant.helpers.dispatcher import (
91
99
  async_dispatcher_connect,
92
100
  async_dispatcher_send,
93
101
  )
94
102
  from homeassistant.helpers.entity import Entity
95
- from homeassistant.helpers.entity_platform import AddEntitiesCallback
103
+ from homeassistant.helpers.entity_platform import (
104
+ AddConfigEntryEntitiesCallback,
105
+ AddEntitiesCallback,
106
+ )
96
107
  from homeassistant.helpers.json import JSONEncoder, _orjson_default_encoder, json_dumps
97
108
  from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
109
+ from homeassistant.util import dt as dt_util, ulid as ulid_util, uuid as uuid_util
98
110
  from homeassistant.util.async_ import (
99
111
  _SHUTDOWN_RUN_CALLBACK_THREADSAFE,
100
112
  get_scheduled_timer_handles,
101
113
  run_callback_threadsafe,
102
114
  )
103
- import homeassistant.util.dt as dt_util
104
115
  from homeassistant.util.event_type import EventType
105
116
  from homeassistant.util.json import (
106
117
  JsonArrayType,
@@ -111,15 +122,16 @@ from homeassistant.util.json import (
111
122
  json_loads_object,
112
123
  )
113
124
  from homeassistant.util.signal_type import SignalType
114
- import homeassistant.util.ulid as ulid_util
115
125
  from homeassistant.util.unit_system import METRIC_SYSTEM
116
- import homeassistant.util.yaml.loader as yaml_loader
117
126
 
118
127
  from .testing_config.custom_components.test_constant_deprecation import (
119
128
  import_deprecated_constant,
120
129
  )
121
130
 
122
- _DataT = TypeVar("_DataT", bound=Mapping[str, Any], default=dict[str, Any])
131
+ __all__ = [
132
+ "async_get_device_automation_capabilities",
133
+ "get_test_instance_port",
134
+ ]
123
135
 
124
136
  _LOGGER = logging.getLogger(__name__)
125
137
  INSTANCES = []
@@ -127,6 +139,14 @@ CLIENT_ID = "https://example.com/app"
127
139
  CLIENT_REDIRECT_URI = "https://example.com/app/callback"
128
140
 
129
141
 
142
+ class QualityScaleStatus(StrEnum):
143
+ """Source of core configuration."""
144
+
145
+ DONE = "done"
146
+ EXEMPT = "exempt"
147
+ TODO = "todo"
148
+
149
+
130
150
  async def async_get_device_automations(
131
151
  hass: HomeAssistant,
132
152
  automation_type: device_automation.DeviceAutomationType,
@@ -282,6 +302,8 @@ async def async_test_home_assistant(
282
302
  # Load the registries
283
303
  entity.async_setup(hass)
284
304
  loader.async_setup(hass)
305
+ await condition.async_setup(hass)
306
+ await trigger.async_setup(hass)
285
307
 
286
308
  # setup translation cache instead of calling translation.async_setup(hass)
287
309
  hass.data[translation.TRANSLATION_FLATTEN_CACHE] = translation._TranslationCache(
@@ -408,6 +430,25 @@ def async_mock_intent(hass: HomeAssistant, intent_typ: str) -> list[intent.Inten
408
430
  return intents
409
431
 
410
432
 
433
+ class MockMqttReasonCode:
434
+ """Class to fake a MQTT ReasonCode."""
435
+
436
+ value: int
437
+ is_failure: bool
438
+
439
+ def __init__(
440
+ self, value: int = 0, is_failure: bool = False, name: str = "Success"
441
+ ) -> None:
442
+ """Initialize the mock reason code."""
443
+ self.value = value
444
+ self.is_failure = is_failure
445
+ self._name = name
446
+
447
+ def getName(self) -> str:
448
+ """Return the name of the reason code."""
449
+ return self._name
450
+
451
+
411
452
  @callback
412
453
  def async_fire_mqtt_message(
413
454
  hass: HomeAssistant,
@@ -420,11 +461,9 @@ def async_fire_mqtt_message(
420
461
  # Local import to avoid processing MQTT modules when running a testcase
421
462
  # which does not use MQTT.
422
463
 
423
- # pylint: disable-next=import-outside-toplevel
424
- from paho.mqtt.client import MQTTMessage
464
+ from paho.mqtt.client import MQTTMessage # noqa: PLC0415
425
465
 
426
- # pylint: disable-next=import-outside-toplevel
427
- from homeassistant.components.mqtt.models import MqttData
466
+ from homeassistant.components.mqtt import MqttData # noqa: PLC0415
428
467
 
429
468
  if isinstance(payload, str):
430
469
  payload = payload.encode("utf-8")
@@ -496,7 +535,7 @@ _MONOTONIC_RESOLUTION = time.get_clock_info("monotonic").resolution
496
535
  def _async_fire_time_changed(
497
536
  hass: HomeAssistant, utc_datetime: datetime | None, fire_all: bool
498
537
  ) -> None:
499
- timestamp = dt_util.utc_to_timestamp(utc_datetime)
538
+ timestamp = utc_datetime.timestamp()
500
539
  for task in list(get_scheduled_timer_handles(hass.loop)):
501
540
  if not isinstance(task, asyncio.TimerHandle):
502
541
  continue
@@ -526,7 +565,9 @@ fire_time_changed = threadsafe_callback_factory(async_fire_time_changed)
526
565
 
527
566
  def get_fixture_path(filename: str, integration: str | None = None) -> pathlib.Path:
528
567
  """Get path of fixture."""
529
- start_path = traceback.extract_stack()[-3].filename
568
+ start_path = (current_file := traceback.extract_stack()[idx:=-1].filename)
569
+ while start_path == current_file:
570
+ start_path = traceback.extract_stack()[idx:=idx-1].filename
530
571
  if integration is None and "/" in filename and not filename.startswith("helpers/"):
531
572
  integration, filename = filename.split("/", 1)
532
573
 
@@ -538,12 +579,25 @@ def get_fixture_path(filename: str, integration: str | None = None) -> pathlib.P
538
579
  )
539
580
 
540
581
 
582
+ @lru_cache
583
+ def load_fixture_bytes(filename: str, integration: str | None = None) -> bytes:
584
+ """Load a fixture."""
585
+ return get_fixture_path(filename, integration).read_bytes()
586
+
587
+
541
588
  @lru_cache
542
589
  def load_fixture(filename: str, integration: str | None = None) -> str:
543
590
  """Load a fixture."""
544
591
  return get_fixture_path(filename, integration).read_text(encoding="utf8")
545
592
 
546
593
 
594
+ async def async_load_fixture(
595
+ hass: HomeAssistant, filename: str, integration: str | None = None
596
+ ) -> str:
597
+ """Load a fixture."""
598
+ return await hass.async_add_executor_job(load_fixture, filename, integration)
599
+
600
+
547
601
  def load_json_value_fixture(
548
602
  filename: str, integration: str | None = None
549
603
  ) -> JsonValueType:
@@ -558,6 +612,13 @@ def load_json_array_fixture(
558
612
  return json_loads_array(load_fixture(filename, integration))
559
613
 
560
614
 
615
+ async def async_load_json_array_fixture(
616
+ hass: HomeAssistant, filename: str, integration: str | None = None
617
+ ) -> JsonArrayType:
618
+ """Load a JSON object from a fixture."""
619
+ return json_loads_array(await async_load_fixture(hass, filename, integration))
620
+
621
+
561
622
  def load_json_object_fixture(
562
623
  filename: str, integration: str | None = None
563
624
  ) -> JsonObjectType:
@@ -565,6 +626,13 @@ def load_json_object_fixture(
565
626
  return json_loads_object(load_fixture(filename, integration))
566
627
 
567
628
 
629
+ async def async_load_json_object_fixture(
630
+ hass: HomeAssistant, filename: str, integration: str | None = None
631
+ ) -> JsonObjectType:
632
+ """Load a JSON object from a fixture."""
633
+ return json_loads_object(await async_load_fixture(hass, filename, integration))
634
+
635
+
568
636
  def json_round_trip(obj: Any) -> Any:
569
637
  """Round trip an object to JSON."""
570
638
  return json_loads(json_dumps(obj))
@@ -620,6 +688,35 @@ def mock_registry(
620
688
  return registry
621
689
 
622
690
 
691
+ @attr.s(frozen=True, kw_only=True, slots=True)
692
+ class RegistryEntryWithDefaults(er.RegistryEntry):
693
+ """Helper to create a registry entry with defaults."""
694
+
695
+ capabilities: Mapping[str, Any] | None = attr.ib(default=None)
696
+ config_entry_id: str | None = attr.ib(default=None)
697
+ config_subentry_id: str | None = attr.ib(default=None)
698
+ created_at: datetime = attr.ib(factory=dt_util.utcnow)
699
+ device_id: str | None = attr.ib(default=None)
700
+ disabled_by: er.RegistryEntryDisabler | None = attr.ib(default=None)
701
+ entity_category: er.EntityCategory | None = attr.ib(default=None)
702
+ hidden_by: er.RegistryEntryHider | None = attr.ib(default=None)
703
+ id: str = attr.ib(
704
+ default=None,
705
+ converter=attr.converters.default_if_none(factory=uuid_util.random_uuid_hex), # type: ignore[misc]
706
+ )
707
+ has_entity_name: bool = attr.ib(default=False)
708
+ options: er.ReadOnlyEntityOptionsType = attr.ib(
709
+ default=None, converter=er._protect_entity_options
710
+ )
711
+ original_device_class: str | None = attr.ib(default=None)
712
+ original_icon: str | None = attr.ib(default=None)
713
+ original_name: str | None = attr.ib(default=None)
714
+ suggested_object_id: str | None = attr.ib(default=None)
715
+ supported_features: int = attr.ib(default=0)
716
+ translation_key: str | None = attr.ib(default=None)
717
+ unit_of_measurement: str | None = attr.ib(default=None)
718
+
719
+
623
720
  def mock_area_registry(
624
721
  hass: HomeAssistant, mock_entries: dict[str, ar.AreaEntry] | None = None
625
722
  ) -> ar.AreaRegistry:
@@ -845,6 +942,7 @@ class MockModule:
845
942
  def mock_manifest(self):
846
943
  """Generate a mock manifest to represent this module."""
847
944
  return {
945
+ "integration_type": "hub",
848
946
  **loader.manifest_from_legacy_module(self.DOMAIN, self),
849
947
  **(self._partial_manifest or {}),
850
948
  }
@@ -996,6 +1094,7 @@ class MockConfigEntry(config_entries.ConfigEntry):
996
1094
  *,
997
1095
  data=None,
998
1096
  disabled_by=None,
1097
+ discovery_keys=None,
999
1098
  domain="test",
1000
1099
  entry_id=None,
1001
1100
  minor_version=1,
@@ -1005,20 +1104,24 @@ class MockConfigEntry(config_entries.ConfigEntry):
1005
1104
  reason=None,
1006
1105
  source=config_entries.SOURCE_USER,
1007
1106
  state=None,
1107
+ subentries_data=None,
1008
1108
  title="Mock Title",
1009
1109
  unique_id=None,
1010
1110
  version=1,
1011
1111
  ) -> None:
1012
1112
  """Initialize a mock config entry."""
1113
+ discovery_keys = discovery_keys or {}
1013
1114
  kwargs = {
1014
1115
  "data": data or {},
1015
1116
  "disabled_by": disabled_by,
1117
+ "discovery_keys": discovery_keys,
1016
1118
  "domain": domain,
1017
1119
  "entry_id": entry_id or ulid_util.ulid_now(),
1018
1120
  "minor_version": minor_version,
1019
1121
  "options": options or {},
1020
1122
  "pref_disable_new_entities": pref_disable_new_entities,
1021
1123
  "pref_disable_polling": pref_disable_polling,
1124
+ "subentries_data": subentries_data or (),
1022
1125
  "title": title,
1023
1126
  "unique_id": unique_id,
1024
1127
  "version": version,
@@ -1067,19 +1170,77 @@ class MockConfigEntry(config_entries.ConfigEntry):
1067
1170
  data: dict[str, Any] | None = None,
1068
1171
  ) -> ConfigFlowResult:
1069
1172
  """Start a reauthentication flow."""
1173
+ if self.entry_id not in hass.config_entries._entries:
1174
+ raise ValueError("Config entry must be added to hass to start reauth flow")
1175
+ return await start_reauth_flow(hass, self, context, data)
1176
+
1177
+ async def start_reconfigure_flow(
1178
+ self,
1179
+ hass: HomeAssistant,
1180
+ *,
1181
+ show_advanced_options: bool = False,
1182
+ ) -> ConfigFlowResult:
1183
+ """Start a reconfiguration flow."""
1184
+ if self.entry_id not in hass.config_entries._entries:
1185
+ raise ValueError(
1186
+ "Config entry must be added to hass to start reconfiguration flow"
1187
+ )
1070
1188
  return await hass.config_entries.flow.async_init(
1071
1189
  self.domain,
1072
1190
  context={
1073
- "source": config_entries.SOURCE_REAUTH,
1191
+ "source": config_entries.SOURCE_RECONFIGURE,
1074
1192
  "entry_id": self.entry_id,
1075
- "title_placeholders": {"name": self.title},
1076
- "unique_id": self.unique_id,
1077
- }
1078
- | (context or {}),
1079
- data=self.data | (data or {}),
1193
+ "show_advanced_options": show_advanced_options,
1194
+ },
1195
+ )
1196
+
1197
+ async def start_subentry_reconfigure_flow(
1198
+ self,
1199
+ hass: HomeAssistant,
1200
+ subentry_id: str,
1201
+ *,
1202
+ show_advanced_options: bool = False,
1203
+ ) -> ConfigFlowResult:
1204
+ """Start a subentry reconfiguration flow."""
1205
+ if self.entry_id not in hass.config_entries._entries:
1206
+ raise ValueError(
1207
+ "Config entry must be added to hass to start reconfiguration flow"
1208
+ )
1209
+ # Derive subentry_flow_type from the subentry_id
1210
+ subentry_flow_type = self.subentries[subentry_id].subentry_type
1211
+ return await hass.config_entries.subentries.async_init(
1212
+ (self.entry_id, subentry_flow_type),
1213
+ context={
1214
+ "source": config_entries.SOURCE_RECONFIGURE,
1215
+ "subentry_id": subentry_id,
1216
+ "show_advanced_options": show_advanced_options,
1217
+ },
1080
1218
  )
1081
1219
 
1082
1220
 
1221
+ async def start_reauth_flow(
1222
+ hass: HomeAssistant,
1223
+ entry: ConfigEntry,
1224
+ context: dict[str, Any] | None = None,
1225
+ data: dict[str, Any] | None = None,
1226
+ ) -> ConfigFlowResult:
1227
+ """Start a reauthentication flow for a config entry.
1228
+
1229
+ This helper method should be aligned with `ConfigEntry._async_init_reauth`.
1230
+ """
1231
+ return await hass.config_entries.flow.async_init(
1232
+ entry.domain,
1233
+ context={
1234
+ "source": config_entries.SOURCE_REAUTH,
1235
+ "entry_id": entry.entry_id,
1236
+ "title_placeholders": {"name": entry.title},
1237
+ "unique_id": entry.unique_id,
1238
+ }
1239
+ | (context or {}),
1240
+ data=entry.data | (data or {}),
1241
+ )
1242
+
1243
+
1083
1244
  def patch_yaml_files(files_dict, endswith=True):
1084
1245
  """Patch load_yaml with a dictionary of yaml files."""
1085
1246
  # match using endswith, start search with longest string
@@ -1157,16 +1318,16 @@ def assert_setup_component(count, domain=None):
1157
1318
  yield config
1158
1319
 
1159
1320
  if domain is None:
1160
- assert (
1161
- len(config) == 1
1162
- ), f"assert_setup_component requires DOMAIN: {list(config.keys())}"
1321
+ assert len(config) == 1, (
1322
+ f"assert_setup_component requires DOMAIN: {list(config.keys())}"
1323
+ )
1163
1324
  domain = list(config.keys())[0]
1164
1325
 
1165
1326
  res = config.get(domain)
1166
1327
  res_len = 0 if res is None else len(res)
1167
- assert (
1168
- res_len == count
1169
- ), f"setup_component failed, expected {count} got {res_len}: {res}"
1328
+ assert res_len == count, (
1329
+ f"setup_component failed, expected {count} got {res_len}: {res}"
1330
+ )
1170
1331
 
1171
1332
 
1172
1333
  def mock_restore_cache(hass: HomeAssistant, states: Sequence[State]) -> None:
@@ -1378,7 +1539,7 @@ def mock_storage(data: dict[str, Any] | None = None) -> Generator[dict[str, Any]
1378
1539
  return loaded
1379
1540
 
1380
1541
  async def mock_write_data(
1381
- store: storage.Store, path: str, data_to_write: dict[str, Any]
1542
+ store: storage.Store, data_to_write: dict[str, Any]
1382
1543
  ) -> None:
1383
1544
  """Mock version of write data."""
1384
1545
  # To ensure that the data can be serialized
@@ -1455,12 +1616,16 @@ def mock_integration(
1455
1616
  top_level_files: set[str] | None = None,
1456
1617
  ) -> loader.Integration:
1457
1618
  """Mock an integration."""
1458
- integration = loader.Integration(
1459
- hass,
1619
+ path = (
1460
1620
  f"{loader.PACKAGE_BUILTIN}.{module.DOMAIN}"
1461
1621
  if built_in
1462
- else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}",
1463
- pathlib.Path(""),
1622
+ else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}"
1623
+ )
1624
+
1625
+ integration = loader.Integration(
1626
+ hass,
1627
+ path,
1628
+ pathlib.Path(path.replace(".", "/")),
1464
1629
  module.mock_manifest(),
1465
1630
  top_level_files,
1466
1631
  )
@@ -1505,7 +1670,7 @@ def mock_platform(
1505
1670
  module_cache[platform_path] = module or Mock()
1506
1671
 
1507
1672
 
1508
- def async_capture_events(
1673
+ def async_capture_events[_DataT: Mapping[str, Any] = dict[str, Any]](
1509
1674
  hass: HomeAssistant, event_name: EventType[_DataT] | str
1510
1675
  ) -> list[Event[_DataT]]:
1511
1676
  """Create a helper that captures events."""
@@ -1593,8 +1758,7 @@ def async_get_persistent_notifications(
1593
1758
 
1594
1759
  def async_mock_cloud_connection_status(hass: HomeAssistant, connected: bool) -> None:
1595
1760
  """Mock a signal the cloud disconnected."""
1596
- # pylint: disable-next=import-outside-toplevel
1597
- from homeassistant.components.cloud import (
1761
+ from homeassistant.components.cloud import ( # noqa: PLC0415
1598
1762
  SIGNAL_CLOUD_CONNECTION_STATE,
1599
1763
  CloudConnectionState,
1600
1764
  )
@@ -1606,6 +1770,28 @@ def async_mock_cloud_connection_status(hass: HomeAssistant, connected: bool) ->
1606
1770
  async_dispatcher_send(hass, SIGNAL_CLOUD_CONNECTION_STATE, state)
1607
1771
 
1608
1772
 
1773
+ @asynccontextmanager
1774
+ async def async_call_logger_set_level(
1775
+ logger: str,
1776
+ level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "FATAL", "CRITICAL"],
1777
+ *,
1778
+ hass: HomeAssistant,
1779
+ caplog: pytest.LogCaptureFixture,
1780
+ ) -> AsyncGenerator[None]:
1781
+ """Context manager to reset loggers after logger.set_level call."""
1782
+ assert LOGGER_DOMAIN in hass.data, "'logger' integration not setup"
1783
+ with caplog.at_level(logging.NOTSET, logger):
1784
+ await hass.services.async_call(
1785
+ LOGGER_DOMAIN,
1786
+ SERVICE_SET_LEVEL,
1787
+ {logger: level},
1788
+ blocking=True,
1789
+ )
1790
+ await hass.async_block_till_done()
1791
+ yield
1792
+ _clear_logger_overwrites(hass)
1793
+
1794
+
1609
1795
  def import_and_test_deprecated_constant_enum(
1610
1796
  caplog: pytest.LogCaptureFixture,
1611
1797
  module: ModuleType,
@@ -1653,9 +1839,9 @@ def import_and_test_deprecated_constant(
1653
1839
  module.__name__,
1654
1840
  logging.WARNING,
1655
1841
  (
1656
- f"{constant_name} was used from test_constant_deprecation,"
1657
- f" this is a deprecated constant which will be removed in HA Core {breaks_in_ha_version}. "
1658
- f"Use {replacement_name} instead, please report "
1842
+ f"The deprecated constant {constant_name} was used from "
1843
+ "test_constant_deprecation. It will be removed in HA Core "
1844
+ f"{breaks_in_ha_version}. Use {replacement_name} instead, please report "
1659
1845
  "it to the author of the 'test_constant_deprecation' custom integration"
1660
1846
  ),
1661
1847
  ) in caplog.record_tuples
@@ -1687,9 +1873,9 @@ def import_and_test_deprecated_alias(
1687
1873
  module.__name__,
1688
1874
  logging.WARNING,
1689
1875
  (
1690
- f"{alias_name} was used from test_constant_deprecation,"
1691
- f" this is a deprecated alias which will be removed in HA Core {breaks_in_ha_version}. "
1692
- f"Use {replacement_name} instead, please report "
1876
+ f"The deprecated alias {alias_name} was used from "
1877
+ "test_constant_deprecation. It will be removed in HA Core "
1878
+ f"{breaks_in_ha_version}. Use {replacement_name} instead, please report "
1693
1879
  "it to the author of the 'test_constant_deprecation' custom integration"
1694
1880
  ),
1695
1881
  ) in caplog.record_tuples
@@ -1753,7 +1939,7 @@ def setup_test_component_platform(
1753
1939
  async def _async_setup_entry(
1754
1940
  hass: HomeAssistant,
1755
1941
  entry: ConfigEntry,
1756
- async_add_entities: AddEntitiesCallback,
1942
+ async_add_entities: AddConfigEntryEntitiesCallback,
1757
1943
  ) -> None:
1758
1944
  """Set up a test component platform."""
1759
1945
  async_add_entities(entities)
@@ -1774,12 +1960,69 @@ async def snapshot_platform(
1774
1960
  """Snapshot a platform."""
1775
1961
  entity_entries = er.async_entries_for_config_entry(entity_registry, config_entry_id)
1776
1962
  assert entity_entries
1777
- assert (
1778
- len({entity_entry.domain for entity_entry in entity_entries}) == 1
1779
- ), "Please limit the loaded platforms to 1 platform."
1963
+ assert len({entity_entry.domain for entity_entry in entity_entries}) == 1, (
1964
+ "Please limit the loaded platforms to 1 platform."
1965
+ )
1780
1966
  for entity_entry in entity_entries:
1781
1967
  assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry")
1782
1968
  assert entity_entry.disabled_by is None, "Please enable all entities."
1783
1969
  state = hass.states.get(entity_entry.entity_id)
1784
1970
  assert state, f"State not found for {entity_entry.entity_id}"
1785
1971
  assert state == snapshot(name=f"{entity_entry.entity_id}-state")
1972
+
1973
+
1974
+ @lru_cache
1975
+ def get_quality_scale(integration: str) -> dict[str, QualityScaleStatus]:
1976
+ """Load quality scale for integration."""
1977
+ quality_scale_file = pathlib.Path(
1978
+ f"homeassistant/components/{integration}/quality_scale.yaml"
1979
+ )
1980
+ if not quality_scale_file.exists():
1981
+ return {}
1982
+ raw = load_yaml_dict(quality_scale_file)
1983
+ return {
1984
+ rule: (
1985
+ QualityScaleStatus(details)
1986
+ if isinstance(details, str)
1987
+ else QualityScaleStatus(details["status"])
1988
+ )
1989
+ for rule, details in raw["rules"].items()
1990
+ }
1991
+
1992
+
1993
+ def get_schema_suggested_value(schema: vol.Schema, key: str) -> Any | None:
1994
+ """Get suggested value for key in voluptuous schema."""
1995
+ for schema_key in schema:
1996
+ if schema_key == key:
1997
+ if (
1998
+ schema_key.description is None
1999
+ or "suggested_value" not in schema_key.description
2000
+ ):
2001
+ return None
2002
+ return schema_key.description["suggested_value"]
2003
+ return None
2004
+
2005
+
2006
+ def get_sensor_display_state(
2007
+ hass: HomeAssistant, entity_registry: er.EntityRegistry, entity_id: str
2008
+ ) -> str:
2009
+ """Return the state rounded for presentation."""
2010
+ state = hass.states.get(entity_id)
2011
+ assert state
2012
+ value = state.state
2013
+
2014
+ entity_entry = entity_registry.async_get(entity_id)
2015
+ if entity_entry is None:
2016
+ return value
2017
+
2018
+ if (
2019
+ precision := entity_entry.options.get("sensor", {}).get(
2020
+ "suggested_display_precision"
2021
+ )
2022
+ ) is None:
2023
+ return value
2024
+
2025
+ with suppress(TypeError, ValueError):
2026
+ numerical_value = float(value)
2027
+ value = f"{numerical_value:z.{precision}f}"
2028
+ return value