Skip to content

Commit 06dd643

Browse files
authored
Merge pull request #49 from graphras-com/refactor/timer-separation
Separate Timer proxy mode from managed lifecycle
2 parents 5c5c007 + ecb8d8b commit 06dd643

3 files changed

Lines changed: 202 additions & 214 deletions

File tree

src/haclient/client.py

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -569,48 +569,24 @@ async def apply_scene(
569569
data["transition"] = transition
570570
await self._call_service("scene", "apply", data)
571571

572-
def timer(self, name: str | None = None, *, persistent: bool = False) -> Timer:
573-
"""Return a `Timer`, creating the Python object if needed.
572+
def timer(self, name: str) -> Timer:
573+
"""Return the `Timer` for *name*, creating the Python proxy if needed.
574574
575-
Timers are **ephemeral by default**: the HA helper is created on the
576-
first action and deleted automatically when the timer returns to idle.
577-
Pass ``persistent=True`` to keep the helper alive.
575+
This returns a proxy to an **existing** Home Assistant timer.
576+
To create a library-managed ephemeral timer, use
577+
``await Timer.create(client, ...)`` instead.
578578
579579
Parameters
580580
----------
581-
name : str or None, optional
581+
name : str
582582
Short object-id (e.g. ``"my_timer"``). The ``timer.`` prefix
583-
is added automatically. When ``None`` a unique id is
584-
generated automatically (only allowed for ephemeral timers).
585-
persistent : bool, optional
586-
If ``True``, the HA helper is **not** deleted on idle.
587-
Requires an explicit *name*.
583+
is added automatically.
588584
589585
Returns
590586
-------
591587
Timer
592588
The timer entity.
593-
594-
Raises
595-
------
596-
ValueError
597-
If ``persistent=True`` and *name* is ``None``.
598589
"""
599590
from .domains.timer import Timer as _Timer
600-
from .domains.timer import _generate_timer_id
601-
602-
if name is None:
603-
if persistent:
604-
raise ValueError("Persistent timers require an explicit name")
605-
name = _generate_timer_id()
606591

607-
entity_id = self.registry.resolve("timer", name)
608-
existing = self.registry.get(entity_id)
609-
if existing is not None:
610-
if not isinstance(existing, _Timer):
611-
raise HAClientError(
612-
f"Entity {entity_id} is registered as {type(existing).__name__}, "
613-
f"not {_Timer.__name__}"
614-
)
615-
return existing
616-
return _Timer(entity_id, self, persistent=persistent)
592+
return self._get_or_create("timer", name, _Timer)

src/haclient/domains/timer.py

Lines changed: 92 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,27 @@ class Timer(Entity):
3030
Actions use intent-specific names: ``start``, ``pause``, ``cancel``,
3131
``finish``, ``change``.
3232
33-
Timers are **ephemeral by default**: the HA helper is created
34-
automatically on the first action and deleted when the timer returns
35-
to idle (natural finish or cancellation). The same ``Timer`` object
36-
can be restarted afterwards — the helper is transparently re-created.
33+
Obtain a proxy to an **existing** Home Assistant timer via the domain
34+
accessor::
3735
36+
t = client.timer("my_timer")
37+
38+
To let the library **create and manage** a timer helper, use the async
39+
`create` classmethod instead::
40+
41+
t = await Timer.create(client, name="countdown", duration="00:05:00")
42+
43+
Timers returned by `create` are **ephemeral by default**: the HA
44+
helper is deleted automatically when the timer returns to idle
45+
(natural finish or cancellation). The same ``Timer`` object can be
46+
restarted afterwards — the helper is transparently re-created.
3847
Pass ``persistent=True`` to keep the HA helper alive after the timer
39-
finishes. Persistent timers require an explicit *name*; ephemeral
40-
timers auto-generate one when no name is provided.
48+
finishes.
4149
42-
Timers that already exist in Home Assistant (e.g. created via the UI)
43-
are never auto-deleted, regardless of the ``persistent`` flag. Only
44-
helpers created by the library are eligible for auto-cleanup.
50+
Timers obtained via the domain accessor (``client.timer("name")``)
51+
are never auto-deleted, regardless of how they were originally
52+
created in Home Assistant. Only helpers created by `create` are
53+
eligible for auto-cleanup.
4554
4655
In addition to the generic ``on_idle`` listener (which fires for both
4756
natural expiry and explicit cancellation), the timer provides
@@ -62,21 +71,90 @@ class Timer(Entity):
6271
automatically.
6372
client : HAClient
6473
The owning client instance.
65-
persistent : bool, optional
66-
If ``False`` (default), the HA helper is deleted automatically
67-
when the timer returns to idle.
6874
"""
6975

7076
domain = "timer"
7177

72-
def __init__(self, entity_id: str, client: Any, *, persistent: bool = False) -> None:
78+
def __init__(self, entity_id: str, client: Any) -> None:
7379
super().__init__(entity_id, client)
7480
self._finished_listeners: list[ValueChangeHandler] = []
7581
self._cancelled_listeners: list[ValueChangeHandler] = []
7682
self._ensured: bool = False
77-
self._persistent: bool = persistent
83+
self._persistent: bool = False
7884
self._created_by_us: bool = False
7985

86+
@classmethod
87+
async def create(
88+
cls,
89+
client: Any,
90+
*,
91+
name: str | None = None,
92+
duration: str = "00:01:00",
93+
persistent: bool = False,
94+
) -> Timer:
95+
"""Create a library-managed timer helper in Home Assistant.
96+
97+
This sends a ``timer/create`` WebSocket command to Home Assistant
98+
and returns a ``Timer`` instance that tracks the new helper.
99+
100+
Ephemeral timers (the default) are automatically deleted when
101+
they return to idle. Pass ``persistent=True`` to keep the HA
102+
helper alive.
103+
104+
Parameters
105+
----------
106+
client : HAClient
107+
The client instance to use.
108+
name : str or None, optional
109+
Short object-id (e.g. ``"my_timer"``). When ``None`` a
110+
unique id is generated automatically (only allowed for
111+
ephemeral timers).
112+
duration : str, optional
113+
Initial duration for the helper (e.g. ``"00:05:00"``).
114+
Defaults to ``"00:01:00"``.
115+
persistent : bool, optional
116+
If ``True``, the HA helper is **not** deleted on idle.
117+
Requires an explicit *name*.
118+
119+
Returns
120+
-------
121+
Timer
122+
The newly created timer entity.
123+
124+
Raises
125+
------
126+
ValueError
127+
If ``persistent=True`` and *name* is ``None``.
128+
"""
129+
if name is None:
130+
if persistent:
131+
raise ValueError("Persistent timers require an explicit name")
132+
name = _generate_timer_id()
133+
134+
entity_id = client.registry.resolve("timer", name)
135+
existing = client.registry.get(entity_id)
136+
timer: Timer
137+
if existing is not None and isinstance(existing, cls):
138+
timer = existing
139+
if timer._ensured:
140+
return timer
141+
else:
142+
timer = cls(entity_id, client)
143+
144+
timer._persistent = persistent # noqa: SLF001
145+
146+
object_id = entity_id.split(".", 1)[1]
147+
await client.ws.send_command(
148+
{
149+
"type": "timer/create",
150+
"name": object_id,
151+
"duration": duration,
152+
}
153+
)
154+
timer._ensured = True # noqa: SLF001
155+
timer._created_by_us = True # noqa: SLF001
156+
return timer
157+
80158
@property
81159
def persistent(self) -> bool:
82160
"""Whether this timer keeps its HA helper after returning to idle.
@@ -241,29 +319,6 @@ async def _auto_cleanup(self, _old: Any, _new: Any) -> None:
241319
self.state = "unknown"
242320
self._created_by_us = False
243321

244-
async def _ensure_exists(self) -> None:
245-
"""Create the timer helper in Home Assistant if it does not exist.
246-
247-
Uses the ``timer/create`` WebSocket command. The call is idempotent:
248-
once the helper has been confirmed (either via the initial state fetch
249-
or a prior ``_ensure_exists`` call), subsequent invocations are no-ops.
250-
251-
The object-id is extracted from the ``entity_id`` (the part after
252-
``timer.``).
253-
"""
254-
if self._ensured or self.state != "unknown":
255-
return
256-
object_id = self.entity_id.split(".", 1)[1]
257-
await self._client.ws.send_command(
258-
{
259-
"type": "timer/create",
260-
"name": object_id,
261-
"duration": "00:01:00",
262-
}
263-
)
264-
self._ensured = True
265-
self._created_by_us = True
266-
267322
async def delete(self) -> None:
268323
"""Delete the timer helper from Home Assistant.
269324
@@ -296,23 +351,19 @@ async def start(self, *, duration: str | None = None) -> None:
296351
duration : str or None, optional
297352
Override duration (e.g. ``"00:05:00"``).
298353
"""
299-
await self._ensure_exists()
300354
data: dict[str, Any] | None = {"duration": duration} if duration else None
301355
await self._call_service("start", data)
302356

303357
async def pause(self) -> None:
304358
"""Pause the timer."""
305-
await self._ensure_exists()
306359
await self._call_service("pause")
307360

308361
async def cancel(self) -> None:
309362
"""Cancel the timer (returns to idle)."""
310-
await self._ensure_exists()
311363
await self._call_service("cancel")
312364

313365
async def finish(self) -> None:
314366
"""Finish the timer immediately."""
315-
await self._ensure_exists()
316367
await self._call_service("finish")
317368

318369
async def change(self, *, duration: str) -> None:
@@ -323,7 +374,6 @@ async def change(self, *, duration: str) -> None:
323374
duration : str
324375
Duration to add/subtract (e.g. ``"00:01:00"`` or ``"-00:00:30"``).
325376
"""
326-
await self._ensure_exists()
327377
await self._call_service("change", {"duration": duration})
328378

329379
# -- Listener decorators --

0 commit comments

Comments
 (0)