pytest-homeassistant-custom-component 0.13.289__tar.gz → 0.13.305__tar.gz

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 (38) hide show
  1. {pytest_homeassistant_custom_component-0.13.289/src/pytest_homeassistant_custom_component.egg-info → pytest_homeassistant_custom_component-0.13.305}/PKG-INFO +7 -10
  2. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/README.md +1 -3
  3. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/common.py +15 -11
  4. pytest_homeassistant_custom_component-0.13.305/src/pytest_homeassistant_custom_component/components/__init__.py +399 -0
  5. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/components/recorder/common.py +117 -2
  6. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/const.py +3 -3
  7. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/ignore_uncaught_exceptions.py +9 -9
  8. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/patch_time.py +25 -2
  9. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/plugins.py +71 -39
  10. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/test_util/aiohttp.py +2 -2
  11. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/typing.py +1 -1
  12. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305/src/pytest_homeassistant_custom_component.egg-info}/PKG-INFO +7 -10
  13. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component.egg-info/requires.txt +5 -6
  14. pytest_homeassistant_custom_component-0.13.289/src/pytest_homeassistant_custom_component/components/__init__.py +0 -5
  15. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/LICENSE +0 -0
  16. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/LICENSE_HA_CORE.md +0 -0
  17. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/setup.cfg +0 -0
  18. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/setup.py +0 -0
  19. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/__init__.py +0 -0
  20. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/asyncio_legacy.py +0 -0
  21. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/components/diagnostics/__init__.py +0 -0
  22. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/components/recorder/__init__.py +0 -0
  23. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/components/recorder/db_schema_0.py +0 -0
  24. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/patch_json.py +0 -0
  25. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/patch_recorder.py +0 -0
  26. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/syrupy.py +0 -0
  27. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/test_util/__init__.py +0 -0
  28. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/testing_config/__init__.py +0 -0
  29. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/testing_config/custom_components/__init__.py +0 -0
  30. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component/testing_config/custom_components/test_constant_deprecation/__init__.py +0 -0
  31. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component.egg-info/SOURCES.txt +0 -0
  32. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component.egg-info/dependency_links.txt +0 -0
  33. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component.egg-info/entry_points.txt +0 -0
  34. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/src/pytest_homeassistant_custom_component.egg-info/top_level.txt +0 -0
  35. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/tests/test_common.py +0 -0
  36. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/tests/test_config_flow.py +0 -0
  37. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/tests/test_diagnostics.py +0 -0
  38. {pytest_homeassistant_custom_component-0.13.289 → pytest_homeassistant_custom_component-0.13.305}/tests/test_sensor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-homeassistant-custom-component
3
- Version: 0.13.289
3
+ Version: 0.13.305
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
@@ -20,13 +20,12 @@ License-File: LICENSE_HA_CORE.md
20
20
  Requires-Dist: sqlalchemy
21
21
  Requires-Dist: coverage==7.10.6
22
22
  Requires-Dist: freezegun==1.5.2
23
- Requires-Dist: go2rtc-client==0.2.1
24
23
  Requires-Dist: license-expression==30.4.3
25
24
  Requires-Dist: mock-open==1.4.0
26
- Requires-Dist: pydantic==2.11.9
25
+ Requires-Dist: pydantic==2.12.2
27
26
  Requires-Dist: pylint-per-file-ignores==1.4.0
28
27
  Requires-Dist: pipdeptree==2.26.1
29
- Requires-Dist: pytest-asyncio==1.2.0
28
+ Requires-Dist: pytest-asyncio==1.3.0
30
29
  Requires-Dist: pytest-aiohttp==1.1.0
31
30
  Requires-Dist: pytest-cov==7.0.0
32
31
  Requires-Dist: pytest-freezer==0.4.9
@@ -37,12 +36,12 @@ Requires-Dist: pytest-timeout==2.4.0
37
36
  Requires-Dist: pytest-unordered==0.7.0
38
37
  Requires-Dist: pytest-picked==0.5.1
39
38
  Requires-Dist: pytest-xdist==3.8.0
40
- Requires-Dist: pytest==8.4.2
39
+ Requires-Dist: pytest==9.0.0
41
40
  Requires-Dist: requests-mock==1.12.1
42
41
  Requires-Dist: respx==0.22.0
43
- Requires-Dist: syrupy==4.9.1
42
+ Requires-Dist: syrupy==5.0.0
44
43
  Requires-Dist: tqdm==4.67.1
45
- Requires-Dist: homeassistant==2025.10.4
44
+ Requires-Dist: homeassistant==2026.1.0
46
45
  Requires-Dist: SQLAlchemy==2.0.41
47
46
  Requires-Dist: paho-mqtt==2.1.0
48
47
  Requires-Dist: numpy==2.3.2
@@ -60,9 +59,7 @@ Dynamic: summary
60
59
 
61
60
  # pytest-homeassistant-custom-component
62
61
 
63
- ![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2025.10.4&labelColor=blue)
64
-
65
- [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/MatthewFlamm/pytest-homeassistant-custom-component)
62
+ ![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2026.1.0&labelColor=blue)
66
63
 
67
64
  Package to automatically extract testing plugins from Home Assistant for custom component testing.
68
65
  The goal is to provide the same functionality as the tests in home-assistant/core.
@@ -1,8 +1,6 @@
1
1
  # pytest-homeassistant-custom-component
2
2
 
3
- ![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2025.10.4&labelColor=blue)
4
-
5
- [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/MatthewFlamm/pytest-homeassistant-custom-component)
3
+ ![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2026.1.0&labelColor=blue)
6
4
 
7
5
  Package to automatically extract testing plugins from Home Assistant for custom component testing.
8
6
  The goal is to provide the same functionality as the tests in home-assistant/core.
@@ -1,5 +1,5 @@
1
1
  """
2
- Test the helper method for writing .
2
+ Test the helper method for writing tests.
3
3
 
4
4
  This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component.
5
5
  """
@@ -204,14 +204,14 @@ class StoreWithoutWriteLoad[_T: (Mapping[str, Any] | Sequence[Any])](storage.Sto
204
204
  async def async_save(self, *args: Any, **kwargs: Any) -> None:
205
205
  """Save the data.
206
206
 
207
- This function is mocked out in .
207
+ This function is mocked out in tests.
208
208
  """
209
209
 
210
210
  @callback
211
211
  def async_save_delay(self, *args: Any, **kwargs: Any) -> None:
212
212
  """Save data with an optional delay.
213
213
 
214
- This function is mocked out in .
214
+ This function is mocked out in tests.
215
215
  """
216
216
 
217
217
 
@@ -1148,7 +1148,7 @@ class MockConfigEntry(config_entries.ConfigEntry):
1148
1148
  state: config_entries.ConfigEntryState,
1149
1149
  reason: str | None = None,
1150
1150
  ) -> None:
1151
- """Mock the state of a config entry to be used in .
1151
+ """Mock the state of a config entry to be used in tests.
1152
1152
 
1153
1153
  Currently this is a wrapper around _async_set_state, but it may
1154
1154
  change in the future.
@@ -1159,7 +1159,7 @@ class MockConfigEntry(config_entries.ConfigEntry):
1159
1159
 
1160
1160
  When in doubt, this helper should not be used in new code
1161
1161
  and is only intended for backwards compatibility with existing
1162
- .
1162
+ tests.
1163
1163
  """
1164
1164
  self._async_set_state(hass, state, reason)
1165
1165
 
@@ -1539,7 +1539,7 @@ def mock_storage(data: dict[str, Any] | None = None) -> Generator[dict[str, Any]
1539
1539
  return loaded
1540
1540
 
1541
1541
  async def mock_write_data(
1542
- store: storage.Store, path: str, data_to_write: dict[str, Any]
1542
+ store: storage.Store, data_to_write: dict[str, Any]
1543
1543
  ) -> None:
1544
1544
  """Mock version of write data."""
1545
1545
  # To ensure that the data can be serialized
@@ -1616,12 +1616,16 @@ def mock_integration(
1616
1616
  top_level_files: set[str] | None = None,
1617
1617
  ) -> loader.Integration:
1618
1618
  """Mock an integration."""
1619
- integration = loader.Integration(
1620
- hass,
1619
+ path = (
1621
1620
  f"{loader.PACKAGE_BUILTIN}.{module.DOMAIN}"
1622
1621
  if built_in
1623
- else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}",
1624
- 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(".", "/")),
1625
1629
  module.mock_manifest(),
1626
1630
  top_level_files,
1627
1631
  )
@@ -1914,7 +1918,7 @@ def setup_test_component_platform(
1914
1918
  from_config_entry: bool = False,
1915
1919
  built_in: bool = True,
1916
1920
  ) -> MockPlatform:
1917
- """Mock a test component platform for ."""
1921
+ """Mock a test component platform for tests."""
1918
1922
 
1919
1923
  async def _async_setup_platform(
1920
1924
  hass: HomeAssistant,
@@ -0,0 +1,399 @@
1
+ """
2
+ The tests for components.
3
+
4
+ This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component.
5
+ """
6
+
7
+ from collections.abc import Iterable
8
+ from enum import StrEnum
9
+ import itertools
10
+ from typing import TypedDict
11
+
12
+ from homeassistant.const import (
13
+ ATTR_AREA_ID,
14
+ ATTR_DEVICE_ID,
15
+ ATTR_FLOOR_ID,
16
+ ATTR_LABEL_ID,
17
+ CONF_ENTITY_ID,
18
+ CONF_OPTIONS,
19
+ CONF_PLATFORM,
20
+ CONF_TARGET,
21
+ STATE_UNAVAILABLE,
22
+ STATE_UNKNOWN,
23
+ )
24
+ from homeassistant.core import HomeAssistant
25
+ from homeassistant.helpers import (
26
+ area_registry as ar,
27
+ device_registry as dr,
28
+ entity_registry as er,
29
+ floor_registry as fr,
30
+ label_registry as lr,
31
+ )
32
+ from homeassistant.setup import async_setup_component
33
+
34
+ from ..common import MockConfigEntry, mock_device_registry
35
+
36
+
37
+ async def target_entities(
38
+ hass: HomeAssistant, domain: str
39
+ ) -> tuple[list[str], list[str]]:
40
+ """Create multiple entities associated with different targets.
41
+
42
+ Returns a dict with the following keys:
43
+ - included: List of entity_ids meant to be targeted.
44
+ - excluded: List of entity_ids not meant to be targeted.
45
+ """
46
+ config_entry = MockConfigEntry(domain="test")
47
+ config_entry.add_to_hass(hass)
48
+
49
+ floor_reg = fr.async_get(hass)
50
+ floor = floor_reg.async_get_floor_by_name("Test Floor") or floor_reg.async_create(
51
+ "Test Floor"
52
+ )
53
+
54
+ area_reg = ar.async_get(hass)
55
+ area = area_reg.async_get_area_by_name("Test Area") or area_reg.async_create(
56
+ "Test Area", floor_id=floor.floor_id
57
+ )
58
+
59
+ label_reg = lr.async_get(hass)
60
+ label = label_reg.async_get_label_by_name("Test Label") or label_reg.async_create(
61
+ "Test Label"
62
+ )
63
+
64
+ device = dr.DeviceEntry(id="test_device", area_id=area.id, labels={label.label_id})
65
+ mock_device_registry(hass, {device.id: device})
66
+
67
+ entity_reg = er.async_get(hass)
68
+ # Entities associated with area
69
+ entity_area = entity_reg.async_get_or_create(
70
+ domain=domain,
71
+ platform="test",
72
+ unique_id=f"{domain}_area",
73
+ suggested_object_id=f"area_{domain}",
74
+ )
75
+ entity_reg.async_update_entity(entity_area.entity_id, area_id=area.id)
76
+ entity_area_excluded = entity_reg.async_get_or_create(
77
+ domain=domain,
78
+ platform="test",
79
+ unique_id=f"{domain}_area_excluded",
80
+ suggested_object_id=f"area_{domain}_excluded",
81
+ )
82
+ entity_reg.async_update_entity(entity_area_excluded.entity_id, area_id=area.id)
83
+
84
+ # Entities associated with device
85
+ entity_reg.async_get_or_create(
86
+ domain=domain,
87
+ platform="test",
88
+ unique_id=f"{domain}_device",
89
+ suggested_object_id=f"device_{domain}",
90
+ device_id=device.id,
91
+ )
92
+ entity_reg.async_get_or_create(
93
+ domain=domain,
94
+ platform="test",
95
+ unique_id=f"{domain}_device_excluded",
96
+ suggested_object_id=f"device_{domain}_excluded",
97
+ device_id=device.id,
98
+ )
99
+
100
+ # Entities associated with label
101
+ entity_label = entity_reg.async_get_or_create(
102
+ domain=domain,
103
+ platform="test",
104
+ unique_id=f"{domain}_label",
105
+ suggested_object_id=f"label_{domain}",
106
+ )
107
+ entity_reg.async_update_entity(entity_label.entity_id, labels={label.label_id})
108
+ entity_label_excluded = entity_reg.async_get_or_create(
109
+ domain=domain,
110
+ platform="test",
111
+ unique_id=f"{domain}_label_excluded",
112
+ suggested_object_id=f"label_{domain}_excluded",
113
+ )
114
+ entity_reg.async_update_entity(
115
+ entity_label_excluded.entity_id, labels={label.label_id}
116
+ )
117
+
118
+ # Return all available entities
119
+ return {
120
+ "included": [
121
+ f"{domain}.standalone_{domain}",
122
+ f"{domain}.label_{domain}",
123
+ f"{domain}.area_{domain}",
124
+ f"{domain}.device_{domain}",
125
+ ],
126
+ "excluded": [
127
+ f"{domain}.standalone_{domain}_excluded",
128
+ f"{domain}.label_{domain}_excluded",
129
+ f"{domain}.area_{domain}_excluded",
130
+ f"{domain}.device_{domain}_excluded",
131
+ ],
132
+ }
133
+
134
+
135
+ def parametrize_target_entities(domain: str) -> list[tuple[dict, str, int]]:
136
+ """Parametrize target entities for different target types.
137
+
138
+ Meant to be used with target_entities.
139
+ """
140
+ return [
141
+ (
142
+ {CONF_ENTITY_ID: f"{domain}.standalone_{domain}"},
143
+ f"{domain}.standalone_{domain}",
144
+ 1,
145
+ ),
146
+ ({ATTR_LABEL_ID: "test_label"}, f"{domain}.label_{domain}", 2),
147
+ ({ATTR_AREA_ID: "test_area"}, f"{domain}.area_{domain}", 2),
148
+ ({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.area_{domain}", 2),
149
+ ({ATTR_LABEL_ID: "test_label"}, f"{domain}.device_{domain}", 2),
150
+ ({ATTR_AREA_ID: "test_area"}, f"{domain}.device_{domain}", 2),
151
+ ({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.device_{domain}", 2),
152
+ ({ATTR_DEVICE_ID: "test_device"}, f"{domain}.device_{domain}", 1),
153
+ ]
154
+
155
+
156
+ class _StateDescription(TypedDict):
157
+ """Test state and expected service call count."""
158
+
159
+ state: str | None
160
+ attributes: dict
161
+
162
+
163
+ class StateDescription(TypedDict):
164
+ """Test state and expected service call count."""
165
+
166
+ included: _StateDescription
167
+ excluded: _StateDescription
168
+ count: int
169
+
170
+
171
+ def parametrize_trigger_states(
172
+ *,
173
+ trigger: str,
174
+ target_states: list[str | None | tuple[str | None, dict]],
175
+ other_states: list[str | None | tuple[str | None, dict]],
176
+ additional_attributes: dict | None = None,
177
+ trigger_from_none: bool = True,
178
+ retrigger_on_target_state: bool = False,
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
+ Set `retrigger_on_target_state` to True if the trigger is expected to fire
189
+ when the state changes to another target state.
190
+
191
+ Returns a list of tuples with (trigger, list of states),
192
+ where states is a list of StateDescription dicts.
193
+ """
194
+
195
+ additional_attributes = additional_attributes or {}
196
+
197
+ def state_with_attributes(
198
+ state: str | None | tuple[str | None, dict], count: int
199
+ ) -> dict:
200
+ """Return (state, attributes) dict."""
201
+ if isinstance(state, str) or state is None:
202
+ return {
203
+ "included": {
204
+ "state": state,
205
+ "attributes": additional_attributes,
206
+ },
207
+ "excluded": {
208
+ "state": state,
209
+ "attributes": {},
210
+ },
211
+ "count": count,
212
+ }
213
+ return {
214
+ "included": {
215
+ "state": state[0],
216
+ "attributes": state[1] | additional_attributes,
217
+ },
218
+ "excluded": {
219
+ "state": state[0],
220
+ "attributes": state[1],
221
+ },
222
+ "count": count,
223
+ }
224
+
225
+ tests = [
226
+ # Initial state None
227
+ (
228
+ trigger,
229
+ list(
230
+ itertools.chain.from_iterable(
231
+ (
232
+ state_with_attributes(None, 0),
233
+ state_with_attributes(target_state, 0),
234
+ state_with_attributes(other_state, 0),
235
+ state_with_attributes(
236
+ target_state, 1 if trigger_from_none else 0
237
+ ),
238
+ )
239
+ for target_state in target_states
240
+ for other_state in other_states
241
+ )
242
+ ),
243
+ ),
244
+ # Initial state different from target state
245
+ (
246
+ trigger,
247
+ # other_state,
248
+ list(
249
+ itertools.chain.from_iterable(
250
+ (
251
+ state_with_attributes(other_state, 0),
252
+ state_with_attributes(target_state, 1),
253
+ state_with_attributes(other_state, 0),
254
+ state_with_attributes(target_state, 1),
255
+ )
256
+ for target_state in target_states
257
+ for other_state in other_states
258
+ )
259
+ ),
260
+ ),
261
+ # Initial state same as target state
262
+ (
263
+ trigger,
264
+ list(
265
+ itertools.chain.from_iterable(
266
+ (
267
+ state_with_attributes(target_state, 0),
268
+ state_with_attributes(target_state, 0),
269
+ state_with_attributes(other_state, 0),
270
+ state_with_attributes(target_state, 1),
271
+ # Repeat target state to test retriggering
272
+ state_with_attributes(target_state, 0),
273
+ state_with_attributes(STATE_UNAVAILABLE, 0),
274
+ )
275
+ for target_state in target_states
276
+ for other_state in other_states
277
+ )
278
+ ),
279
+ ),
280
+ # Initial state unavailable / unknown
281
+ (
282
+ trigger,
283
+ list(
284
+ itertools.chain.from_iterable(
285
+ (
286
+ state_with_attributes(STATE_UNAVAILABLE, 0),
287
+ state_with_attributes(target_state, 0),
288
+ state_with_attributes(other_state, 0),
289
+ state_with_attributes(target_state, 1),
290
+ )
291
+ for target_state in target_states
292
+ for other_state in other_states
293
+ )
294
+ ),
295
+ ),
296
+ (
297
+ trigger,
298
+ list(
299
+ itertools.chain.from_iterable(
300
+ (
301
+ state_with_attributes(STATE_UNKNOWN, 0),
302
+ state_with_attributes(target_state, 0),
303
+ state_with_attributes(other_state, 0),
304
+ state_with_attributes(target_state, 1),
305
+ )
306
+ for target_state in target_states
307
+ for other_state in other_states
308
+ )
309
+ ),
310
+ ),
311
+ ]
312
+
313
+ if len(target_states) > 1:
314
+ # If more than one target state, test state change between target states
315
+ tests.append(
316
+ (
317
+ trigger,
318
+ list(
319
+ itertools.chain.from_iterable(
320
+ (
321
+ state_with_attributes(target_states[idx - 1], 0),
322
+ state_with_attributes(
323
+ target_state, 1 if retrigger_on_target_state else 0
324
+ ),
325
+ state_with_attributes(other_state, 0),
326
+ state_with_attributes(target_states[idx - 1], 1),
327
+ state_with_attributes(
328
+ target_state, 1 if retrigger_on_target_state else 0
329
+ ),
330
+ state_with_attributes(STATE_UNAVAILABLE, 0),
331
+ )
332
+ for idx, target_state in enumerate(target_states[1:], start=1)
333
+ for other_state in other_states
334
+ )
335
+ ),
336
+ ),
337
+ )
338
+
339
+ return tests
340
+
341
+
342
+ async def arm_trigger(
343
+ hass: HomeAssistant,
344
+ trigger: str,
345
+ trigger_options: dict | None,
346
+ trigger_target: dict,
347
+ ) -> None:
348
+ """Arm the specified trigger, call service test.automation when it triggers."""
349
+
350
+ # Local include to avoid importing the automation component unnecessarily
351
+ from homeassistant.components import automation # noqa: PLC0415
352
+
353
+ options = {CONF_OPTIONS: {**trigger_options}} if trigger_options is not None else {}
354
+
355
+ await async_setup_component(
356
+ hass,
357
+ automation.DOMAIN,
358
+ {
359
+ automation.DOMAIN: {
360
+ "trigger": {
361
+ CONF_PLATFORM: trigger,
362
+ CONF_TARGET: {**trigger_target},
363
+ }
364
+ | options,
365
+ "action": {
366
+ "service": "test.automation",
367
+ "data_template": {CONF_ENTITY_ID: "{{ trigger.entity_id }}"},
368
+ },
369
+ }
370
+ },
371
+ )
372
+
373
+
374
+ def set_or_remove_state(
375
+ hass: HomeAssistant,
376
+ entity_id: str,
377
+ state: StateDescription,
378
+ ) -> None:
379
+ """Set or remove the state of an entity."""
380
+ if state["state"] is None:
381
+ hass.states.async_remove(entity_id)
382
+ else:
383
+ hass.states.async_set(
384
+ entity_id, state["state"], state["attributes"], force_update=True
385
+ )
386
+
387
+
388
+ def other_states(state: StrEnum | Iterable[StrEnum]) -> list[str]:
389
+ """Return a sorted list with all states except the specified one."""
390
+ if isinstance(state, StrEnum):
391
+ excluded_values = {state.value}
392
+ enum_class = state.__class__
393
+ else:
394
+ if len(state) == 0:
395
+ raise ValueError("state iterable must not be empty")
396
+ excluded_values = {s.value for s in state}
397
+ enum_class = list(state)[0].__class__
398
+
399
+ return sorted({s.value for s in enum_class} - excluded_values)
@@ -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
 
@@ -200,7 +209,7 @@ def statistics_during_period(
200
209
  types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]]
201
210
  | None = None,
202
211
  ) -> dict[str, list[dict[str, Any]]]:
203
- """Call statistics_during_period with defaults for simpler ..."""
212
+ """Call statistics_during_period with defaults for simpler tests."""
204
213
  if statistic_ids is not None and not isinstance(statistic_ids, set):
205
214
  statistic_ids = set(statistic_ids)
206
215
  if types is None:
@@ -453,7 +462,14 @@ def create_engine_test_for_schema_version_postfix(
453
462
 
454
463
  def get_schema_module_path(schema_version_postfix: str) -> str:
455
464
  """Return the path to the schema module."""
456
- return f"...components.recorder.db_schema_{schema_version_postfix}"
465
+ return f"tests.components.recorder.db_schema_{schema_version_postfix}"
466
+
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
+ )
457
473
 
458
474
 
459
475
  @contextmanager
@@ -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
@@ -4,9 +4,9 @@ Constants used by Home Assistant components.
4
4
  This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component.
5
5
  """
6
6
  from typing import TYPE_CHECKING, Final
7
- MAJOR_VERSION: Final = 2025
8
- MINOR_VERSION: Final = 10
9
- PATCH_VERSION: Final = "4"
7
+ MAJOR_VERSION: Final = 2026
8
+ MINOR_VERSION: Final = 1
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
12
  CONF_API_VERSION: Final = "api_version"
@@ -8,25 +8,25 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [
8
8
  (
9
9
  # This test explicitly throws an uncaught exception
10
10
  # and should not be removed.
11
- ".test_runner",
11
+ "tests.test_runner",
12
12
  "test_unhandled_exception_traceback",
13
13
  ),
14
14
  (
15
15
  # This test explicitly throws an uncaught exception
16
16
  # and should not be removed.
17
- ".helpers.test_event",
17
+ "tests.helpers.test_event",
18
18
  "test_track_point_in_time_repr",
19
19
  ),
20
20
  (
21
21
  # This test explicitly throws an uncaught exception
22
22
  # and should not be removed.
23
- ".test_config_entries",
23
+ "tests.test_config_entries",
24
24
  "test_config_entry_unloaded_during_platform_setups",
25
25
  ),
26
26
  (
27
27
  # This test explicitly throws an uncaught exception
28
28
  # and should not be removed.
29
- ".test_config_entries",
29
+ "tests.test_config_entries",
30
30
  "test_config_entry_unloaded_during_platform_setup",
31
31
  ),
32
32
  (
@@ -34,17 +34,17 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [
34
34
  "test_homeassistant_bridge_fan_setup",
35
35
  ),
36
36
  (
37
- ".components.owntracks.test_device_tracker",
37
+ "tests.components.owntracks.test_device_tracker",
38
38
  "test_mobile_multiple_async_enter_exit",
39
39
  ),
40
40
  (
41
- ".components.smartthings.test_init",
41
+ "tests.components.smartthings.test_init",
42
42
  "test_event_handler_dispatches_updated_devices",
43
43
  ),
44
44
  (
45
- ".components.unifi.test_controller",
45
+ "tests.components.unifi.test_controller",
46
46
  "test_wireless_client_event_calls_update_wireless_devices",
47
47
  ),
48
- (".components.iaqualink.test_config_flow", "test_with_invalid_credentials"),
49
- (".components.iaqualink.test_config_flow", "test_with_existing_config"),
48
+ ("tests.components.iaqualink.test_config_flow", "test_with_invalid_credentials"),
49
+ ("tests.components.iaqualink.test_config_flow", "test_with_existing_config"),
50
50
  ]
@@ -30,8 +30,31 @@ def ha_datetime_to_fakedatetime(datetime) -> freezegun.api.FakeDatetime: # type
30
30
  )
31
31
 
32
32
 
33
- class HAFakeDatetime(freezegun.api.FakeDatetime): # type: ignore[name-defined]
34
- """Modified to include https://github.com/spulec/freezegun/pull/424."""
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):
@@ -131,7 +131,7 @@ if TYPE_CHECKING:
131
131
  from homeassistant.components import recorder
132
132
 
133
133
 
134
- pytest.register_assert_rewrite(".common")
134
+ pytest.register_assert_rewrite("tests.common")
135
135
 
136
136
  from .common import ( # noqa: E402, isort:skip
137
137
  CLIENT_ID,
@@ -163,6 +163,7 @@ asyncio.set_event_loop_policy = lambda policy: None
163
163
  def pytest_addoption(parser: pytest.Parser) -> None:
164
164
  """Register custom pytest options."""
165
165
  parser.addoption("--dburl", action="store", default="sqlite://")
166
+ parser.addoption("--drop-existing-db", action="store_const", const=True)
166
167
 
167
168
 
168
169
  def pytest_configure(config: pytest.Config) -> None:
@@ -190,11 +191,13 @@ def pytest_runtest_setup() -> None:
190
191
  destinations will be allowed.
191
192
 
192
193
  freezegun:
193
- 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.
194
195
  """
195
196
  pytest_socket.socket_allow_hosts(["127.0.0.1"])
196
197
  pytest_socket.disable_socket(allow_unix_socket=True)
197
198
 
199
+ freezegun.api.FakeDate = patch_time.HAFakeDate # type: ignore[attr-defined]
200
+
198
201
  freezegun.api.datetime_to_fakedatetime = patch_time.ha_datetime_to_fakedatetime # type: ignore[attr-defined]
199
202
  freezegun.api.FakeDatetime = patch_time.HAFakeDatetime # type: ignore[attr-defined]
200
203
 
@@ -1271,9 +1274,11 @@ def evict_faked_translations(translations_once) -> Generator[_patch]:
1271
1274
  component_paths = components.__path__
1272
1275
 
1273
1276
  for call in mock_component_strings.mock_calls:
1277
+ _components: set[str] = call.args[2]
1274
1278
  integrations: dict[str, loader.Integration] = call.args[3]
1275
- for domain, integration in integrations.items():
1276
- if any(
1279
+ for domain in _components:
1280
+ # If the integration exists, don't evict from cache
1281
+ if (integration := integrations.get(domain)) and any(
1277
1282
  pathlib.Path(f"{component_path}/{domain}") == integration.file_path
1278
1283
  for component_path in component_paths
1279
1284
  ):
@@ -1475,7 +1480,7 @@ def persistent_database() -> bool:
1475
1480
  When using sqlite, this uses on disk database instead of in memory database.
1476
1481
  This does nothing when using mysql or postgresql.
1477
1482
 
1478
- Note that the database is always destroyed in between .
1483
+ Note that the database is always destroyed in between tests.
1479
1484
 
1480
1485
  To use a persistent database, tests can be marked with:
1481
1486
  @pytest.mark.parametrize("persistent_database", [True])
@@ -1494,44 +1499,58 @@ def recorder_db_url(
1494
1499
  assert not hass_fixture_setup
1495
1500
 
1496
1501
  db_url = cast(str, pytestconfig.getoption("dburl"))
1502
+ drop_existing_db = pytestconfig.getoption("drop_existing_db")
1503
+
1504
+ def drop_db() -> None:
1505
+ import sqlalchemy as sa # noqa: PLC0415
1506
+ import sqlalchemy_utils # noqa: PLC0415
1507
+
1508
+ if db_url.startswith("mysql://"):
1509
+ made_url = sa.make_url(db_url)
1510
+ db = made_url.database
1511
+ engine = sa.create_engine(db_url)
1512
+ # Check for any open connections to the database before dropping it
1513
+ # to ensure that InnoDB does not deadlock.
1514
+ with engine.begin() as connection:
1515
+ query = sa.text(
1516
+ "select id FROM information_schema.processlist WHERE db=:db and id != CONNECTION_ID()"
1517
+ )
1518
+ rows = connection.execute(query, parameters={"db": db}).fetchall()
1519
+ if rows:
1520
+ raise RuntimeError(
1521
+ f"Unable to drop database {db} because it is in use by {rows}"
1522
+ )
1523
+ engine.dispose()
1524
+ sqlalchemy_utils.drop_database(db_url)
1525
+ elif db_url.startswith("postgresql://"):
1526
+ sqlalchemy_utils.drop_database(db_url)
1527
+
1497
1528
  if db_url == "sqlite://" and persistent_database:
1498
1529
  tmp_path = tmp_path_factory.mktemp("recorder")
1499
1530
  db_url = "sqlite:///" + str(tmp_path / "pytest.db")
1500
- elif db_url.startswith("mysql://"):
1531
+ elif db_url.startswith(("mysql://", "postgresql://")):
1501
1532
  import sqlalchemy_utils # noqa: PLC0415
1502
1533
 
1503
- charset = "utf8mb4' COLLATE = 'utf8mb4_unicode_ci"
1504
- assert not sqlalchemy_utils.database_exists(db_url)
1505
- sqlalchemy_utils.create_database(db_url, encoding=charset)
1506
- elif db_url.startswith("postgresql://"):
1507
- import sqlalchemy_utils # noqa: PLC0415
1534
+ if drop_existing_db and sqlalchemy_utils.database_exists(db_url):
1535
+ drop_db()
1536
+
1537
+ if sqlalchemy_utils.database_exists(db_url):
1538
+ raise RuntimeError(
1539
+ f"Database {db_url} already exists. Use --drop-existing-db "
1540
+ "to automatically drop existing database before start of test."
1541
+ )
1508
1542
 
1509
- assert not sqlalchemy_utils.database_exists(db_url)
1510
- sqlalchemy_utils.create_database(db_url, encoding="utf8")
1543
+ sqlalchemy_utils.create_database(
1544
+ db_url,
1545
+ encoding="utf8mb4' COLLATE = 'utf8mb4_unicode_ci"
1546
+ if db_url.startswith("mysql://")
1547
+ else "utf8",
1548
+ )
1511
1549
  yield db_url
1512
1550
  if db_url == "sqlite://" and persistent_database:
1513
1551
  rmtree(tmp_path, ignore_errors=True)
1514
- elif db_url.startswith("mysql://"):
1515
- import sqlalchemy as sa # noqa: PLC0415
1516
-
1517
- made_url = sa.make_url(db_url)
1518
- db = made_url.database
1519
- engine = sa.create_engine(db_url)
1520
- # Check for any open connections to the database before dropping it
1521
- # to ensure that InnoDB does not deadlock.
1522
- with engine.begin() as connection:
1523
- query = sa.text(
1524
- "select id FROM information_schema.processlist WHERE db=:db and id != CONNECTION_ID()"
1525
- )
1526
- rows = connection.execute(query, parameters={"db": db}).fetchall()
1527
- if rows:
1528
- raise RuntimeError(
1529
- f"Unable to drop database {db} because it is in use by {rows}"
1530
- )
1531
- engine.dispose()
1532
- sqlalchemy_utils.drop_database(db_url)
1533
- elif db_url.startswith("postgresql://"):
1534
- sqlalchemy_utils.drop_database(db_url)
1552
+ elif db_url.startswith(("mysql://", "postgresql://")):
1553
+ drop_db()
1535
1554
 
1536
1555
 
1537
1556
  async def _async_init_recorder_component(
@@ -1661,10 +1680,12 @@ async def async_test_recorder(
1661
1680
  migrate_entity_ids = (
1662
1681
  migration.EntityIDMigration.migrate_data if enable_migrate_entity_ids else None
1663
1682
  )
1664
- legacy_event_id_foreign_key_exists = (
1665
- migration.EventIDPostMigration._legacy_event_id_foreign_key_exists
1683
+ post_migrate_event_ids = (
1684
+ migration.EventIDPostMigration.needs_migrate_impl
1666
1685
  if enable_migrate_event_ids
1667
- else lambda _: None
1686
+ else lambda _1, _2, _3: migration.DataMigrationStatus(
1687
+ needs_migrate=False, migration_done=True
1688
+ )
1668
1689
  )
1669
1690
  with (
1670
1691
  patch(
@@ -1703,8 +1724,8 @@ async def async_test_recorder(
1703
1724
  autospec=True,
1704
1725
  ),
1705
1726
  patch(
1706
- "homeassistant.components.recorder.migration.EventIDPostMigration._legacy_event_id_foreign_key_exists",
1707
- side_effect=legacy_event_id_foreign_key_exists,
1727
+ "homeassistant.components.recorder.migration.EventIDPostMigration.needs_migrate_impl",
1728
+ side_effect=post_migrate_event_ids,
1708
1729
  autospec=True,
1709
1730
  ),
1710
1731
  patch(
@@ -2114,3 +2135,14 @@ def _dhcp_service_info_init(self: DhcpServiceInfo, *args: Any, **kwargs: Any) ->
2114
2135
 
2115
2136
 
2116
2137
  DhcpServiceInfo.__init__ = _dhcp_service_info_init
2138
+
2139
+
2140
+ @pytest.fixture(autouse=True)
2141
+ def disable_http_server() -> Generator[None]:
2142
+ """Disable automatic start of HTTP server during tests.
2143
+
2144
+ This prevents the HTTP server from starting in tests that setup
2145
+ integrations which depend on the HTTP component.
2146
+ """
2147
+ with patch("homeassistant.components.http.start_http_server_and_save_config"):
2148
+ yield
@@ -225,8 +225,8 @@ class AiohttpClientMockResponse:
225
225
 
226
226
  if (
227
227
  self._url.scheme != url.scheme
228
- or self._url.host != url.host
229
- or self._url.path != url.path
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,5 +1,5 @@
1
1
  """
2
- Typing helpers for Home Assistant .
2
+ Typing helpers for Home Assistant tests.
3
3
 
4
4
  This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component.
5
5
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-homeassistant-custom-component
3
- Version: 0.13.289
3
+ Version: 0.13.305
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
@@ -20,13 +20,12 @@ License-File: LICENSE_HA_CORE.md
20
20
  Requires-Dist: sqlalchemy
21
21
  Requires-Dist: coverage==7.10.6
22
22
  Requires-Dist: freezegun==1.5.2
23
- Requires-Dist: go2rtc-client==0.2.1
24
23
  Requires-Dist: license-expression==30.4.3
25
24
  Requires-Dist: mock-open==1.4.0
26
- Requires-Dist: pydantic==2.11.9
25
+ Requires-Dist: pydantic==2.12.2
27
26
  Requires-Dist: pylint-per-file-ignores==1.4.0
28
27
  Requires-Dist: pipdeptree==2.26.1
29
- Requires-Dist: pytest-asyncio==1.2.0
28
+ Requires-Dist: pytest-asyncio==1.3.0
30
29
  Requires-Dist: pytest-aiohttp==1.1.0
31
30
  Requires-Dist: pytest-cov==7.0.0
32
31
  Requires-Dist: pytest-freezer==0.4.9
@@ -37,12 +36,12 @@ Requires-Dist: pytest-timeout==2.4.0
37
36
  Requires-Dist: pytest-unordered==0.7.0
38
37
  Requires-Dist: pytest-picked==0.5.1
39
38
  Requires-Dist: pytest-xdist==3.8.0
40
- Requires-Dist: pytest==8.4.2
39
+ Requires-Dist: pytest==9.0.0
41
40
  Requires-Dist: requests-mock==1.12.1
42
41
  Requires-Dist: respx==0.22.0
43
- Requires-Dist: syrupy==4.9.1
42
+ Requires-Dist: syrupy==5.0.0
44
43
  Requires-Dist: tqdm==4.67.1
45
- Requires-Dist: homeassistant==2025.10.4
44
+ Requires-Dist: homeassistant==2026.1.0
46
45
  Requires-Dist: SQLAlchemy==2.0.41
47
46
  Requires-Dist: paho-mqtt==2.1.0
48
47
  Requires-Dist: numpy==2.3.2
@@ -60,9 +59,7 @@ Dynamic: summary
60
59
 
61
60
  # pytest-homeassistant-custom-component
62
61
 
63
- ![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2025.10.4&labelColor=blue)
64
-
65
- [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/MatthewFlamm/pytest-homeassistant-custom-component)
62
+ ![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2026.1.0&labelColor=blue)
66
63
 
67
64
  Package to automatically extract testing plugins from Home Assistant for custom component testing.
68
65
  The goal is to provide the same functionality as the tests in home-assistant/core.
@@ -1,13 +1,12 @@
1
1
  sqlalchemy
2
2
  coverage==7.10.6
3
3
  freezegun==1.5.2
4
- go2rtc-client==0.2.1
5
4
  license-expression==30.4.3
6
5
  mock-open==1.4.0
7
- pydantic==2.11.9
6
+ pydantic==2.12.2
8
7
  pylint-per-file-ignores==1.4.0
9
8
  pipdeptree==2.26.1
10
- pytest-asyncio==1.2.0
9
+ pytest-asyncio==1.3.0
11
10
  pytest-aiohttp==1.1.0
12
11
  pytest-cov==7.0.0
13
12
  pytest-freezer==0.4.9
@@ -18,12 +17,12 @@ pytest-timeout==2.4.0
18
17
  pytest-unordered==0.7.0
19
18
  pytest-picked==0.5.1
20
19
  pytest-xdist==3.8.0
21
- pytest==8.4.2
20
+ pytest==9.0.0
22
21
  requests-mock==1.12.1
23
22
  respx==0.22.0
24
- syrupy==4.9.1
23
+ syrupy==5.0.0
25
24
  tqdm==4.67.1
26
- homeassistant==2025.10.4
25
+ homeassistant==2026.1.0
27
26
  SQLAlchemy==2.0.41
28
27
  paho-mqtt==2.1.0
29
28
  numpy==2.3.2
@@ -1,5 +0,0 @@
1
- """
2
- The tests for components.
3
-
4
- This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component.
5
- """