Skip to content

Commit d04ac0a

Browse files
Add entity registry update handling to CalendarEventBinarySensor for timer cancellation; implement tests for disabled state behavior
1 parent bf71cdb commit d04ac0a

File tree

2 files changed

+179
-1
lines changed

2 files changed

+179
-1
lines changed

custom_components/calendar_event/binary_sensor.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from homeassistant.const import EVENT_STATE_CHANGED
1010
from homeassistant.core import Event, HomeAssistant, callback
1111
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
12+
from homeassistant.helpers.event import async_track_entity_registry_updated_event
1213

1314
from .const import (
1415
ATTR_DESCRIPTION,
@@ -98,8 +99,30 @@ async def async_added_to_hass(self) -> None:
9899
self._hass.bus.async_listen(EVENT_STATE_CHANGED, self._state_changed)
99100
)
100101

102+
# Track entity registry updates to detect when entity is disabled/enabled
103+
self.async_on_remove(
104+
async_track_entity_registry_updated_event(
105+
self._hass, self.entity_id, self._entity_registry_updated
106+
)
107+
)
108+
101109
await self._update_state()
102110

111+
@callback
112+
def _entity_registry_updated(self, event: Event) -> None:
113+
"""Handle entity registry update."""
114+
# Cancel any pending timers if the entity is disabled
115+
if not self.enabled:
116+
self._cancel_call_later()
117+
118+
async def async_entity_registry_updated(self) -> None:
119+
"""Handle entity registry update."""
120+
await super().async_entity_registry_updated()
121+
122+
# Cancel any pending timers if the entity is disabled
123+
if not self.enabled:
124+
self._cancel_call_later()
125+
103126
def _cancel_call_later(self) -> None:
104127
"""Cancel any pending call_later."""
105128
if self._call_later_handle is not None:
@@ -115,11 +138,21 @@ async def async_will_remove_from_hass(self) -> None:
115138
async def _state_changed(self, event: Event) -> None:
116139
"""Handle calendar entity state changes."""
117140
if event.data.get("entity_id") == self._calendar_entity_id:
118-
await self._update_state()
141+
# Only update state if the entity is enabled
142+
if self.enabled:
143+
await self._update_state()
144+
else:
145+
# Cancel any pending timers if disabled
146+
self._cancel_call_later()
119147

120148
async def _update_state(self) -> None:
121149
"""Update the binary sensor state based on calendar events."""
122150

151+
# Don't update if the entity is disabled
152+
if not self.enabled:
153+
self._cancel_call_later()
154+
return
155+
123156
calendar_state = self._hass.states.get(self._calendar_entity_id)
124157

125158
if calendar_state is None:

tests/test_timer_cancellation.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""Test timer cancellation when binary sensor is disabled."""
2+
3+
from __future__ import annotations
4+
5+
from unittest.mock import MagicMock, patch
6+
7+
import pytest
8+
from homeassistant.core import HomeAssistant
9+
from homeassistant.helpers.entity_registry import RegistryEntry
10+
from pytest_homeassistant_custom_component.common import MockConfigEntry
11+
12+
from custom_components.calendar_event.binary_sensor import CalendarEventBinarySensor
13+
14+
15+
@pytest.mark.asyncio
16+
async def test_timer_cancelled_when_entity_disabled_directly(
17+
hass: HomeAssistant,
18+
) -> None:
19+
"""Test that pending timers are cancelled when entity is disabled."""
20+
21+
# Create a mock config entry
22+
config_entry = MockConfigEntry(
23+
domain="calendar_event",
24+
options={
25+
"calendar_entity": "calendar.test",
26+
"summary": "Test",
27+
"name": "Test",
28+
},
29+
)
30+
31+
# Create the entity
32+
entity = CalendarEventBinarySensor(
33+
hass=hass,
34+
config_entry=config_entry,
35+
name="Test",
36+
unique_id="test_id",
37+
calendar_entity_id="calendar.test",
38+
summary="Test",
39+
comparison_method="contains",
40+
)
41+
42+
# Create a mock handle for the timer
43+
mock_handle = MagicMock()
44+
entity._call_later_handle = mock_handle
45+
46+
# Mock registry entry as disabled
47+
mock_registry_entry = MagicMock(spec=RegistryEntry)
48+
mock_registry_entry.disabled_by = "user"
49+
50+
with patch.object(entity, "registry_entry", mock_registry_entry):
51+
# Simulate the entity registry update callback
52+
mock_event = MagicMock()
53+
entity._entity_registry_updated(mock_event)
54+
55+
# Verify the timer was cancelled
56+
mock_handle.cancel.assert_called_once()
57+
assert entity._call_later_handle is None
58+
59+
60+
@pytest.mark.asyncio
61+
async def test_timer_not_scheduled_when_disabled(hass: HomeAssistant) -> None:
62+
"""Test that no timers are scheduled when entity is disabled."""
63+
64+
# Create a mock config entry
65+
config_entry = MockConfigEntry(
66+
domain="calendar_event",
67+
options={
68+
"calendar_entity": "calendar.test",
69+
"summary": "Test",
70+
"name": "Test",
71+
},
72+
)
73+
74+
# Create the entity
75+
entity = CalendarEventBinarySensor(
76+
hass=hass,
77+
config_entry=config_entry,
78+
name="Test",
79+
unique_id="test_id",
80+
calendar_entity_id="calendar.test",
81+
summary="Test",
82+
comparison_method="contains",
83+
)
84+
85+
# Mock registry entry as disabled
86+
mock_registry_entry = MagicMock(spec=RegistryEntry)
87+
mock_registry_entry.disabled_by = "user"
88+
89+
with (
90+
patch.object(entity, "registry_entry", mock_registry_entry),
91+
patch.object(hass.loop, "call_later") as mock_call_later,
92+
):
93+
# Try to update state when disabled
94+
await entity._update_state()
95+
96+
# Verify no timer was scheduled
97+
mock_call_later.assert_not_called()
98+
99+
100+
@pytest.mark.asyncio
101+
async def test_timer_cancelled_in_state_changed_when_disabled(
102+
hass: HomeAssistant,
103+
) -> None:
104+
"""Test that timers are cancelled in state changed callback when disabled."""
105+
106+
# Create a mock config entry
107+
config_entry = MockConfigEntry(
108+
domain="calendar_event",
109+
options={
110+
"calendar_entity": "calendar.test",
111+
"summary": "Test",
112+
"name": "Test",
113+
},
114+
)
115+
116+
# Create the entity
117+
entity = CalendarEventBinarySensor(
118+
hass=hass,
119+
config_entry=config_entry,
120+
name="Test",
121+
unique_id="test_id",
122+
calendar_entity_id="calendar.test",
123+
summary="Test",
124+
comparison_method="contains",
125+
)
126+
127+
# Create a mock handle for the timer
128+
mock_handle = MagicMock()
129+
entity._call_later_handle = mock_handle
130+
131+
# Mock registry entry as disabled
132+
mock_registry_entry = MagicMock(spec=RegistryEntry)
133+
mock_registry_entry.disabled_by = "user"
134+
135+
with patch.object(entity, "registry_entry", mock_registry_entry):
136+
# Create a mock event for the calendar entity state change
137+
mock_event = MagicMock()
138+
mock_event.data = {"entity_id": "calendar.test"}
139+
140+
# Call the state changed callback
141+
await entity._state_changed(mock_event)
142+
143+
# Verify the timer was cancelled
144+
mock_handle.cancel.assert_called_once()
145+
assert entity._call_later_handle is None

0 commit comments

Comments
 (0)