pytest-homeassistant-custom-component 0.13.269__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 +10 -5
- pytest_homeassistant_custom_component/components/__init__.py +351 -0
- pytest_homeassistant_custom_component/components/recorder/common.py +115 -0
- pytest_homeassistant_custom_component/const.py +2 -1
- pytest_homeassistant_custom_component/patch_time.py +25 -2
- pytest_homeassistant_custom_component/plugins.py +98 -36
- pytest_homeassistant_custom_component/test_util/aiohttp.py +2 -2
- {pytest_homeassistant_custom_component-0.13.269.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/METADATA +11 -10
- {pytest_homeassistant_custom_component-0.13.269.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/RECORD +14 -14
- {pytest_homeassistant_custom_component-0.13.269.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/WHEEL +0 -0
- {pytest_homeassistant_custom_component-0.13.269.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/entry_points.txt +0 -0
- {pytest_homeassistant_custom_component-0.13.269.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/licenses/LICENSE +0 -0
- {pytest_homeassistant_custom_component-0.13.269.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/licenses/LICENSE_HA_CORE.md +0 -0
- {pytest_homeassistant_custom_component-0.13.269.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/top_level.txt +0 -0
|
@@ -942,6 +942,7 @@ class MockModule:
|
|
|
942
942
|
def mock_manifest(self):
|
|
943
943
|
"""Generate a mock manifest to represent this module."""
|
|
944
944
|
return {
|
|
945
|
+
"integration_type": "hub",
|
|
945
946
|
**loader.manifest_from_legacy_module(self.DOMAIN, self),
|
|
946
947
|
**(self._partial_manifest or {}),
|
|
947
948
|
}
|
|
@@ -1538,7 +1539,7 @@ def mock_storage(data: dict[str, Any] | None = None) -> Generator[dict[str, Any]
|
|
|
1538
1539
|
return loaded
|
|
1539
1540
|
|
|
1540
1541
|
async def mock_write_data(
|
|
1541
|
-
store: storage.Store,
|
|
1542
|
+
store: storage.Store, data_to_write: dict[str, Any]
|
|
1542
1543
|
) -> None:
|
|
1543
1544
|
"""Mock version of write data."""
|
|
1544
1545
|
# To ensure that the data can be serialized
|
|
@@ -1615,12 +1616,16 @@ def mock_integration(
|
|
|
1615
1616
|
top_level_files: set[str] | None = None,
|
|
1616
1617
|
) -> loader.Integration:
|
|
1617
1618
|
"""Mock an integration."""
|
|
1618
|
-
|
|
1619
|
-
hass,
|
|
1619
|
+
path = (
|
|
1620
1620
|
f"{loader.PACKAGE_BUILTIN}.{module.DOMAIN}"
|
|
1621
1621
|
if built_in
|
|
1622
|
-
else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}"
|
|
1623
|
-
|
|
1622
|
+
else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}"
|
|
1623
|
+
)
|
|
1624
|
+
|
|
1625
|
+
integration = loader.Integration(
|
|
1626
|
+
hass,
|
|
1627
|
+
path,
|
|
1628
|
+
pathlib.Path(path.replace(".", "/")),
|
|
1624
1629
|
module.mock_manifest(),
|
|
1625
1630
|
top_level_files,
|
|
1626
1631
|
)
|
|
@@ -3,3 +3,354 @@ The tests for components.
|
|
|
3
3
|
|
|
4
4
|
This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component.
|
|
5
5
|
"""
|
|
6
|
+
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
import itertools
|
|
9
|
+
from typing import TypedDict
|
|
10
|
+
|
|
11
|
+
from homeassistant.const import (
|
|
12
|
+
ATTR_AREA_ID,
|
|
13
|
+
ATTR_DEVICE_ID,
|
|
14
|
+
ATTR_FLOOR_ID,
|
|
15
|
+
ATTR_LABEL_ID,
|
|
16
|
+
CONF_ENTITY_ID,
|
|
17
|
+
CONF_OPTIONS,
|
|
18
|
+
CONF_PLATFORM,
|
|
19
|
+
CONF_TARGET,
|
|
20
|
+
STATE_UNAVAILABLE,
|
|
21
|
+
STATE_UNKNOWN,
|
|
22
|
+
)
|
|
23
|
+
from homeassistant.core import HomeAssistant
|
|
24
|
+
from homeassistant.helpers import (
|
|
25
|
+
area_registry as ar,
|
|
26
|
+
device_registry as dr,
|
|
27
|
+
entity_registry as er,
|
|
28
|
+
floor_registry as fr,
|
|
29
|
+
label_registry as lr,
|
|
30
|
+
)
|
|
31
|
+
from homeassistant.setup import async_setup_component
|
|
32
|
+
|
|
33
|
+
from ..common import MockConfigEntry, mock_device_registry
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def target_entities(
|
|
37
|
+
hass: HomeAssistant, domain: str
|
|
38
|
+
) -> tuple[list[str], list[str]]:
|
|
39
|
+
"""Create multiple entities associated with different targets.
|
|
40
|
+
|
|
41
|
+
Returns a dict with the following keys:
|
|
42
|
+
- included: List of entity_ids meant to be targeted.
|
|
43
|
+
- excluded: List of entity_ids not meant to be targeted.
|
|
44
|
+
"""
|
|
45
|
+
await async_setup_component(hass, domain, {})
|
|
46
|
+
|
|
47
|
+
config_entry = MockConfigEntry(domain="test")
|
|
48
|
+
config_entry.add_to_hass(hass)
|
|
49
|
+
|
|
50
|
+
floor_reg = fr.async_get(hass)
|
|
51
|
+
floor = floor_reg.async_get_floor_by_name("Test Floor") or floor_reg.async_create(
|
|
52
|
+
"Test Floor"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
area_reg = ar.async_get(hass)
|
|
56
|
+
area = area_reg.async_get_area_by_name("Test Area") or area_reg.async_create(
|
|
57
|
+
"Test Area", floor_id=floor.floor_id
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
label_reg = lr.async_get(hass)
|
|
61
|
+
label = label_reg.async_get_label_by_name("Test Label") or label_reg.async_create(
|
|
62
|
+
"Test Label"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
device = dr.DeviceEntry(id="test_device", area_id=area.id, labels={label.label_id})
|
|
66
|
+
mock_device_registry(hass, {device.id: device})
|
|
67
|
+
|
|
68
|
+
entity_reg = er.async_get(hass)
|
|
69
|
+
# Entities associated with area
|
|
70
|
+
entity_area = entity_reg.async_get_or_create(
|
|
71
|
+
domain=domain,
|
|
72
|
+
platform="test",
|
|
73
|
+
unique_id=f"{domain}_area",
|
|
74
|
+
suggested_object_id=f"area_{domain}",
|
|
75
|
+
)
|
|
76
|
+
entity_reg.async_update_entity(entity_area.entity_id, area_id=area.id)
|
|
77
|
+
entity_area_excluded = entity_reg.async_get_or_create(
|
|
78
|
+
domain=domain,
|
|
79
|
+
platform="test",
|
|
80
|
+
unique_id=f"{domain}_area_excluded",
|
|
81
|
+
suggested_object_id=f"area_{domain}_excluded",
|
|
82
|
+
)
|
|
83
|
+
entity_reg.async_update_entity(entity_area_excluded.entity_id, area_id=area.id)
|
|
84
|
+
|
|
85
|
+
# Entities associated with device
|
|
86
|
+
entity_reg.async_get_or_create(
|
|
87
|
+
domain=domain,
|
|
88
|
+
platform="test",
|
|
89
|
+
unique_id=f"{domain}_device",
|
|
90
|
+
suggested_object_id=f"device_{domain}",
|
|
91
|
+
device_id=device.id,
|
|
92
|
+
)
|
|
93
|
+
entity_reg.async_get_or_create(
|
|
94
|
+
domain=domain,
|
|
95
|
+
platform="test",
|
|
96
|
+
unique_id=f"{domain}_device_excluded",
|
|
97
|
+
suggested_object_id=f"device_{domain}_excluded",
|
|
98
|
+
device_id=device.id,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Entities associated with label
|
|
102
|
+
entity_label = entity_reg.async_get_or_create(
|
|
103
|
+
domain=domain,
|
|
104
|
+
platform="test",
|
|
105
|
+
unique_id=f"{domain}_label",
|
|
106
|
+
suggested_object_id=f"label_{domain}",
|
|
107
|
+
)
|
|
108
|
+
entity_reg.async_update_entity(entity_label.entity_id, labels={label.label_id})
|
|
109
|
+
entity_label_excluded = entity_reg.async_get_or_create(
|
|
110
|
+
domain=domain,
|
|
111
|
+
platform="test",
|
|
112
|
+
unique_id=f"{domain}_label_excluded",
|
|
113
|
+
suggested_object_id=f"label_{domain}_excluded",
|
|
114
|
+
)
|
|
115
|
+
entity_reg.async_update_entity(
|
|
116
|
+
entity_label_excluded.entity_id, labels={label.label_id}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Return all available entities
|
|
120
|
+
return {
|
|
121
|
+
"included": [
|
|
122
|
+
f"{domain}.standalone_{domain}",
|
|
123
|
+
f"{domain}.label_{domain}",
|
|
124
|
+
f"{domain}.area_{domain}",
|
|
125
|
+
f"{domain}.device_{domain}",
|
|
126
|
+
],
|
|
127
|
+
"excluded": [
|
|
128
|
+
f"{domain}.standalone_{domain}_excluded",
|
|
129
|
+
f"{domain}.label_{domain}_excluded",
|
|
130
|
+
f"{domain}.area_{domain}_excluded",
|
|
131
|
+
f"{domain}.device_{domain}_excluded",
|
|
132
|
+
],
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def parametrize_target_entities(domain: str) -> list[tuple[dict, str, int]]:
|
|
137
|
+
"""Parametrize target entities for different target types.
|
|
138
|
+
|
|
139
|
+
Meant to be used with target_entities.
|
|
140
|
+
"""
|
|
141
|
+
return [
|
|
142
|
+
(
|
|
143
|
+
{CONF_ENTITY_ID: f"{domain}.standalone_{domain}"},
|
|
144
|
+
f"{domain}.standalone_{domain}",
|
|
145
|
+
1,
|
|
146
|
+
),
|
|
147
|
+
({ATTR_LABEL_ID: "test_label"}, f"{domain}.label_{domain}", 2),
|
|
148
|
+
({ATTR_AREA_ID: "test_area"}, f"{domain}.area_{domain}", 2),
|
|
149
|
+
({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.area_{domain}", 2),
|
|
150
|
+
({ATTR_LABEL_ID: "test_label"}, f"{domain}.device_{domain}", 2),
|
|
151
|
+
({ATTR_AREA_ID: "test_area"}, f"{domain}.device_{domain}", 2),
|
|
152
|
+
({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.device_{domain}", 2),
|
|
153
|
+
({ATTR_DEVICE_ID: "test_device"}, f"{domain}.device_{domain}", 1),
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class _StateDescription(TypedDict):
|
|
158
|
+
"""Test state and expected service call count."""
|
|
159
|
+
|
|
160
|
+
state: str | None
|
|
161
|
+
attributes: dict
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class StateDescription(TypedDict):
|
|
165
|
+
"""Test state and expected service call count."""
|
|
166
|
+
|
|
167
|
+
included: _StateDescription
|
|
168
|
+
excluded: _StateDescription
|
|
169
|
+
count: int
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def parametrize_trigger_states(
|
|
173
|
+
*,
|
|
174
|
+
trigger: str,
|
|
175
|
+
target_states: list[str | None | tuple[str | None, dict]],
|
|
176
|
+
other_states: list[str | None | tuple[str | None, dict]],
|
|
177
|
+
additional_attributes: dict | None = None,
|
|
178
|
+
trigger_from_none: bool = True,
|
|
179
|
+
) -> list[tuple[str, list[StateDescription]]]:
|
|
180
|
+
"""Parametrize states and expected service call counts.
|
|
181
|
+
|
|
182
|
+
The target_states and other_states iterables are either iterables of
|
|
183
|
+
states or iterables of (state, attributes) tuples.
|
|
184
|
+
|
|
185
|
+
Set `trigger_from_none` to False if the trigger is not expected to fire
|
|
186
|
+
when the initial state is None.
|
|
187
|
+
|
|
188
|
+
Returns a list of tuples with (trigger, list of states),
|
|
189
|
+
where states is a list of StateDescription dicts.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
additional_attributes = additional_attributes or {}
|
|
193
|
+
|
|
194
|
+
def state_with_attributes(
|
|
195
|
+
state: str | None | tuple[str | None, dict], count: int
|
|
196
|
+
) -> dict:
|
|
197
|
+
"""Return (state, attributes) dict."""
|
|
198
|
+
if isinstance(state, str) or state is None:
|
|
199
|
+
return {
|
|
200
|
+
"included": {
|
|
201
|
+
"state": state,
|
|
202
|
+
"attributes": additional_attributes,
|
|
203
|
+
},
|
|
204
|
+
"excluded": {
|
|
205
|
+
"state": state,
|
|
206
|
+
"attributes": {},
|
|
207
|
+
},
|
|
208
|
+
"count": count,
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
"included": {
|
|
212
|
+
"state": state[0],
|
|
213
|
+
"attributes": state[1] | additional_attributes,
|
|
214
|
+
},
|
|
215
|
+
"excluded": {
|
|
216
|
+
"state": state[0],
|
|
217
|
+
"attributes": state[1],
|
|
218
|
+
},
|
|
219
|
+
"count": count,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return [
|
|
223
|
+
# Initial state None
|
|
224
|
+
(
|
|
225
|
+
trigger,
|
|
226
|
+
list(
|
|
227
|
+
itertools.chain.from_iterable(
|
|
228
|
+
(
|
|
229
|
+
state_with_attributes(None, 0),
|
|
230
|
+
state_with_attributes(target_state, 0),
|
|
231
|
+
state_with_attributes(other_state, 0),
|
|
232
|
+
state_with_attributes(
|
|
233
|
+
target_state, 1 if trigger_from_none else 0
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
for target_state in target_states
|
|
237
|
+
for other_state in other_states
|
|
238
|
+
)
|
|
239
|
+
),
|
|
240
|
+
),
|
|
241
|
+
# Initial state different from target state
|
|
242
|
+
(
|
|
243
|
+
trigger,
|
|
244
|
+
# other_state,
|
|
245
|
+
list(
|
|
246
|
+
itertools.chain.from_iterable(
|
|
247
|
+
(
|
|
248
|
+
state_with_attributes(other_state, 0),
|
|
249
|
+
state_with_attributes(target_state, 1),
|
|
250
|
+
state_with_attributes(other_state, 0),
|
|
251
|
+
state_with_attributes(target_state, 1),
|
|
252
|
+
)
|
|
253
|
+
for target_state in target_states
|
|
254
|
+
for other_state in other_states
|
|
255
|
+
)
|
|
256
|
+
),
|
|
257
|
+
),
|
|
258
|
+
# Initial state same as target state
|
|
259
|
+
(
|
|
260
|
+
trigger,
|
|
261
|
+
list(
|
|
262
|
+
itertools.chain.from_iterable(
|
|
263
|
+
(
|
|
264
|
+
state_with_attributes(target_state, 0),
|
|
265
|
+
state_with_attributes(target_state, 0),
|
|
266
|
+
state_with_attributes(other_state, 0),
|
|
267
|
+
state_with_attributes(target_state, 1),
|
|
268
|
+
)
|
|
269
|
+
for target_state in target_states
|
|
270
|
+
for other_state in other_states
|
|
271
|
+
)
|
|
272
|
+
),
|
|
273
|
+
),
|
|
274
|
+
# Initial state unavailable / unknown
|
|
275
|
+
(
|
|
276
|
+
trigger,
|
|
277
|
+
list(
|
|
278
|
+
itertools.chain.from_iterable(
|
|
279
|
+
(
|
|
280
|
+
state_with_attributes(STATE_UNAVAILABLE, 0),
|
|
281
|
+
state_with_attributes(target_state, 0),
|
|
282
|
+
state_with_attributes(other_state, 0),
|
|
283
|
+
state_with_attributes(target_state, 1),
|
|
284
|
+
)
|
|
285
|
+
for target_state in target_states
|
|
286
|
+
for other_state in other_states
|
|
287
|
+
)
|
|
288
|
+
),
|
|
289
|
+
),
|
|
290
|
+
(
|
|
291
|
+
trigger,
|
|
292
|
+
list(
|
|
293
|
+
itertools.chain.from_iterable(
|
|
294
|
+
(
|
|
295
|
+
state_with_attributes(STATE_UNKNOWN, 0),
|
|
296
|
+
state_with_attributes(target_state, 0),
|
|
297
|
+
state_with_attributes(other_state, 0),
|
|
298
|
+
state_with_attributes(target_state, 1),
|
|
299
|
+
)
|
|
300
|
+
for target_state in target_states
|
|
301
|
+
for other_state in other_states
|
|
302
|
+
)
|
|
303
|
+
),
|
|
304
|
+
),
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
async def arm_trigger(
|
|
309
|
+
hass: HomeAssistant,
|
|
310
|
+
trigger: str,
|
|
311
|
+
trigger_options: dict | None,
|
|
312
|
+
trigger_target: dict,
|
|
313
|
+
) -> None:
|
|
314
|
+
"""Arm the specified trigger, call service test.automation when it triggers."""
|
|
315
|
+
|
|
316
|
+
# Local include to avoid importing the automation component unnecessarily
|
|
317
|
+
from homeassistant.components import automation # noqa: PLC0415
|
|
318
|
+
|
|
319
|
+
options = {CONF_OPTIONS: {**trigger_options}} if trigger_options is not None else {}
|
|
320
|
+
|
|
321
|
+
await async_setup_component(
|
|
322
|
+
hass,
|
|
323
|
+
automation.DOMAIN,
|
|
324
|
+
{
|
|
325
|
+
automation.DOMAIN: {
|
|
326
|
+
"trigger": {
|
|
327
|
+
CONF_PLATFORM: trigger,
|
|
328
|
+
CONF_TARGET: {**trigger_target},
|
|
329
|
+
}
|
|
330
|
+
| options,
|
|
331
|
+
"action": {
|
|
332
|
+
"service": "test.automation",
|
|
333
|
+
"data_template": {CONF_ENTITY_ID: "{{ trigger.entity_id }}"},
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def set_or_remove_state(
|
|
341
|
+
hass: HomeAssistant,
|
|
342
|
+
entity_id: str,
|
|
343
|
+
state: StateDescription,
|
|
344
|
+
) -> None:
|
|
345
|
+
"""Set or remove the state of an entity."""
|
|
346
|
+
if state["state"] is None:
|
|
347
|
+
hass.states.async_remove(entity_id)
|
|
348
|
+
else:
|
|
349
|
+
hass.states.async_set(
|
|
350
|
+
entity_id, state["state"], state["attributes"], force_update=True
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def other_states(state: StrEnum) -> list[str]:
|
|
355
|
+
"""Return a sorted list with all states except the specified one."""
|
|
356
|
+
return sorted({s.value for s in state.__class__} - {state.value})
|
|
@@ -15,10 +15,12 @@ from functools import partial
|
|
|
15
15
|
import importlib
|
|
16
16
|
import sys
|
|
17
17
|
import time
|
|
18
|
+
from types import ModuleType
|
|
18
19
|
from typing import Any, Literal, cast
|
|
19
20
|
from unittest.mock import MagicMock, patch, sentinel
|
|
20
21
|
|
|
21
22
|
from freezegun import freeze_time
|
|
23
|
+
import pytest
|
|
22
24
|
from sqlalchemy import create_engine, event as sqlalchemy_event
|
|
23
25
|
from sqlalchemy.orm.session import Session
|
|
24
26
|
|
|
@@ -32,18 +34,25 @@ from homeassistant.components.recorder import (
|
|
|
32
34
|
statistics,
|
|
33
35
|
)
|
|
34
36
|
from homeassistant.components.recorder.db_schema import (
|
|
37
|
+
EventData,
|
|
35
38
|
Events,
|
|
36
39
|
EventTypes,
|
|
37
40
|
RecorderRuns,
|
|
41
|
+
StateAttributes,
|
|
38
42
|
States,
|
|
39
43
|
StatesMeta,
|
|
40
44
|
)
|
|
45
|
+
from homeassistant.components.recorder.models import (
|
|
46
|
+
bytes_to_ulid_or_none,
|
|
47
|
+
bytes_to_uuid_hex_or_none,
|
|
48
|
+
)
|
|
41
49
|
from homeassistant.components.recorder.tasks import RecorderTask, StatisticsTask
|
|
42
50
|
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
|
43
51
|
from homeassistant.const import DEGREE, UnitOfTemperature
|
|
44
52
|
from homeassistant.core import Event, HomeAssistant, State
|
|
45
53
|
from homeassistant.helpers import recorder as recorder_helper
|
|
46
54
|
from homeassistant.util import dt as dt_util
|
|
55
|
+
from homeassistant.util.json import json_loads, json_loads_object
|
|
47
56
|
|
|
48
57
|
from . import db_schema_0
|
|
49
58
|
|
|
@@ -456,6 +465,13 @@ def get_schema_module_path(schema_version_postfix: str) -> str:
|
|
|
456
465
|
return f"...components.recorder.db_schema_{schema_version_postfix}"
|
|
457
466
|
|
|
458
467
|
|
|
468
|
+
def get_patched_live_version(old_db_schema: ModuleType) -> int:
|
|
469
|
+
"""Return the patched live migration version."""
|
|
470
|
+
return min(
|
|
471
|
+
migration.LIVE_MIGRATION_MIN_SCHEMA_VERSION, old_db_schema.SCHEMA_VERSION
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
459
475
|
@contextmanager
|
|
460
476
|
def old_db_schema(hass: HomeAssistant, schema_version_postfix: str) -> Iterator[None]:
|
|
461
477
|
"""Fixture to initialize the db with the old schema."""
|
|
@@ -466,6 +482,11 @@ def old_db_schema(hass: HomeAssistant, schema_version_postfix: str) -> Iterator[
|
|
|
466
482
|
with (
|
|
467
483
|
patch.object(recorder, "db_schema", old_db_schema),
|
|
468
484
|
patch.object(migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION),
|
|
485
|
+
patch.object(
|
|
486
|
+
migration,
|
|
487
|
+
"LIVE_MIGRATION_MIN_SCHEMA_VERSION",
|
|
488
|
+
get_patched_live_version(old_db_schema),
|
|
489
|
+
),
|
|
469
490
|
patch.object(migration, "non_live_data_migration_needed", return_value=False),
|
|
470
491
|
patch.object(core, "StatesMeta", old_db_schema.StatesMeta),
|
|
471
492
|
patch.object(core, "EventTypes", old_db_schema.EventTypes),
|
|
@@ -496,3 +517,97 @@ async def async_attach_db_engine(hass: HomeAssistant) -> None:
|
|
|
496
517
|
)
|
|
497
518
|
|
|
498
519
|
await instance.async_add_executor_job(_mock_setup_recorder_connection)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
EVENT_ORIGIN_ORDER = [ha.EventOrigin.local, ha.EventOrigin.remote]
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def db_event_to_native(event: Events, validate_entity_id: bool = True) -> Event | None:
|
|
526
|
+
"""Convert to a native HA Event."""
|
|
527
|
+
context = ha.Context(
|
|
528
|
+
id=bytes_to_ulid_or_none(event.context_id_bin),
|
|
529
|
+
user_id=bytes_to_uuid_hex_or_none(event.context_user_id_bin),
|
|
530
|
+
parent_id=bytes_to_ulid_or_none(event.context_parent_id_bin),
|
|
531
|
+
)
|
|
532
|
+
return Event(
|
|
533
|
+
event.event_type or "",
|
|
534
|
+
json_loads_object(event.event_data) if event.event_data else {},
|
|
535
|
+
ha.EventOrigin(event.origin)
|
|
536
|
+
if event.origin
|
|
537
|
+
else EVENT_ORIGIN_ORDER[event.origin_idx or 0],
|
|
538
|
+
event.time_fired_ts or 0,
|
|
539
|
+
context=context,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def db_event_data_to_native(event_data: EventData) -> dict[str, Any]:
|
|
544
|
+
"""Convert to an event data dictionary."""
|
|
545
|
+
shared_data = event_data.shared_data
|
|
546
|
+
if shared_data is None:
|
|
547
|
+
return {}
|
|
548
|
+
return cast(dict[str, Any], json_loads(shared_data))
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def db_state_to_native(state: States, validate_entity_id: bool = True) -> State | None:
|
|
552
|
+
"""Convert to an HA state object."""
|
|
553
|
+
context = ha.Context(
|
|
554
|
+
id=bytes_to_ulid_or_none(state.context_id_bin),
|
|
555
|
+
user_id=bytes_to_uuid_hex_or_none(state.context_user_id_bin),
|
|
556
|
+
parent_id=bytes_to_ulid_or_none(state.context_parent_id_bin),
|
|
557
|
+
)
|
|
558
|
+
attrs = json_loads_object(state.attributes) if state.attributes else {}
|
|
559
|
+
last_updated = dt_util.utc_from_timestamp(state.last_updated_ts or 0)
|
|
560
|
+
if state.last_changed_ts is None or state.last_changed_ts == state.last_updated_ts:
|
|
561
|
+
last_changed = dt_util.utc_from_timestamp(state.last_updated_ts or 0)
|
|
562
|
+
else:
|
|
563
|
+
last_changed = dt_util.utc_from_timestamp(state.last_changed_ts or 0)
|
|
564
|
+
if (
|
|
565
|
+
state.last_reported_ts is None
|
|
566
|
+
or state.last_reported_ts == state.last_updated_ts
|
|
567
|
+
):
|
|
568
|
+
last_reported = dt_util.utc_from_timestamp(state.last_updated_ts or 0)
|
|
569
|
+
else:
|
|
570
|
+
last_reported = dt_util.utc_from_timestamp(state.last_reported_ts or 0)
|
|
571
|
+
return State(
|
|
572
|
+
state.entity_id or "",
|
|
573
|
+
state.state, # type: ignore[arg-type]
|
|
574
|
+
# Join the state_attributes table on attributes_id to get the attributes
|
|
575
|
+
# for newer states
|
|
576
|
+
attrs,
|
|
577
|
+
last_changed=last_changed,
|
|
578
|
+
last_reported=last_reported,
|
|
579
|
+
last_updated=last_updated,
|
|
580
|
+
context=context,
|
|
581
|
+
validate_entity_id=validate_entity_id,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def db_state_attributes_to_native(state_attrs: StateAttributes) -> dict[str, Any]:
|
|
586
|
+
"""Convert to a state attributes dictionary."""
|
|
587
|
+
shared_attrs = state_attrs.shared_attrs
|
|
588
|
+
if shared_attrs is None:
|
|
589
|
+
return {}
|
|
590
|
+
return cast(dict[str, Any], json_loads(shared_attrs))
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
async def async_drop_index(
|
|
594
|
+
recorder: Recorder, table: str, index: str, caplog: pytest.LogCaptureFixture
|
|
595
|
+
) -> None:
|
|
596
|
+
"""Drop an index from the database.
|
|
597
|
+
|
|
598
|
+
migration._drop_index does not return or raise, so we verify the result
|
|
599
|
+
by checking the log for success or failure messages.
|
|
600
|
+
"""
|
|
601
|
+
|
|
602
|
+
finish_msg = f"Finished dropping index `{index}` from table `{table}`"
|
|
603
|
+
fail_msg = f"Failed to drop index `{index}` from table `{table}`"
|
|
604
|
+
|
|
605
|
+
count_finish = caplog.text.count(finish_msg)
|
|
606
|
+
count_fail = caplog.text.count(fail_msg)
|
|
607
|
+
|
|
608
|
+
await recorder.async_add_executor_job(
|
|
609
|
+
migration._drop_index, recorder.get_session, table, index
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
assert caplog.text.count(finish_msg) == count_finish + 1
|
|
613
|
+
assert caplog.text.count(fail_msg) == count_fail
|
|
@@ -5,7 +5,8 @@ This file is originally from homeassistant/core and modified by pytest-homeassis
|
|
|
5
5
|
"""
|
|
6
6
|
from typing import TYPE_CHECKING, Final
|
|
7
7
|
MAJOR_VERSION: Final = 2025
|
|
8
|
-
MINOR_VERSION: Final =
|
|
8
|
+
MINOR_VERSION: Final = 12
|
|
9
9
|
PATCH_VERSION: Final = "0"
|
|
10
10
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
|
11
11
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
|
12
|
+
CONF_API_VERSION: Final = "api_version"
|
|
@@ -30,8 +30,31 @@ def ha_datetime_to_fakedatetime(datetime) -> freezegun.api.FakeDatetime: # type
|
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
class
|
|
34
|
-
"""Modified to
|
|
33
|
+
class HAFakeDateMeta(freezegun.api.FakeDateMeta):
|
|
34
|
+
"""Modified to override the string representation."""
|
|
35
|
+
|
|
36
|
+
def __str__(cls) -> str: # noqa: N805 (ruff doesn't know this is a metaclass)
|
|
37
|
+
"""Return the string representation of the class."""
|
|
38
|
+
return "<class 'datetime.date'>"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class HAFakeDate(freezegun.api.FakeDate, metaclass=HAFakeDateMeta): # type: ignore[name-defined]
|
|
42
|
+
"""Modified to improve class str."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class HAFakeDatetimeMeta(freezegun.api.FakeDatetimeMeta):
|
|
46
|
+
"""Modified to override the string representation."""
|
|
47
|
+
|
|
48
|
+
def __str__(cls) -> str: # noqa: N805 (ruff doesn't know this is a metaclass)
|
|
49
|
+
"""Return the string representation of the class."""
|
|
50
|
+
return "<class 'datetime.datetime'>"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class HAFakeDatetime(freezegun.api.FakeDatetime, metaclass=HAFakeDatetimeMeta): # type: ignore[name-defined]
|
|
54
|
+
"""Modified to include basic fold support and improve class str.
|
|
55
|
+
|
|
56
|
+
Fold support submitted to upstream in https://github.com/spulec/freezegun/pull/424.
|
|
57
|
+
"""
|
|
35
58
|
|
|
36
59
|
@classmethod
|
|
37
60
|
def now(cls, tz=None):
|
|
@@ -103,6 +103,7 @@ from homeassistant.helpers import (
|
|
|
103
103
|
translation as translation_helper,
|
|
104
104
|
)
|
|
105
105
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|
106
|
+
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
|
106
107
|
from homeassistant.helpers.translation import _TranslationsCacheData
|
|
107
108
|
from homeassistant.helpers.typing import ConfigType
|
|
108
109
|
from homeassistant.setup import async_setup_component
|
|
@@ -162,6 +163,7 @@ asyncio.set_event_loop_policy = lambda policy: None
|
|
|
162
163
|
def pytest_addoption(parser: pytest.Parser) -> None:
|
|
163
164
|
"""Register custom pytest options."""
|
|
164
165
|
parser.addoption("--dburl", action="store", default="sqlite://")
|
|
166
|
+
parser.addoption("--drop-existing-db", action="store_const", const=True)
|
|
165
167
|
|
|
166
168
|
|
|
167
169
|
def pytest_configure(config: pytest.Config) -> None:
|
|
@@ -189,11 +191,13 @@ def pytest_runtest_setup() -> None:
|
|
|
189
191
|
destinations will be allowed.
|
|
190
192
|
|
|
191
193
|
freezegun:
|
|
192
|
-
Modified to include https://github.com/spulec/freezegun/pull/424
|
|
194
|
+
Modified to include https://github.com/spulec/freezegun/pull/424 and improve class str.
|
|
193
195
|
"""
|
|
194
196
|
pytest_socket.socket_allow_hosts(["127.0.0.1"])
|
|
195
197
|
pytest_socket.disable_socket(allow_unix_socket=True)
|
|
196
198
|
|
|
199
|
+
freezegun.api.FakeDate = patch_time.HAFakeDate # type: ignore[attr-defined]
|
|
200
|
+
|
|
197
201
|
freezegun.api.datetime_to_fakedatetime = patch_time.ha_datetime_to_fakedatetime # type: ignore[attr-defined]
|
|
198
202
|
freezegun.api.FakeDatetime = patch_time.HAFakeDatetime # type: ignore[attr-defined]
|
|
199
203
|
|
|
@@ -1493,44 +1497,58 @@ def recorder_db_url(
|
|
|
1493
1497
|
assert not hass_fixture_setup
|
|
1494
1498
|
|
|
1495
1499
|
db_url = cast(str, pytestconfig.getoption("dburl"))
|
|
1500
|
+
drop_existing_db = pytestconfig.getoption("drop_existing_db")
|
|
1501
|
+
|
|
1502
|
+
def drop_db() -> None:
|
|
1503
|
+
import sqlalchemy as sa # noqa: PLC0415
|
|
1504
|
+
import sqlalchemy_utils # noqa: PLC0415
|
|
1505
|
+
|
|
1506
|
+
if db_url.startswith("mysql://"):
|
|
1507
|
+
made_url = sa.make_url(db_url)
|
|
1508
|
+
db = made_url.database
|
|
1509
|
+
engine = sa.create_engine(db_url)
|
|
1510
|
+
# Check for any open connections to the database before dropping it
|
|
1511
|
+
# to ensure that InnoDB does not deadlock.
|
|
1512
|
+
with engine.begin() as connection:
|
|
1513
|
+
query = sa.text(
|
|
1514
|
+
"select id FROM information_schema.processlist WHERE db=:db and id != CONNECTION_ID()"
|
|
1515
|
+
)
|
|
1516
|
+
rows = connection.execute(query, parameters={"db": db}).fetchall()
|
|
1517
|
+
if rows:
|
|
1518
|
+
raise RuntimeError(
|
|
1519
|
+
f"Unable to drop database {db} because it is in use by {rows}"
|
|
1520
|
+
)
|
|
1521
|
+
engine.dispose()
|
|
1522
|
+
sqlalchemy_utils.drop_database(db_url)
|
|
1523
|
+
elif db_url.startswith("postgresql://"):
|
|
1524
|
+
sqlalchemy_utils.drop_database(db_url)
|
|
1525
|
+
|
|
1496
1526
|
if db_url == "sqlite://" and persistent_database:
|
|
1497
1527
|
tmp_path = tmp_path_factory.mktemp("recorder")
|
|
1498
1528
|
db_url = "sqlite:///" + str(tmp_path / "pytest.db")
|
|
1499
|
-
elif db_url.startswith("mysql://"):
|
|
1529
|
+
elif db_url.startswith(("mysql://", "postgresql://")):
|
|
1500
1530
|
import sqlalchemy_utils # noqa: PLC0415
|
|
1501
1531
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
sqlalchemy_utils.create_database(db_url, encoding=charset)
|
|
1505
|
-
elif db_url.startswith("postgresql://"):
|
|
1506
|
-
import sqlalchemy_utils # noqa: PLC0415
|
|
1532
|
+
if drop_existing_db and sqlalchemy_utils.database_exists(db_url):
|
|
1533
|
+
drop_db()
|
|
1507
1534
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1535
|
+
if sqlalchemy_utils.database_exists(db_url):
|
|
1536
|
+
raise RuntimeError(
|
|
1537
|
+
f"Database {db_url} already exists. Use --drop-existing-db "
|
|
1538
|
+
"to automatically drop existing database before start of test."
|
|
1539
|
+
)
|
|
1540
|
+
|
|
1541
|
+
sqlalchemy_utils.create_database(
|
|
1542
|
+
db_url,
|
|
1543
|
+
encoding="utf8mb4' COLLATE = 'utf8mb4_unicode_ci"
|
|
1544
|
+
if db_url.startswith("mysql://")
|
|
1545
|
+
else "utf8",
|
|
1546
|
+
)
|
|
1510
1547
|
yield db_url
|
|
1511
1548
|
if db_url == "sqlite://" and persistent_database:
|
|
1512
1549
|
rmtree(tmp_path, ignore_errors=True)
|
|
1513
|
-
elif db_url.startswith("mysql://"):
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
made_url = sa.make_url(db_url)
|
|
1517
|
-
db = made_url.database
|
|
1518
|
-
engine = sa.create_engine(db_url)
|
|
1519
|
-
# Check for any open connections to the database before dropping it
|
|
1520
|
-
# to ensure that InnoDB does not deadlock.
|
|
1521
|
-
with engine.begin() as connection:
|
|
1522
|
-
query = sa.text(
|
|
1523
|
-
"select id FROM information_schema.processlist WHERE db=:db and id != CONNECTION_ID()"
|
|
1524
|
-
)
|
|
1525
|
-
rows = connection.execute(query, parameters={"db": db}).fetchall()
|
|
1526
|
-
if rows:
|
|
1527
|
-
raise RuntimeError(
|
|
1528
|
-
f"Unable to drop database {db} because it is in use by {rows}"
|
|
1529
|
-
)
|
|
1530
|
-
engine.dispose()
|
|
1531
|
-
sqlalchemy_utils.drop_database(db_url)
|
|
1532
|
-
elif db_url.startswith("postgresql://"):
|
|
1533
|
-
sqlalchemy_utils.drop_database(db_url)
|
|
1550
|
+
elif db_url.startswith(("mysql://", "postgresql://")):
|
|
1551
|
+
drop_db()
|
|
1534
1552
|
|
|
1535
1553
|
|
|
1536
1554
|
async def _async_init_recorder_component(
|
|
@@ -1660,10 +1678,12 @@ async def async_test_recorder(
|
|
|
1660
1678
|
migrate_entity_ids = (
|
|
1661
1679
|
migration.EntityIDMigration.migrate_data if enable_migrate_entity_ids else None
|
|
1662
1680
|
)
|
|
1663
|
-
|
|
1664
|
-
migration.EventIDPostMigration.
|
|
1681
|
+
post_migrate_event_ids = (
|
|
1682
|
+
migration.EventIDPostMigration.needs_migrate_impl
|
|
1665
1683
|
if enable_migrate_event_ids
|
|
1666
|
-
else lambda
|
|
1684
|
+
else lambda _1, _2, _3: migration.DataMigrationStatus(
|
|
1685
|
+
needs_migrate=False, migration_done=True
|
|
1686
|
+
)
|
|
1667
1687
|
)
|
|
1668
1688
|
with (
|
|
1669
1689
|
patch(
|
|
@@ -1702,8 +1722,8 @@ async def async_test_recorder(
|
|
|
1702
1722
|
autospec=True,
|
|
1703
1723
|
),
|
|
1704
1724
|
patch(
|
|
1705
|
-
"homeassistant.components.recorder.migration.EventIDPostMigration.
|
|
1706
|
-
side_effect=
|
|
1725
|
+
"homeassistant.components.recorder.migration.EventIDPostMigration.needs_migrate_impl",
|
|
1726
|
+
side_effect=post_migrate_event_ids,
|
|
1707
1727
|
autospec=True,
|
|
1708
1728
|
),
|
|
1709
1729
|
patch(
|
|
@@ -1821,6 +1841,7 @@ async def mock_enable_bluetooth(
|
|
|
1821
1841
|
def mock_bluetooth_adapters() -> Generator[None]:
|
|
1822
1842
|
"""Fixture to mock bluetooth adapters."""
|
|
1823
1843
|
with (
|
|
1844
|
+
patch("habluetooth.util.recover_adapter"),
|
|
1824
1845
|
patch("bluetooth_auto_recovery.recover_adapter"),
|
|
1825
1846
|
patch("bluetooth_adapters.systems.platform.system", return_value="Linux"),
|
|
1826
1847
|
patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"),
|
|
@@ -1849,19 +1870,31 @@ def mock_bleak_scanner_start() -> Generator[MagicMock]:
|
|
|
1849
1870
|
|
|
1850
1871
|
# Late imports to avoid loading bleak unless we need it
|
|
1851
1872
|
|
|
1852
|
-
from habluetooth import
|
|
1873
|
+
from habluetooth import ( # noqa: PLC0415
|
|
1874
|
+
manager as bluetooth_manager,
|
|
1875
|
+
scanner as bluetooth_scanner,
|
|
1876
|
+
)
|
|
1853
1877
|
|
|
1854
1878
|
# We need to drop the stop method from the object since we patched
|
|
1855
1879
|
# out start and this fixture will expire before the stop method is called
|
|
1856
1880
|
# when EVENT_HOMEASSISTANT_STOP is fired.
|
|
1857
1881
|
# pylint: disable-next=c-extension-no-member
|
|
1858
1882
|
bluetooth_scanner.OriginalBleakScanner.stop = AsyncMock() # type: ignore[assignment]
|
|
1883
|
+
|
|
1884
|
+
# Mock BlueZ management controller to successfully setup
|
|
1885
|
+
# This prevents the manager from operating in degraded mode
|
|
1886
|
+
mock_mgmt_bluetooth_ctl = Mock()
|
|
1887
|
+
mock_mgmt_bluetooth_ctl.setup = AsyncMock(return_value=None)
|
|
1888
|
+
|
|
1859
1889
|
with (
|
|
1860
1890
|
patch.object(
|
|
1861
1891
|
bluetooth_scanner.OriginalBleakScanner, # pylint: disable=c-extension-no-member
|
|
1862
1892
|
"start",
|
|
1863
1893
|
) as mock_bleak_scanner_start,
|
|
1864
1894
|
patch.object(bluetooth_scanner, "HaScanner"),
|
|
1895
|
+
patch.object(
|
|
1896
|
+
bluetooth_manager, "MGMTBluetoothCtl", return_value=mock_mgmt_bluetooth_ctl
|
|
1897
|
+
),
|
|
1865
1898
|
):
|
|
1866
1899
|
yield mock_bleak_scanner_start
|
|
1867
1900
|
|
|
@@ -2082,3 +2115,32 @@ def disable_block_async_io() -> Generator[None]:
|
|
|
2082
2115
|
blocking_call.object, blocking_call.function, blocking_call.original_func
|
|
2083
2116
|
)
|
|
2084
2117
|
calls.clear()
|
|
2118
|
+
|
|
2119
|
+
|
|
2120
|
+
# Ensure that incorrectly formatted mac addresses are rejected during
|
|
2121
|
+
# DhcpServiceInfo initialisation
|
|
2122
|
+
_real_dhcp_service_info_init = DhcpServiceInfo.__init__
|
|
2123
|
+
|
|
2124
|
+
|
|
2125
|
+
def _dhcp_service_info_init(self: DhcpServiceInfo, *args: Any, **kwargs: Any) -> None:
|
|
2126
|
+
"""Override __init__ for DhcpServiceInfo.
|
|
2127
|
+
|
|
2128
|
+
Ensure that the macaddress is always in lowercase and without colons to match DHCP service.
|
|
2129
|
+
"""
|
|
2130
|
+
_real_dhcp_service_info_init(self, *args, **kwargs)
|
|
2131
|
+
if self.macaddress != self.macaddress.lower().replace(":", ""):
|
|
2132
|
+
raise ValueError("macaddress is not correctly formatted")
|
|
2133
|
+
|
|
2134
|
+
|
|
2135
|
+
DhcpServiceInfo.__init__ = _dhcp_service_info_init
|
|
2136
|
+
|
|
2137
|
+
|
|
2138
|
+
@pytest.fixture(autouse=True)
|
|
2139
|
+
def disable_http_server() -> Generator[None]:
|
|
2140
|
+
"""Disable automatic start of HTTP server during .
|
|
2141
|
+
|
|
2142
|
+
This prevents the HTTP server from starting in tests that setup
|
|
2143
|
+
integrations which depend on the HTTP component.
|
|
2144
|
+
"""
|
|
2145
|
+
with patch("homeassistant.components.http.start_http_server_and_save_config"):
|
|
2146
|
+
yield
|
|
@@ -225,8 +225,8 @@ class AiohttpClientMockResponse:
|
|
|
225
225
|
|
|
226
226
|
if (
|
|
227
227
|
self._url.scheme != url.scheme
|
|
228
|
-
or self._url.
|
|
229
|
-
or self._url.
|
|
228
|
+
or self._url.raw_host != url.raw_host
|
|
229
|
+
or self._url.raw_path != url.raw_path
|
|
230
230
|
):
|
|
231
231
|
return False
|
|
232
232
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-homeassistant-custom-component
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.298
|
|
4
4
|
Summary: Experimental package to automatically extract test plugins for Home Assistant custom components
|
|
5
5
|
Home-page: https://github.com/MatthewFlamm/pytest-homeassistant-custom-component
|
|
6
6
|
Author: Matthew Flamm
|
|
@@ -18,17 +18,18 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
License-File: LICENSE_HA_CORE.md
|
|
20
20
|
Requires-Dist: sqlalchemy
|
|
21
|
-
Requires-Dist: coverage==7.10.
|
|
21
|
+
Requires-Dist: coverage==7.10.6
|
|
22
22
|
Requires-Dist: freezegun==1.5.2
|
|
23
|
-
Requires-Dist: go2rtc-client==0.
|
|
23
|
+
Requires-Dist: go2rtc-client==0.3.0
|
|
24
|
+
Requires-Dist: librt==0.2.1
|
|
24
25
|
Requires-Dist: license-expression==30.4.3
|
|
25
26
|
Requires-Dist: mock-open==1.4.0
|
|
26
|
-
Requires-Dist: pydantic==2.
|
|
27
|
+
Requires-Dist: pydantic==2.12.2
|
|
27
28
|
Requires-Dist: pylint-per-file-ignores==1.4.0
|
|
28
29
|
Requires-Dist: pipdeptree==2.26.1
|
|
29
|
-
Requires-Dist: pytest-asyncio==1.
|
|
30
|
+
Requires-Dist: pytest-asyncio==1.3.0
|
|
30
31
|
Requires-Dist: pytest-aiohttp==1.1.0
|
|
31
|
-
Requires-Dist: pytest-cov==
|
|
32
|
+
Requires-Dist: pytest-cov==7.0.0
|
|
32
33
|
Requires-Dist: pytest-freezer==0.4.9
|
|
33
34
|
Requires-Dist: pytest-github-actions-annotate-failures==0.3.0
|
|
34
35
|
Requires-Dist: pytest-socket==0.7.0
|
|
@@ -37,12 +38,12 @@ Requires-Dist: pytest-timeout==2.4.0
|
|
|
37
38
|
Requires-Dist: pytest-unordered==0.7.0
|
|
38
39
|
Requires-Dist: pytest-picked==0.5.1
|
|
39
40
|
Requires-Dist: pytest-xdist==3.8.0
|
|
40
|
-
Requires-Dist: pytest==
|
|
41
|
+
Requires-Dist: pytest==9.0.0
|
|
41
42
|
Requires-Dist: requests-mock==1.12.1
|
|
42
43
|
Requires-Dist: respx==0.22.0
|
|
43
|
-
Requires-Dist: syrupy==
|
|
44
|
+
Requires-Dist: syrupy==5.0.0
|
|
44
45
|
Requires-Dist: tqdm==4.67.1
|
|
45
|
-
Requires-Dist: homeassistant==2025.
|
|
46
|
+
Requires-Dist: homeassistant==2025.12.0
|
|
46
47
|
Requires-Dist: SQLAlchemy==2.0.41
|
|
47
48
|
Requires-Dist: paho-mqtt==2.1.0
|
|
48
49
|
Requires-Dist: numpy==2.3.2
|
|
@@ -60,7 +61,7 @@ Dynamic: summary
|
|
|
60
61
|
|
|
61
62
|
# pytest-homeassistant-custom-component
|
|
62
63
|
|
|
63
|
-

|
|
64
65
|
|
|
65
66
|
[](https://gitpod.io/#https://github.com/MatthewFlamm/pytest-homeassistant-custom-component)
|
|
66
67
|
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
pytest_homeassistant_custom_component/__init__.py,sha256=pUI8j-H-57ncCLnvZSDWZPCtJpvi3ACZqPtH5SbedZA,138
|
|
2
2
|
pytest_homeassistant_custom_component/asyncio_legacy.py,sha256=UdkV2mKqeS21QX9LSdBYsBRbm2h4JCVVZeesaOLKOAE,3886
|
|
3
|
-
pytest_homeassistant_custom_component/common.py,sha256=
|
|
4
|
-
pytest_homeassistant_custom_component/const.py,sha256=
|
|
3
|
+
pytest_homeassistant_custom_component/common.py,sha256=1UprBAnCk8VgxwD2Py893jNr0Fsjn-1Q_vx7pYcjB1M,65480
|
|
4
|
+
pytest_homeassistant_custom_component/const.py,sha256=ytAygDIdVtA6OiG_jBkWveMPpCDEz52T9zCZ4vsuQJ8,440
|
|
5
5
|
pytest_homeassistant_custom_component/ignore_uncaught_exceptions.py,sha256=rilak_dQGMNhDqST1ZzhjZl_qmytFjkcez0vYmLMQ4Q,1601
|
|
6
6
|
pytest_homeassistant_custom_component/patch_json.py,sha256=hNUeb1yxAr7ONfvX-o_WkI6zhQDCdKl7GglPjkVUiHo,1063
|
|
7
7
|
pytest_homeassistant_custom_component/patch_recorder.py,sha256=lW8N_3ZIKQ5lsVjRc-ROo7d0egUZcpjquWKqe7iEF94,819
|
|
8
|
-
pytest_homeassistant_custom_component/patch_time.py,sha256=
|
|
9
|
-
pytest_homeassistant_custom_component/plugins.py,sha256=
|
|
8
|
+
pytest_homeassistant_custom_component/patch_time.py,sha256=jdnOAXDxUA0AKqvyeSrRC18rHDGfcpWYuLhmUglebCE,3374
|
|
9
|
+
pytest_homeassistant_custom_component/plugins.py,sha256=ui8WsonovfIEb0eI-UkV6IE80fm63_YUHD9RzD6wj2k,69969
|
|
10
10
|
pytest_homeassistant_custom_component/syrupy.py,sha256=N_g_90dWqruzUogQi0rJsuN0XRbA6ffJen62r8P9cdo,15588
|
|
11
11
|
pytest_homeassistant_custom_component/typing.py,sha256=zGhdf6U6aRq5cPwIfRUdtZeApLOyPD2EArjznKoIRZM,1734
|
|
12
|
-
pytest_homeassistant_custom_component/components/__init__.py,sha256=
|
|
12
|
+
pytest_homeassistant_custom_component/components/__init__.py,sha256=49s3Tf-mHQQnQPnjuD94LeCnYDypnm6y7Dhfr5lJkCQ,11427
|
|
13
13
|
pytest_homeassistant_custom_component/components/diagnostics/__init__.py,sha256=O_ys8t0iHvRorFr4TrR9k3sa3Xh5qBb4HsylY775UFA,2431
|
|
14
14
|
pytest_homeassistant_custom_component/components/recorder/__init__.py,sha256=ugrLzvjSQFSmYRjy88ZZSiyA-NLgKlLkFp0OKguy6a4,225
|
|
15
|
-
pytest_homeassistant_custom_component/components/recorder/common.py,sha256=
|
|
15
|
+
pytest_homeassistant_custom_component/components/recorder/common.py,sha256=8c_oqbQtg7dI-JoOaZrLYKFAjEJvjvSIA1atfui-WpQ,22091
|
|
16
16
|
pytest_homeassistant_custom_component/components/recorder/db_schema_0.py,sha256=0mez9slhL-I286dDAxq06UDvWRU6RzCA2GKOwtj9JOI,5547
|
|
17
17
|
pytest_homeassistant_custom_component/test_util/__init__.py,sha256=ljLmNeblq1vEgP0vhf2P1-SuyGSHvLKVA0APSYA0Xl8,1034
|
|
18
|
-
pytest_homeassistant_custom_component/test_util/aiohttp.py,sha256=
|
|
18
|
+
pytest_homeassistant_custom_component/test_util/aiohttp.py,sha256=sJHmGf4Oig0SUMvfylBaZIsDTfpTwmYvuLfE--OXYx4,11536
|
|
19
19
|
pytest_homeassistant_custom_component/testing_config/__init__.py,sha256=SRp6h9HJi2I_vA6cPNkMiR0BTYib5XVmL03H-l3BPL0,158
|
|
20
20
|
pytest_homeassistant_custom_component/testing_config/custom_components/__init__.py,sha256=-l6KCBLhwEDkCztlY6S-j53CjmKY6-A_3eX5JVS02NY,173
|
|
21
21
|
pytest_homeassistant_custom_component/testing_config/custom_components/test_constant_deprecation/__init__.py,sha256=2vF_C-VP9tDjZMX7h6iJRAugtH2Bf3b4fE3i9j4vGeY,383
|
|
22
|
-
pytest_homeassistant_custom_component-0.13.
|
|
23
|
-
pytest_homeassistant_custom_component-0.13.
|
|
24
|
-
pytest_homeassistant_custom_component-0.13.
|
|
25
|
-
pytest_homeassistant_custom_component-0.13.
|
|
26
|
-
pytest_homeassistant_custom_component-0.13.
|
|
27
|
-
pytest_homeassistant_custom_component-0.13.
|
|
28
|
-
pytest_homeassistant_custom_component-0.13.
|
|
22
|
+
pytest_homeassistant_custom_component-0.13.298.dist-info/licenses/LICENSE,sha256=7h-vqUxyeQNXiQgRJ8350CSHOy55M07DZuv4KG70AS8,1070
|
|
23
|
+
pytest_homeassistant_custom_component-0.13.298.dist-info/licenses/LICENSE_HA_CORE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
24
|
+
pytest_homeassistant_custom_component-0.13.298.dist-info/METADATA,sha256=a6kWgh27JmV2RF1vvLgklpSKMsH206TFldacwEQBwM0,5958
|
|
25
|
+
pytest_homeassistant_custom_component-0.13.298.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
26
|
+
pytest_homeassistant_custom_component-0.13.298.dist-info/entry_points.txt,sha256=bOCTSuP8RSPg0QfwdfurUShvMGWg4MI2F8rxbWx-VtQ,73
|
|
27
|
+
pytest_homeassistant_custom_component-0.13.298.dist-info/top_level.txt,sha256=PR2cize2la22eOO7dQChJWK8dkJnuMmDC-fhafmdOWw,38
|
|
28
|
+
pytest_homeassistant_custom_component-0.13.298.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|