@@ -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