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.
- pytest_homeassistant_custom_component/common.py +289 -46
- pytest_homeassistant_custom_component/components/__init__.py +351 -0
- pytest_homeassistant_custom_component/components/diagnostics/__init__.py +71 -0
- pytest_homeassistant_custom_component/components/recorder/common.py +143 -13
- pytest_homeassistant_custom_component/components/recorder/db_schema_0.py +1 -1
- pytest_homeassistant_custom_component/const.py +4 -3
- pytest_homeassistant_custom_component/patch_json.py +41 -0
- pytest_homeassistant_custom_component/patch_recorder.py +1 -1
- pytest_homeassistant_custom_component/patch_time.py +66 -0
- pytest_homeassistant_custom_component/plugins.py +429 -177
- pytest_homeassistant_custom_component/syrupy.py +188 -1
- pytest_homeassistant_custom_component/test_util/aiohttp.py +56 -20
- pytest_homeassistant_custom_component/typing.py +5 -0
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/METADATA +54 -29
- pytest_homeassistant_custom_component-0.13.298.dist-info/RECORD +28 -0
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/WHEEL +1 -1
- pytest_homeassistant_custom_component-0.13.163.dist-info/RECORD +0 -26
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/entry_points.txt +0 -0
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info/licenses}/LICENSE +0 -0
- {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
- {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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
424
|
-
from paho.mqtt.client import MQTTMessage
|
|
464
|
+
from paho.mqtt.client import MQTTMessage # noqa: PLC0415
|
|
425
465
|
|
|
426
|
-
#
|
|
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 =
|
|
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()[
|
|
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.
|
|
1191
|
+
"source": config_entries.SOURCE_RECONFIGURE,
|
|
1074
1192
|
"entry_id": self.entry_id,
|
|
1075
|
-
"
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
1162
|
-
)
|
|
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
|
-
|
|
1169
|
-
)
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
1657
|
-
|
|
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
|
|
1691
|
-
|
|
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:
|
|
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
|
-
|
|
1779
|
-
)
|
|
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
|